From 45a75c136435a5e6996300acefba5a3e4dcfddf1 Mon Sep 17 00:00:00 2001 From: TingshanLiu Date: Wed, 24 May 2023 16:37:20 -0400 Subject: [PATCH 001/344] add basic gmIC --- examples/mixture/plot_gmIC_selection.py | 157 +++++++ sklearn/mixture/__init__.py | 3 +- sklearn/mixture/_gaussian_mixture_ic.py | 416 ++++++++++++++++++ .../mixture/tests/test_gaussian_mixture_ic.py | 190 ++++++++ 4 files changed, 765 insertions(+), 1 deletion(-) create mode 100644 examples/mixture/plot_gmIC_selection.py create mode 100644 sklearn/mixture/_gaussian_mixture_ic.py create mode 100644 sklearn/mixture/tests/test_gaussian_mixture_ic.py diff --git a/examples/mixture/plot_gmIC_selection.py b/examples/mixture/plot_gmIC_selection.py new file mode 100644 index 0000000000000..ec980d182e862 --- /dev/null +++ b/examples/mixture/plot_gmIC_selection.py @@ -0,0 +1,157 @@ +""" +================================ +Gaussian Mixture Model Selection +================================ + +This example shows that model selection can be performed with Gaussian Mixture +Models (GMM) using :ref:`information-theory criteria `. Model selection +concerns both the covariance type and the number of components in the model. + +In this case, both the Akaike Information Criterion (AIC) and the Bayes +Information Criterion (BIC) provide the right result, but we only demo the +latter as BIC is better suited to identify the true model among a set of +candidates. Unlike Bayesian procedures, such inferences are prior-free. + +""" + +# %% +# Data generation +# --------------- +# +# We generate two components (each one containing `n_samples`) by randomly +# sampling the standard normal distribution as returned by `numpy.random.randn`. +# One component is kept spherical yet shifted and re-scaled. The other one is +# deformed to have a more general covariance matrix. + +import numpy as np + +n_samples = 500 +np.random.seed(0) +C = np.array([[0.0, -0.1], [1.7, 0.4]]) +component_1 = np.dot(np.random.randn(n_samples, 2), C) # general +component_2 = 0.7 * np.random.randn(n_samples, 2) + np.array([-4, 1]) # spherical + +X = np.concatenate([component_1, component_2]) + +# %% +# We can visualize the different components: + +import matplotlib.pyplot as plt + +plt.scatter(component_1[:, 0], component_1[:, 1], s=0.8) +plt.scatter(component_2[:, 0], component_2[:, 1], s=0.8) +plt.title("Gaussian Mixture components") +plt.axis("equal") +plt.show() + +# %% +# Model training and selection +# ---------------------------- +# +# We vary the number of components from 1 to 6 and the type of covariance +# parameters to use: +# +# - `"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. +# +# We score the different models and keep the best model (the lowest BIC). This +# is done by using :class:`~sklearn.model_selection.GridSearchCV` and a +# user-defined score function which returns the negative BIC score, as +# :class:`~sklearn.model_selection.GridSearchCV` is designed to **maximize** a +# score (maximizing the negative BIC is equivalent to minimizing the BIC). +# +# The best set of parameters and estimator are stored in `best_parameters_` and +# `best_estimator_`, respectively. + +from sklearn.mixture import GaussianMixtureIC +gm_ic = GaussianMixtureIC(min_components=1, max_components=6, covariance_type='all') +gm_ic.fit(X) + + + +# %% +# Plot the BIC scores +# ------------------- +# +# To ease the plotting we can create a `pandas.DataFrame` from the results of +# the cross-validation done by the grid search. We re-inverse the sign of the +# BIC score to show the effect of minimizing it. + +import pandas as pd + +df = pd.DataFrame( + [( + model.n_components, model.covariance_type, model.criterion + ) for model in gm_ic.results_] +) +df.columns = ["Number of components", "Type of covariance", "BIC score"] +df.sort_values(by="BIC score").head() + +# %% +import seaborn as sns + +sns.catplot( + data=df, + kind="bar", + x="Number of components", + y="BIC score", + hue="Type of covariance", +) +plt.show() + +# %% +# In the present case, the model with 2 components and full covariance (which +# corresponds to the true generative model) has the lowest BIC score and is +# therefore selected by the grid search. +# +# Plot the best model +# ------------------- +# +# We plot an ellipse to show each Gaussian component of the selected model. For +# such purpose, one needs to find the eigenvalues of the covariance matrices as +# returned by the `covariances_` attribute. The shape of such matrices depends +# on the `covariance_type`: +# +# - `"full"`: (`n_components`, `n_features`, `n_features`) +# - `"tied"`: (`n_features`, `n_features`) +# - `"diag"`: (`n_components`, `n_features`) +# - `"spherical"`: (`n_components`,) + +from matplotlib.patches import Ellipse +from scipy import linalg + +color_iter = sns.color_palette("tab10", 2)[::-1] +Y_ = gm_ic.best_model_.predict(X) + +fig, ax = plt.subplots() + +for i, (mean, cov, color) in enumerate( + zip( + gm_ic.best_model_.means_, + gm_ic.best_model_.covariances_, + color_iter, + ) +): + v, w = linalg.eigh(cov) + if not np.any(Y_ == i): + continue + plt.scatter(X[Y_ == i, 0], X[Y_ == i, 1], 0.8, color=color) + + angle = np.arctan2(w[0][1], w[0][0]) + angle = 180.0 * angle / np.pi # convert to degrees + v = 2.0 * np.sqrt(2.0) * np.sqrt(v) + ellipse = Ellipse(mean, v[0], v[1], angle=180.0 + angle, color=color) + ellipse.set_clip_box(fig.bbox) + ellipse.set_alpha(0.5) + ax.add_artist(ellipse) + +plt.title( + f"Selected GMM: {gm_ic.covariance_type_} model, " + f"{gm_ic.n_components_} components" +) +plt.axis("equal") +plt.show() + +# %% diff --git a/sklearn/mixture/__init__.py b/sklearn/mixture/__init__.py index c5c20aa38eb18..6d8f9b5d56ff7 100644 --- a/sklearn/mixture/__init__.py +++ b/sklearn/mixture/__init__.py @@ -4,6 +4,7 @@ from ._gaussian_mixture import GaussianMixture from ._bayesian_mixture import BayesianGaussianMixture +from ._gaussian_mixture_ic import GaussianMixtureIC -__all__ = ["GaussianMixture", "BayesianGaussianMixture"] +__all__ = ["GaussianMixture", "BayesianGaussianMixture", "GaussianMixtureIC"] diff --git a/sklearn/mixture/_gaussian_mixture_ic.py b/sklearn/mixture/_gaussian_mixture_ic.py new file mode 100644 index 0000000000000..877358cff2092 --- /dev/null +++ b/sklearn/mixture/_gaussian_mixture_ic.py @@ -0,0 +1,416 @@ +"""GaussianMixtureIC""" + +# Author: Thomas Athey +# Modified by: Benjamin Pedigo +# Tingshan Liu + + +import numpy as np +import joblib +from ..utils.fixes import parse_version +from ..utils.parallel import delayed, Parallel +from ..utils import check_scalar +from ..utils.validation import check_is_fitted, check_random_state + +from . import GaussianMixture +from ..base import BaseEstimator, ClusterMixin +from ..model_selection import ParameterGrid + + + + +class GaussianMixtureIC(ClusterMixin, BaseEstimator): + """Gaussian mixture with BIC/AIC. + + Automatic Gaussian Mixture Model (GMM) selection via the + Bayesian Information Criterion (BIC) + or the Akaike Information Criterion (AIC). + + Different combinations of initialization, GMM, + and cluster numbers are used and the clustering + with the best selection criterion (BIC or AIC) is chosen. + + Parameters + ---------- + min_components : int, default=2 + The minimum number of mixture components to consider. + If ``max_components`` is not None, ``min_components`` must be + less than or equal to ``max_components``. + + max_components : int or None, default=10 + The maximum number of mixture components to consider. + Must be greater than or equal to ``min_components``. + + covariance_type : {'full', 'tied', 'diag', 'spherical', 'all' (default)}, + optional + String or list/array describing the type of covariance parameters + to use. + If a string, it 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 + - 'all' + considers all covariance structures in + ['spherical', 'diag', 'tied', 'full'] + + If a list/array, it must be a list/array of strings containing only + 'spherical', 'tied', 'diag', and/or 'spherical'. + + random_state : int, RandomState instance or None, optional (default=None) + There is randomness in k-means initialization of + :class:`sklearn.mixture.GaussianMixture`. This parameter is passed to + :class:`~sklearn.mixture.GaussianMixture` to control the random state. + If int, ``random_state`` is used as the random number generator seed. + If RandomState instance, ``random_state`` is the random number + generator; If None, the random number generator is the + RandomState instance used by ``np.random``. + + n_init : int, optional (default = 1) + If ``n_init`` is larger than 1, additional + ``n_init``-1 runs of :class:`sklearn.mixture.GaussianMixture` + initialized with k-means will be performed + for all covariance parameters in ``covariance_type``. + + init_params : {‘kmeans’ (default), ‘k-means++’, ‘random’, ‘random_from_data’} + The method used to initialize the weights, the means and the precisions + for Gaussian mixture modeling. + + max_iter : int, optional (default = 100) + The maximum number of EM iterations to perform. + + verbose : int, optional (default = 0) + 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. + + criterion : str {"bic" or "aic"}, optional, (default = "bic") + Select the best model based on Bayesian Information Criterion (bic) or + Aikake Information Criterion (aic). + + + Attributes + ---------- + best_criterion_ : float + The best (lowest) Bayesian or Aikake Information Criterion. + + n_components_ : int + Number of clusters for the model with the best bic/aic. + + covariance_type_ : str + Covariance type for the model with the best bic/aic. + + best_model_ : :class:`sklearn.mixture.GaussianMixture` + Object with the best bic/aic. + + labels_ : array-like, shape (n_samples,) + Labels of each point predicted by the best model. + + n_iter_ : int + Number of step used by the best fit of EM for the best model + to reach the convergence. + + results_ : list + Contains exhaustive information about all the clustering runs. + Each item represents a class object storing the results + for a single run with the following attributes: + + model : :class:`~sklearn.mixture.GaussianMixture` object + GMM clustering fit to the data. + criterion_score : float + Bayesian or Aikake Information Criterion score. + n_components : int + Number of clusters. + covariance_type : {'full', 'tied', 'diag', 'spherical'} + Covariance type used for the GMM. + + 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. + + See Also + -------- + GaussianMixture : Fit Gaussian mixture model. + BayesianGaussianMixture : Gaussian mixture model fit with a variational + inference. + + Notes + ----- + This algorithm was strongly inspired by mclust [3]_, + a clustering package for R. + + References + ---------- + .. [1] `Fraley, C., & Raftery, A. E. (2002). Model-based clustering, + discriminant analysis, and density estimation. + Journal of the American statistical Association, 97(458), 611-631. + _` + + .. [2] `Athey, T. L., Pedigo, B. D., Liu, T., & Vogelstein, J. T. (2019). + AutoGMM: Automatic and Hierarchical Gaussian Mixture Modeling + in Python. arXiv preprint arXiv:1909.02688. + _` + + .. [3] `Scrucca, L., Fop, M., Murphy, T. B., & Raftery, A. E. (2016). + mclust 5: Clustering, Classification and Density Estimation Using + Gaussian Finite Mixture Models. The R journal, 8(1), 289–317. + _` + + Examples + -------- + >>> import numpy as np + >>> from sklearn.mixture import GaussianMixtureIC + >>> X = np.array([[1, 2], [1, 4], [1, 0], [10, 2], [10, 4], [10, 0]]) + >>> gmIC = GaussianMixtureIC(max_components=4, random_state=0) + >>> gmIC.fit_predict(X) + array([0, 0, 0, 1, 1, 1]) + >>> print(gmIC.n_components_) + 2 + """ + + def __init__( + self, + min_components=2, + max_components=10, + covariance_type="all", + random_state=None, + n_init=1, + init_params="kmeans", + max_iter=100, + verbose=0, + criterion="bic", + n_jobs=None, + ): + + self.min_components = min_components + self.max_components = max_components + self.covariance_type = covariance_type + self.random_state = random_state + self.n_init = n_init + self.init_params = init_params + self.max_iter = max_iter + self.verbose = verbose + self.criterion = criterion + self.n_jobs = n_jobs + + def _check_multi_comp_inputs(self, input, name, default): + if isinstance(input, (np.ndarray, list)): + input = list(np.unique(input)) + elif isinstance(input, str): + if input not in default: + raise ValueError(f"{name} is {input} but must be one of {default}.") + if input != "all": + input = [input] + else: + input = default.copy() + input.remove("all") + else: + raise TypeError( + f"{name} is a {type(input)} but must be a numpy array, " + "a list, or a string." + ) + return input + + def _check_parameters(self): + check_scalar( + self.min_components, + min_val=1, + max_val=self.max_components, + name="min_components", + target_type=int, + ) + check_scalar( + self.max_components, min_val=1, name="max_components", target_type=int + ) + + covariance_type = self._check_multi_comp_inputs( + self.covariance_type, + "covariance_type", + ["spherical", "diag", "tied", "full", "all"], + ) + + check_scalar(self.n_init, name="n_init", target_type=int, min_val=1) + + if self.criterion not in ["aic", "bic"]: + raise ValueError( + f'criterion is {self.criterion} but must be "aic" or "bic".' + ) + + return covariance_type + + + def _fit_cluster(self, X, gm_params, seed): + gm_params["init_params"] = self.init_params + gm_params["max_iter"] = self.max_iter + gm_params["n_init"] = self.n_init + gm_params["random_state"] = seed + + model = GaussianMixture(**gm_params) + model.fit(X) + + if self.criterion == "bic": + criterion_value = model.bic(X) + else: + criterion_value = model.aic(X) + + # change the precision of "criterion_value" based on sample size + criterion_value = round(criterion_value, int(np.log10(X.shape[0]))) + results = _CollectResults(model, criterion_value, gm_params) + return results + + def fit(self, X, y=None): + """Fit several Gaussian mixture models to the data. + + Initialize with agglomerative clustering then + estimate model parameters with EM algorithm. + Select the best model according to the chosen + information criterion. + + Parameters + ---------- + X : array-like, shape (n_samples, n_features) + List of n_features-dimensional data points. Each row + corresponds to a single data point. + + y : Ignored + Not used, present for API consistency by convention. + + Returns + ------- + self : object + Returns an instance of self. + """ + + covariance_type = self._check_parameters() + X = self._validate_data(X, dtype=[np.float64, np.float32], ensure_min_samples=1) + + random_state = check_random_state(self.random_state) + + # check n_components against sample size + n_comps = [self.max_components, self.min_components] + names = ["max_components", "min_components"] + for i in range(len(names)): + if n_comps[i] > X.shape[0]: + msg = names[i] + "must be <= n_samples, but" + names[i] + msg += "= {}, n_samples = {}".format(n_comps[i], X.shape[0]) + raise ValueError(msg) + + param_grid = dict( + covariance_type=covariance_type, + n_components=range(self.min_components, self.max_components + 1), + ) + param_grid = list(ParameterGrid(param_grid)) + + seeds = random_state.randint(np.iinfo(np.int32).max, size=len(param_grid)) + + if parse_version(joblib.__version__) < parse_version("0.12"): + parallel_kwargs = {"backend": "threading"} + else: + parallel_kwargs = {"prefer": "threads"} + + results = Parallel(n_jobs=self.n_jobs, verbose=self.verbose, **parallel_kwargs)( + delayed(self._fit_cluster)(X, gm_params, seed) + for gm_params, seed in zip(param_grid, seeds) + ) + best_criter = [result.criterion for result in results] + + if sum(best_criter == np.min(best_criter)) == 1: + best_idx = np.argmin(best_criter) + else: + # in case there is a tie, + # select the model with the least number of parameters + ties = np.where(best_criter == np.min(best_criter))[0] + n_params = [results[tie].model._n_parameters() for tie in ties] + best_idx = ties[np.argmin(n_params)] + + self.best_criterion_ = results[best_idx].criterion + self.n_components_ = results[best_idx].n_components + self.covariance_type_ = results[best_idx].covariance_type + self.best_model_ = results[best_idx].model + self.n_iter_ = results[best_idx].model.n_iter_ + self.labels_ = results[best_idx].model.predict(X) + self.results_ = results + self.n_features_in_ = X.shape[1] + + return self + + def predict(self, X): + """Predict clusters based on the best Gaussian mixture model. + + Parameters + ---------- + X : array-like, shape (n_samples, n_features) + List of n_features-dimensional data points. Each row + corresponds to a single data point. + + Returns + ------- + labels : array, shape (n_samples,) + Component labels. + """ + check_is_fitted(self, ["best_model_"], all_or_any=all) + X = self._validate_data(X, reset=False) + labels = self.best_model_.predict(X) + + return labels + + def fit_predict(self, X, y=None): + """Fit the models and predict clusters based on the best model. + + Parameters + ---------- + X : array-like, shape (n_samples, n_features) + List of n_features-dimensional data points. Each row + corresponds to a single data point. + + y : Ignored + Not used, present for API consistency by convention. + + Returns + ------- + labels : array, shape (n_samples,) + Component labels. + """ + self.fit(X, y) + + labels = self.predict(X) + return labels + + + +class _CollectResults: + """Collect intermediary results. + + Represent the intermediary results for a single GMM clustering run + of :class:`sklearn.mixture.GaussianMixtureIC` + + Attributes + ---------- + + model : GaussianMixture object + GMM clustering fit to the data. + + criterion : float + Bayesian or Aikake Information Criterion. + + n_components : int + Number of components. + + covariance_type : {'full', 'tied', 'diag', 'spherical'} + Covariance type used for the GMM. + + """ + + def __init__(self, model, criter, gm_params): + self.model = model + self.criterion = criter + self.n_components = gm_params["n_components"] + self.covariance_type = gm_params["covariance_type"] \ No newline at end of file diff --git a/sklearn/mixture/tests/test_gaussian_mixture_ic.py b/sklearn/mixture/tests/test_gaussian_mixture_ic.py new file mode 100644 index 0000000000000..50eba6ff82a5a --- /dev/null +++ b/sklearn/mixture/tests/test_gaussian_mixture_ic.py @@ -0,0 +1,190 @@ +"""Testing for GaussianMixtureIC""" + +import pytest +import numpy as np +from numpy.testing import assert_allclose, assert_equal +from sklearn.exceptions import NotFittedError +from sklearn.metrics import adjusted_rand_score + +from sklearn.mixture import GaussianMixtureIC + + +def _test_inputs(X, error_type, **kws): + with pytest.raises(error_type): + gmIC = GaussianMixtureIC(**kws) + gmIC.fit(X) + + +def test_n_components(): + X = np.random.normal(0, 1, size=(100, 3)) + + # min_components must be less than 1 + _test_inputs(X, ValueError, min_components=0) + + # min_components must be an integer + _test_inputs(X, TypeError, min_components="1") + + # max_components must be at least min_components + _test_inputs(X, ValueError, max_components=0) + + # max_components must be an integer + _test_inputs(X, TypeError, max_components="1") + + # max_components must be at most n_samples + _test_inputs(X, ValueError, max_components=101) + + # min_components must be at most n_samples + _test_inputs(X, ValueError, **{"min_components": 101, "max_components": 102}) + + +def test_input_param(): + X = np.random.normal(0, 1, size=(100, 3)) + + # covariance type is not an array, string or list + _test_inputs(X, TypeError, covariance_type=1) + + # covariance type is not in ['spherical', 'diag', 'tied', 'full', 'all'] + _test_inputs(X, ValueError, covariance_type="1") + + # criterion is not "aic" or "bic" + _test_inputs(X, ValueError, criterion="cic") + + # n_init is not an integer + _test_inputs(X, TypeError, n_init="1") + + # n_init must be at least 1 + _test_inputs(X, ValueError, n_init=0) + + +def test_predict_without_fit(): + X = np.random.normal(0, 1, size=(100, 3)) + + with pytest.raises(NotFittedError): + gmIC = GaussianMixtureIC(min_components=2) + gmIC.predict(X) + + +def _test_two_class(**kws): + """ + Easily separable two gaussian problem. + """ + np.random.seed(1) + + n = 100 + d = 3 + + X1 = np.random.normal(2, 0.5, size=(n, d)) + X2 = np.random.normal(-2, 0.5, size=(n, d)) + X = np.vstack((X1, X2)) + y = np.repeat([0, 1], n) + + # test BIC + gmIC = GaussianMixtureIC(max_components=5, criterion="bic", **kws) + gmIC.fit(X, y) + n_components = gmIC.n_components_ + + # Assert that the two cluster model is the best + assert_equal(n_components, 2) + + # Assert that we get perfect clustering + ari = adjusted_rand_score(y, gmIC.fit_predict(X)) + assert_allclose(ari, 1) + + # test AIC + gmIC = GaussianMixtureIC(max_components=5, criterion="aic", **kws) + gmIC.fit(X, y) + n_components = gmIC.n_components_ + + # AIC gets the number of components wrong + assert_equal(n_components >= 1, True) + assert_equal(n_components <= 5, True) + + +def test_two_class(): + _test_two_class() + + + +def test_two_class_sequential_v_parallel(): + """ + Testing independence of results from the execution mode + (sequential vs. parallel using ``joblib.Parallel``). + """ + np.random.seed(1) + + n = 100 + d = 3 + + X1 = np.random.normal(2, 0.75, size=(n, d)) + X2 = np.random.normal(-2, 0.5, size=(n, d)) + X = np.vstack((X1, X2)) + + gmIC_parallel = GaussianMixtureIC( + max_components=5, criterion="bic", n_jobs=-1, random_state=1 + ) + preds_parallel = gmIC_parallel.fit_predict(X) + + gmIC_sequential = GaussianMixtureIC( + max_components=5, criterion="bic", n_jobs=1, random_state=1 + ) + preds_sequential = gmIC_sequential.fit_predict(X) + + # Results obtained with sequential and parallel executions + # must be identical + assert_equal(preds_parallel, preds_sequential) + + +def test_five_class(): + """ + Easily separable five gaussian problem. + """ + np.random.seed(1) + + n = 100 + mus = [[i * 5, 0] for i in range(5)] + cov = np.eye(2) # balls + + X = np.vstack([np.random.multivariate_normal(mu, cov, n) for mu in mus]) + + # test BIC + gmIC = GaussianMixtureIC( + min_components=3, max_components=10, criterion="bic" + ) + gmIC.fit(X) + assert_equal(gmIC.n_components_, 5) + + # test AIC + gmIC = GaussianMixtureIC( + min_components=3, max_components=10, criterion="aic" + ) + gmIC.fit(X) + # AIC fails often so there is no assertion here + assert_equal(gmIC.n_components_ >= 3, True) + assert_equal(gmIC.n_components_ <= 10, True) + + +@pytest.mark.parametrize( + "cov1, cov2, expected_cov_type", + [ + (2 * np.eye(2), 2 * np.eye(2), "spherical"), + (np.diag([1, 1]), np.diag([2, 1]), "diag"), + (np.array([[2, 1], [1, 2]]), np.array([[2, 1], [1, 2]]), "tied"), + (np.array([[2, -1], [-1, 2]]), np.array([[2, 1], [1, 2]]), "full"), + ], +) +def test_covariances(cov1, cov2, expected_cov_type): + """ + Testing that the predicted covariance type is correct + on an easily separable two gaussian problem for each covariance type. + """ + np.random.seed(1) + n = 100 + mu1 = [-10, 0] + mu2 = [10, 0] + X1 = np.random.multivariate_normal(mu1, cov1, n) + X2 = np.random.multivariate_normal(mu2, cov2, n) + X = np.concatenate((X1, X2)) + + gmIC = GaussianMixtureIC(min_components=2) + gmIC.fit(X) + assert_equal(gmIC.covariance_type_, expected_cov_type) \ No newline at end of file From 87cc2e8212e7e3e3f9de0afd7dbe4a1595ba45ce Mon Sep 17 00:00:00 2001 From: TingshanLiu Date: Wed, 21 Jun 2023 12:48:39 -0400 Subject: [PATCH 002/344] update code --- examples/mixture/plot_gmIC_selection.py | 157 --------------- examples/mixture/plot_gmm_selection.py | 59 ++---- sklearn/mixture/_gaussian_mixture_ic.py | 247 +++++++++--------------- 3 files changed, 110 insertions(+), 353 deletions(-) delete mode 100644 examples/mixture/plot_gmIC_selection.py diff --git a/examples/mixture/plot_gmIC_selection.py b/examples/mixture/plot_gmIC_selection.py deleted file mode 100644 index ec980d182e862..0000000000000 --- a/examples/mixture/plot_gmIC_selection.py +++ /dev/null @@ -1,157 +0,0 @@ -""" -================================ -Gaussian Mixture Model Selection -================================ - -This example shows that model selection can be performed with Gaussian Mixture -Models (GMM) using :ref:`information-theory criteria `. Model selection -concerns both the covariance type and the number of components in the model. - -In this case, both the Akaike Information Criterion (AIC) and the Bayes -Information Criterion (BIC) provide the right result, but we only demo the -latter as BIC is better suited to identify the true model among a set of -candidates. Unlike Bayesian procedures, such inferences are prior-free. - -""" - -# %% -# Data generation -# --------------- -# -# We generate two components (each one containing `n_samples`) by randomly -# sampling the standard normal distribution as returned by `numpy.random.randn`. -# One component is kept spherical yet shifted and re-scaled. The other one is -# deformed to have a more general covariance matrix. - -import numpy as np - -n_samples = 500 -np.random.seed(0) -C = np.array([[0.0, -0.1], [1.7, 0.4]]) -component_1 = np.dot(np.random.randn(n_samples, 2), C) # general -component_2 = 0.7 * np.random.randn(n_samples, 2) + np.array([-4, 1]) # spherical - -X = np.concatenate([component_1, component_2]) - -# %% -# We can visualize the different components: - -import matplotlib.pyplot as plt - -plt.scatter(component_1[:, 0], component_1[:, 1], s=0.8) -plt.scatter(component_2[:, 0], component_2[:, 1], s=0.8) -plt.title("Gaussian Mixture components") -plt.axis("equal") -plt.show() - -# %% -# Model training and selection -# ---------------------------- -# -# We vary the number of components from 1 to 6 and the type of covariance -# parameters to use: -# -# - `"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. -# -# We score the different models and keep the best model (the lowest BIC). This -# is done by using :class:`~sklearn.model_selection.GridSearchCV` and a -# user-defined score function which returns the negative BIC score, as -# :class:`~sklearn.model_selection.GridSearchCV` is designed to **maximize** a -# score (maximizing the negative BIC is equivalent to minimizing the BIC). -# -# The best set of parameters and estimator are stored in `best_parameters_` and -# `best_estimator_`, respectively. - -from sklearn.mixture import GaussianMixtureIC -gm_ic = GaussianMixtureIC(min_components=1, max_components=6, covariance_type='all') -gm_ic.fit(X) - - - -# %% -# Plot the BIC scores -# ------------------- -# -# To ease the plotting we can create a `pandas.DataFrame` from the results of -# the cross-validation done by the grid search. We re-inverse the sign of the -# BIC score to show the effect of minimizing it. - -import pandas as pd - -df = pd.DataFrame( - [( - model.n_components, model.covariance_type, model.criterion - ) for model in gm_ic.results_] -) -df.columns = ["Number of components", "Type of covariance", "BIC score"] -df.sort_values(by="BIC score").head() - -# %% -import seaborn as sns - -sns.catplot( - data=df, - kind="bar", - x="Number of components", - y="BIC score", - hue="Type of covariance", -) -plt.show() - -# %% -# In the present case, the model with 2 components and full covariance (which -# corresponds to the true generative model) has the lowest BIC score and is -# therefore selected by the grid search. -# -# Plot the best model -# ------------------- -# -# We plot an ellipse to show each Gaussian component of the selected model. For -# such purpose, one needs to find the eigenvalues of the covariance matrices as -# returned by the `covariances_` attribute. The shape of such matrices depends -# on the `covariance_type`: -# -# - `"full"`: (`n_components`, `n_features`, `n_features`) -# - `"tied"`: (`n_features`, `n_features`) -# - `"diag"`: (`n_components`, `n_features`) -# - `"spherical"`: (`n_components`,) - -from matplotlib.patches import Ellipse -from scipy import linalg - -color_iter = sns.color_palette("tab10", 2)[::-1] -Y_ = gm_ic.best_model_.predict(X) - -fig, ax = plt.subplots() - -for i, (mean, cov, color) in enumerate( - zip( - gm_ic.best_model_.means_, - gm_ic.best_model_.covariances_, - color_iter, - ) -): - v, w = linalg.eigh(cov) - if not np.any(Y_ == i): - continue - plt.scatter(X[Y_ == i, 0], X[Y_ == i, 1], 0.8, color=color) - - angle = np.arctan2(w[0][1], w[0][0]) - angle = 180.0 * angle / np.pi # convert to degrees - v = 2.0 * np.sqrt(2.0) * np.sqrt(v) - ellipse = Ellipse(mean, v[0], v[1], angle=180.0 + angle, color=color) - ellipse.set_clip_box(fig.bbox) - ellipse.set_alpha(0.5) - ax.add_artist(ellipse) - -plt.title( - f"Selected GMM: {gm_ic.covariance_type_} model, " - f"{gm_ic.n_components_} components" -) -plt.axis("equal") -plt.show() - -# %% diff --git a/examples/mixture/plot_gmm_selection.py b/examples/mixture/plot_gmm_selection.py index cd84c03ab7d13..d2ec74ca2308b 100644 --- a/examples/mixture/plot_gmm_selection.py +++ b/examples/mixture/plot_gmm_selection.py @@ -56,33 +56,10 @@ # - `"diag"`: each component has its own diagonal covariance matrix. # - `"spherical"`: each component has its own single variance. # -# We score the different models and keep the best model (the lowest BIC). This -# is done by using :class:`~sklearn.model_selection.GridSearchCV` and a -# user-defined score function which returns the negative BIC score, as -# :class:`~sklearn.model_selection.GridSearchCV` is designed to **maximize** a -# score (maximizing the negative BIC is equivalent to minimizing the BIC). -# -# The best set of parameters and estimator are stored in `best_parameters_` and -# `best_estimator_`, respectively. - -from sklearn.mixture import GaussianMixture -from sklearn.model_selection import GridSearchCV - -def gmm_bic_score(estimator, X): - """Callable to pass to GridSearchCV that will use the BIC score.""" - # Make it negative since GridSearchCV expects a score to maximize - return -estimator.bic(X) - - -param_grid = { - "n_components": range(1, 7), - "covariance_type": ["spherical", "tied", "diag", "full"], -} -grid_search = GridSearchCV( - GaussianMixture(), param_grid=param_grid, scoring=gmm_bic_score -) -grid_search.fit(X) +from sklearn.mixture import GaussianMixtureIC +gm_ic = GaussianMixtureIC(min_components=1, max_components=6, covariance_type='all') +gm_ic.fit(X) # %% # Plot the BIC scores @@ -94,17 +71,15 @@ def gmm_bic_score(estimator, X): import pandas as pd -df = pd.DataFrame(grid_search.cv_results_)[ - ["param_n_components", "param_covariance_type", "mean_test_score"] -] -df["mean_test_score"] = -df["mean_test_score"] -df = df.rename( - columns={ - "param_n_components": "Number of components", - "param_covariance_type": "Type of covariance", - "mean_test_score": "BIC score", - } -) +from sklearn.model_selection import ParameterGrid + +param_grid = list(ParameterGrid({ + "n_components": range(1, 7), + "covariance_type": ["spherical", "tied", "diag", "full"], +})) +df = pd.DataFrame(param_grid) +df.columns = ["Type of covariance", "Number of components"] +df["BIC score"] = gm_ic.criterion_ df.sort_values(by="BIC score").head() # %% @@ -141,14 +116,14 @@ def gmm_bic_score(estimator, X): from scipy import linalg color_iter = sns.color_palette("tab10", 2)[::-1] -Y_ = grid_search.predict(X) +Y_ = gm_ic.predict(X) fig, ax = plt.subplots() for i, (mean, cov, color) in enumerate( zip( - grid_search.best_estimator_.means_, - grid_search.best_estimator_.covariances_, + gm_ic.means_, + gm_ic.covariances_, color_iter, ) ): @@ -166,8 +141,8 @@ def gmm_bic_score(estimator, X): ax.add_artist(ellipse) plt.title( - f"Selected GMM: {grid_search.best_params_['covariance_type']} model, " - f"{grid_search.best_params_['n_components']} components" + f"Selected GMM: {gm_ic.covariance_type_} model, " + f"{gm_ic.n_components_} components" ) plt.axis("equal") plt.show() diff --git a/sklearn/mixture/_gaussian_mixture_ic.py b/sklearn/mixture/_gaussian_mixture_ic.py index 877358cff2092..35edaaf727a04 100644 --- a/sklearn/mixture/_gaussian_mixture_ic.py +++ b/sklearn/mixture/_gaussian_mixture_ic.py @@ -6,18 +6,31 @@ import numpy as np -import joblib -from ..utils.fixes import parse_version -from ..utils.parallel import delayed, Parallel from ..utils import check_scalar -from ..utils.validation import check_is_fitted, check_random_state +from ..utils.validation import check_is_fitted +from ..model_selection import GridSearchCV -from . import GaussianMixture from ..base import BaseEstimator, ClusterMixin -from ..model_selection import ParameterGrid - +from . import GaussianMixture +def _check_multi_comp_inputs(input, name, default): + if isinstance(input, (np.ndarray, list)): + input = list(np.unique(input)) + elif isinstance(input, str): + if input not in default: + raise ValueError(f"{name} is {input} but must be one of {default}.") + if input != "all": + input = [input] + else: + input = default.copy() + input.remove("all") + else: + raise TypeError( + f"{name} is a {type(input)} but must be a numpy array, " + "a list, or a string." + ) + return input class GaussianMixtureIC(ClusterMixin, BaseEstimator): """Gaussian mixture with BIC/AIC. @@ -26,9 +39,9 @@ class GaussianMixtureIC(ClusterMixin, BaseEstimator): Bayesian Information Criterion (BIC) or the Akaike Information Criterion (AIC). - Different combinations of initialization, GMM, - and cluster numbers are used and the clustering - with the best selection criterion (BIC or AIC) is chosen. + Such criteria are useful to select the value + of the gaussian mixture parameters by making a trade-off + between the goodness of fit and the complexity of the model. Parameters ---------- @@ -62,22 +75,10 @@ class GaussianMixtureIC(ClusterMixin, BaseEstimator): If a list/array, it must be a list/array of strings containing only 'spherical', 'tied', 'diag', and/or 'spherical'. - random_state : int, RandomState instance or None, optional (default=None) - There is randomness in k-means initialization of - :class:`sklearn.mixture.GaussianMixture`. This parameter is passed to - :class:`~sklearn.mixture.GaussianMixture` to control the random state. - If int, ``random_state`` is used as the random number generator seed. - If RandomState instance, ``random_state`` is the random number - generator; If None, the random number generator is the - RandomState instance used by ``np.random``. - n_init : int, optional (default = 1) - If ``n_init`` is larger than 1, additional - ``n_init``-1 runs of :class:`sklearn.mixture.GaussianMixture` - initialized with k-means will be performed - for all covariance parameters in ``covariance_type``. + The number of initializations to perform. - init_params : {‘kmeans’ (default), ‘k-means++’, ‘random’, ‘random_from_data’} + init_params : {'kmeans' (default), 'k-means++', 'random', 'random_from_data'} The method used to initialize the weights, the means and the precisions for Gaussian mixture modeling. @@ -96,8 +97,10 @@ class GaussianMixtureIC(ClusterMixin, BaseEstimator): Attributes ---------- - best_criterion_ : float - The best (lowest) Bayesian or Aikake Information Criterion. + criterion_ : array-like + The value of the information criteria ('aic', 'bic') across all + numbers of components. The number of component which has the smallest + information criterion is chosen. n_components_ : int Number of clusters for the model with the best bic/aic. @@ -105,30 +108,37 @@ class GaussianMixtureIC(ClusterMixin, BaseEstimator): covariance_type_ : str Covariance type for the model with the best bic/aic. - best_model_ : :class:`sklearn.mixture.GaussianMixture` + best_estimator_ : :class:`sklearn.mixture.GaussianMixture` Object with the best bic/aic. - labels_ : array-like, shape (n_samples,) - Labels of each point predicted by the best model. + weights_ : array-like of shape (n_components,) + The weights of each mixture components for the model with the best bic/aic. + + means_ : array-like of shape (n_components, n_features) + The mean of each mixture component for the model with the best bic/aic. + + covariances_ : array-like + The covariance of each mixture component for the model with the best bic/aic. + The shape depends on `covariance_type_`. See + :class:`~sklearn.mixture.GaussianMixture` for details. + + precisions_ : array-like + The precision matrices for each component in the mixture for the model + with the best bic/aic. See :class:`~sklearn.mixture.GaussianMixture` for details. + + precisions_cholesky_ : array-like + The cholesky decomposition of the precision matrices of each mixture component + for the model with the best bic/aic. + See :class:`~sklearn.mixture.GaussianMixture` for details. + + converged_: bool + True when convergence was reached in :term:`fit` for the model + with the best bic/aic, False otherwise. n_iter_ : int Number of step used by the best fit of EM for the best model to reach the convergence. - results_ : list - Contains exhaustive information about all the clustering runs. - Each item represents a class object storing the results - for a single run with the following attributes: - - model : :class:`~sklearn.mixture.GaussianMixture` object - GMM clustering fit to the data. - criterion_score : float - Bayesian or Aikake Information Criterion score. - n_components : int - Number of clusters. - covariance_type : {'full', 'tied', 'diag', 'spherical'} - Covariance type used for the GMM. - n_features_in_ : int Number of features seen during :term:`fit`. @@ -161,7 +171,7 @@ class GaussianMixtureIC(ClusterMixin, BaseEstimator): .. [3] `Scrucca, L., Fop, M., Murphy, T. B., & Raftery, A. E. (2016). mclust 5: Clustering, Classification and Density Estimation Using - Gaussian Finite Mixture Models. The R journal, 8(1), 289–317. + Gaussian Finite Mixture Models. The R journal, 8(1), 289-317. _` Examples @@ -169,7 +179,7 @@ class GaussianMixtureIC(ClusterMixin, BaseEstimator): >>> import numpy as np >>> from sklearn.mixture import GaussianMixtureIC >>> X = np.array([[1, 2], [1, 4], [1, 0], [10, 2], [10, 4], [10, 0]]) - >>> gmIC = GaussianMixtureIC(max_components=4, random_state=0) + >>> gmIC = GaussianMixtureIC(max_components=4) >>> gmIC.fit_predict(X) array([0, 0, 0, 1, 1, 1]) >>> print(gmIC.n_components_) @@ -181,7 +191,6 @@ def __init__( min_components=2, max_components=10, covariance_type="all", - random_state=None, n_init=1, init_params="kmeans", max_iter=100, @@ -193,7 +202,6 @@ def __init__( self.min_components = min_components self.max_components = max_components self.covariance_type = covariance_type - self.random_state = random_state self.n_init = n_init self.init_params = init_params self.max_iter = max_iter @@ -201,24 +209,6 @@ def __init__( self.criterion = criterion self.n_jobs = n_jobs - def _check_multi_comp_inputs(self, input, name, default): - if isinstance(input, (np.ndarray, list)): - input = list(np.unique(input)) - elif isinstance(input, str): - if input not in default: - raise ValueError(f"{name} is {input} but must be one of {default}.") - if input != "all": - input = [input] - else: - input = default.copy() - input.remove("all") - else: - raise TypeError( - f"{name} is a {type(input)} but must be a numpy array, " - "a list, or a string." - ) - return input - def _check_parameters(self): check_scalar( self.min_components, @@ -228,10 +218,13 @@ def _check_parameters(self): target_type=int, ) check_scalar( - self.max_components, min_val=1, name="max_components", target_type=int + self.max_components, + min_val=self.min_components, + name="max_components", + target_type=int ) - covariance_type = self._check_multi_comp_inputs( + covariance_type = _check_multi_comp_inputs( self.covariance_type, "covariance_type", ["spherical", "diag", "tied", "full", "all"], @@ -246,25 +239,11 @@ def _check_parameters(self): return covariance_type - - def _fit_cluster(self, X, gm_params, seed): - gm_params["init_params"] = self.init_params - gm_params["max_iter"] = self.max_iter - gm_params["n_init"] = self.n_init - gm_params["random_state"] = seed - - model = GaussianMixture(**gm_params) - model.fit(X) - + def criterion_score(self, estimator, X): if self.criterion == "bic": - criterion_value = model.bic(X) + return -estimator.bic(X) else: - criterion_value = model.aic(X) - - # change the precision of "criterion_value" based on sample size - criterion_value = round(criterion_value, int(np.log10(X.shape[0]))) - results = _CollectResults(model, criterion_value, gm_params) - return results + return -estimator.aic(X) def fit(self, X, y=None): """Fit several Gaussian mixture models to the data. @@ -292,8 +271,6 @@ def fit(self, X, y=None): covariance_type = self._check_parameters() X = self._validate_data(X, dtype=[np.float64, np.float32], ensure_min_samples=1) - random_state = check_random_state(self.random_state) - # check n_components against sample size n_comps = [self.max_components, self.min_components] names = ["max_components", "min_components"] @@ -303,41 +280,34 @@ def fit(self, X, y=None): msg += "= {}, n_samples = {}".format(n_comps[i], X.shape[0]) raise ValueError(msg) - param_grid = dict( - covariance_type=covariance_type, - n_components=range(self.min_components, self.max_components + 1), + param_grid = { + "covariance_type": covariance_type, + "n_components": range(self.min_components, self.max_components + 1), + } + + grid_search = GridSearchCV( + GaussianMixture( + init_params=self.init_params, + max_iter=self.max_iter, + n_init=self.n_init + ), param_grid=param_grid, scoring=self.criterion_score ) - param_grid = list(ParameterGrid(param_grid)) - - seeds = random_state.randint(np.iinfo(np.int32).max, size=len(param_grid)) - - if parse_version(joblib.__version__) < parse_version("0.12"): - parallel_kwargs = {"backend": "threading"} - else: - parallel_kwargs = {"prefer": "threads"} - - results = Parallel(n_jobs=self.n_jobs, verbose=self.verbose, **parallel_kwargs)( - delayed(self._fit_cluster)(X, gm_params, seed) - for gm_params, seed in zip(param_grid, seeds) - ) - best_criter = [result.criterion for result in results] - - if sum(best_criter == np.min(best_criter)) == 1: - best_idx = np.argmin(best_criter) - else: - # in case there is a tie, - # select the model with the least number of parameters - ties = np.where(best_criter == np.min(best_criter))[0] - n_params = [results[tie].model._n_parameters() for tie in ties] - best_idx = ties[np.argmin(n_params)] - - self.best_criterion_ = results[best_idx].criterion - self.n_components_ = results[best_idx].n_components - self.covariance_type_ = results[best_idx].covariance_type - self.best_model_ = results[best_idx].model - self.n_iter_ = results[best_idx].model.n_iter_ - self.labels_ = results[best_idx].model.predict(X) - self.results_ = results + grid_search.fit(X) + + self.criterion_ = -grid_search.cv_results_['mean_test_score'] + self.n_components_ = grid_search.best_params_['n_components'] + self.covariance_type_ = grid_search.best_params_['covariance_type'] + + best_estimator = grid_search.best_estimator_ + self.best_estimator_ = best_estimator + self.weights_ = best_estimator.weights_ + self.means_ = best_estimator.means_ + self.covariances_ = best_estimator.covariances_ + self.precisions_ = best_estimator.precisions_ + self.precisions_cholesky_ = best_estimator.precisions_cholesky_ + self.converged_ = best_estimator.converged_ + self.n_iter_ = best_estimator.n_iter_ + self.lower_bound_ = best_estimator.lower_bound_ self.n_features_in_ = X.shape[1] return self @@ -356,9 +326,9 @@ def predict(self, X): labels : array, shape (n_samples,) Component labels. """ - check_is_fitted(self, ["best_model_"], all_or_any=all) + check_is_fitted(self, ["best_estimator_"], all_or_any=all) X = self._validate_data(X, reset=False) - labels = self.best_model_.predict(X) + labels = self.best_estimator_.predict(X) return labels @@ -382,35 +352,4 @@ def fit_predict(self, X, y=None): self.fit(X, y) labels = self.predict(X) - return labels - - - -class _CollectResults: - """Collect intermediary results. - - Represent the intermediary results for a single GMM clustering run - of :class:`sklearn.mixture.GaussianMixtureIC` - - Attributes - ---------- - - model : GaussianMixture object - GMM clustering fit to the data. - - criterion : float - Bayesian or Aikake Information Criterion. - - n_components : int - Number of components. - - covariance_type : {'full', 'tied', 'diag', 'spherical'} - Covariance type used for the GMM. - - """ - - def __init__(self, model, criter, gm_params): - self.model = model - self.criterion = criter - self.n_components = gm_params["n_components"] - self.covariance_type = gm_params["covariance_type"] \ No newline at end of file + return labels \ No newline at end of file From e3b98f7dca497ad4bd6530acce42eec283443d43 Mon Sep 17 00:00:00 2001 From: TingshanLiu Date: Thu, 29 Jun 2023 19:20:57 -0400 Subject: [PATCH 003/344] fix linting --- examples/mixture/plot_gmm_selection.py | 16 +++--- sklearn/mixture/_gaussian_mixture_ic.py | 49 +++++++++---------- .../mixture/tests/test_gaussian_mixture_ic.py | 11 ++--- 3 files changed, 37 insertions(+), 39 deletions(-) diff --git a/examples/mixture/plot_gmm_selection.py b/examples/mixture/plot_gmm_selection.py index d2ec74ca2308b..229938026353e 100644 --- a/examples/mixture/plot_gmm_selection.py +++ b/examples/mixture/plot_gmm_selection.py @@ -58,6 +58,7 @@ # from sklearn.mixture import GaussianMixtureIC + gm_ic = GaussianMixtureIC(min_components=1, max_components=6, covariance_type='all') gm_ic.fit(X) @@ -73,10 +74,14 @@ from sklearn.model_selection import ParameterGrid -param_grid = list(ParameterGrid({ - "n_components": range(1, 7), - "covariance_type": ["spherical", "tied", "diag", "full"], -})) +param_grid = list( + ParameterGrid( + { + "n_components": range(1, 7), + "covariance_type": ["spherical", "tied", "diag", "full"], + } + ) +) df = pd.DataFrame(param_grid) df.columns = ["Type of covariance", "Number of components"] df["BIC score"] = gm_ic.criterion_ @@ -141,8 +146,7 @@ ax.add_artist(ellipse) plt.title( - f"Selected GMM: {gm_ic.covariance_type_} model, " - f"{gm_ic.n_components_} components" + f"Selected GMM: {gm_ic.covariance_type_} model, {gm_ic.n_components_} components" ) plt.axis("equal") plt.show() diff --git a/sklearn/mixture/_gaussian_mixture_ic.py b/sklearn/mixture/_gaussian_mixture_ic.py index 35edaaf727a04..49a421fdd6221 100644 --- a/sklearn/mixture/_gaussian_mixture_ic.py +++ b/sklearn/mixture/_gaussian_mixture_ic.py @@ -15,22 +15,22 @@ def _check_multi_comp_inputs(input, name, default): - if isinstance(input, (np.ndarray, list)): - input = list(np.unique(input)) - elif isinstance(input, str): - if input not in default: - raise ValueError(f"{name} is {input} but must be one of {default}.") - if input != "all": - input = [input] - else: - input = default.copy() - input.remove("all") + if isinstance(input, (np.ndarray, list)): + input = list(np.unique(input)) + elif isinstance(input, str): + if input not in default: + raise ValueError(f"{name} is {input} but must be one of {default}.") + if input != "all": + input = [input] else: - raise TypeError( - f"{name} is a {type(input)} but must be a numpy array, " - "a list, or a string." - ) - return input + input = default.copy() + input.remove("all") + else: + raise TypeError( + f"{name} is a {type(input)} but must be a numpy array, a list, or a string." + ) + return input + class GaussianMixtureIC(ClusterMixin, BaseEstimator): """Gaussian mixture with BIC/AIC. @@ -198,7 +198,6 @@ def __init__( criterion="bic", n_jobs=None, ): - self.min_components = min_components self.max_components = max_components self.covariance_type = covariance_type @@ -221,7 +220,7 @@ def _check_parameters(self): self.max_components, min_val=self.min_components, name="max_components", - target_type=int + target_type=int, ) covariance_type = _check_multi_comp_inputs( @@ -287,16 +286,16 @@ def fit(self, X, y=None): grid_search = GridSearchCV( GaussianMixture( - init_params=self.init_params, - max_iter=self.max_iter, - n_init=self.n_init - ), param_grid=param_grid, scoring=self.criterion_score + init_params=self.init_params, max_iter=self.max_iter, n_init=self.n_init + ), + param_grid=param_grid, + scoring=self.criterion_score, ) grid_search.fit(X) - self.criterion_ = -grid_search.cv_results_['mean_test_score'] - self.n_components_ = grid_search.best_params_['n_components'] - self.covariance_type_ = grid_search.best_params_['covariance_type'] + self.criterion_ = -grid_search.cv_results_["mean_test_score"] + self.n_components_ = grid_search.best_params_["n_components"] + self.covariance_type_ = grid_search.best_params_["covariance_type"] best_estimator = grid_search.best_estimator_ self.best_estimator_ = best_estimator @@ -352,4 +351,4 @@ def fit_predict(self, X, y=None): self.fit(X, y) labels = self.predict(X) - return labels \ No newline at end of file + return labels diff --git a/sklearn/mixture/tests/test_gaussian_mixture_ic.py b/sklearn/mixture/tests/test_gaussian_mixture_ic.py index 50eba6ff82a5a..5054c8122430b 100644 --- a/sklearn/mixture/tests/test_gaussian_mixture_ic.py +++ b/sklearn/mixture/tests/test_gaussian_mixture_ic.py @@ -104,7 +104,6 @@ def test_two_class(): _test_two_class() - def test_two_class_sequential_v_parallel(): """ Testing independence of results from the execution mode @@ -147,16 +146,12 @@ def test_five_class(): X = np.vstack([np.random.multivariate_normal(mu, cov, n) for mu in mus]) # test BIC - gmIC = GaussianMixtureIC( - min_components=3, max_components=10, criterion="bic" - ) + gmIC = GaussianMixtureIC(min_components=3, max_components=10, criterion="bic") gmIC.fit(X) assert_equal(gmIC.n_components_, 5) # test AIC - gmIC = GaussianMixtureIC( - min_components=3, max_components=10, criterion="aic" - ) + gmIC = GaussianMixtureIC(min_components=3, max_components=10, criterion="aic") gmIC.fit(X) # AIC fails often so there is no assertion here assert_equal(gmIC.n_components_ >= 3, True) @@ -187,4 +182,4 @@ def test_covariances(cov1, cov2, expected_cov_type): gmIC = GaussianMixtureIC(min_components=2) gmIC.fit(X) - assert_equal(gmIC.covariance_type_, expected_cov_type) \ No newline at end of file + assert_equal(gmIC.covariance_type_, expected_cov_type) From a37949ef45c08fae5b2fca5ee26adbf0183e05d8 Mon Sep 17 00:00:00 2001 From: TingshanLiu Date: Thu, 29 Jun 2023 20:44:16 -0400 Subject: [PATCH 004/344] fix linting --- examples/mixture/plot_gmm_selection.py | 2 +- sklearn/mixture/__init__.py | 2 +- sklearn/mixture/_gaussian_mixture_ic.py | 10 +++++----- sklearn/mixture/tests/test_gaussian_mixture_ic.py | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/examples/mixture/plot_gmm_selection.py b/examples/mixture/plot_gmm_selection.py index 229938026353e..4b4c52334eeca 100644 --- a/examples/mixture/plot_gmm_selection.py +++ b/examples/mixture/plot_gmm_selection.py @@ -59,7 +59,7 @@ from sklearn.mixture import GaussianMixtureIC -gm_ic = GaussianMixtureIC(min_components=1, max_components=6, covariance_type='all') +gm_ic = GaussianMixtureIC(min_components=1, max_components=6, covariance_type="all") gm_ic.fit(X) # %% diff --git a/sklearn/mixture/__init__.py b/sklearn/mixture/__init__.py index 1e3ce5dabd1df..0a9ab2ccc365e 100644 --- a/sklearn/mixture/__init__.py +++ b/sklearn/mixture/__init__.py @@ -3,7 +3,7 @@ """ from ._bayesian_mixture import BayesianGaussianMixture -from ._gaussian_mixture_ic import GaussianMixtureIC from ._gaussian_mixture import GaussianMixture +from ._gaussian_mixture_ic import GaussianMixtureIC __all__ = ["GaussianMixture", "BayesianGaussianMixture", "GaussianMixtureIC"] diff --git a/sklearn/mixture/_gaussian_mixture_ic.py b/sklearn/mixture/_gaussian_mixture_ic.py index 49a421fdd6221..d3975536171dd 100644 --- a/sklearn/mixture/_gaussian_mixture_ic.py +++ b/sklearn/mixture/_gaussian_mixture_ic.py @@ -6,11 +6,11 @@ import numpy as np -from ..utils import check_scalar -from ..utils.validation import check_is_fitted -from ..model_selection import GridSearchCV from ..base import BaseEstimator, ClusterMixin +from ..model_selection import GridSearchCV +from ..utils import check_scalar +from ..utils.validation import check_is_fitted from . import GaussianMixture @@ -123,8 +123,8 @@ class GaussianMixtureIC(ClusterMixin, BaseEstimator): :class:`~sklearn.mixture.GaussianMixture` for details. precisions_ : array-like - The precision matrices for each component in the mixture for the model - with the best bic/aic. See :class:`~sklearn.mixture.GaussianMixture` for details. + The precision matrices for each component in the mixture for the model with + the best bic/aic. See :class:`~sklearn.mixture.GaussianMixture` for details. precisions_cholesky_ : array-like The cholesky decomposition of the precision matrices of each mixture component diff --git a/sklearn/mixture/tests/test_gaussian_mixture_ic.py b/sklearn/mixture/tests/test_gaussian_mixture_ic.py index 5054c8122430b..d6610fd9788dd 100644 --- a/sklearn/mixture/tests/test_gaussian_mixture_ic.py +++ b/sklearn/mixture/tests/test_gaussian_mixture_ic.py @@ -1,11 +1,11 @@ """Testing for GaussianMixtureIC""" -import pytest import numpy as np +import pytest from numpy.testing import assert_allclose, assert_equal + from sklearn.exceptions import NotFittedError from sklearn.metrics import adjusted_rand_score - from sklearn.mixture import GaussianMixtureIC From a6ee20114af50844add5158346a367a92f4742c4 Mon Sep 17 00:00:00 2001 From: TingshanLiu Date: Fri, 30 Jun 2023 11:59:14 -0400 Subject: [PATCH 005/344] fix tests --- sklearn/mixture/_gaussian_mixture_ic.py | 6 ++ .../mixture/tests/test_gaussian_mixture_ic.py | 60 +------------------ 2 files changed, 8 insertions(+), 58 deletions(-) diff --git a/sklearn/mixture/_gaussian_mixture_ic.py b/sklearn/mixture/_gaussian_mixture_ic.py index d3975536171dd..651f3c34e7e75 100644 --- a/sklearn/mixture/_gaussian_mixture_ic.py +++ b/sklearn/mixture/_gaussian_mixture_ic.py @@ -10,6 +10,7 @@ from ..base import BaseEstimator, ClusterMixin from ..model_selection import GridSearchCV from ..utils import check_scalar +from ..utils._param_validation import StrOptions from ..utils.validation import check_is_fitted from . import GaussianMixture @@ -186,6 +187,11 @@ class GaussianMixtureIC(ClusterMixin, BaseEstimator): 2 """ + _parameter_constraints: dict = { + **GaussianMixture._parameter_constraints, + "criterion": [StrOptions({"aic", "bic"})], + } + def __init__( self, min_components=2, diff --git a/sklearn/mixture/tests/test_gaussian_mixture_ic.py b/sklearn/mixture/tests/test_gaussian_mixture_ic.py index d6610fd9788dd..dbf5aa4a0c1a4 100644 --- a/sklearn/mixture/tests/test_gaussian_mixture_ic.py +++ b/sklearn/mixture/tests/test_gaussian_mixture_ic.py @@ -118,68 +118,12 @@ def test_two_class_sequential_v_parallel(): X2 = np.random.normal(-2, 0.5, size=(n, d)) X = np.vstack((X1, X2)) - gmIC_parallel = GaussianMixtureIC( - max_components=5, criterion="bic", n_jobs=-1, random_state=1 - ) + gmIC_parallel = GaussianMixtureIC(max_components=5, criterion="bic", n_jobs=-1) preds_parallel = gmIC_parallel.fit_predict(X) - gmIC_sequential = GaussianMixtureIC( - max_components=5, criterion="bic", n_jobs=1, random_state=1 - ) + gmIC_sequential = GaussianMixtureIC(max_components=5, criterion="bic", n_jobs=1) preds_sequential = gmIC_sequential.fit_predict(X) # Results obtained with sequential and parallel executions # must be identical assert_equal(preds_parallel, preds_sequential) - - -def test_five_class(): - """ - Easily separable five gaussian problem. - """ - np.random.seed(1) - - n = 100 - mus = [[i * 5, 0] for i in range(5)] - cov = np.eye(2) # balls - - X = np.vstack([np.random.multivariate_normal(mu, cov, n) for mu in mus]) - - # test BIC - gmIC = GaussianMixtureIC(min_components=3, max_components=10, criterion="bic") - gmIC.fit(X) - assert_equal(gmIC.n_components_, 5) - - # test AIC - gmIC = GaussianMixtureIC(min_components=3, max_components=10, criterion="aic") - gmIC.fit(X) - # AIC fails often so there is no assertion here - assert_equal(gmIC.n_components_ >= 3, True) - assert_equal(gmIC.n_components_ <= 10, True) - - -@pytest.mark.parametrize( - "cov1, cov2, expected_cov_type", - [ - (2 * np.eye(2), 2 * np.eye(2), "spherical"), - (np.diag([1, 1]), np.diag([2, 1]), "diag"), - (np.array([[2, 1], [1, 2]]), np.array([[2, 1], [1, 2]]), "tied"), - (np.array([[2, -1], [-1, 2]]), np.array([[2, 1], [1, 2]]), "full"), - ], -) -def test_covariances(cov1, cov2, expected_cov_type): - """ - Testing that the predicted covariance type is correct - on an easily separable two gaussian problem for each covariance type. - """ - np.random.seed(1) - n = 100 - mu1 = [-10, 0] - mu2 = [10, 0] - X1 = np.random.multivariate_normal(mu1, cov1, n) - X2 = np.random.multivariate_normal(mu2, cov2, n) - X = np.concatenate((X1, X2)) - - gmIC = GaussianMixtureIC(min_components=2) - gmIC.fit(X) - assert_equal(gmIC.covariance_type_, expected_cov_type) From ebb86fe378a70a868a02808211978cdd3ece29e6 Mon Sep 17 00:00:00 2001 From: TingshanLiu Date: Tue, 25 Jul 2023 17:18:17 -0400 Subject: [PATCH 006/344] Update _gaussian_mixture_ic.py --- sklearn/mixture/_gaussian_mixture_ic.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/sklearn/mixture/_gaussian_mixture_ic.py b/sklearn/mixture/_gaussian_mixture_ic.py index 651f3c34e7e75..f643b9eb8af16 100644 --- a/sklearn/mixture/_gaussian_mixture_ic.py +++ b/sklearn/mixture/_gaussian_mixture_ic.py @@ -1,8 +1,8 @@ """GaussianMixtureIC""" -# Author: Thomas Athey -# Modified by: Benjamin Pedigo -# Tingshan Liu +# Authors: Tingshan Liu +# Thomas Athey +# Benjamin Pedigo import numpy as np @@ -277,13 +277,10 @@ def fit(self, X, y=None): X = self._validate_data(X, dtype=[np.float64, np.float32], ensure_min_samples=1) # check n_components against sample size - n_comps = [self.max_components, self.min_components] - names = ["max_components", "min_components"] - for i in range(len(names)): - if n_comps[i] > X.shape[0]: - msg = names[i] + "must be <= n_samples, but" + names[i] - msg += "= {}, n_samples = {}".format(n_comps[i], X.shape[0]) - raise ValueError(msg) + if self.max_components > X.shape[0]: + msg = "max_components must be <= n_samples, but max_components" + msg += "= {}, n_samples = {}".format(self.max_components, X.shape[0]) + raise ValueError(msg) param_grid = { "covariance_type": covariance_type, From 24b0e3faed79ac74752141add4586fd6bbb315dd Mon Sep 17 00:00:00 2001 From: Adrin Jalali Date: Wed, 10 Apr 2024 19:08:21 +0200 Subject: [PATCH 007/344] MNT upgrade mypy version (#28803) Co-authored-by: Olivier Grisel --- .pre-commit-config.yaml | 2 +- pyproject.toml | 2 +- sklearn/_min_dependencies.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 83840774441d5..31af43b6bbab0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,7 +16,7 @@ repos: hooks: - id: black - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.3.0 + rev: v1.9.0 hooks: - id: mypy files: sklearn/ diff --git a/pyproject.toml b/pyproject.toml index 33b8a32909d09..828569ecc71ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,7 +80,7 @@ tests = [ "pytest-cov>=2.9.0", "ruff>=0.2.1", "black>=24.3.0", - "mypy>=1.3", + "mypy>=1.9", "pyamg>=4.0.0", "polars>=0.19.12", "pyarrow>=12.0.0", diff --git a/sklearn/_min_dependencies.py b/sklearn/_min_dependencies.py index b6e6e0792d0c5..a487a048c53c1 100644 --- a/sklearn/_min_dependencies.py +++ b/sklearn/_min_dependencies.py @@ -31,7 +31,7 @@ "pytest-cov": ("2.9.0", "tests"), "ruff": ("0.2.1", "tests"), "black": ("24.3.0", "tests"), - "mypy": ("1.3", "tests"), + "mypy": ("1.9", "tests"), "pyamg": ("4.0.0", "tests"), "polars": ("0.19.12", "docs, tests"), "pyarrow": ("12.0.0", "tests"), From d59f2cee601b0e34ab126d036b9d5b41d41dca69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Est=C3=A8ve?= Date: Thu, 11 Apr 2024 09:39:26 +0200 Subject: [PATCH 008/344] CI Fix title in update_tracking_issue.py (#28810) --- maint_tools/update_tracking_issue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maint_tools/update_tracking_issue.py b/maint_tools/update_tracking_issue.py index 46d9e56a190e8..b40e8222fefae 100644 --- a/maint_tools/update_tracking_issue.py +++ b/maint_tools/update_tracking_issue.py @@ -59,7 +59,7 @@ issue_repo = gh.get_repo(args.issue_repo) dt_now = datetime.now(tz=timezone.utc) date_str = dt_now.strftime("%b %d, %Y") -title_query = "CI failed on {args.ci_name}" +title_query = f"CI failed on {args.ci_name}" title = f"⚠️ {title_query} (last failure: {date_str}) ⚠️" From 819b2860fa6e52b8d563e7b17a5c4577e5a54dd4 Mon Sep 17 00:00:00 2001 From: Marija Vlajic Date: Thu, 11 Apr 2024 10:17:09 +0200 Subject: [PATCH 009/344] DOC Separate predefined scorer names from the ones requiring make_scorer (#28750) Co-authored-by: Olivier Grisel Co-authored-by: Guillaume Lemaitre Co-authored-by: Tim Head Co-authored-by: Arturo Amor <86408019+ArturoAmorQ@users.noreply.github.com> --- doc/modules/model_evaluation.rst | 55 ++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/doc/modules/model_evaluation.rst b/doc/modules/model_evaluation.rst index 9b79a2c7e151f..d045f2002fe1c 100644 --- a/doc/modules/model_evaluation.rst +++ b/doc/modules/model_evaluation.rst @@ -102,12 +102,9 @@ Scoring Function 'neg_mean_poisson_deviance' :func:`metrics.mean_poisson_deviance` 'neg_mean_gamma_deviance' :func:`metrics.mean_gamma_deviance` 'neg_mean_absolute_percentage_error' :func:`metrics.mean_absolute_percentage_error` -'d2_absolute_error_score' :func:`metrics.d2_absolute_error_score` -'d2_pinball_score' :func:`metrics.d2_pinball_score` -'d2_tweedie_score' :func:`metrics.d2_tweedie_score` +'d2_absolute_error_score' :func:`metrics.d2_absolute_error_score` ==================================== ============================================== ================================== - Usage examples: >>> from sklearn import svm, datasets @@ -130,27 +127,25 @@ Usage examples: Defining your scoring strategy from metric functions ----------------------------------------------------- -The module :mod:`sklearn.metrics` also exposes a set of simple functions -measuring a prediction error given ground truth and prediction: - -- functions ending with ``_score`` return a value to - maximize, the higher the better. - -- functions ending with ``_error`` or ``_loss`` return a - value to minimize, the lower the better. When converting - into a scorer object using :func:`make_scorer`, set - the ``greater_is_better`` parameter to ``False`` (``True`` by default; see the - parameter description below). - -Metrics available for various machine learning tasks are detailed in sections -below. - -Many metrics are not given names to be used as ``scoring`` values, +The following metrics functions are not implemented as named scorers, sometimes because they require additional parameters, such as -:func:`fbeta_score`. In such cases, you need to generate an appropriate -scoring object. The simplest way to generate a callable object for scoring -is by using :func:`make_scorer`. That function converts metrics -into callables that can be used for model evaluation. +:func:`fbeta_score`. They cannot be passed to the ``scoring`` +parameters; instead their callable needs to be passed to +:func:`make_scorer` together with the value of the user-settable +parameters. + +===================================== ========= ============================================== +Function Parameter Example usage +===================================== ========= ============================================== +**Classification** +:func:`metrics.fbeta_score` ``beta`` ``make_scorer(fbeta_score, beta=2)`` + +**Regression** +:func:`metrics.mean_tweedie_deviance` ``power`` ``make_scorer(mean_tweedie_deviance, power=1.5)`` +:func:`metrics.mean_pinball_loss` ``alpha`` ``make_scorer(mean_pinball_loss, alpha=0.95)`` +:func:`metrics.d2_tweedie_score` ``power`` ``make_scorer(d2_tweedie_score, power=1.5)`` +:func:`metrics.d2_pinball_score` ``alpha`` ``make_scorer(d2_pinball_score, alpha=0.95)`` +===================================== ========= ============================================== One typical use case is to wrap an existing metric function from the library with non-default values for its parameters, such as the ``beta`` parameter for @@ -163,6 +158,18 @@ the :func:`fbeta_score` function:: >>> grid = GridSearchCV(LinearSVC(dual="auto"), param_grid={'C': [1, 10]}, ... scoring=ftwo_scorer, cv=5) +The module :mod:`sklearn.metrics` also exposes a set of simple functions +measuring a prediction error given ground truth and prediction: + +- functions ending with ``_score`` return a value to + maximize, the higher the better. + +- functions ending with ``_error``, ``_loss``, or ``_deviance`` return a + value to minimize, the lower the better. When converting + into a scorer object using :func:`make_scorer`, set + the ``greater_is_better`` parameter to ``False`` (``True`` by default; see the + parameter description below). + |details-start| **Custom scorer objects** From ba1751f0433f880717bafcd8cd116a96da1908ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20du=20Boisberranger?= Date: Thu, 11 Apr 2024 13:53:20 +0200 Subject: [PATCH 010/344] MAINT Clean up deprecations for 1.5: in Displays (#28806) --- sklearn/model_selection/_plot.py | 47 ++++------------------ sklearn/model_selection/tests/test_plot.py | 23 ----------- 2 files changed, 7 insertions(+), 63 deletions(-) diff --git a/sklearn/model_selection/_plot.py b/sklearn/model_selection/_plot.py index b07a41289f748..08518cf2482d4 100644 --- a/sklearn/model_selection/_plot.py +++ b/sklearn/model_selection/_plot.py @@ -1,5 +1,3 @@ -import warnings - import numpy as np from ..utils._optional_dependencies import check_matplotlib_support @@ -16,7 +14,6 @@ def _plot_curve( negate_score=False, score_name=None, score_type="test", - log_scale="deprecated", std_display_style="fill_between", line_kw=None, fill_between_kw=None, @@ -108,25 +105,14 @@ def _plot_curve( ax.legend() - # TODO(1.5): to be removed - if log_scale != "deprecated": - warnings.warn( - ( - "The `log_scale` parameter is deprecated as of version 1.3 " - "and will be removed in 1.5. You can use display.ax_.set_xscale " - "and display.ax_.set_yscale instead." - ), - FutureWarning, - ) - xscale = "log" if log_scale else "linear" + # 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: - # 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" + xscale = "linear" + ax.set_xscale(xscale) ax.set_ylabel(f"{score_name}") @@ -226,7 +212,6 @@ def plot( negate_score=False, score_name=None, score_type="both", - log_scale="deprecated", std_display_style="fill_between", line_kw=None, fill_between_kw=None, @@ -259,13 +244,6 @@ def plot( The type of score to plot. Can be one of `"test"`, `"train"`, or `"both"`. - log_scale : bool, default="deprecated" - Whether or not to use a logarithmic scale for the x-axis. - - .. deprecated:: 1.3 - `log_scale` is deprecated in 1.3 and will be removed in 1.5. - Use `display.ax_.set_xscale` and `display.ax_.set_yscale` instead. - 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 @@ -294,7 +272,6 @@ def plot( negate_score=negate_score, score_name=score_name, score_type=score_type, - log_scale=log_scale, std_display_style=std_display_style, line_kw=line_kw, fill_between_kw=fill_between_kw, @@ -326,7 +303,6 @@ def from_estimator( negate_score=False, score_name=None, score_type="both", - log_scale="deprecated", std_display_style="fill_between", line_kw=None, fill_between_kw=None, @@ -451,13 +427,6 @@ def from_estimator( The type of score to plot. Can be one of `"test"`, `"train"`, or `"both"`. - log_scale : bool, default="deprecated" - Whether or not to use a logarithmic scale for the x-axis. - - .. deprecated:: 1.3 - `log_scale` is deprecated in 1.3 and will be removed in 1.5. - Use `display.ax_.xscale` and `display.ax_.yscale` instead. - 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 representation of the standard deviation @@ -525,7 +494,6 @@ def from_estimator( ax=ax, negate_score=negate_score, score_type=score_type, - log_scale=log_scale, std_display_style=std_display_style, line_kw=line_kw, fill_between_kw=fill_between_kw, @@ -694,7 +662,6 @@ def plot( negate_score=negate_score, score_name=score_name, score_type=score_type, - log_scale="deprecated", std_display_style=std_display_style, line_kw=line_kw, fill_between_kw=fill_between_kw, diff --git a/sklearn/model_selection/tests/test_plot.py b/sklearn/model_selection/tests/test_plot.py index 1a7268150fd90..4e88475517454 100644 --- a/sklearn/model_selection/tests/test_plot.py +++ b/sklearn/model_selection/tests/test_plot.py @@ -526,29 +526,6 @@ def test_curve_display_plot_kwargs(pyplot, data, CurveDisplay, specific_params): assert display.errorbar_[0].lines[0].get_color() == "red" -# TODO(1.5): to be removed -def test_learning_curve_display_deprecate_log_scale(data, pyplot): - """Check that we warn for the deprecated parameter `log_scale`.""" - X, y = data - estimator = DecisionTreeClassifier(random_state=0) - - with pytest.warns(FutureWarning, match="`log_scale` parameter is deprecated"): - display = LearningCurveDisplay.from_estimator( - estimator, X, y, train_sizes=[0.3, 0.6, 0.9], log_scale=True - ) - - assert display.ax_.get_xscale() == "log" - assert display.ax_.get_yscale() == "linear" - - with pytest.warns(FutureWarning, match="`log_scale` parameter is deprecated"): - display = LearningCurveDisplay.from_estimator( - estimator, X, y, train_sizes=[0.3, 0.6, 0.9], log_scale=False - ) - - assert display.ax_.get_xscale() == "linear" - assert display.ax_.get_yscale() == "linear" - - @pytest.mark.parametrize( "param_range, xscale", [([5, 10, 15], "linear"), ([-50, 5, 50, 500], "symlog"), ([5, 50, 500], "log")], From 10b5c6628630d57e3025a17381f71172d2649d8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20du=20Boisberranger?= Date: Thu, 11 Apr 2024 13:55:20 +0200 Subject: [PATCH 011/344] MAINT Clean up deprecations for 1.5: in linear_model._bayes (#28807) --- sklearn/linear_model/_bayes.py | 89 ++---------------------- sklearn/linear_model/tests/test_bayes.py | 30 -------- 2 files changed, 7 insertions(+), 112 deletions(-) diff --git a/sklearn/linear_model/_bayes.py b/sklearn/linear_model/_bayes.py index 5ea9c7a0fa66a..bf11f36cec599 100644 --- a/sklearn/linear_model/_bayes.py +++ b/sklearn/linear_model/_bayes.py @@ -5,7 +5,6 @@ # Authors: V. Michel, F. Pedregosa, A. Gramfort # License: BSD 3 clause -import warnings from math import log from numbers import Integral, Real @@ -15,55 +14,11 @@ from ..base import RegressorMixin, _fit_context from ..utils import _safe_indexing -from ..utils._param_validation import Hidden, Interval, StrOptions +from ..utils._param_validation import Interval from ..utils.extmath import fast_logdet from ..utils.validation import _check_sample_weight from ._base import LinearModel, _preprocess_data, _rescale_data - -# TODO(1.5) Remove -def _deprecate_n_iter(n_iter, max_iter): - """Deprecates n_iter in favour of max_iter. Checks if the n_iter has been - used instead of max_iter and generates a deprecation warning if True. - - Parameters - ---------- - n_iter : int, - Value of n_iter attribute passed by the estimator. - - max_iter : int, default=None - Value of max_iter attribute passed by the estimator. - If `None`, it corresponds to `max_iter=300`. - - Returns - ------- - max_iter : int, - Value of max_iter which shall further be used by the estimator. - - Notes - ----- - This function should be completely removed in 1.5. - """ - if n_iter != "deprecated": - if max_iter is not None: - raise ValueError( - "Both `n_iter` and `max_iter` attributes were set. Attribute" - " `n_iter` was deprecated in version 1.3 and will be removed in" - " 1.5. To avoid this error, only set the `max_iter` attribute." - ) - warnings.warn( - ( - "'n_iter' was renamed to 'max_iter' in version 1.3 and " - "will be removed in 1.5" - ), - FutureWarning, - ) - max_iter = n_iter - elif max_iter is None: - max_iter = 300 - return max_iter - - ############################################################################### # BayesianRidge regression @@ -133,13 +88,6 @@ class BayesianRidge(RegressorMixin, LinearModel): verbose : bool, default=False Verbose mode when fitting the model. - n_iter : int - Maximum number of iterations. Should be greater than or equal to 1. - - .. deprecated:: 1.3 - `n_iter` is deprecated in 1.3 and will be removed in 1.5. Use - `max_iter` instead. - Attributes ---------- coef_ : array-like of shape (n_features,) @@ -219,7 +167,7 @@ class BayesianRidge(RegressorMixin, LinearModel): """ _parameter_constraints: dict = { - "max_iter": [Interval(Integral, 1, None, closed="left"), None], + "max_iter": [Interval(Integral, 1, None, closed="left")], "tol": [Interval(Real, 0, None, closed="neither")], "alpha_1": [Interval(Real, 0, None, closed="left")], "alpha_2": [Interval(Real, 0, None, closed="left")], @@ -231,16 +179,12 @@ class BayesianRidge(RegressorMixin, LinearModel): "fit_intercept": ["boolean"], "copy_X": ["boolean"], "verbose": ["verbose"], - "n_iter": [ - Interval(Integral, 1, None, closed="left"), - Hidden(StrOptions({"deprecated"})), - ], } def __init__( self, *, - max_iter=None, # TODO(1.5): Set to 300 + max_iter=300, tol=1.0e-3, alpha_1=1.0e-6, alpha_2=1.0e-6, @@ -252,7 +196,6 @@ def __init__( fit_intercept=True, copy_X=True, verbose=False, - n_iter="deprecated", # TODO(1.5): Remove ): self.max_iter = max_iter self.tol = tol @@ -266,7 +209,6 @@ def __init__( self.fit_intercept = fit_intercept self.copy_X = copy_X self.verbose = verbose - self.n_iter = n_iter @_fit_context(prefer_skip_nested_validation=True) def fit(self, X, y, sample_weight=None): @@ -290,8 +232,6 @@ def fit(self, X, y, sample_weight=None): self : object Returns the instance itself. """ - max_iter = _deprecate_n_iter(self.n_iter, self.max_iter) - X, y = self._validate_data(X, y, dtype=[np.float64, np.float32], y_numeric=True) dtype = X.dtype @@ -343,7 +283,7 @@ def fit(self, X, y, sample_weight=None): eigen_vals_ = S**2 # Convergence loop of the bayesian ridge regression - for iter_ in range(max_iter): + for iter_ in range(self.max_iter): # update posterior mean coef_ based on alpha_ and lambda_ and # compute corresponding rmse coef_, rmse_ = self._update_coef_( @@ -540,13 +480,6 @@ class ARDRegression(RegressorMixin, LinearModel): verbose : bool, default=False Verbose mode when fitting the model. - n_iter : int - Maximum number of iterations. - - .. deprecated:: 1.3 - `n_iter` is deprecated in 1.3 and will be removed in 1.5. Use - `max_iter` instead. - Attributes ---------- coef_ : array-like of shape (n_features,) @@ -624,7 +557,7 @@ class ARDRegression(RegressorMixin, LinearModel): """ _parameter_constraints: dict = { - "max_iter": [Interval(Integral, 1, None, closed="left"), None], + "max_iter": [Interval(Integral, 1, None, closed="left")], "tol": [Interval(Real, 0, None, closed="left")], "alpha_1": [Interval(Real, 0, None, closed="left")], "alpha_2": [Interval(Real, 0, None, closed="left")], @@ -635,16 +568,12 @@ class ARDRegression(RegressorMixin, LinearModel): "fit_intercept": ["boolean"], "copy_X": ["boolean"], "verbose": ["verbose"], - "n_iter": [ - Interval(Integral, 1, None, closed="left"), - Hidden(StrOptions({"deprecated"})), - ], } def __init__( self, *, - max_iter=None, # TODO(1.5): Set to 300 + max_iter=300, tol=1.0e-3, alpha_1=1.0e-6, alpha_2=1.0e-6, @@ -655,7 +584,6 @@ def __init__( fit_intercept=True, copy_X=True, verbose=False, - n_iter="deprecated", # TODO(1.5): Remove ): self.max_iter = max_iter self.tol = tol @@ -668,7 +596,6 @@ def __init__( self.threshold_lambda = threshold_lambda self.copy_X = copy_X self.verbose = verbose - self.n_iter = n_iter @_fit_context(prefer_skip_nested_validation=True) def fit(self, X, y): @@ -689,8 +616,6 @@ def fit(self, X, y): self : object Fitted estimator. """ - max_iter = _deprecate_n_iter(self.n_iter, self.max_iter) - X, y = self._validate_data( X, y, dtype=[np.float64, np.float32], y_numeric=True, ensure_min_samples=2 ) @@ -738,7 +663,7 @@ def update_coeff(X, y, coef_, alpha_, keep_lambda, sigma_): else self._update_sigma_woodbury ) # Iterative procedure of ARDRegression - for iter_ in range(max_iter): + for iter_ in range(self.max_iter): sigma_ = update_sigma(X, alpha_, lambda_, keep_lambda) coef_ = update_coeff(X, y, coef_, alpha_, keep_lambda, sigma_) diff --git a/sklearn/linear_model/tests/test_bayes.py b/sklearn/linear_model/tests/test_bayes.py index a700a98dbbc45..48fa42b81dfd0 100644 --- a/sklearn/linear_model/tests/test_bayes.py +++ b/sklearn/linear_model/tests/test_bayes.py @@ -297,33 +297,3 @@ 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) - - -# TODO(1.5) remove -@pytest.mark.parametrize("Estimator", [BayesianRidge, ARDRegression]) -def test_bayesian_ridge_ard_n_iter_deprecated(Estimator): - """Check the deprecation warning of `n_iter`.""" - depr_msg = ( - "'n_iter' was renamed to 'max_iter' in version 1.3 and will be removed in 1.5" - ) - X, y = diabetes.data, diabetes.target - model = Estimator(n_iter=5) - - with pytest.warns(FutureWarning, match=depr_msg): - model.fit(X, y) - - -# TODO(1.5) remove -@pytest.mark.parametrize("Estimator", [BayesianRidge, ARDRegression]) -def test_bayesian_ridge_ard_max_iter_and_n_iter_both_set(Estimator): - """Check that a ValueError is raised when both `max_iter` and `n_iter` are set.""" - err_msg = ( - "Both `n_iter` and `max_iter` attributes were set. Attribute" - " `n_iter` was deprecated in version 1.3 and will be removed in" - " 1.5. To avoid this error, only set the `max_iter` attribute." - ) - X, y = diabetes.data, diabetes.target - model = Estimator(n_iter=5, max_iter=5) - - with pytest.raises(ValueError, match=err_msg): - model.fit(X, y) From 311c6e2beff577afc84c4328dc5dfa00be24a238 Mon Sep 17 00:00:00 2001 From: Olivier Grisel Date: Thu, 11 Apr 2024 16:02:00 +0200 Subject: [PATCH 012/344] ENH new svd_solver='covariance_eigh' for PCA (#27491) Co-authored-by: Christian Lorentzen Co-authored-by: Tim Head Co-authored-by: Guillaume Lemaitre --- .gitignore | 1 + benchmarks/bench_pca_solvers.py | 165 +++++++++++ doc/modules/compose.rst | 16 +- doc/whats_new/v1.5.rst | 30 ++ sklearn/decomposition/_base.py | 30 +- sklearn/decomposition/_kernel_pca.py | 4 +- sklearn/decomposition/_pca.py | 258 +++++++++++++----- sklearn/decomposition/_sparse_pca.py | 2 +- sklearn/decomposition/_truncated_svd.py | 5 +- .../tests/test_incremental_pca.py | 31 ++- sklearn/decomposition/tests/test_pca.py | 219 ++++++++++++--- .../decomposition/tests/test_sparse_pca.py | 6 +- sklearn/pipeline.py | 8 +- sklearn/utils/extmath.py | 17 +- 14 files changed, 634 insertions(+), 158 deletions(-) create mode 100644 benchmarks/bench_pca_solvers.py diff --git a/.gitignore b/.gitignore index 8a31fc8f542c4..9f3b453bbfd74 100644 --- a/.gitignore +++ b/.gitignore @@ -55,6 +55,7 @@ examples/cluster/joblib reuters/ benchmarks/bench_covertype_data/ benchmarks/HIGGS.csv.gz +bench_pca_solvers.csv *.prefs .pydevproject diff --git a/benchmarks/bench_pca_solvers.py b/benchmarks/bench_pca_solvers.py new file mode 100644 index 0000000000000..337af3a42e900 --- /dev/null +++ b/benchmarks/bench_pca_solvers.py @@ -0,0 +1,165 @@ +# %% +# +# This benchmark compares the speed of PCA solvers on datasets of different +# sizes in order to determine the best solver to select by default via the +# "auto" heuristic. +# +# Note: we do not control for the accuracy of the solvers: we assume that all +# solvers yield transformed data with similar explained variance. This +# assumption is generally true, except for the randomized solver that might +# require more power iterations. +# +# We generate synthetic data with dimensions that are useful to plot: +# - time vs n_samples for a fixed n_features and, +# - time vs n_features for a fixed n_samples for a fixed n_features. +import itertools +from math import log10 +from time import perf_counter + +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd + +from sklearn import config_context +from sklearn.decomposition import PCA + +REF_DIMS = [100, 1000, 10_000] +data_shapes = [] +for ref_dim in REF_DIMS: + data_shapes.extend([(ref_dim, 10**i) for i in range(1, 8 - int(log10(ref_dim)))]) + data_shapes.extend( + [(ref_dim, 3 * 10**i) for i in range(1, 8 - int(log10(ref_dim)))] + ) + data_shapes.extend([(10**i, ref_dim) for i in range(1, 8 - int(log10(ref_dim)))]) + data_shapes.extend( + [(3 * 10**i, ref_dim) for i in range(1, 8 - int(log10(ref_dim)))] + ) + +# Remove duplicates: +data_shapes = sorted(set(data_shapes)) + +print("Generating test datasets...") +rng = np.random.default_rng(0) +datasets = [rng.normal(size=shape) for shape in data_shapes] + + +# %% +def measure_one(data, n_components, solver, method_name="fit"): + print( + f"Benchmarking {solver=!r}, {n_components=}, {method_name=!r} on data with" + f" shape {data.shape}" + ) + pca = PCA(n_components=n_components, svd_solver=solver, random_state=0) + timings = [] + elapsed = 0 + method = getattr(pca, method_name) + with config_context(assume_finite=True): + while elapsed < 0.5: + tic = perf_counter() + method(data) + duration = perf_counter() - tic + timings.append(duration) + elapsed += duration + return np.median(timings) + + +SOLVERS = ["full", "covariance_eigh", "arpack", "randomized", "auto"] +measurements = [] +for data, n_components, method_name in itertools.product( + datasets, [2, 50], ["fit", "fit_transform"] +): + if n_components >= min(data.shape): + continue + for solver in SOLVERS: + if solver == "covariance_eigh" and data.shape[1] > 5000: + # Too much memory and too slow. + continue + if solver in ["arpack", "full"] and log10(data.size) > 7: + # Too slow, in particular for the full solver. + continue + time = measure_one(data, n_components, solver, method_name=method_name) + measurements.append( + { + "n_components": n_components, + "n_samples": data.shape[0], + "n_features": data.shape[1], + "time": time, + "solver": solver, + "method_name": method_name, + } + ) +measurements = pd.DataFrame(measurements) +measurements.to_csv("bench_pca_solvers.csv", index=False) + +# %% +all_method_names = measurements["method_name"].unique() +all_n_components = measurements["n_components"].unique() + +for method_name in all_method_names: + fig, axes = plt.subplots( + figsize=(16, 16), + nrows=len(REF_DIMS), + ncols=len(all_n_components), + sharey=True, + constrained_layout=True, + ) + fig.suptitle(f"Benchmarks for PCA.{method_name}, varying n_samples", fontsize=16) + + for row_idx, ref_dim in enumerate(REF_DIMS): + for n_components, ax in zip(all_n_components, axes[row_idx]): + for solver in SOLVERS: + if solver == "auto": + style_kwargs = dict(linewidth=2, color="black", style="--") + else: + style_kwargs = dict(style="o-") + ax.set( + title=f"n_components={n_components}, n_features={ref_dim}", + ylabel="time (s)", + ) + measurements.query( + "n_components == @n_components and n_features == @ref_dim" + " and solver == @solver and method_name == @method_name" + ).plot.line( + x="n_samples", + y="time", + label=solver, + logx=True, + logy=True, + ax=ax, + **style_kwargs, + ) +# %% +for method_name in all_method_names: + fig, axes = plt.subplots( + figsize=(16, 16), + nrows=len(REF_DIMS), + ncols=len(all_n_components), + sharey=True, + ) + fig.suptitle(f"Benchmarks for PCA.{method_name}, varying n_features", fontsize=16) + + for row_idx, ref_dim in enumerate(REF_DIMS): + for n_components, ax in zip(all_n_components, axes[row_idx]): + for solver in SOLVERS: + if solver == "auto": + style_kwargs = dict(linewidth=2, color="black", style="--") + else: + style_kwargs = dict(style="o-") + ax.set( + title=f"n_components={n_components}, n_samples={ref_dim}", + ylabel="time (s)", + ) + measurements.query( + "n_components == @n_components and n_samples == @ref_dim " + " and solver == @solver and method_name == @method_name" + ).plot.line( + x="n_features", + y="time", + label=solver, + logx=True, + logy=True, + ax=ax, + **style_kwargs, + ) + +# %% diff --git a/doc/modules/compose.rst b/doc/modules/compose.rst index 0047ec7d8a2f0..28931cf52f283 100644 --- a/doc/modules/compose.rst +++ b/doc/modules/compose.rst @@ -254,14 +254,14 @@ inspect the original instance such as:: >>> from sklearn.datasets import load_digits >>> X_digits, y_digits = load_digits(return_X_y=True) - >>> pca1 = PCA() + >>> pca1 = PCA(n_components=10) >>> svm1 = SVC() >>> pipe = Pipeline([('reduce_dim', pca1), ('clf', svm1)]) >>> pipe.fit(X_digits, y_digits) - Pipeline(steps=[('reduce_dim', PCA()), ('clf', SVC())]) + Pipeline(steps=[('reduce_dim', PCA(n_components=10)), ('clf', SVC())]) >>> # The pca instance can be inspected directly - >>> print(pca1.components_) - [[-1.77484909e-19 ... 4.07058917e-18]] + >>> pca1.components_.shape + (10, 64) Enabling caching triggers a clone of the transformers before fitting. @@ -274,15 +274,15 @@ Instead, use the attribute ``named_steps`` to inspect estimators within the pipeline:: >>> cachedir = mkdtemp() - >>> pca2 = PCA() + >>> pca2 = PCA(n_components=10) >>> svm2 = SVC() >>> cached_pipe = Pipeline([('reduce_dim', pca2), ('clf', svm2)], ... memory=cachedir) >>> cached_pipe.fit(X_digits, y_digits) Pipeline(memory=..., - steps=[('reduce_dim', PCA()), ('clf', SVC())]) - >>> print(cached_pipe.named_steps['reduce_dim'].components_) - [[-1.77484909e-19 ... 4.07058917e-18]] + steps=[('reduce_dim', PCA(n_components=10)), ('clf', SVC())]) + >>> cached_pipe.named_steps['reduce_dim'].components_.shape + (10, 64) >>> # Remove the cache directory >>> rmtree(cachedir) diff --git a/doc/whats_new/v1.5.rst b/doc/whats_new/v1.5.rst index 9e13171e88528..1a8c50e408a0b 100644 --- a/doc/whats_new/v1.5.rst +++ b/doc/whats_new/v1.5.rst @@ -31,6 +31,13 @@ Changed models properties). :pr:`27344` by :user:`Xuefeng Xu `. +- |Enhancement| :class:`decomposition.PCA`, :class:`decomposition.SparsePCA` + and :class:`decomposition.TruncatedSVD` now set the sign of the `components_` + attribute based on the component values instead of using the transformed data + as reference. This change is needed to be able to offer consistent component + signs across all `PCA` solvers, including the new + `svd_solver="covariance_eigh"` option introduced in this release. + Support for Array API --------------------- @@ -169,10 +176,33 @@ Changelog :mod:`sklearn.decomposition` ............................ +- |Efficiency| :class:`decomposition.PCA` with `svd_solver="full"` now assigns + a contiguous `components_` attribute instead of an non-contiguous slice of + the singular vectors. When `n_components << n_features`, this can save some + memory and, more importantly, help speed-up subsequent calls to the `transform` + method by more than an order of magnitude by leveraging cache locality of + BLAS GEMM on contiguous arrays. + :pr:`27491` by :user:`Olivier Grisel `. + - |Enhancement| :class:`~decomposition.PCA` now automatically selects the ARPACK solver for sparse inputs when `svd_solver="auto"` instead of raising an error. :pr:`28498` by :user:`Thanh Lam Dang `. +- |Enhancement| :class:`decomposition.PCA` now supports a new solver option + named `svd_solver="covariance_eigh"` which offers an order of magnitude + speed-up and reduced memory usage for datasets with a large number of data + points and a small number of features (say, `n_samples >> 1000 > + n_features`). The `svd_solver="auto"` option has been updated to use the new + solver automatically for such datasets. This solver also accepts sparse input + data. + :pr:`27491` by :user:`Olivier Grisel `. + +- |Fix| :class:`decomposition.PCA` fit with `svd_solver="arpack"`, + `whiten=True` and a value for `n_components` that is larger than the rank of + the training set, no longer returns infinite values when transforming + hold-out data. + :pr:`27491` by :user:`Olivier Grisel `. + :mod:`sklearn.dummy` .................... diff --git a/sklearn/decomposition/_base.py b/sklearn/decomposition/_base.py index 9fa720751774f..5c9d8419f675e 100644 --- a/sklearn/decomposition/_base.py +++ b/sklearn/decomposition/_base.py @@ -12,11 +12,9 @@ import numpy as np from scipy import linalg -from scipy.sparse import issparse from ..base import BaseEstimator, ClassNamePrefixFeaturesOutMixin, TransformerMixin from ..utils._array_api import _add_to_diagonal, device, get_namespace -from ..utils.sparsefuncs import _implicit_column_offset from ..utils.validation import check_is_fitted @@ -138,21 +136,33 @@ def transform(self, X): 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) + xp, _ = get_namespace(X, self.components_, self.explained_variance_) check_is_fitted(self) X = self._validate_data( - X, accept_sparse=("csr", "csc"), dtype=[xp.float64, xp.float32], reset=False + X, dtype=[xp.float64, xp.float32], accept_sparse=("csr", "csc"), reset=False ) - if self.mean_ is not None: - if issparse(X): - X = _implicit_column_offset(X, self.mean_) - else: - X = X - self.mean_ + 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 if self.whiten: - X_transformed /= xp.sqrt(self.explained_variance_) + # 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 def inverse_transform(self, X): diff --git a/sklearn/decomposition/_kernel_pca.py b/sklearn/decomposition/_kernel_pca.py index 8fc4aa26a6dfb..edfd49c2e87a0 100644 --- a/sklearn/decomposition/_kernel_pca.py +++ b/sklearn/decomposition/_kernel_pca.py @@ -366,9 +366,7 @@ def _fit_transform(self, K): ) # flip eigenvectors' sign to enforce deterministic output - self.eigenvectors_, _ = svd_flip( - self.eigenvectors_, np.zeros_like(self.eigenvectors_).T - ) + self.eigenvectors_, _ = svd_flip(u=self.eigenvectors_, v=None) # sort eigenvectors in descending order indices = self.eigenvalues_.argsort()[::-1] diff --git a/sklearn/decomposition/_pca.py b/sklearn/decomposition/_pca.py index 4c49337e88093..852547daab04d 100644 --- a/sklearn/decomposition/_pca.py +++ b/sklearn/decomposition/_pca.py @@ -133,6 +133,10 @@ class PCA(_BasePCA): 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` @@ -176,26 +180,43 @@ class PCA(_BasePCA): improve the predictive accuracy of the downstream estimators by making their data respect some hard-wired assumptions. - svd_solver : {'auto', 'full', 'arpack', 'randomized'}, default='auto' - If auto : - The solver is selected by a default policy based on `X.shape` and - `n_components`: 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 enabled. Otherwise the exact full SVD is computed and - optionally truncated afterwards. - If full : - run exact full SVD calling the standard LAPACK solver via + svd_solver : {'auto', 'full', 'covariance_eigh', 'arpack', 'randomized'},\ + default='auto' + "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 - If arpack : - run SVD truncated to n_components calling ARPACK solver via + "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) - If randomized : - run randomized SVD by the method of Halko et al. + `0 < n_components < min(X.shape)` + "randomized" : + Run randomized SVD by the method of Halko et al. .. versionadded:: 0.18.0 + .. versionchanged:: 1.4 + 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). @@ -370,7 +391,9 @@ class PCA(_BasePCA): ], "copy": ["boolean"], "whiten": ["boolean"], - "svd_solver": [StrOptions({"auto", "full", "arpack", "randomized"})], + "svd_solver": [ + StrOptions({"auto", "full", "covariance_eigh", "arpack", "randomized"}) + ], "tol": [Interval(Real, 0, None, closed="left")], "iterated_power": [ StrOptions({"auto"}), @@ -448,39 +471,49 @@ def fit_transform(self, X, y=None): This method returns a Fortran-ordered array. To convert it to a C-ordered array, use 'np.ascontiguousarray'. """ - U, S, Vt = self._fit(X) - U = U[:, : self.n_components_] + 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_] + 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 + 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.""" xp, is_array_api_compliant = get_namespace(X) - if issparse(X) and self.svd_solver not in {"arpack", "auto"}: + # Raise an error for sparse input and unsupported svd_solver + if issparse(X) and self.svd_solver not in ["auto", "arpack", "covariance_eigh"]: raise TypeError( - 'PCA only support sparse inputs with the "arpack" solver, while ' - f'"{self.svd_solver}" was passed. See TruncatedSVD for a possible' - " alternative." + '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: raise ValueError( "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. X = self._validate_data( X, dtype=[xp.float64, xp.float32], accept_sparse=("csr", "csc"), ensure_2d=True, - copy=self.copy, + copy=False, ) self._fit_svd_solver = self.svd_solver if self._fit_svd_solver == "auto" and issparse(X): @@ -495,8 +528,12 @@ def _fit(self, X): n_components = self.n_components 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 - if max(X.shape) <= 500 or n_components == "mle": + 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" @@ -504,15 +541,14 @@ def _fit(self, X): else: self._fit_svd_solver = "full" - if self._fit_svd_solver == "full": - return self._fit_full(X, n_components) + # 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, self._fit_svd_solver) + return self._fit_truncated(X, n_components, xp) - def _fit_full(self, X, n_components): + def _fit_full(self, X, n_components, xp, is_array_api_compliant): """Fit the model by computing full SVD on X.""" - xp, is_array_api_compliant = get_namespace(X) - n_samples, n_features = X.shape if n_components == "mle": @@ -522,33 +558,96 @@ def _fit_full(self, X, n_components): ) elif not 0 <= n_components <= min(n_samples, n_features): raise ValueError( - "n_components=%r must be between 0 and " - "min(n_samples, n_features)=%r with " - "svd_solver='full'" % (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}" ) - # Center data self.mean_ = xp.mean(X, axis=0) - X -= self.mean_ - - 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, full_matrices=False) + # 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: - U, S, Vt = xp.linalg.svd(X, full_matrices=False) + 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 + # flip eigenvectors' sign to enforce deterministic output - U, Vt = svd_flip(U, Vt) + U, Vt = svd_flip(U, Vt, u_based_decision=False) components_ = Vt # Get variance explained by singular values - explained_variance_ = (S**2) / (n_samples - 1) total_var = xp.sum(explained_variance_) explained_variance_ratio_ = explained_variance_ / total_var singular_values_ = xp.asarray(S, copy=True) # Store the singular values. @@ -588,22 +687,33 @@ def _fit_full(self, X, n_components): self.noise_variance_ = 0.0 self.n_samples_ = n_samples - self.components_ = components_[:n_components, :] self.n_components_ = n_components - self.explained_variance_ = explained_variance_[:n_components] - self.explained_variance_ratio_ = explained_variance_ratio_[:n_components] - self.singular_values_ = singular_values_[: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 + return U, S, Vt, X, x_is_centered, xp - def _fit_truncated(self, X, n_components, svd_solver): + def _fit_truncated(self, X, n_components, xp): """Fit the model by computing truncated SVD (by ARPACK or randomized) on X. """ - xp, _ = get_namespace(X) - n_samples, n_features = X.shape + svd_solver = self._fit_svd_solver if isinstance(n_components, str): raise ValueError( "n_components=%r cannot be a string with svd_solver='%s'" @@ -631,31 +741,35 @@ def _fit_truncated(self, X, n_components, svd_solver): if issparse(X): self.mean_, var = mean_variance_axis(X, axis=0) total_var = var.sum() * n_samples / (n_samples - 1) # ddof=1 - X = _implicit_column_offset(X, self.mean_) + X_centered = _implicit_column_offset(X, self.mean_) + x_is_centered = False else: self.mean_ = xp.mean(X, axis=0) - X -= self.mean_ + X_centered = xp.asarray(X, copy=True) if self.copy else X + X_centered -= self.mean_ + x_is_centered = not self.copy if svd_solver == "arpack": v0 = _init_arpack_v0(min(X.shape), random_state) - U, S, Vt = svds(X, k=n_components, tol=self.tol, v0=v0) + 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, 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, + 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=True, + flip_sign=False, random_state=random_state, ) + U, Vt = svd_flip(U, Vt, u_based_decision=False) self.n_samples_ = n_samples self.components_ = Vt @@ -673,8 +787,8 @@ def _fit_truncated(self, X, n_components, svd_solver): # See: https://github.com/scikit-learn/scikit-learn/pull/18689#discussion_r1335540991 if total_var is None: N = X.shape[0] - 1 - X **= 2 - total_var = xp.sum(X) / N + 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. @@ -685,7 +799,7 @@ def _fit_truncated(self, X, n_components, svd_solver): else: self.noise_variance_ = 0.0 - return U, S, Vt + return U, S, Vt, X, x_is_centered, xp def score_samples(self, X): """Return the log-likelihood of each sample. diff --git a/sklearn/decomposition/_sparse_pca.py b/sklearn/decomposition/_sparse_pca.py index fa711ce8c0703..b284e784d4466 100644 --- a/sklearn/decomposition/_sparse_pca.py +++ b/sklearn/decomposition/_sparse_pca.py @@ -325,7 +325,7 @@ def _fit(self, X, n_components, random_state): return_n_iter=True, ) # flip eigenvectors' sign to enforce deterministic output - code, dictionary = svd_flip(code, dictionary, u_based_decision=False) + 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 diff --git a/sklearn/decomposition/_truncated_svd.py b/sklearn/decomposition/_truncated_svd.py index d238f35cb2167..d978191f104f7 100644 --- a/sklearn/decomposition/_truncated_svd.py +++ b/sklearn/decomposition/_truncated_svd.py @@ -234,7 +234,8 @@ def fit_transform(self, X, y=None): # svds doesn't abide by scipy.linalg.svd/randomized_svd # conventions, so reverse its outputs. Sigma = Sigma[::-1] - U, VT = svd_flip(U[:, ::-1], VT[::-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]: @@ -249,7 +250,9 @@ def fit_transform(self, X, y=None): n_oversamples=self.n_oversamples, power_iteration_normalizer=self.power_iteration_normalizer, random_state=random_state, + flip_sign=False, ) + U, VT = svd_flip(U, VT, u_based_decision=False) self.components_ = VT diff --git a/sklearn/decomposition/tests/test_incremental_pca.py b/sklearn/decomposition/tests/test_incremental_pca.py index 646aad2db795d..50ddf39b04503 100644 --- a/sklearn/decomposition/tests/test_incremental_pca.py +++ b/sklearn/decomposition/tests/test_incremental_pca.py @@ -4,7 +4,7 @@ import numpy as np import pytest -from numpy.testing import assert_array_equal +from numpy.testing import assert_allclose, assert_array_equal from sklearn import datasets from sklearn.decomposition import PCA, IncrementalPCA @@ -384,25 +384,38 @@ def test_singular_values(): assert_array_almost_equal(ipca.singular_values_, [3.142, 2.718, 1.0], 14) -def test_whitening(): +def test_whitening(global_random_seed): # Test that PCA and IncrementalPCA transforms match to sign flip. X = datasets.make_low_rank_matrix( - 1000, 10, tail_strength=0.0, effective_rank=2, random_state=1999 + 1000, 10, tail_strength=0.0, effective_rank=2, random_state=global_random_seed ) - prec = 3 - n_samples, n_features = X.shape + 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) - assert_almost_equal(np.abs(Xt_pca), np.abs(Xt_ipca), decimal=prec) + assert_allclose( + np.abs(Xt_pca)[:, stable_mask], + np.abs(Xt_ipca)[:, stable_mask], + atol=atol, + ) + + # 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_almost_equal(X, Xinv_ipca, decimal=prec) - assert_almost_equal(X, Xinv_pca, decimal=prec) - assert_almost_equal(Xinv_pca, Xinv_ipca, decimal=prec) + 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(): diff --git a/sklearn/decomposition/tests/test_pca.py b/sklearn/decomposition/tests/test_pca.py index b0fd32d1cbf62..d099bf9a91e00 100644 --- a/sklearn/decomposition/tests/test_pca.py +++ b/sklearn/decomposition/tests/test_pca.py @@ -8,7 +8,7 @@ from sklearn import config_context, datasets from sklearn.base import clone -from sklearn.datasets import load_iris, make_classification +from sklearn.datasets import load_iris, make_classification, make_low_rank_matrix from sklearn.decomposition import PCA from sklearn.decomposition._pca import _assess_dimension, _infer_dimension from sklearn.utils._array_api import ( @@ -25,7 +25,7 @@ from sklearn.utils.fixes import CSC_CONTAINERS, CSR_CONTAINERS iris = datasets.load_iris() -PCA_SOLVERS = ["full", "arpack", "randomized", "auto"] +PCA_SOLVERS = ["full", "covariance_eigh", "arpack", "randomized", "auto"] # `SPARSE_M` and `SPARSE_N` could be larger, but be aware: # * SciPy's generation of random sparse matrix can be costly @@ -70,7 +70,7 @@ def test_pca(svd_solver, n_components): @pytest.mark.parametrize("density", [0.01, 0.1, 0.30]) @pytest.mark.parametrize("n_components", [1, 2, 10]) @pytest.mark.parametrize("sparse_container", CSR_CONTAINERS + CSC_CONTAINERS) -@pytest.mark.parametrize("svd_solver", ["arpack"]) +@pytest.mark.parametrize("svd_solver", ["arpack", "covariance_eigh"]) @pytest.mark.parametrize("scale", [1, 10, 100]) def test_pca_sparse( global_random_seed, svd_solver, sparse_container, n_components, density, scale @@ -172,8 +172,8 @@ def test_sparse_pca_solver_error(global_random_seed, svd_solver, sparse_containe ) pca = PCA(n_components=30, svd_solver=svd_solver) error_msg_pattern = ( - f'PCA only support sparse inputs with the "arpack" solver, while "{svd_solver}"' - " was passed" + '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) @@ -263,35 +263,154 @@ def test_whitening(solver, copy): # we always center, so no test for non-centering. -@pytest.mark.parametrize("svd_solver", ["arpack", "randomized"]) -def test_pca_explained_variance_equivalence_solver(svd_solver): - rng = np.random.RandomState(0) - n_samples, n_features = 100, 80 - X = rng.randn(n_samples, n_features) - - pca_full = PCA(n_components=2, svd_solver="full") - pca_other = PCA(n_components=2, svd_solver=svd_solver, random_state=0) - - pca_full.fit(X) - pca_other.fit(X) - - assert_allclose( - pca_full.explained_variance_, pca_other.explained_variance_, rtol=5e-2 +@pytest.mark.parametrize( + "other_svd_solver", sorted(list(set(PCA_SOLVERS) - {"full", "auto"})) +) +@pytest.mark.parametrize("data_shape", ["tall", "wide"]) +@pytest.mark.parametrize("rank_deficient", [False, True]) +@pytest.mark.parametrize("whiten", [False, True]) +def test_pca_solver_equivalence( + other_svd_solver, + data_shape, + rank_deficient, + whiten, + global_random_seed, + global_dtype, +): + 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: + X = make_low_rank_matrix( + n_samples=n_samples + n_samples_test, + n_features=n_features, + tail_strength=0.5, + random_state=global_random_seed, + ) + # With a non-zero tail strength, the data is actually full-rank. + rank = min(n_samples, n_features) + + X = X.astype(global_dtype, copy=False) + X_train, X_test = X[:n_samples], X[n_samples:] + + if global_dtype == np.float32: + tols = dict(atol=1e-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( + n_components=n_components, + svd_solver=other_svd_solver, + whiten=whiten, + random_state=global_random_seed, + **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) assert_allclose( pca_full.explained_variance_ratio_, pca_other.explained_variance_ratio_, - rtol=5e-2, + **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: + assert_allclose( + 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: + assert_allclose( + pca_full.transform(X_recons_full_test)[:, stable], + pca_other.transform(X_recons_other_test)[:, stable], + **tols, + ) + @pytest.mark.parametrize( "X", [ np.random.RandomState(0).randn(100, 80), datasets.make_classification(100, 80, n_informative=78, random_state=0)[0], + np.random.RandomState(0).randn(10, 100), ], - ids=["random-data", "correlated-data"], + ids=["random-tall", "correlated-tall", "random-wide"], ) @pytest.mark.parametrize("svd_solver", PCA_SOLVERS) def test_pca_explained_variance_empirical(X, svd_solver): @@ -629,23 +748,28 @@ def test_pca_zero_noise_variance_edge_cases(svd_solver): @pytest.mark.parametrize( - "data, n_components, expected_solver", - [ # case: n_components in (0,1) => 'full' - (np.random.RandomState(0).uniform(size=(1000, 50)), 0.5, "full"), - # case: max(X.shape) <= 500 => 'full' - (np.random.RandomState(0).uniform(size=(10, 50)), 5, "full"), + "n_samples, n_features, n_components, expected_solver", + [ + # case: n_samples < 10 * n_features and max(X.shape) <= 500 => 'full' + (10, 50, 5, "full"), + # case: n_samples > 10 * n_features and n_features < 500 => 'covariance_eigh' + (1000, 50, 50, "covariance_eigh"), # case: n_components >= .8 * min(X.shape) => 'full' - (np.random.RandomState(0).uniform(size=(1000, 50)), 50, "full"), + (1000, 500, 400, "full"), # n_components >= 1 and n_components < .8*min(X.shape) => 'randomized' - (np.random.RandomState(0).uniform(size=(1000, 50)), 10, "randomized"), + (1000, 500, 10, "randomized"), + # case: n_components in (0,1) => 'full' + (1000, 500, 0.5, "full"), ], ) -def test_pca_svd_solver_auto(data, n_components, expected_solver): +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_) @@ -663,28 +787,33 @@ def test_pca_deterministic_output(svd_solver): @pytest.mark.parametrize("svd_solver", PCA_SOLVERS) -def test_pca_dtype_preservation(svd_solver): - check_pca_float_dtype_preservation(svd_solver) +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): +def check_pca_float_dtype_preservation(svd_solver, seed): # Ensure that PCA does not upscale the dtype when input is float32 - X_64 = np.random.RandomState(0).rand(1000, 4).astype(np.float64, copy=False) - X_32 = X_64.astype(np.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=0).fit(X_64) - pca_32 = PCA(n_components=3, svd_solver=svd_solver, random_state=0).fit(X_32) + 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_64).dtype == np.float64 - assert pca_32.transform(X_32).dtype == np.float32 + assert pca_64.transform(X_float64).dtype == np.float64 + assert pca_32.transform(X_float32).dtype == np.float32 - # the rtol is set such that the test passes on all platforms tested on - # conda-forge: PR#15775 - # see: https://github.com/conda-forge/scikit-learn-feedstock/pull/113 - assert_allclose(pca_64.components_, pca_32.components_, rtol=2e-4) + # 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): @@ -844,6 +973,7 @@ def check_array_api_get_precision(name, estimator, array_namespace, device, dtyp precision_np = estimator.get_precision() covariance_np = estimator.get_covariance() + rtol = 2e-4 if iris_np.dtype == "float32" else 2e-7 with config_context(array_api_dispatch=True): estimator_xp = clone(estimator).fit(iris_xp) precision_xp = estimator_xp.get_precision() @@ -853,6 +983,7 @@ def check_array_api_get_precision(name, estimator, array_namespace, device, dtyp assert_allclose( _convert_to_numpy(precision_xp, xp=xp), precision_np, + rtol=rtol, atol=_atol_for_type(dtype_name), ) covariance_xp = estimator_xp.get_covariance() @@ -862,6 +993,7 @@ def check_array_api_get_precision(name, estimator, array_namespace, device, dtyp assert_allclose( _convert_to_numpy(covariance_xp, xp=xp), covariance_np, + rtol=rtol, atol=_atol_for_type(dtype_name), ) @@ -878,7 +1010,10 @@ def check_array_api_get_precision(name, estimator, array_namespace, device, dtyp "estimator", [ PCA(n_components=2, svd_solver="full"), + PCA(n_components=2, svd_solver="full", whiten=True), PCA(n_components=0.1, svd_solver="full", whiten=True), + PCA(n_components=2, svd_solver="covariance_eigh"), + PCA(n_components=2, svd_solver="covariance_eigh", whiten=True), PCA( n_components=2, svd_solver="randomized", diff --git a/sklearn/decomposition/tests/test_sparse_pca.py b/sklearn/decomposition/tests/test_sparse_pca.py index 3797970e3d6ba..532d8dbd5e82f 100644 --- a/sklearn/decomposition/tests/test_sparse_pca.py +++ b/sklearn/decomposition/tests/test_sparse_pca.py @@ -14,6 +14,7 @@ assert_array_almost_equal, if_safe_multiprocessing_with_blas, ) +from sklearn.utils.extmath import svd_flip def generate_toy_data(n_components, n_samples, image_size, random_state=None): @@ -114,7 +115,10 @@ def test_initialization(): n_components=3, U_init=U_init, V_init=V_init, max_iter=0, random_state=rng ) model.fit(rng.randn(5, 4)) - assert_allclose(model.components_, V_init / np.linalg.norm(V_init, axis=1)[:, None]) + + 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(): diff --git a/sklearn/pipeline.py b/sklearn/pipeline.py index b26b83e66510f..1b17599068d7a 100644 --- a/sklearn/pipeline.py +++ b/sklearn/pipeline.py @@ -1412,12 +1412,12 @@ class FeatureUnion(TransformerMixin, _BaseComposition): ... ("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...]]) + array([[-1.5 , 3.0..., -0.8...], + [ 1.5 , 5.7..., 0.4...]]) >>> # An estimator's parameter can be set using '__' syntax >>> union.set_params(svd__n_components=1).fit_transform(X) - array([[ 1.5 , 3.0...], - [-1.5 , 5.7...]]) + array([[-1.5 , 3.0...], + [ 1.5 , 5.7...]]) For a more detailed example of usage, see :ref:`sphx_glr_auto_examples_compose_plot_feature_union.py`. diff --git a/sklearn/utils/extmath.py b/sklearn/utils/extmath.py index 2fe7dbc3cc179..44f70deaa3f18 100644 --- a/sklearn/utils/extmath.py +++ b/sklearn/utils/extmath.py @@ -864,12 +864,14 @@ def svd_flip(u, v, u_based_decision=True): 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 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)`. 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. @@ -884,24 +886,25 @@ def svd_flip(u, v, u_based_decision=True): v_adjusted : ndarray Array v with adjusted rows and the same dimensions as v. """ - xp, _ = get_namespace(u, v) - device = getattr(u, "device", None) + 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) + 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, :] - v *= 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) + 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)) - u *= signs[np.newaxis, :] + 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 From 57a72222da091cb7fb4e35c989f9d93e96813fd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20du=20Boisberranger?= Date: Thu, 11 Apr 2024 19:59:05 +0200 Subject: [PATCH 013/344] MAINT Clean up deprecations for 1.5: in KBinsDiscretizer (#28817) --- examples/cluster/plot_face_compress.py | 2 - ...plot_poisson_regression_non_normal_loss.py | 2 +- ...lot_tweedie_regression_insurance_claims.py | 2 +- .../plot_discretization_strategies.py | 4 +- sklearn/preprocessing/_discretization.py | 38 ++++++------------- .../tests/test_discretization.py | 23 ----------- .../tests/test_target_encoder.py | 2 - 7 files changed, 14 insertions(+), 59 deletions(-) diff --git a/examples/cluster/plot_face_compress.py b/examples/cluster/plot_face_compress.py index e873bd96b08e7..a632d783e6f02 100644 --- a/examples/cluster/plot_face_compress.py +++ b/examples/cluster/plot_face_compress.py @@ -81,7 +81,6 @@ encode="ordinal", strategy="uniform", random_state=0, - subsample=200_000, ) compressed_raccoon_uniform = encoder.fit_transform(raccoon_face.reshape(-1, 1)).reshape( raccoon_face.shape @@ -130,7 +129,6 @@ encode="ordinal", strategy="kmeans", random_state=0, - subsample=200_000, ) compressed_raccoon_kmeans = encoder.fit_transform(raccoon_face.reshape(-1, 1)).reshape( raccoon_face.shape diff --git a/examples/linear_model/plot_poisson_regression_non_normal_loss.py b/examples/linear_model/plot_poisson_regression_non_normal_loss.py index 2999d46596518..2a80c3db0ff40 100644 --- a/examples/linear_model/plot_poisson_regression_non_normal_loss.py +++ b/examples/linear_model/plot_poisson_regression_non_normal_loss.py @@ -112,7 +112,7 @@ ("passthrough_numeric", "passthrough", ["BonusMalus"]), ( "binned_numeric", - KBinsDiscretizer(n_bins=10, subsample=int(2e5), random_state=0), + KBinsDiscretizer(n_bins=10, random_state=0), ["VehAge", "DrivAge"], ), ("log_scaled_numeric", log_scale_transformer, ["Density"]), diff --git a/examples/linear_model/plot_tweedie_regression_insurance_claims.py b/examples/linear_model/plot_tweedie_regression_insurance_claims.py index 9e5ebb7c1b29b..96e32ee031190 100644 --- a/examples/linear_model/plot_tweedie_regression_insurance_claims.py +++ b/examples/linear_model/plot_tweedie_regression_insurance_claims.py @@ -241,7 +241,7 @@ def score_estimator( [ ( "binned_numeric", - KBinsDiscretizer(n_bins=10, subsample=int(2e5), random_state=0), + KBinsDiscretizer(n_bins=10, random_state=0), ["VehAge", "DrivAge"], ), ( diff --git a/examples/preprocessing/plot_discretization_strategies.py b/examples/preprocessing/plot_discretization_strategies.py index 105fcdb3b4868..b4c2f3ca1858d 100644 --- a/examples/preprocessing/plot_discretization_strategies.py +++ b/examples/preprocessing/plot_discretization_strategies.py @@ -76,9 +76,7 @@ i += 1 # transform the dataset with KBinsDiscretizer for strategy in strategies: - enc = KBinsDiscretizer( - n_bins=4, encode="ordinal", strategy=strategy, subsample=200_000 - ) + enc = KBinsDiscretizer(n_bins=4, encode="ordinal", strategy=strategy) enc.fit(X) grid_encoded = enc.transform(grid) diff --git a/sklearn/preprocessing/_discretization.py b/sklearn/preprocessing/_discretization.py index 5854bdc6425e7..86e0b870675af 100644 --- a/sklearn/preprocessing/_discretization.py +++ b/sklearn/preprocessing/_discretization.py @@ -11,7 +11,7 @@ from ..base import BaseEstimator, TransformerMixin, _fit_context from ..utils import resample -from ..utils._param_validation import Hidden, Interval, Options, StrOptions +from ..utils._param_validation import Interval, Options, StrOptions from ..utils.stats import _weighted_percentile from ..utils.validation import ( _check_feature_names_in, @@ -64,10 +64,9 @@ class KBinsDiscretizer(TransformerMixin, BaseEstimator): .. versionadded:: 0.24 - subsample : int or None, default='warn' + subsample : int or None, default=200_000 Maximum number of samples, used to fit the model, for computational - efficiency. Defaults to 200_000 when `strategy='quantile'` and to `None` - when `strategy='uniform'` or `strategy='kmeans'`. + 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 @@ -147,7 +146,7 @@ class KBinsDiscretizer(TransformerMixin, BaseEstimator): ... [ 0, 3, -2, 0.5], ... [ 1, 4, -1, 2]] >>> est = KBinsDiscretizer( - ... n_bins=3, encode='ordinal', strategy='uniform', subsample=None + ... n_bins=3, encode='ordinal', strategy='uniform' ... ) >>> est.fit(X) KBinsDiscretizer(...) @@ -177,11 +176,7 @@ class KBinsDiscretizer(TransformerMixin, BaseEstimator): "encode": [StrOptions({"onehot", "onehot-dense", "ordinal"})], "strategy": [StrOptions({"uniform", "quantile", "kmeans"})], "dtype": [Options(type, {np.float64, np.float32}), None], - "subsample": [ - Interval(Integral, 1, None, closed="left"), - None, - Hidden(StrOptions({"warn"})), - ], + "subsample": [Interval(Integral, 1, None, closed="left"), None], "random_state": ["random_state"], } @@ -192,7 +187,7 @@ def __init__( encode="onehot", strategy="quantile", dtype=None, - subsample="warn", + subsample=200_000, random_state=None, ): self.n_bins = n_bins @@ -243,24 +238,13 @@ def fit(self, X, y=None, sample_weight=None): f"{self.strategy!r} instead." ) - if self.strategy in ("uniform", "kmeans") and self.subsample == "warn": - warnings.warn( - ( - "In version 1.5 onwards, subsample=200_000 " - "will be used by default. Set subsample explicitly to " - "silence this warning in the mean time. Set " - "subsample=None to disable subsampling explicitly." - ), - FutureWarning, - ) - - subsample = self.subsample - if subsample == "warn": - subsample = 200000 if self.strategy == "quantile" else None - if subsample is not None and n_samples > subsample: + if self.subsample is not None and n_samples > self.subsample: # Take a subsample of `X` X = resample( - X, replace=False, n_samples=subsample, random_state=self.random_state + X, + replace=False, + n_samples=self.subsample, + random_state=self.random_state, ) n_features = X.shape[1] diff --git a/sklearn/preprocessing/tests/test_discretization.py b/sklearn/preprocessing/tests/test_discretization.py index 46ec86f7a75d4..19aaa5bdba850 100644 --- a/sklearn/preprocessing/tests/test_discretization.py +++ b/sklearn/preprocessing/tests/test_discretization.py @@ -49,8 +49,6 @@ ), ], ) -# TODO(1.5) remove warning filter when kbd's subsample default is changed -@pytest.mark.filterwarnings("ignore:In version 1.5 onwards, subsample=200_000") def test_fit_transform(strategy, expected, sample_weight): est = KBinsDiscretizer(n_bins=3, encode="ordinal", strategy=strategy) est.fit(X, sample_weight=sample_weight) @@ -149,8 +147,6 @@ def test_invalid_n_bins_array(): ), ], ) -# TODO(1.5) remove warning filter when kbd's subsample default is changed -@pytest.mark.filterwarnings("ignore:In version 1.5 onwards, subsample=200_000") def test_fit_transform_n_bins_array(strategy, expected, sample_weight): est = KBinsDiscretizer( n_bins=[2, 3, 3, 3], encode="ordinal", strategy=strategy @@ -176,8 +172,6 @@ def test_kbinsdiscretizer_effect_sample_weight(): assert_allclose(est.transform(X), [[0.0], [1.0], [2.0], [2.0], [2.0], [2.0]]) -# TODO(1.5) remove warning filter when kbd's subsample default is changed -@pytest.mark.filterwarnings("ignore:In version 1.5 onwards, subsample=200_000") @pytest.mark.parametrize("strategy", ["kmeans", "quantile"]) def test_kbinsdiscretizer_no_mutating_sample_weight(strategy): """Make sure that `sample_weight` is not changed in place.""" @@ -258,8 +252,6 @@ def test_encode_options(): ("quantile", [0, 0, 0, 1, 1, 1], [0, 0, 1, 1, 2, 2], [0, 1, 2, 3, 4, 4]), ], ) -# TODO(1.5) remove warning filter when kbd's subsample default is changed -@pytest.mark.filterwarnings("ignore:In version 1.5 onwards, subsample=200_000") def test_nonuniform_strategies( strategy, expected_2bins, expected_3bins, expected_5bins ): @@ -313,8 +305,6 @@ def test_nonuniform_strategies( ), ], ) -# TODO(1.5) remove warning filter when kbd's subsample default is changed -@pytest.mark.filterwarnings("ignore:In version 1.5 onwards, subsample=200_000") @pytest.mark.parametrize("encode", ["ordinal", "onehot", "onehot-dense"]) def test_inverse_transform(strategy, encode, expected_inv): kbd = KBinsDiscretizer(n_bins=3, strategy=strategy, encode=encode) @@ -323,8 +313,6 @@ def test_inverse_transform(strategy, encode, expected_inv): assert_array_almost_equal(expected_inv, Xinv) -# TODO(1.5) remove warning filter when kbd's subsample default is changed -@pytest.mark.filterwarnings("ignore:In version 1.5 onwards, subsample=200_000") @pytest.mark.parametrize("strategy", ["uniform", "kmeans", "quantile"]) def test_transform_outside_fit_range(strategy): X = np.array([0, 1, 2, 3])[:, None] @@ -490,14 +478,3 @@ def test_kbinsdiscretizer_subsample(strategy, global_random_seed): assert_allclose( kbd_subsampling.bin_edges_[0], kbd_no_subsampling.bin_edges_[0], rtol=1e-2 ) - - -# TODO(1.5) remove this test -@pytest.mark.parametrize("strategy", ["uniform", "kmeans"]) -def test_kbd_subsample_warning(strategy): - # Check the future warning for the change of default of subsample - X = np.random.RandomState(0).random_sample((100, 1)) - - kbd = KBinsDiscretizer(strategy=strategy, random_state=0) - with pytest.warns(FutureWarning, match="subsample=200_000 will be used by default"): - kbd.fit(X) diff --git a/sklearn/preprocessing/tests/test_target_encoder.py b/sklearn/preprocessing/tests/test_target_encoder.py index 81b0f32d04d68..c1e707b9bff98 100644 --- a/sklearn/preprocessing/tests/test_target_encoder.py +++ b/sklearn/preprocessing/tests/test_target_encoder.py @@ -586,8 +586,6 @@ def test_invariance_of_encoding_under_label_permutation(smooth, global_random_se assert_allclose(X_test_encoded, X_test_permuted_encoded) -# TODO(1.5) remove warning filter when kbd's subsample default is changed -@pytest.mark.filterwarnings("ignore:In version 1.5 onwards, subsample=200_000") @pytest.mark.parametrize("smooth", [0.0, "auto"]) def test_target_encoding_for_linear_regression(smooth, global_random_seed): # Check some expected statistical properties when fitting a linear From c799133710d518f5fba2958bb0e0765ee280df12 Mon Sep 17 00:00:00 2001 From: Adrin Jalali Date: Thu, 11 Apr 2024 21:11:08 +0200 Subject: [PATCH 014/344] MNT git ignore black upgrade (#28812) --- .git-blame-ignore-revs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 1c7043f0bd7ca..b261320543fa7 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -31,3 +31,6 @@ d4aad64b1eb2e42e76f49db2ccfbe4b4660d092b # PR 26649: Add isort and ruff rules 42173fdb34b5aded79664e045cada719dfbe39dc + +# PR #28802: Update black to 24.3.0 +c4c546355667b070edd5c892b206aa4a97af9a0b From bf616746ac644fdfb56e7d1ac3911a9c63e8985b Mon Sep 17 00:00:00 2001 From: Yao Xiao <108576690+Charlie-XIAO@users.noreply.github.com> Date: Fri, 12 Apr 2024 04:09:11 +0800 Subject: [PATCH 015/344] ENH HTML repr show best estimator in `*SearchCV` when `refit=True` (#28722) Co-authored-by: Thomas J. Fan --- doc/whats_new/v1.5.rst | 13 ++++++-- sklearn/model_selection/_search.py | 14 +++++++++ sklearn/model_selection/tests/test_search.py | 32 ++++++++++++++++++++ 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/doc/whats_new/v1.5.rst b/doc/whats_new/v1.5.rst index 1a8c50e408a0b..01f0384af5c1d 100644 --- a/doc/whats_new/v1.5.rst +++ b/doc/whats_new/v1.5.rst @@ -347,6 +347,8 @@ Changelog - |Enhancement| :term:`CV splitters ` that ignores the group parameter now raises a warning when groups are passed in to :term:`split`. :pr:`28210` by + `Thomas Fan`_. + - |Fix| the ``cv_results_`` attribute (of :class:`model_selection.GridSearchCV`) now returns masked arrays of the appropriate NumPy dtype, as opposed to always returning dtype ``object``. :pr:`28352` by :user:`Marco Gorelli`. @@ -354,12 +356,19 @@ Changelog - |Fix| :func:`sklearn.model_selection.train_test_score` works with Array API inputs. Previously indexing was not handled correctly leading to exceptions when using strict implementations of the Array API like CuPY. - :pr:`28407` by `Tim Head `. + :pr:`28407` by :user:`Tim Head `. + +- |Enhancement| The HTML diagram representation of + :class:`~model_selection.GridSearchCV`, + :class:`~model_selection.RandomizedSearchCV`, + :class:`~model_selection.HalvingGridSearchCV`, and + :class:`~model_selection.HalvingRandomSearchCV` will show the best estimator when + `refit=True`. :pr:`28722` by :user:`Yao Xiao ` and `Thomas Fan`_. :mod:`sklearn.multioutput` .......................... -- |Enhancement| `chain_method` parameter added to `:class:`multioutput.ClassifierChain`. +- |Enhancement| `chain_method` parameter added to :class:`multioutput.ClassifierChain`. :pr:`27700` by :user:`Lucy Liu `. :mod:`sklearn.neighbors` diff --git a/sklearn/model_selection/_search.py b/sklearn/model_selection/_search.py index 9b9072f1491a2..42fde09c16bce 100644 --- a/sklearn/model_selection/_search.py +++ b/sklearn/model_selection/_search.py @@ -33,6 +33,7 @@ get_scorer_names, ) from ..utils import Bunch, check_random_state +from ..utils._estimator_html_repr import _VisualBlock from ..utils._param_validation import HasMethods, Interval, StrOptions from ..utils._tags import _safe_tags from ..utils.metadata_routing import ( @@ -1153,6 +1154,19 @@ def get_metadata_routing(self): ) return router + def _sk_visual_block_(self): + if hasattr(self, "best_estimator_"): + key, estimator = "best_estimator_", self.best_estimator_ + else: + key, estimator = "estimator", self.estimator + + return _VisualBlock( + "parallel", + [estimator], + names=[f"{key}: {estimator.__class__.__name__}"], + name_details=[str(estimator)], + ) + class GridSearchCV(BaseSearchCV): """Exhaustive search over specified parameter values for an estimator. diff --git a/sklearn/model_selection/tests/test_search.py b/sklearn/model_selection/tests/test_search.py index 1ff4520034ff0..1a9230259d22e 100644 --- a/sklearn/model_selection/tests/test_search.py +++ b/sklearn/model_selection/tests/test_search.py @@ -13,6 +13,7 @@ import pytest from scipy.stats import bernoulli, expon, uniform +from sklearn import config_context from sklearn.base import BaseEstimator, ClassifierMixin, is_classifier from sklearn.cluster import KMeans from sklearn.datasets import ( @@ -20,6 +21,7 @@ make_classification, make_multilabel_classification, ) +from sklearn.dummy import DummyClassifier from sklearn.ensemble import HistGradientBoostingClassifier from sklearn.exceptions import FitFailedWarning from sklearn.experimental import enable_halving_search_cv # noqa @@ -27,6 +29,7 @@ from sklearn.impute import SimpleImputer from sklearn.linear_model import ( LinearRegression, + LogisticRegression, Ridge, SGDClassifier, ) @@ -60,6 +63,7 @@ from sklearn.naive_bayes import ComplementNB from sklearn.neighbors import KernelDensity, KNeighborsClassifier, LocalOutlierFactor from sklearn.pipeline import Pipeline +from sklearn.preprocessing import StandardScaler from sklearn.svm import SVC, LinearSVC from sklearn.tests.metadata_routing_common import ( ConsumingScorer, @@ -2523,6 +2527,34 @@ def test_search_with_2d_array(): np.testing.assert_array_equal(result.data, expected_data) +def test_search_html_repr(): + """Test different HTML representations for GridSearchCV.""" + X, y = make_classification(random_state=42) + + 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 "
DummyClassifier()
" in repr_html + + # Fitted with `refit=False` shows the original pipeline + search_cv.fit(X, y) + with config_context(display="diagram"): + repr_html = search_cv._repr_html_() + assert "
DummyClassifier()
" in repr_html + + # Fitted with `refit=True` shows the best estimator + search_cv = GridSearchCV(pipeline, param_grid=param_grid, refit=True) + search_cv.fit(X, y) + with config_context(display="diagram"): + repr_html = search_cv._repr_html_() + assert "
DummyClassifier()
" not in repr_html + assert "
LogisticRegression()
" in repr_html + + # Metadata Routing Tests # ====================== From 4dfe1411f20f37888b4258f3ee954f92e22c17f0 Mon Sep 17 00:00:00 2001 From: Tialo <65392801+Tialo@users.noreply.github.com> Date: Fri, 12 Apr 2024 01:06:46 +0300 Subject: [PATCH 016/344] DOC fix versionchanged in PCA (#28819) --- sklearn/decomposition/_pca.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sklearn/decomposition/_pca.py b/sklearn/decomposition/_pca.py index 852547daab04d..cb0f2e7e02fb3 100644 --- a/sklearn/decomposition/_pca.py +++ b/sklearn/decomposition/_pca.py @@ -214,7 +214,7 @@ class PCA(_BasePCA): .. versionadded:: 0.18.0 - .. versionchanged:: 1.4 + .. versionchanged:: 1.5 Added the 'covariance_eigh' solver. tol : float, default=0.0 From 04e247e5ec264d94328a3c6bd8a8ba88fa6d9028 Mon Sep 17 00:00:00 2001 From: Christian Veenhuis <124370897+ChVeen@users.noreply.github.com> Date: Fri, 12 Apr 2024 08:53:50 +0200 Subject: [PATCH 017/344] MAINT Fix badge links and underscores in `README.rst` file (#28815) --- README.rst | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/README.rst b/README.rst index 221855a6302e5..3f9a4ad726806 100644 --- a/README.rst +++ b/README.rst @@ -1,36 +1,36 @@ .. -*- mode: rst -*- -|Azure|_ |CirrusCI|_ |Codecov|_ |CircleCI|_ |Nightly wheels|_ |Black|_ |PythonVersion|_ |PyPi|_ |DOI|_ |Benchmark|_ +|Azure| |CirrusCI| |Codecov| |CircleCI| |Nightly wheels| |Black| |PythonVersion| |PyPi| |DOI| |Benchmark| .. |Azure| image:: https://dev.azure.com/scikit-learn/scikit-learn/_apis/build/status/scikit-learn.scikit-learn?branchName=main -.. _Azure: https://dev.azure.com/scikit-learn/scikit-learn/_build/latest?definitionId=1&branchName=main + :target: https://dev.azure.com/scikit-learn/scikit-learn/_build/latest?definitionId=1&branchName=main .. |CircleCI| image:: https://circleci.com/gh/scikit-learn/scikit-learn/tree/main.svg?style=shield -.. _CircleCI: https://circleci.com/gh/scikit-learn/scikit-learn + :target: https://circleci.com/gh/scikit-learn/scikit-learn .. |CirrusCI| image:: https://img.shields.io/cirrus/github/scikit-learn/scikit-learn/main?label=Cirrus%20CI -.. _CirrusCI: https://cirrus-ci.com/github/scikit-learn/scikit-learn/main + :target: https://cirrus-ci.com/github/scikit-learn/scikit-learn/main .. |Codecov| image:: https://codecov.io/gh/scikit-learn/scikit-learn/branch/main/graph/badge.svg?token=Pk8G9gg3y9 -.. _Codecov: https://codecov.io/gh/scikit-learn/scikit-learn + :target: https://codecov.io/gh/scikit-learn/scikit-learn .. |Nightly wheels| image:: https://github.com/scikit-learn/scikit-learn/workflows/Wheel%20builder/badge.svg?event=schedule -.. _`Nightly wheels`: https://github.com/scikit-learn/scikit-learn/actions?query=workflow%3A%22Wheel+builder%22+event%3Aschedule + :target: https://github.com/scikit-learn/scikit-learn/actions?query=workflow%3A%22Wheel+builder%22+event%3Aschedule .. |PythonVersion| image:: https://img.shields.io/pypi/pyversions/scikit-learn.svg -.. _PythonVersion: https://pypi.org/project/scikit-learn/ + :target: https://pypi.org/project/scikit-learn/ .. |PyPi| image:: https://img.shields.io/pypi/v/scikit-learn -.. _PyPi: https://pypi.org/project/scikit-learn + :target: https://pypi.org/project/scikit-learn .. |Black| image:: https://img.shields.io/badge/code%20style-black-000000.svg -.. _Black: https://github.com/psf/black + :target: https://github.com/psf/black .. |DOI| image:: https://zenodo.org/badge/21369/scikit-learn/scikit-learn.svg -.. _DOI: https://zenodo.org/badge/latestdoi/21369/scikit-learn/scikit-learn + :target: https://zenodo.org/badge/latestdoi/21369/scikit-learn/scikit-learn .. |Benchmark| image:: https://img.shields.io/badge/Benchmarked%20by-asv-blue -.. _`Benchmark`: https://scikit-learn.org/scikit-learn-benchmarks/ + :target: https://scikit-learn.org/scikit-learn-benchmarks .. |PythonMinVersion| replace:: 3.9 .. |NumPyMinVersion| replace:: 1.19.5 From 8600fb4cec354978f15ded20876590fbb3b37a5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20du=20Boisberranger?= Date: Fri, 12 Apr 2024 09:49:27 +0200 Subject: [PATCH 018/344] MAINT Clean up deprecations for 1.5: in LinearSV* (#28816) --- benchmarks/bench_mnist.py | 4 +- doc/modules/feature_selection.rst | 2 +- doc/modules/model_evaluation.rst | 12 +-- doc/modules/multiclass.rst | 7 +- doc/modules/svm.rst | 4 +- .../calibration/plot_calibration_curve.py | 2 +- .../calibration/plot_compare_calibration.py | 2 +- .../plot_feature_selection.py | 6 +- .../plot_feature_selection_pipeline.py | 2 +- .../plot_scalable_poly_kernels.py | 4 +- .../plot_kernel_approximation.py | 6 +- examples/model_selection/plot_det.py | 2 +- .../plot_grid_search_refit_callable.py | 2 +- .../model_selection/plot_precision_recall.py | 6 +- .../plot_discretization_classification.py | 4 +- examples/svm/plot_iris_svc.py | 2 +- .../svm/plot_linearsvc_support_vectors.py | 2 +- sklearn/calibration.py | 2 +- sklearn/ensemble/_stacking.py | 4 +- sklearn/ensemble/tests/test_common.py | 8 +- sklearn/ensemble/tests/test_stacking.py | 44 +++++----- sklearn/feature_extraction/tests/test_text.py | 6 +- .../tests/test_from_model.py | 2 +- sklearn/feature_selection/tests/test_rfe.py | 4 +- sklearn/kernel_approximation.py | 4 +- sklearn/metrics/_classification.py | 8 +- sklearn/metrics/tests/test_score_objects.py | 12 +-- sklearn/model_selection/tests/test_search.py | 70 ++++++++-------- .../tests/test_successive_halving.py | 2 +- .../model_selection/tests/test_validation.py | 2 +- sklearn/multiclass.py | 2 +- sklearn/svm/_classes.py | 33 +++----- sklearn/svm/tests/test_sparse.py | 10 +-- sklearn/svm/tests/test_svm.py | 81 ++++++++----------- sklearn/tests/test_calibration.py | 18 ++--- sklearn/tests/test_docstring_parameters.py | 4 - sklearn/tests/test_multiclass.py | 42 +++++----- sklearn/tests/test_multioutput.py | 8 +- 38 files changed, 197 insertions(+), 238 deletions(-) diff --git a/benchmarks/bench_mnist.py b/benchmarks/bench_mnist.py index a0c39ca9c5ea4..334e69ed5a30a 100644 --- a/benchmarks/bench_mnist.py +++ b/benchmarks/bench_mnist.py @@ -84,10 +84,10 @@ def load_data(dtype=np.float32, order="F"): "ExtraTrees": ExtraTreesClassifier(), "RandomForest": RandomForestClassifier(), "Nystroem-SVM": make_pipeline( - Nystroem(gamma=0.015, n_components=1000), LinearSVC(C=100, dual="auto") + Nystroem(gamma=0.015, n_components=1000), LinearSVC(C=100) ), "SampledRBF-SVM": make_pipeline( - RBFSampler(gamma=0.015, n_components=1000), LinearSVC(C=100, dual="auto") + RBFSampler(gamma=0.015, n_components=1000), LinearSVC(C=100) ), "LogisticRegression-SAG": LogisticRegression(solver="sag", tol=1e-1, C=1e4), "LogisticRegression-SAGA": LogisticRegression(solver="saga", tol=1e-1, C=1e4), diff --git a/doc/modules/feature_selection.rst b/doc/modules/feature_selection.rst index 1ae950acdfbb6..1b5ce57b0074f 100644 --- a/doc/modules/feature_selection.rst +++ b/doc/modules/feature_selection.rst @@ -340,7 +340,7 @@ the actual learning. The recommended way to do this in scikit-learn is to use a :class:`~pipeline.Pipeline`:: clf = Pipeline([ - ('feature_selection', SelectFromModel(LinearSVC(dual="auto", penalty="l1"))), + ('feature_selection', SelectFromModel(LinearSVC(penalty="l1"))), ('classification', RandomForestClassifier()) ]) clf.fit(X, y) diff --git a/doc/modules/model_evaluation.rst b/doc/modules/model_evaluation.rst index d045f2002fe1c..59f014b732e35 100644 --- a/doc/modules/model_evaluation.rst +++ b/doc/modules/model_evaluation.rst @@ -155,7 +155,7 @@ the :func:`fbeta_score` function:: >>> ftwo_scorer = make_scorer(fbeta_score, beta=2) >>> from sklearn.model_selection import GridSearchCV >>> from sklearn.svm import LinearSVC - >>> grid = GridSearchCV(LinearSVC(dual="auto"), param_grid={'C': [1, 10]}, + >>> grid = GridSearchCV(LinearSVC(), param_grid={'C': [1, 10]}, ... scoring=ftwo_scorer, cv=5) The module :mod:`sklearn.metrics` also exposes a set of simple functions @@ -308,7 +308,7 @@ parameter: >>> from sklearn.metrics import confusion_matrix >>> # A sample toy binary classification dataset >>> X, y = datasets.make_classification(n_classes=2, random_state=0) - >>> svm = LinearSVC(dual="auto", random_state=0) + >>> svm = LinearSVC(random_state=0) >>> def confusion_matrix_scorer(clf, X, y): ... y_pred = clf.predict(X) ... cm = confusion_matrix(y, y_pred) @@ -1148,9 +1148,9 @@ with a svm classifier in a binary class problem:: >>> from sklearn.metrics import hinge_loss >>> X = [[0], [1]] >>> y = [-1, 1] - >>> est = svm.LinearSVC(dual="auto", random_state=0) + >>> est = svm.LinearSVC(random_state=0) >>> est.fit(X, y) - LinearSVC(dual='auto', random_state=0) + LinearSVC(random_state=0) >>> pred_decision = est.decision_function([[-2], [3], [0.5]]) >>> pred_decision array([-2.18..., 2.36..., 0.09...]) @@ -1163,9 +1163,9 @@ with a svm classifier in a multiclass problem:: >>> X = np.array([[0], [1], [2], [3]]) >>> Y = np.array([0, 1, 2, 3]) >>> labels = np.array([0, 1, 2, 3]) - >>> est = svm.LinearSVC(dual="auto") + >>> est = svm.LinearSVC() >>> est.fit(X, Y) - LinearSVC(dual='auto') + LinearSVC() >>> pred_decision = est.decision_function([[-1], [2], [3]]) >>> y_true = [0, 2, 3] >>> hinge_loss(y_true, pred_decision, labels=labels) diff --git a/doc/modules/multiclass.rst b/doc/modules/multiclass.rst index 21bf568ebab97..88f822dad091e 100644 --- a/doc/modules/multiclass.rst +++ b/doc/modules/multiclass.rst @@ -201,7 +201,7 @@ Below is an example of multiclass learning using OvR:: >>> from sklearn.multiclass import OneVsRestClassifier >>> from sklearn.svm import LinearSVC >>> X, y = datasets.load_iris(return_X_y=True) - >>> OneVsRestClassifier(LinearSVC(dual="auto", random_state=0)).fit(X, y).predict(X) + >>> OneVsRestClassifier(LinearSVC(random_state=0)).fit(X, y).predict(X) array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, @@ -253,7 +253,7 @@ Below is an example of multiclass learning using OvO:: >>> from sklearn.multiclass import OneVsOneClassifier >>> from sklearn.svm import LinearSVC >>> X, y = datasets.load_iris(return_X_y=True) - >>> OneVsOneClassifier(LinearSVC(dual="auto", random_state=0)).fit(X, y).predict(X) + >>> OneVsOneClassifier(LinearSVC(random_state=0)).fit(X, y).predict(X) array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, @@ -311,8 +311,7 @@ Below is an example of multiclass learning using Output-Codes:: >>> from sklearn.multiclass import OutputCodeClassifier >>> from sklearn.svm import LinearSVC >>> X, y = datasets.load_iris(return_X_y=True) - >>> clf = OutputCodeClassifier(LinearSVC(dual="auto", random_state=0), - ... code_size=2, random_state=0) + >>> clf = OutputCodeClassifier(LinearSVC(random_state=0), code_size=2, random_state=0) >>> clf.fit(X, y).predict(X) array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, diff --git a/doc/modules/svm.rst b/doc/modules/svm.rst index 32fb9fe3e7d84..6ef4ed495b03f 100644 --- a/doc/modules/svm.rst +++ b/doc/modules/svm.rst @@ -144,9 +144,9 @@ function of shape ``(n_samples, n_classes)``. On the other hand, :class:`LinearSVC` implements "one-vs-the-rest" multi-class strategy, thus training `n_classes` models. - >>> lin_clf = svm.LinearSVC(dual="auto") + >>> lin_clf = svm.LinearSVC() >>> lin_clf.fit(X, Y) - LinearSVC(dual='auto') + LinearSVC() >>> dec = lin_clf.decision_function([[1]]) >>> dec.shape[1] 4 diff --git a/examples/calibration/plot_calibration_curve.py b/examples/calibration/plot_calibration_curve.py index 915d3b7c20cc9..af708346c2b7a 100644 --- a/examples/calibration/plot_calibration_curve.py +++ b/examples/calibration/plot_calibration_curve.py @@ -222,7 +222,7 @@ def predict_proba(self, X): # %% lr = LogisticRegression(C=1.0) -svc = NaivelyCalibratedLinearSVC(max_iter=10_000, dual="auto") +svc = NaivelyCalibratedLinearSVC(max_iter=10_000) svc_isotonic = CalibratedClassifierCV(svc, cv=2, method="isotonic") svc_sigmoid = CalibratedClassifierCV(svc, cv=2, method="sigmoid") diff --git a/examples/calibration/plot_compare_calibration.py b/examples/calibration/plot_compare_calibration.py index 389e231c8f07c..a53a5c5e7a3d1 100644 --- a/examples/calibration/plot_compare_calibration.py +++ b/examples/calibration/plot_compare_calibration.py @@ -107,7 +107,7 @@ def predict_proba(self, X): Cs=np.logspace(-6, 6, 101), cv=10, scoring="neg_log_loss", max_iter=1_000 ) gnb = GaussianNB() -svc = NaivelyCalibratedLinearSVC(C=1.0, dual="auto") +svc = NaivelyCalibratedLinearSVC(C=1.0) rfc = RandomForestClassifier(random_state=42) clf_list = [ diff --git a/examples/feature_selection/plot_feature_selection.py b/examples/feature_selection/plot_feature_selection.py index c57a2d5d6b6f9..2cf64cb6ea598 100644 --- a/examples/feature_selection/plot_feature_selection.py +++ b/examples/feature_selection/plot_feature_selection.py @@ -77,7 +77,7 @@ from sklearn.preprocessing import MinMaxScaler from sklearn.svm import LinearSVC -clf = make_pipeline(MinMaxScaler(), LinearSVC(dual="auto")) +clf = make_pipeline(MinMaxScaler(), LinearSVC()) clf.fit(X_train, y_train) print( "Classification accuracy without selecting features: {:.3f}".format( @@ -90,9 +90,7 @@ # %% # After univariate feature selection -clf_selected = make_pipeline( - SelectKBest(f_classif, k=4), MinMaxScaler(), LinearSVC(dual="auto") -) +clf_selected = make_pipeline(SelectKBest(f_classif, k=4), MinMaxScaler(), LinearSVC()) clf_selected.fit(X_train, y_train) print( "Classification accuracy after univariate feature selection: {:.3f}".format( diff --git a/examples/feature_selection/plot_feature_selection_pipeline.py b/examples/feature_selection/plot_feature_selection_pipeline.py index 42094c452491e..1d7c44050ea78 100644 --- a/examples/feature_selection/plot_feature_selection_pipeline.py +++ b/examples/feature_selection/plot_feature_selection_pipeline.py @@ -46,7 +46,7 @@ from sklearn.svm import LinearSVC anova_filter = SelectKBest(f_classif, k=3) -clf = LinearSVC(dual="auto") +clf = LinearSVC() anova_svm = make_pipeline(anova_filter, clf) anova_svm.fit(X_train, y_train) diff --git a/examples/kernel_approximation/plot_scalable_poly_kernels.py b/examples/kernel_approximation/plot_scalable_poly_kernels.py index c3fe5b405d0d0..13c917da06132 100644 --- a/examples/kernel_approximation/plot_scalable_poly_kernels.py +++ b/examples/kernel_approximation/plot_scalable_poly_kernels.py @@ -85,7 +85,7 @@ results = {} -lsvm = LinearSVC(dual="auto") +lsvm = LinearSVC() start = time.time() lsvm.fit(X_train, y_train) lsvm_time = time.time() - start @@ -126,7 +126,7 @@ for _ in range(n_runs): pipeline = make_pipeline( PolynomialCountSketch(n_components=n_components, degree=4), - LinearSVC(dual="auto"), + LinearSVC(), ) start = time.time() diff --git a/examples/miscellaneous/plot_kernel_approximation.py b/examples/miscellaneous/plot_kernel_approximation.py index 199739016efa8..f61cf5bd23387 100644 --- a/examples/miscellaneous/plot_kernel_approximation.py +++ b/examples/miscellaneous/plot_kernel_approximation.py @@ -72,7 +72,7 @@ # Create a classifier: a support vector classifier kernel_svm = svm.SVC(gamma=0.2) -linear_svm = svm.LinearSVC(dual="auto", random_state=42) +linear_svm = svm.LinearSVC(random_state=42) # create pipeline from kernel approximation # and linear svm @@ -81,14 +81,14 @@ fourier_approx_svm = pipeline.Pipeline( [ ("feature_map", feature_map_fourier), - ("svm", svm.LinearSVC(dual="auto", random_state=42)), + ("svm", svm.LinearSVC(random_state=42)), ] ) nystroem_approx_svm = pipeline.Pipeline( [ ("feature_map", feature_map_nystroem), - ("svm", svm.LinearSVC(dual="auto", random_state=42)), + ("svm", svm.LinearSVC(random_state=42)), ] ) diff --git a/examples/model_selection/plot_det.py b/examples/model_selection/plot_det.py index 7c347651c73a8..3e56b8bd35d31 100644 --- a/examples/model_selection/plot_det.py +++ b/examples/model_selection/plot_det.py @@ -66,7 +66,7 @@ from sklearn.svm import LinearSVC classifiers = { - "Linear SVM": make_pipeline(StandardScaler(), LinearSVC(C=0.025, dual="auto")), + "Linear SVM": make_pipeline(StandardScaler(), LinearSVC(C=0.025)), "Random Forest": RandomForestClassifier( max_depth=5, n_estimators=10, max_features=1 ), diff --git a/examples/model_selection/plot_grid_search_refit_callable.py b/examples/model_selection/plot_grid_search_refit_callable.py index a8dab986a48d2..a851ee5f9bb19 100644 --- a/examples/model_selection/plot_grid_search_refit_callable.py +++ b/examples/model_selection/plot_grid_search_refit_callable.py @@ -81,7 +81,7 @@ def best_low_complexity(cv_results): pipe = Pipeline( [ ("reduce_dim", PCA(random_state=42)), - ("classify", LinearSVC(random_state=42, C=0.01, dual="auto")), + ("classify", LinearSVC(random_state=42, C=0.01)), ] ) diff --git a/examples/model_selection/plot_precision_recall.py b/examples/model_selection/plot_precision_recall.py index 03b273de66b7f..19a93c7324cbb 100644 --- a/examples/model_selection/plot_precision_recall.py +++ b/examples/model_selection/plot_precision_recall.py @@ -125,9 +125,7 @@ from sklearn.preprocessing import StandardScaler from sklearn.svm import LinearSVC -classifier = make_pipeline( - StandardScaler(), LinearSVC(random_state=random_state, dual="auto") -) +classifier = make_pipeline(StandardScaler(), LinearSVC(random_state=random_state)) classifier.fit(X_train, y_train) # %% @@ -191,7 +189,7 @@ from sklearn.multiclass import OneVsRestClassifier classifier = OneVsRestClassifier( - make_pipeline(StandardScaler(), LinearSVC(random_state=random_state, dual="auto")) + make_pipeline(StandardScaler(), LinearSVC(random_state=random_state)) ) classifier.fit(X_train, Y_train) y_score = classifier.decision_function(X_test) diff --git a/examples/preprocessing/plot_discretization_classification.py b/examples/preprocessing/plot_discretization_classification.py index f3edcac0011d7..50b32cd9eaab3 100644 --- a/examples/preprocessing/plot_discretization_classification.py +++ b/examples/preprocessing/plot_discretization_classification.py @@ -68,7 +68,7 @@ def get_name(estimator): {"logisticregression__C": np.logspace(-1, 1, 3)}, ), ( - make_pipeline(StandardScaler(), LinearSVC(random_state=0, dual="auto")), + make_pipeline(StandardScaler(), LinearSVC(random_state=0)), {"linearsvc__C": np.logspace(-1, 1, 3)}, ), ( @@ -86,7 +86,7 @@ def get_name(estimator): make_pipeline( StandardScaler(), KBinsDiscretizer(encode="onehot", random_state=0), - LinearSVC(random_state=0, dual="auto"), + LinearSVC(random_state=0), ), { "kbinsdiscretizer__n_bins": np.arange(5, 8), diff --git a/examples/svm/plot_iris_svc.py b/examples/svm/plot_iris_svc.py index 61aba3cc06602..d13a9fe49c803 100644 --- a/examples/svm/plot_iris_svc.py +++ b/examples/svm/plot_iris_svc.py @@ -50,7 +50,7 @@ C = 1.0 # SVM regularization parameter models = ( svm.SVC(kernel="linear", C=C), - svm.LinearSVC(C=C, max_iter=10000, dual="auto"), + svm.LinearSVC(C=C, max_iter=10000), svm.SVC(kernel="rbf", gamma=0.7, C=C), svm.SVC(kernel="poly", degree=3, gamma="auto", C=C), ) diff --git a/examples/svm/plot_linearsvc_support_vectors.py b/examples/svm/plot_linearsvc_support_vectors.py index 60e9a3e6f32f9..7f82b6c8bb0fe 100644 --- a/examples/svm/plot_linearsvc_support_vectors.py +++ b/examples/svm/plot_linearsvc_support_vectors.py @@ -21,7 +21,7 @@ plt.figure(figsize=(10, 5)) for i, C in enumerate([1, 100]): # "hinge" is the standard SVM loss - clf = LinearSVC(C=C, loss="hinge", random_state=42, dual="auto").fit(X, y) + clf = LinearSVC(C=C, loss="hinge", random_state=42).fit(X, y) # obtain the support vectors through the decision function decision_function = clf.decision_function(X) # we can also calculate the decision function manually diff --git a/sklearn/calibration.py b/sklearn/calibration.py index c3f0b8ec59551..b02236eb600fe 100644 --- a/sklearn/calibration.py +++ b/sklearn/calibration.py @@ -280,7 +280,7 @@ def _get_estimator(self): 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, dual="auto") + estimator = LinearSVC(random_state=0) if _routing_enabled(): estimator.set_fit_request(sample_weight=True) else: diff --git a/sklearn/ensemble/_stacking.py b/sklearn/ensemble/_stacking.py index 0f093e8a6b51d..a18803d507ffa 100644 --- a/sklearn/ensemble/_stacking.py +++ b/sklearn/ensemble/_stacking.py @@ -556,7 +556,7 @@ class StackingClassifier(_RoutingNotSupportedMixin, ClassifierMixin, _BaseStacki >>> estimators = [ ... ('rf', RandomForestClassifier(n_estimators=10, random_state=42)), ... ('svr', make_pipeline(StandardScaler(), - ... LinearSVC(dual="auto", random_state=42))) + ... LinearSVC(random_state=42))) ... ] >>> clf = StackingClassifier( ... estimators=estimators, final_estimator=LogisticRegression() @@ -900,7 +900,7 @@ class StackingRegressor(_RoutingNotSupportedMixin, RegressorMixin, _BaseStacking >>> X, y = load_diabetes(return_X_y=True) >>> estimators = [ ... ('lr', RidgeCV()), - ... ('svr', LinearSVR(dual="auto", random_state=42)) + ... ('svr', LinearSVR(random_state=42)) ... ] >>> reg = StackingRegressor( ... estimators=estimators, diff --git a/sklearn/ensemble/tests/test_common.py b/sklearn/ensemble/tests/test_common.py index 7e14b34993d6f..6e83512ccd1d6 100644 --- a/sklearn/ensemble/tests/test_common.py +++ b/sklearn/ensemble/tests/test_common.py @@ -34,7 +34,7 @@ StackingClassifier( estimators=[ ("lr", LogisticRegression()), - ("svm", LinearSVC(dual="auto")), + ("svm", LinearSVC()), ("rf", RandomForestClassifier(n_estimators=5, max_depth=3)), ], cv=2, @@ -45,7 +45,7 @@ VotingClassifier( estimators=[ ("lr", LogisticRegression()), - ("svm", LinearSVC(dual="auto")), + ("svm", LinearSVC()), ("rf", RandomForestClassifier(n_estimators=5, max_depth=3)), ] ), @@ -55,7 +55,7 @@ StackingRegressor( estimators=[ ("lr", LinearRegression()), - ("svm", LinearSVR(dual="auto")), + ("svm", LinearSVR()), ("rf", RandomForestRegressor(n_estimators=5, max_depth=3)), ], cv=2, @@ -66,7 +66,7 @@ VotingRegressor( estimators=[ ("lr", LinearRegression()), - ("svm", LinearSVR(dual="auto")), + ("svm", LinearSVR()), ("rf", RandomForestRegressor(n_estimators=5, max_depth=3)), ] ), diff --git a/sklearn/ensemble/tests/test_stacking.py b/sklearn/ensemble/tests/test_stacking.py index 0d1493529e318..300b011f661d4 100644 --- a/sklearn/ensemble/tests/test_stacking.py +++ b/sklearn/ensemble/tests/test_stacking.py @@ -69,7 +69,7 @@ def test_stacking_classifier_iris(cv, final_estimator, passthrough): X_train, X_test, y_train, y_test = train_test_split( scale(X_iris), y_iris, stratify=y_iris, random_state=42 ) - estimators = [("lr", LogisticRegression()), ("svc", LinearSVC(dual="auto"))] + estimators = [("lr", LogisticRegression()), ("svc", LinearSVC())] clf = StackingClassifier( estimators=estimators, final_estimator=final_estimator, @@ -121,7 +121,7 @@ def test_stacking_classifier_drop_column_binary_classification(): assert X_trans.shape[1] == 2 # LinearSVC does not implement 'predict_proba' and will not drop one column - estimators = [("lr", LogisticRegression()), ("svc", LinearSVC(dual="auto"))] + estimators = [("lr", LogisticRegression()), ("svc", LinearSVC())] clf.set_params(estimators=estimators) clf.fit(X_train, y_train) @@ -135,10 +135,10 @@ def test_stacking_classifier_drop_estimator(): X_train, X_test, y_train, _ = train_test_split( scale(X_iris), y_iris, stratify=y_iris, random_state=42 ) - estimators = [("lr", "drop"), ("svc", LinearSVC(dual="auto", random_state=0))] + estimators = [("lr", "drop"), ("svc", LinearSVC(random_state=0))] rf = RandomForestClassifier(n_estimators=10, random_state=42) clf = StackingClassifier( - estimators=[("svc", LinearSVC(dual="auto", random_state=0))], + estimators=[("svc", LinearSVC(random_state=0))], final_estimator=rf, cv=5, ) @@ -157,10 +157,10 @@ def test_stacking_regressor_drop_estimator(): X_train, X_test, y_train, _ = train_test_split( scale(X_diabetes), y_diabetes, random_state=42 ) - estimators = [("lr", "drop"), ("svr", LinearSVR(dual="auto", random_state=0))] + estimators = [("lr", "drop"), ("svr", LinearSVR(random_state=0))] rf = RandomForestRegressor(n_estimators=10, random_state=42) reg = StackingRegressor( - estimators=[("svr", LinearSVR(dual="auto", random_state=0))], + estimators=[("svr", LinearSVR(random_state=0))], final_estimator=rf, cv=5, ) @@ -188,7 +188,7 @@ def test_stacking_regressor_diabetes(cv, final_estimator, predict_params, passth X_train, X_test, y_train, _ = train_test_split( scale(X_diabetes), y_diabetes, random_state=42 ) - estimators = [("lr", LinearRegression()), ("svr", LinearSVR(dual="auto"))] + estimators = [("lr", LinearRegression()), ("svr", LinearSVR())] reg = StackingRegressor( estimators=estimators, final_estimator=final_estimator, @@ -226,7 +226,7 @@ def test_stacking_regressor_sparse_passthrough(sparse_container): X_train, X_test, y_train, _ = train_test_split( sparse_container(scale(X_diabetes)), y_diabetes, random_state=42 ) - estimators = [("lr", LinearRegression()), ("svr", LinearSVR(dual="auto"))] + estimators = [("lr", LinearRegression()), ("svr", LinearSVR())] rf = RandomForestRegressor(n_estimators=10, random_state=42) clf = StackingRegressor( estimators=estimators, final_estimator=rf, cv=5, passthrough=True @@ -246,7 +246,7 @@ def test_stacking_classifier_sparse_passthrough(sparse_container): X_train, X_test, y_train, _ = train_test_split( sparse_container(scale(X_iris)), y_iris, random_state=42 ) - estimators = [("lr", LogisticRegression()), ("svc", LinearSVC(dual="auto"))] + estimators = [("lr", LogisticRegression()), ("svc", LinearSVC())] rf = RandomForestClassifier(n_estimators=10, random_state=42) clf = StackingClassifier( estimators=estimators, final_estimator=rf, cv=5, passthrough=True @@ -319,7 +319,7 @@ def fit(self, X, y): { "estimators": [ ("lr", LogisticRegression()), - ("cor", LinearSVC(dual="auto", max_iter=50_000)), + ("cor", LinearSVC(max_iter=50_000)), ], "final_estimator": NoWeightClassifier(), }, @@ -349,7 +349,7 @@ def test_stacking_classifier_error(y, params, type_err, msg_err): { "estimators": [ ("lr", LinearRegression()), - ("cor", LinearSVR(dual="auto")), + ("cor", LinearSVR()), ], "final_estimator": NoWeightRegressor(), }, @@ -371,7 +371,7 @@ def test_stacking_regressor_error(y, params, type_err, msg_err): StackingClassifier( estimators=[ ("lr", LogisticRegression(random_state=0)), - ("svm", LinearSVC(dual="auto", random_state=0)), + ("svm", LinearSVC(random_state=0)), ] ), X_iris[:100], @@ -381,7 +381,7 @@ def test_stacking_regressor_error(y, params, type_err, msg_err): StackingRegressor( estimators=[ ("lr", LinearRegression()), - ("svm", LinearSVR(dual="auto", random_state=0)), + ("svm", LinearSVR(random_state=0)), ] ), X_diabetes, @@ -415,7 +415,7 @@ def test_stacking_classifier_stratify_default(): clf = StackingClassifier( estimators=[ ("lr", LogisticRegression(max_iter=10_000)), - ("svm", LinearSVC(dual="auto", max_iter=10_000)), + ("svm", LinearSVC(max_iter=10_000)), ] ) # since iris is not shuffled, a simple k-fold would not contain the @@ -430,7 +430,7 @@ def test_stacking_classifier_stratify_default(): StackingClassifier( estimators=[ ("lr", LogisticRegression()), - ("svm", LinearSVC(dual="auto", random_state=42)), + ("svm", LinearSVC(random_state=42)), ], final_estimator=LogisticRegression(), cv=KFold(shuffle=True, random_state=42), @@ -441,7 +441,7 @@ def test_stacking_classifier_stratify_default(): StackingRegressor( estimators=[ ("lr", LinearRegression()), - ("svm", LinearSVR(dual="auto", random_state=42)), + ("svm", LinearSVR(random_state=42)), ], final_estimator=LinearRegression(), cv=KFold(shuffle=True, random_state=42), @@ -498,7 +498,7 @@ def test_stacking_classifier_sample_weight_fit_param(): StackingClassifier( estimators=[ ("lr", LogisticRegression()), - ("svm", LinearSVC(dual="auto", random_state=42)), + ("svm", LinearSVC(random_state=42)), ], final_estimator=LogisticRegression(), ), @@ -508,7 +508,7 @@ def test_stacking_classifier_sample_weight_fit_param(): StackingRegressor( estimators=[ ("lr", LinearRegression()), - ("svm", LinearSVR(dual="auto", random_state=42)), + ("svm", LinearSVR(random_state=42)), ], final_estimator=LinearRegression(), ), @@ -614,7 +614,7 @@ def test_stacking_prefit(Stacker, Estimator, stack_method, final_estimator, X, y StackingRegressor( estimators=[ ("lr", LinearRegression()), - ("svm", LinearSVR(dual="auto")), + ("svm", LinearSVR()), ], cv="prefit", ), @@ -780,7 +780,7 @@ def test_stacking_classifier_multilabel_auto_predict(stack_method, passthrough): StackingClassifier( estimators=[ ("lr", LogisticRegression(random_state=0)), - ("svm", LinearSVC(dual="auto", random_state=0)), + ("svm", LinearSVC(random_state=0)), ] ), iris.feature_names, @@ -800,7 +800,7 @@ def test_stacking_classifier_multilabel_auto_predict(stack_method, passthrough): estimators=[ ("lr", LogisticRegression(random_state=0)), ("other", "drop"), - ("svm", LinearSVC(dual="auto", random_state=0)), + ("svm", LinearSVC(random_state=0)), ] ), iris.feature_names, @@ -815,7 +815,7 @@ def test_stacking_classifier_multilabel_auto_predict(stack_method, passthrough): StackingRegressor( estimators=[ ("lr", LinearRegression()), - ("svm", LinearSVR(dual="auto", random_state=0)), + ("svm", LinearSVR(random_state=0)), ] ), diabetes.feature_names, diff --git a/sklearn/feature_extraction/tests/test_text.py b/sklearn/feature_extraction/tests/test_text.py index 16b17fe5541c6..a513b94436d93 100644 --- a/sklearn/feature_extraction/tests/test_text.py +++ b/sklearn/feature_extraction/tests/test_text.py @@ -936,7 +936,7 @@ def test_count_vectorizer_pipeline_grid_selection(): data, target, test_size=0.2, random_state=0 ) - pipeline = Pipeline([("vect", CountVectorizer()), ("svc", LinearSVC(dual="auto"))]) + pipeline = Pipeline([("vect", CountVectorizer()), ("svc", LinearSVC())]) parameters = { "vect__ngram_range": [(1, 1), (1, 2)], @@ -972,7 +972,7 @@ def test_vectorizer_pipeline_grid_selection(): data, target, test_size=0.1, random_state=0 ) - pipeline = Pipeline([("vect", TfidfVectorizer()), ("svc", LinearSVC(dual="auto"))]) + pipeline = Pipeline([("vect", TfidfVectorizer()), ("svc", LinearSVC())]) parameters = { "vect__ngram_range": [(1, 1), (1, 2)], @@ -1006,7 +1006,7 @@ def test_vectorizer_pipeline_cross_validation(): # label junk food as -1, the others as +1 target = [-1] * len(JUNK_FOOD_DOCS) + [1] * len(NOTJUNK_FOOD_DOCS) - pipeline = Pipeline([("vect", TfidfVectorizer()), ("svc", LinearSVC(dual="auto"))]) + pipeline = Pipeline([("vect", TfidfVectorizer()), ("svc", LinearSVC())]) cv_scores = cross_val_score(pipeline, data, target, cv=3) assert_array_equal(cv_scores, [1.0, 1.0, 1.0]) diff --git a/sklearn/feature_selection/tests/test_from_model.py b/sklearn/feature_selection/tests/test_from_model.py index 3573b7a078294..4f8e97948ee7c 100644 --- a/sklearn/feature_selection/tests/test_from_model.py +++ b/sklearn/feature_selection/tests/test_from_model.py @@ -408,7 +408,7 @@ def test_partial_fit(): def test_calling_fit_reinitializes(): - est = LinearSVC(dual="auto", random_state=0) + est = LinearSVC(random_state=0) transformer = SelectFromModel(estimator=est) transformer.fit(data, y) transformer.set_params(estimator__C=100) diff --git a/sklearn/feature_selection/tests/test_rfe.py b/sklearn/feature_selection/tests/test_rfe.py index 01c6194493ab6..11ab086f2f010 100644 --- a/sklearn/feature_selection/tests/test_rfe.py +++ b/sklearn/feature_selection/tests/test_rfe.py @@ -464,7 +464,7 @@ def test_rfe_wrapped_estimator(importance_getter, selector, expected_n_features) # Non-regression test for # https://github.com/scikit-learn/scikit-learn/issues/15312 X, y = make_friedman1(n_samples=50, n_features=10, random_state=0) - estimator = LinearSVR(dual="auto", random_state=0) + estimator = LinearSVR(random_state=0) log_estimator = TransformedTargetRegressor( regressor=estimator, func=np.log, inverse_func=np.exp @@ -486,7 +486,7 @@ def test_rfe_wrapped_estimator(importance_getter, selector, expected_n_features) @pytest.mark.parametrize("Selector", [RFE, RFECV]) 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(dual="auto") + estimator = LinearSVR() log_estimator = TransformedTargetRegressor( regressor=estimator, func=np.log, inverse_func=np.exp ) diff --git a/sklearn/kernel_approximation.py b/sklearn/kernel_approximation.py index bcb1e99520e5b..3df9d318fe2c8 100644 --- a/sklearn/kernel_approximation.py +++ b/sklearn/kernel_approximation.py @@ -968,13 +968,13 @@ class Nystroem(ClassNamePrefixFeaturesOutMixin, TransformerMixin, BaseEstimator) >>> from sklearn.kernel_approximation import Nystroem >>> X, y = datasets.load_digits(n_class=9, return_X_y=True) >>> data = X / 16. - >>> clf = svm.LinearSVC(dual="auto") + >>> 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(dual='auto') + LinearSVC() >>> clf.score(data_transformed, y) 0.9987... """ diff --git a/sklearn/metrics/_classification.py b/sklearn/metrics/_classification.py index c5290fd39eb7e..62a6c87428e9a 100644 --- a/sklearn/metrics/_classification.py +++ b/sklearn/metrics/_classification.py @@ -3061,9 +3061,9 @@ def hinge_loss(y_true, pred_decision, *, labels=None, sample_weight=None): >>> from sklearn.metrics import hinge_loss >>> X = [[0], [1]] >>> y = [-1, 1] - >>> est = svm.LinearSVC(dual="auto", random_state=0) + >>> est = svm.LinearSVC(random_state=0) >>> est.fit(X, y) - LinearSVC(dual='auto', random_state=0) + LinearSVC(random_state=0) >>> pred_decision = est.decision_function([[-2], [3], [0.5]]) >>> pred_decision array([-2.18..., 2.36..., 0.09...]) @@ -3076,9 +3076,9 @@ def hinge_loss(y_true, pred_decision, *, labels=None, sample_weight=None): >>> X = np.array([[0], [1], [2], [3]]) >>> Y = np.array([0, 1, 2, 3]) >>> labels = np.array([0, 1, 2, 3]) - >>> est = svm.LinearSVC(dual="auto") + >>> est = svm.LinearSVC() >>> est.fit(X, Y) - LinearSVC(dual='auto') + LinearSVC() >>> pred_decision = est.decision_function([[-1], [2], [3]]) >>> y_true = [0, 2, 3] >>> hinge_loss(y_true, pred_decision, labels=labels) diff --git a/sklearn/metrics/tests/test_score_objects.py b/sklearn/metrics/tests/test_score_objects.py index c721922f1b2d5..49c671d486550 100644 --- a/sklearn/metrics/tests/test_score_objects.py +++ b/sklearn/metrics/tests/test_score_objects.py @@ -276,7 +276,7 @@ def test_check_scoring_and_check_multimetric_scoring(scoring): # To make sure the check_scoring is correctly applied to the constituent # scorers - estimator = LinearSVC(dual="auto", random_state=0) + estimator = LinearSVC(random_state=0) estimator.fit([[1], [2], [3]], [1, 1, 0]) scorers = _check_multimetric_scoring(estimator, scoring) @@ -337,12 +337,12 @@ def test_check_scoring_gridsearchcv(): # test that check_scoring works on GridSearchCV and pipeline. # slightly redundant non-regression test. - grid = GridSearchCV(LinearSVC(dual="auto"), param_grid={"C": [0.1, 1]}, cv=3) + grid = GridSearchCV(LinearSVC(), param_grid={"C": [0.1, 1]}, cv=3) scorer = check_scoring(grid, scoring="f1") assert isinstance(scorer, _Scorer) assert scorer._response_method == "predict" - pipe = make_pipeline(LinearSVC(dual="auto")) + pipe = make_pipeline(LinearSVC()) scorer = check_scoring(pipe, scoring="f1") assert isinstance(scorer, _Scorer) assert scorer._response_method == "predict" @@ -384,7 +384,7 @@ def test_classification_binary_scores(scorer_name, metric): # binary classification. X, y = make_blobs(random_state=0, centers=2) X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0) - clf = LinearSVC(dual="auto", random_state=0) + clf = LinearSVC(random_state=0) clf.fit(X_train, y_train) score = get_scorer(scorer_name)(clf, X_test, y_test) @@ -434,7 +434,7 @@ def test_custom_scorer_pickling(): # test that custom scorer can be pickled X, y = make_blobs(random_state=0, centers=2) X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0) - clf = LinearSVC(dual="auto", random_state=0) + clf = LinearSVC(random_state=0) clf.fit(X_train, y_train) scorer = make_scorer(fbeta_score, beta=2) @@ -532,7 +532,7 @@ def test_thresholded_scorers_multilabel_indicator_data(): assert_almost_equal(score1, score2) # Multilabel decision function - clf = OneVsRestClassifier(LinearSVC(dual="auto", random_state=0)) + clf = OneVsRestClassifier(LinearSVC(random_state=0)) clf.fit(X_train, y_train) score1 = get_scorer("roc_auc")(clf, X_test, y_test) score2 = roc_auc_score(y_test, clf.decision_function(X_test)) diff --git a/sklearn/model_selection/tests/test_search.py b/sklearn/model_selection/tests/test_search.py index 1a9230259d22e..9eb647df887c0 100644 --- a/sklearn/model_selection/tests/test_search.py +++ b/sklearn/model_selection/tests/test_search.py @@ -263,10 +263,10 @@ def test_SearchCV_with_fit_params(SearchCV): @ignore_warnings def test_grid_search_no_score(): # Test grid-search on classifier that has no score function. - clf = LinearSVC(dual="auto", random_state=0) + clf = LinearSVC(random_state=0) X, y = make_blobs(random_state=0, centers=2) Cs = [0.1, 1, 10] - clf_no_score = LinearSVCNoScore(dual="auto", random_state=0) + clf_no_score = LinearSVCNoScore(random_state=0) grid_search = GridSearchCV(clf, {"C": Cs}, scoring="accuracy") grid_search.fit(X, y) @@ -287,13 +287,13 @@ def test_grid_search_no_score(): def test_grid_search_score_method(): X, y = make_classification(n_samples=100, n_classes=2, flip_y=0.2, random_state=0) - clf = LinearSVC(dual="auto", random_state=0) + clf = LinearSVC(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(dual="auto"), grid, scoring="roc_auc" + LinearSVCNoScore(), grid, scoring="roc_auc" ).fit(X, y) search_auc = GridSearchCV(clf, grid, scoring="roc_auc").fit(X, y) @@ -321,7 +321,7 @@ def test_grid_search_groups(): X, y = make_classification(n_samples=15, n_classes=2, random_state=0) groups = rng.randint(0, 3, 15) - clf = LinearSVC(dual="auto", random_state=0) + clf = LinearSVC(random_state=0) grid = {"C": [1]} group_cvs = [ @@ -350,7 +350,7 @@ def test_classes__property(): y = np.array([0] * 5 + [1] * 5) Cs = [0.1, 1, 10] - grid_search = GridSearchCV(LinearSVC(dual="auto", random_state=0), {"C": Cs}) + grid_search = GridSearchCV(LinearSVC(random_state=0), {"C": Cs}) grid_search.fit(X, y) assert_array_equal(grid_search.best_estimator_.classes_, grid_search.classes_) @@ -360,13 +360,11 @@ def test_classes__property(): assert not hasattr(grid_search, "classes_") # Test that the grid searcher has no classes_ attribute before it's fit - grid_search = GridSearchCV(LinearSVC(dual="auto", random_state=0), {"C": Cs}) + grid_search = GridSearchCV(LinearSVC(random_state=0), {"C": Cs}) assert not hasattr(grid_search, "classes_") # Test that the grid searcher has no classes_ attribute without a refit - grid_search = GridSearchCV( - LinearSVC(dual="auto", random_state=0), {"C": Cs}, refit=False - ) + grid_search = GridSearchCV(LinearSVC(random_state=0), {"C": Cs}, refit=False) grid_search.fit(X, y) assert not hasattr(grid_search, "classes_") @@ -430,7 +428,7 @@ 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(dual="auto") + clf = LinearSVC() cv = GridSearchCV(clf, {"C": [0.1, 1.0]}) with pytest.raises(ValueError): cv.fit(X_[:180], y_) @@ -504,14 +502,14 @@ def test_grid_search_sparse(csr_container): # Test that grid search works with both dense and sparse matrices X_, y_ = make_classification(n_samples=200, n_features=100, random_state=0) - clf = LinearSVC(dual="auto") + clf = LinearSVC() cv = GridSearchCV(clf, {"C": [0.1, 1.0]}) cv.fit(X_[:180], y_[:180]) y_pred = cv.predict(X_[180:]) C = cv.best_estimator_.C X_ = csr_container(X_) - clf = LinearSVC(dual="auto") + clf = LinearSVC() cv = GridSearchCV(clf, {"C": [0.1, 1.0]}) cv.fit(X_[:180].tocoo(), y_[:180]) y_pred2 = cv.predict(X_[180:]) @@ -525,14 +523,14 @@ def test_grid_search_sparse(csr_container): def test_grid_search_sparse_scoring(csr_container): X_, y_ = make_classification(n_samples=200, n_features=100, random_state=0) - clf = LinearSVC(dual="auto") + clf = LinearSVC() cv = GridSearchCV(clf, {"C": [0.1, 1.0]}, scoring="f1") cv.fit(X_[:180], y_[:180]) y_pred = cv.predict(X_[180:]) C = cv.best_estimator_.C X_ = csr_container(X_) - clf = LinearSVC(dual="auto") + clf = LinearSVC() cv = GridSearchCV(clf, {"C": [0.1, 1.0]}, scoring="f1") cv.fit(X_[:180], y_[:180]) y_pred2 = cv.predict(X_[180:]) @@ -642,7 +640,7 @@ def refit_callable(cv_results): # clf.cv_results_. X, y = make_classification(n_samples=100, n_features=4, random_state=42) clf = GridSearchCV( - LinearSVC(dual="auto", random_state=42), + LinearSVC(random_state=42), {"C": [0.01, 0.1, 1]}, scoring="precision", refit=True, @@ -659,7 +657,7 @@ def refit_callable(cv_results): X, y = make_classification(n_samples=100, n_features=4, random_state=42) clf = GridSearchCV( - LinearSVC(dual="auto", random_state=42), + LinearSVC(random_state=42), {"C": [0.01, 0.1, 1]}, scoring="precision", refit=refit_callable, @@ -686,7 +684,7 @@ def refit_callable_invalid_type(cv_results): X, y = make_classification(n_samples=100, n_features=4, random_state=42) clf = GridSearchCV( - LinearSVC(dual="auto", random_state=42), + LinearSVC(random_state=42), {"C": [0.1, 1]}, scoring="precision", refit=refit_callable_invalid_type, @@ -712,7 +710,7 @@ def refit_callable_out_bound(cv_results): X, y = make_classification(n_samples=100, n_features=4, random_state=42) clf = search_cv( - LinearSVC(dual="auto", random_state=42), + LinearSVC(random_state=42), {"C": [0.1, 1]}, scoring="precision", refit=refit_callable_out_bound, @@ -738,7 +736,7 @@ def refit_callable(cv_results): X, y = make_classification(n_samples=100, n_features=4, random_state=42) scoring = {"Accuracy": make_scorer(accuracy_score), "prec": "precision"} clf = GridSearchCV( - LinearSVC(dual="auto", random_state=42), + LinearSVC(random_state=42), {"C": [0.01, 0.1, 1]}, scoring=scoring, refit=refit_callable, @@ -1411,7 +1409,7 @@ def test_search_cv_results_none_param(): @ignore_warnings() def test_search_cv_timing(): - svc = LinearSVC(dual="auto", random_state=0) + svc = LinearSVC(random_state=0) X = [ [ @@ -1453,7 +1451,7 @@ def test_search_cv_timing(): def test_grid_search_correct_score_results(): # test that correct scores are used n_splits = 3 - clf = LinearSVC(dual="auto", random_state=0) + clf = LinearSVC(random_state=0) X, y = make_blobs(random_state=0, centers=2) Cs = [0.1, 1, 10] for score in ["f1", "roc_auc"]: @@ -1792,7 +1790,7 @@ def test_stochastic_gradient_loss_param(): def test_search_train_scores_set_to_false(): X = np.arange(6).reshape(6, -1) y = [0, 0, 0, 1, 1, 1] - clf = LinearSVC(dual="auto", random_state=0) + clf = LinearSVC(random_state=0) gs = GridSearchCV(clf, param_grid={"C": [0.1, 0.2]}, cv=3) gs.fit(X, y) @@ -1805,7 +1803,7 @@ def test_grid_search_cv_splits_consistency(): X, y = make_classification(n_samples=n_samples, random_state=0) gs = GridSearchCV( - LinearSVC(dual="auto", random_state=0), + LinearSVC(random_state=0), param_grid={"C": [0.1, 0.2, 0.3]}, cv=OneTimeSplitter(n_splits=n_splits, n_samples=n_samples), return_train_score=True, @@ -1813,7 +1811,7 @@ def test_grid_search_cv_splits_consistency(): gs.fit(X, y) gs2 = GridSearchCV( - LinearSVC(dual="auto", random_state=0), + LinearSVC(random_state=0), param_grid={"C": [0.1, 0.2, 0.3]}, cv=KFold(n_splits=n_splits), return_train_score=True, @@ -1826,7 +1824,7 @@ def test_grid_search_cv_splits_consistency(): GeneratorType, ) gs3 = GridSearchCV( - LinearSVC(dual="auto", random_state=0), + LinearSVC(random_state=0), param_grid={"C": [0.1, 0.2, 0.3]}, cv=KFold(n_splits=n_splits, shuffle=True, random_state=0).split(X, y), return_train_score=True, @@ -1834,7 +1832,7 @@ def test_grid_search_cv_splits_consistency(): gs3.fit(X, y) gs4 = GridSearchCV( - LinearSVC(dual="auto", random_state=0), + LinearSVC(random_state=0), param_grid={"C": [0.1, 0.2, 0.3]}, cv=KFold(n_splits=n_splits, shuffle=True, random_state=0), return_train_score=True, @@ -1870,7 +1868,7 @@ def _pop_time_keys(cv_results): # Check consistency of folds across the parameters gs = GridSearchCV( - LinearSVC(dual="auto", random_state=0), + LinearSVC(random_state=0), param_grid={"C": [0.1, 0.1, 0.2, 0.2]}, cv=KFold(n_splits=n_splits, shuffle=True), return_train_score=True, @@ -2094,7 +2092,7 @@ def custom_scorer(clf, X, y): 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(dual="auto", random_state=42) + est = LinearSVC(random_state=42) search = GridSearchCV(est, {"C": [0.1, 1]}, scoring=custom_scorer, refit="fp") search.fit(X, y) @@ -2118,7 +2116,7 @@ def custom_scorer(est, X, y): } X, y = make_classification(n_samples=40, n_features=4, random_state=42) - est = LinearSVC(dual="auto", random_state=42) + est = LinearSVC(random_state=42) search_callable = GridSearchCV( est, {"C": [0.1, 1]}, scoring=custom_scorer, refit="recall" ) @@ -2141,7 +2139,7 @@ def custom_scorer(est, X, y): return recall_score(y, y_pred) X, y = make_classification(n_samples=40, n_features=4, random_state=42) - est = LinearSVC(dual="auto", random_state=42) + est = LinearSVC(random_state=42) search_callable = GridSearchCV( est, {"C": [0.1, 1]}, scoring=custom_scorer, refit=True ) @@ -2169,7 +2167,7 @@ def bad_scorer(est, X, y): X, y = make_classification(n_samples=40, n_features=4, random_state=42) clf = GridSearchCV( - LinearSVC(dual="auto", random_state=42), + LinearSVC(random_state=42), {"C": [0.1, 1]}, scoring=bad_scorer, refit="good_name", @@ -2451,7 +2449,7 @@ 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.""" X, y = make_classification(n_samples=100, n_classes=2, flip_y=0.2, random_state=0) - clf = LinearSVC(dual="auto", random_state=0) + clf = LinearSVC(random_state=0) grid = {"C": [0.1]} GridSearchCV( @@ -2482,7 +2480,7 @@ def test_search_estimator_param(SearchCV, param_search): # test that SearchCV object doesn't change the object given in the parameter grid X, y = make_classification(random_state=42) - params = {"clf": [LinearSVC(dual="auto")], "clf__C": [0.01]} + params = {"clf": [LinearSVC()], "clf__C": [0.01]} orig_C = params["clf"][0].C pipe = Pipeline([("trs", MinimalTransformer()), ("clf", None)]) @@ -2575,7 +2573,7 @@ def test_multi_metric_search_forwards_metadata(SearchCV, param_search): score_weights = rng.rand(n_samples) score_metadata = rng.rand(n_samples) - est = LinearSVC(dual="auto") + est = LinearSVC() param_grid_search = {param_search: {"C": [1]}} scorer_registry = _Registry() @@ -2609,7 +2607,7 @@ 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.""" X, y = make_classification(random_state=42) - est = LinearSVC(dual="auto") + est = LinearSVC() param_grid_search = {param_search: {"C": [1]}} gs = SearchCV(est, cv=2, **param_grid_search).fit(X, y) diff --git a/sklearn/model_selection/tests/test_successive_halving.py b/sklearn/model_selection/tests/test_successive_halving.py index b7047c7537871..a792f18e0b42f 100644 --- a/sklearn/model_selection/tests/test_successive_halving.py +++ b/sklearn/model_selection/tests/test_successive_halving.py @@ -732,7 +732,7 @@ def test_groups_support(Est): X, y = make_classification(n_samples=50, n_classes=2, random_state=0) groups = rng.randint(0, 3, 50) - clf = LinearSVC(dual="auto", random_state=0) + clf = LinearSVC(random_state=0) grid = {"C": [1]} group_cvs = [ diff --git a/sklearn/model_selection/tests/test_validation.py b/sklearn/model_selection/tests/test_validation.py index 43916d8cecb2e..a1a860b243249 100644 --- a/sklearn/model_selection/tests/test_validation.py +++ b/sklearn/model_selection/tests/test_validation.py @@ -2403,7 +2403,7 @@ def custom_scorer(clf, X, y): 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(dual="auto", random_state=42) + est = LinearSVC(random_state=42) est.fit(X, y) cv_results = cross_validate(est, X, y, cv=5, scoring=custom_scorer) diff --git a/sklearn/multiclass.py b/sklearn/multiclass.py index 914aac99d82b5..075095ad4146e 100644 --- a/sklearn/multiclass.py +++ b/sklearn/multiclass.py @@ -738,7 +738,7 @@ class OneVsOneClassifier(MetaEstimatorMixin, ClassifierMixin, BaseEstimator): >>> 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(dual="auto", random_state=0)).fit(X_train, y_train) + ... 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]) """ diff --git a/sklearn/svm/_classes.py b/sklearn/svm/_classes.py index 00854f47d9a84..b8709b43a5a17 100644 --- a/sklearn/svm/_classes.py +++ b/sklearn/svm/_classes.py @@ -1,11 +1,10 @@ -import warnings from numbers import Integral, Real import numpy as np from ..base import BaseEstimator, OutlierMixin, RegressorMixin, _fit_context from ..linear_model._base import LinearClassifierMixin, LinearModel, SparseCoefMixin -from ..utils._param_validation import Hidden, Interval, StrOptions +from ..utils._param_validation import Interval, StrOptions from ..utils.multiclass import check_classification_targets from ..utils.validation import _num_samples from ._base import BaseLibSVM, BaseSVC, _fit_liblinear, _get_liblinear_solver_type @@ -26,16 +25,6 @@ def _validate_dual_parameter(dual, loss, penalty, multi_class, X): return False except ValueError: # primal not supported by the combination return True - # TODO 1.5 - elif dual == "warn": - warnings.warn( - ( - "The default value of `dual` will change from `True` to `'auto'` in" - " 1.5. Set the value of `dual` explicitly to suppress the warning." - ), - FutureWarning, - ) - return True else: return dual @@ -70,7 +59,7 @@ class LinearSVC(LinearClassifierMixin, SparseCoefMixin, BaseEstimator): square of the hinge loss. The combination of ``penalty='l1'`` and ``loss='hinge'`` is not supported. - dual : "auto" or bool, default=True + 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, @@ -224,10 +213,10 @@ class LinearSVC(LinearClassifierMixin, SparseCoefMixin, BaseEstimator): >>> from sklearn.datasets import make_classification >>> X, y = make_classification(n_features=4, random_state=0) >>> clf = make_pipeline(StandardScaler(), - ... LinearSVC(dual="auto", random_state=0, tol=1e-5)) + ... LinearSVC(random_state=0, tol=1e-5)) >>> clf.fit(X, y) Pipeline(steps=[('standardscaler', StandardScaler()), - ('linearsvc', LinearSVC(dual='auto', random_state=0, tol=1e-05))]) + ('linearsvc', LinearSVC(random_state=0, tol=1e-05))]) >>> print(clf.named_steps['linearsvc'].coef_) [[0.141... 0.526... 0.679... 0.493...]] @@ -241,7 +230,7 @@ class LinearSVC(LinearClassifierMixin, SparseCoefMixin, BaseEstimator): _parameter_constraints: dict = { "penalty": [StrOptions({"l1", "l2"})], "loss": [StrOptions({"hinge", "squared_hinge"})], - "dual": ["boolean", StrOptions({"auto"}), Hidden(StrOptions({"warn"}))], + "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"})], @@ -258,7 +247,7 @@ def __init__( penalty="l2", loss="squared_hinge", *, - dual="warn", + dual="auto", tol=1e-4, C=1.0, multi_class="ovr", @@ -423,7 +412,7 @@ class LinearSVR(RegressorMixin, LinearModel): `intercept_scaling`. This scaling allows the intercept term to have a different regularization behavior compared to the other features. - dual : "auto" or bool, default=True + 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, @@ -498,10 +487,10 @@ class LinearSVR(RegressorMixin, LinearModel): >>> from sklearn.datasets import make_regression >>> X, y = make_regression(n_features=4, random_state=0) >>> regr = make_pipeline(StandardScaler(), - ... LinearSVR(dual="auto", random_state=0, tol=1e-5)) + ... LinearSVR(random_state=0, tol=1e-5)) >>> regr.fit(X, y) Pipeline(steps=[('standardscaler', StandardScaler()), - ('linearsvr', LinearSVR(dual='auto', random_state=0, tol=1e-05))]) + ('linearsvr', LinearSVR(random_state=0, tol=1e-05))]) >>> print(regr.named_steps['linearsvr'].coef_) [18.582... 27.023... 44.357... 64.522...] @@ -518,7 +507,7 @@ class LinearSVR(RegressorMixin, LinearModel): "loss": [StrOptions({"epsilon_insensitive", "squared_epsilon_insensitive"})], "fit_intercept": ["boolean"], "intercept_scaling": [Interval(Real, 0, None, closed="neither")], - "dual": ["boolean", StrOptions({"auto"}), Hidden(StrOptions({"warn"}))], + "dual": ["boolean", StrOptions({"auto"})], "verbose": ["verbose"], "random_state": ["random_state"], "max_iter": [Interval(Integral, 0, None, closed="left")], @@ -533,7 +522,7 @@ def __init__( loss="epsilon_insensitive", fit_intercept=True, intercept_scaling=1.0, - dual="warn", + dual="auto", verbose=0, random_state=None, max_iter=1000, diff --git a/sklearn/svm/tests/test_sparse.py b/sklearn/svm/tests/test_sparse.py index a7e517fdce893..59fede29f359c 100644 --- a/sklearn/svm/tests/test_sparse.py +++ b/sklearn/svm/tests/test_sparse.py @@ -242,8 +242,8 @@ def test_linearsvc(lil_container, dok_container): X_sp = lil_container(X) X2_sp = dok_container(X2) - clf = svm.LinearSVC(dual="auto", random_state=0).fit(X, Y) - sp_clf = svm.LinearSVC(dual="auto", random_state=0).fit(X_sp, Y) + 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 @@ -264,8 +264,8 @@ def test_linearsvc_iris(csr_container): # Test the sparse LinearSVC with the iris dataset iris_data_sp = csr_container(iris.data) - sp_clf = svm.LinearSVC(dual="auto", random_state=0).fit(iris_data_sp, iris.target) - clf = svm.LinearSVC(dual="auto", random_state=0).fit(iris.data, iris.target) + 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 @@ -295,7 +295,7 @@ def test_weight(csr_container): X_ = csr_container(X_) for clf in ( linear_model.LogisticRegression(), - svm.LinearSVC(dual="auto", random_state=0), + svm.LinearSVC(random_state=0), svm.SVC(), ): clf.set_params(class_weight={0: 5}) diff --git a/sklearn/svm/tests/test_svm.py b/sklearn/svm/tests/test_svm.py index f728136b0f98c..2735dc0651d89 100644 --- a/sklearn/svm/tests/test_svm.py +++ b/sklearn/svm/tests/test_svm.py @@ -4,8 +4,6 @@ TODO: remove hard coded numerical results when possible """ -import re - import numpy as np import pytest from numpy.testing import ( @@ -227,8 +225,8 @@ def test_svr(): 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(dual="auto", C=10.0), - svm.LinearSVR(dual="auto", C=10.0), + svm.LinearSVR(C=10.0), + svm.LinearSVR(C=10.0), ): clf.fit(diabetes.data, diabetes.target) assert clf.score(diabetes.data, diabetes.target) > 0.02 @@ -236,14 +234,14 @@ def test_svr(): # 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(dual="auto").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 diabetes = datasets.load_diabetes() - lsvr = svm.LinearSVR(C=1e3, dual="auto").fit(diabetes.data, diabetes.target) + 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) @@ -260,12 +258,12 @@ def test_linearsvr_fit_sampleweight(): diabetes = datasets.load_diabetes() n_samples = len(diabetes.target) unit_weight = np.ones(n_samples) - lsvr = svm.LinearSVR(dual="auto", C=1e3, tol=1e-12, max_iter=10000).fit( + lsvr = svm.LinearSVR(C=1e3, tol=1e-12, max_iter=10000).fit( diabetes.data, diabetes.target, sample_weight=unit_weight ) score1 = lsvr.score(diabetes.data, diabetes.target) - lsvr_no_weight = svm.LinearSVR(dual="auto", C=1e3, tol=1e-12, max_iter=10000).fit( + 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) @@ -279,7 +277,7 @@ def test_linearsvr_fit_sampleweight(): # X = X1 repeated n1 times, X2 repeated n2 times and so forth random_state = check_random_state(0) random_weight = random_state.randint(0, 10, n_samples) - lsvr_unflat = svm.LinearSVR(dual="auto", C=1e3, tol=1e-12, max_iter=10000).fit( + 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( @@ -288,9 +286,7 @@ def test_linearsvr_fit_sampleweight(): X_flat = np.repeat(diabetes.data, random_weight, axis=0) y_flat = np.repeat(diabetes.target, random_weight, axis=0) - lsvr_flat = svm.LinearSVR(dual="auto", C=1e3, tol=1e-12, max_iter=10000).fit( - X_flat, y_flat - ) + 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) @@ -490,7 +486,7 @@ def test_weight(): for clf in ( linear_model.LogisticRegression(), - svm.LinearSVC(dual="auto", random_state=0), + svm.LinearSVC(random_state=0), svm.SVC(), ): clf.set_params(class_weight={0: 0.1, 1: 10}) @@ -667,7 +663,7 @@ def test_auto_weight(): for clf in ( svm.SVC(kernel="linear"), - svm.LinearSVC(dual="auto", random_state=0), + svm.LinearSVC(random_state=0), LogisticRegression(), ): # check that score is better when class='balanced' is set. @@ -690,7 +686,7 @@ def test_bad_input(lil_container): svm.SVC().fit(X, Y2) # Test with arrays that are non-contiguous. - for clf in (svm.SVC(), svm.LinearSVC(dual="auto", random_state=0)): + for clf in (svm.SVC(), svm.LinearSVC(random_state=0)): Xf = np.asfortranarray(X) assert not Xf.flags["C_CONTIGUOUS"] yf = np.ascontiguousarray(np.tile(Y, (2, 1)).T) @@ -790,7 +786,7 @@ def test_linearsvc_parameters(loss, penalty, dual): def test_linearsvc(): # Test basic routines using LinearSVC - clf = svm.LinearSVC(dual="auto", random_state=0).fit(X, Y) + clf = svm.LinearSVC(random_state=0).fit(X, Y) # by default should have intercept assert clf.fit_intercept @@ -821,8 +817,8 @@ def test_linearsvc(): def test_linearsvc_crammer_singer(): # Test LinearSVC with crammer_singer multi-class svm - ovr_clf = svm.LinearSVC(dual="auto", random_state=0).fit(iris.data, iris.target) - cs_clf = svm.LinearSVC(dual="auto", multi_class="crammer_singer", random_state=0) + 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: @@ -844,10 +840,10 @@ def test_linearsvc_fit_sampleweight(): # check correct result when sample_weight is 1 n_samples = len(X) unit_weight = np.ones(n_samples) - clf = svm.LinearSVC(dual="auto", random_state=0).fit(X, Y) - clf_unitweight = svm.LinearSVC( - dual="auto", random_state=0, tol=1e-12, max_iter=1000 - ).fit(X, Y, sample_weight=unit_weight) + clf = svm.LinearSVC(random_state=0).fit(X, Y) + 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)) @@ -858,17 +854,17 @@ def test_linearsvc_fit_sampleweight(): random_state = check_random_state(0) random_weight = random_state.randint(0, 10, n_samples) - lsvc_unflat = svm.LinearSVC( - dual="auto", random_state=0, tol=1e-12, max_iter=1000 - ).fit(X, Y, sample_weight=random_weight) + 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( - dual="auto", random_state=0, tol=1e-12, max_iter=1000 - ).fit(X_flat, y_flat) + lsvc_flat = svm.LinearSVC(random_state=0, tol=1e-12, max_iter=1000).fit( + X_flat, y_flat + ) pred2 = lsvc_flat.predict(T) assert_array_equal(pred1, pred2) @@ -882,7 +878,6 @@ def test_crammer_singer_binary(): for fit_intercept in (True, False): acc = ( svm.LinearSVC( - dual="auto", fit_intercept=fit_intercept, multi_class="crammer_singer", random_state=0, @@ -897,7 +892,7 @@ 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(dual="auto", random_state=0).fit(iris.data, 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 @@ -945,7 +940,7 @@ def test_dense_liblinear_intercept_handling(classifier=svm.LinearSVC): def test_liblinear_set_coef(): # multi-class case - clf = svm.LinearSVC(dual="auto").fit(iris.data, iris.target) + clf = svm.LinearSVC().fit(iris.data, iris.target) values = clf.decision_function(iris.data) clf.coef_ = clf.coef_.copy() clf.intercept_ = clf.intercept_.copy() @@ -956,7 +951,7 @@ def test_liblinear_set_coef(): X = [[2, 1], [3, 1], [1, 3], [2, 3]] y = [0, 0, 1, 1] - clf = svm.LinearSVC(dual="auto").fit(X, y) + clf = svm.LinearSVC().fit(X, y) values = clf.decision_function(X) clf.coef_ = clf.coef_.copy() clf.intercept_ = clf.intercept_.copy() @@ -988,7 +983,7 @@ def test_linearsvc_verbose(): os.dup2(os.pipe()[1], 1) # replace it # actual call - clf = svm.LinearSVC(dual="auto", verbose=1) + clf = svm.LinearSVC(verbose=1) clf.fit(X, Y) # stdout: restore @@ -1072,7 +1067,7 @@ def test_consistent_proba(): def test_linear_svm_convergence_warnings(): # Test that warnings are raised if model does not converge - lsvc = svm.LinearSVC(dual="auto", random_state=0, max_iter=2) + lsvc = svm.LinearSVC(random_state=0, max_iter=2) warning_msg = "Liblinear failed to converge, increase the number of iterations." with pytest.warns(ConvergenceWarning, match=warning_msg): lsvc.fit(X, Y) @@ -1081,7 +1076,7 @@ def test_linear_svm_convergence_warnings(): assert isinstance(lsvc.n_iter_, int) assert lsvc.n_iter_ == 2 - lsvr = svm.LinearSVR(dual="auto", random_state=0, max_iter=2) + lsvr = svm.LinearSVR(random_state=0, max_iter=2) with pytest.warns(ConvergenceWarning, match=warning_msg): lsvr.fit(iris.data, iris.target) assert isinstance(lsvr.n_iter_, int) @@ -1097,7 +1092,7 @@ def test_svr_coef_sign(): for svr in [ svm.SVR(kernel="linear"), svm.NuSVR(kernel="linear"), - svm.LinearSVR(dual="auto"), + svm.LinearSVR(), ]: svr.fit(X, y) assert_array_almost_equal( @@ -1108,7 +1103,7 @@ def test_svr_coef_sign(): def test_lsvc_intercept_scaling_zero(): # Test that intercept_scaling is ignored when fit_intercept is False - lsvc = svm.LinearSVC(dual="auto", fit_intercept=False) + lsvc = svm.LinearSVC(fit_intercept=False) lsvc.fit(X, Y) assert lsvc.intercept_ == 0.0 @@ -1398,18 +1393,6 @@ def test_n_iter_libsvm(estimator, expected_n_iter_type, dataset): assert n_iter.shape == (n_classes * (n_classes - 1) // 2,) -# TODO(1.5): Remove -@pytest.mark.parametrize("Estimator", [LinearSVR, LinearSVC]) -def test_dual_auto_deprecation_warning(Estimator): - svm = Estimator() - msg = ( - "The default value of `dual` will change from `True` to `'auto'` in" - " 1.5. Set the value of `dual` explicitly to suppress the warning." - ) - with pytest.warns(FutureWarning, match=re.escape(msg)): - svm.fit(X, Y) - - @pytest.mark.parametrize("loss", ["squared_hinge", "squared_epsilon_insensitive"]) def test_dual_auto(loss): # OvR, L2, N > M (6,2) diff --git a/sklearn/tests/test_calibration.py b/sklearn/tests/test_calibration.py index e74ff76b48355..eb7204b366729 100644 --- a/sklearn/tests/test_calibration.py +++ b/sklearn/tests/test_calibration.py @@ -156,7 +156,7 @@ def test_sample_weight(data, method, ensemble): X_train, y_train, sw_train = X[:n_samples], y[:n_samples], sample_weight[:n_samples] X_test = X[n_samples:] - estimator = LinearSVC(dual="auto", random_state=42) + 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) @@ -177,7 +177,7 @@ def test_parallel_execution(data, method, ensemble): X, y = data X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42) - estimator = make_pipeline(StandardScaler(), LinearSVC(dual="auto", random_state=42)) + estimator = make_pipeline(StandardScaler(), LinearSVC(random_state=42)) cal_clf_parallel = CalibratedClassifierCV( estimator, method=method, n_jobs=2, ensemble=ensemble @@ -206,7 +206,7 @@ def multiclass_brier(y_true, proba_pred, n_classes): # Test calibration for multiclass with classifier that implements # only decision function. - clf = LinearSVC(dual="auto", random_state=7) + clf = LinearSVC(random_state=7) X, y = make_blobs( n_samples=500, n_features=100, random_state=seed, centers=10, cluster_std=15.0 ) @@ -338,7 +338,7 @@ 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 - clf = LinearSVC(dual="auto", random_state=7) + clf = LinearSVC(random_state=7) cal_clf = CalibratedClassifierCV(clf, method=method, cv=3, ensemble=False) cal_clf.fit(X, y) @@ -427,7 +427,7 @@ def test_calibration_prob_sum(ensemble): # issue #7796 num_classes = 2 X, y = make_classification(n_samples=10, n_features=5, n_classes=num_classes) - clf = LinearSVC(dual="auto", C=1.0, random_state=7) + clf = LinearSVC(C=1.0, random_state=7) clf_prob = CalibratedClassifierCV( clf, method="sigmoid", cv=LeaveOneOut(), ensemble=ensemble ) @@ -445,7 +445,7 @@ def test_calibration_less_classes(ensemble): # class label X = np.random.randn(10, 5) y = np.arange(10) - clf = LinearSVC(dual="auto", C=1.0, random_state=7) + clf = LinearSVC(C=1.0, random_state=7) cal_clf = CalibratedClassifierCV( clf, method="sigmoid", cv=LeaveOneOut(), ensemble=ensemble ) @@ -542,8 +542,8 @@ def test_calibration_dict_pipeline(dict_data, dict_data_pipeline): @pytest.mark.parametrize( "clf, cv", [ - pytest.param(LinearSVC(dual="auto", C=1), 2), - pytest.param(LinearSVC(dual="auto", C=1), "prefit"), + pytest.param(LinearSVC(C=1), 2), + pytest.param(LinearSVC(C=1), "prefit"), ], ) def test_calibration_attributes(clf, cv): @@ -567,7 +567,7 @@ def test_calibration_inconsistent_prefit_n_features_in(): # Check that `n_features_in_` from prefit base estimator # is consistent with training set X, y = make_classification(n_samples=10, n_features=5, n_classes=2, random_state=7) - clf = LinearSVC(dual="auto", C=1).fit(X, y) + clf = LinearSVC(C=1).fit(X, y) calib_clf = CalibratedClassifierCV(clf, cv="prefit") msg = "X has 3 features, but LinearSVC is expecting 5 features as input." diff --git a/sklearn/tests/test_docstring_parameters.py b/sklearn/tests/test_docstring_parameters.py index 3edaa05100520..bafd1de54f5aa 100644 --- a/sklearn/tests/test_docstring_parameters.py +++ b/sklearn/tests/test_docstring_parameters.py @@ -225,10 +225,6 @@ def test_fit_docstring_attributes(name, Estimator): # default raises an error, perplexity must be less than n_samples est.set_params(perplexity=2) - # TODO(1.5): TO BE REMOVED for 1.5 (avoid FutureWarning) - if Estimator.__name__ in ("LinearSVC", "LinearSVR"): - est.set_params(dual="auto") - # TODO(1.6): remove (avoid FutureWarning) if Estimator.__name__ in ("NMF", "MiniBatchNMF"): est.set_params(n_components="auto") diff --git a/sklearn/tests/test_multiclass.py b/sklearn/tests/test_multiclass.py index b57d681d7ebfa..4bc96bf60b805 100644 --- a/sklearn/tests/test_multiclass.py +++ b/sklearn/tests/test_multiclass.py @@ -57,7 +57,7 @@ def test_ovr_exceptions(): - ovr = OneVsRestClassifier(LinearSVC(dual="auto", random_state=0)) + ovr = OneVsRestClassifier(LinearSVC(random_state=0)) # test predicting without fitting with pytest.raises(NotFittedError): @@ -86,11 +86,11 @@ def test_check_classification_targets(): def test_ovr_fit_predict(): # A classifier which implements decision_function. - ovr = OneVsRestClassifier(LinearSVC(dual="auto", random_state=0)) + ovr = OneVsRestClassifier(LinearSVC(random_state=0)) pred = ovr.fit(iris.data, iris.target).predict(iris.data) assert len(ovr.estimators_) == n_classes - clf = LinearSVC(dual="auto", random_state=0) + clf = LinearSVC(random_state=0) pred2 = clf.fit(iris.data, iris.target).predict(iris.data) assert np.mean(iris.target == pred) == np.mean(iris.target == pred2) @@ -258,7 +258,7 @@ def test_ovr_multiclass(): for base_clf in ( MultinomialNB(), - LinearSVC(dual="auto", random_state=0), + LinearSVC(random_state=0), LinearRegression(), Ridge(), ElasticNet(), @@ -303,7 +303,7 @@ def conduct_test(base_clf, test_predict_proba=False): assert y_pred == 1 for base_clf in ( - LinearSVC(dual="auto", random_state=0), + LinearSVC(random_state=0), LinearRegression(), Ridge(), ElasticNet(), @@ -321,7 +321,7 @@ def test_ovr_multilabel(): for base_clf in ( MultinomialNB(), - LinearSVC(dual="auto", random_state=0), + LinearSVC(random_state=0), LinearRegression(), Ridge(), ElasticNet(), @@ -459,7 +459,7 @@ def test_ovr_single_label_decision_function(): def test_ovr_gridsearch(): - ovr = OneVsRestClassifier(LinearSVC(dual="auto", random_state=0)) + ovr = OneVsRestClassifier(LinearSVC(random_state=0)) Cs = [0.1, 0.5, 0.8] cv = GridSearchCV(ovr, {"estimator__C": Cs}) cv.fit(iris.data, iris.target) @@ -480,7 +480,7 @@ def test_ovr_pipeline(): def test_ovo_exceptions(): - ovo = OneVsOneClassifier(LinearSVC(dual="auto", random_state=0)) + ovo = OneVsOneClassifier(LinearSVC(random_state=0)) with pytest.raises(NotFittedError): ovo.predict([]) @@ -488,7 +488,7 @@ def test_ovo_exceptions(): 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 - ovo = OneVsOneClassifier(LinearSVC(dual="auto", random_state=0)) + ovo = OneVsOneClassifier(LinearSVC(random_state=0)) 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( @@ -499,7 +499,7 @@ def test_ovo_fit_on_list(): def test_ovo_fit_predict(): # A classifier which implements decision_function. - ovo = OneVsOneClassifier(LinearSVC(dual="auto", random_state=0)) + ovo = OneVsOneClassifier(LinearSVC(random_state=0)) ovo.fit(iris.data, iris.target).predict(iris.data) assert len(ovo.estimators_) == n_classes * (n_classes - 1) / 2 @@ -565,7 +565,7 @@ def test_ovo_partial_fit_predict(): def test_ovo_decision_function(): n_samples = iris.data.shape[0] - ovo_clf = OneVsOneClassifier(LinearSVC(dual="auto", random_state=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) @@ -610,7 +610,7 @@ def test_ovo_decision_function(): def test_ovo_gridsearch(): - ovo = OneVsOneClassifier(LinearSVC(dual="auto", random_state=0)) + ovo = OneVsOneClassifier(LinearSVC(random_state=0)) Cs = [0.1, 0.5, 0.8] cv = GridSearchCV(ovo, {"estimator__C": Cs}) cv.fit(iris.data, iris.target) @@ -660,7 +660,7 @@ def test_ovo_string_y(): X = np.eye(4) y = np.array(["a", "b", "c", "d"]) - ovo = OneVsOneClassifier(LinearSVC(dual="auto")) + ovo = OneVsOneClassifier(LinearSVC()) ovo.fit(X, y) assert_array_equal(y, ovo.predict(X)) @@ -670,7 +670,7 @@ def test_ovo_one_class(): X = np.eye(4) y = np.array(["a"] * 4) - ovo = OneVsOneClassifier(LinearSVC(dual="auto")) + ovo = OneVsOneClassifier(LinearSVC()) msg = "when only one class" with pytest.raises(ValueError, match=msg): ovo.fit(X, y) @@ -681,23 +681,21 @@ def test_ovo_float_y(): X = iris.data y = iris.data[:, 0] - ovo = OneVsOneClassifier(LinearSVC(dual="auto")) + ovo = OneVsOneClassifier(LinearSVC()) msg = "Unknown label type" with pytest.raises(ValueError, match=msg): ovo.fit(X, y) def test_ecoc_exceptions(): - ecoc = OutputCodeClassifier(LinearSVC(dual="auto", random_state=0)) + ecoc = OutputCodeClassifier(LinearSVC(random_state=0)) with pytest.raises(NotFittedError): ecoc.predict([]) def test_ecoc_fit_predict(): # A classifier which implements decision_function. - ecoc = OutputCodeClassifier( - LinearSVC(dual="auto", random_state=0), code_size=2, random_state=0 - ) + 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 @@ -708,7 +706,7 @@ def test_ecoc_fit_predict(): def test_ecoc_gridsearch(): - ecoc = OutputCodeClassifier(LinearSVC(dual="auto", random_state=0), random_state=0) + ecoc = OutputCodeClassifier(LinearSVC(random_state=0), random_state=0) Cs = [0.1, 0.5, 0.8] cv = GridSearchCV(ecoc, {"estimator__C": Cs}) cv.fit(iris.data, iris.target) @@ -721,7 +719,7 @@ def test_ecoc_float_y(): X = iris.data y = iris.data[:, 0] - ovo = OutputCodeClassifier(LinearSVC(dual="auto")) + ovo = OutputCodeClassifier(LinearSVC()) msg = "Unknown label type" with pytest.raises(ValueError, match=msg): ovo.fit(X, y) @@ -749,7 +747,7 @@ def test_ecoc_delegate_sparse_base_estimator(csc_container): ecoc.predict(X_sp) # smoke test to check when sparse input should be supported - ecoc = OutputCodeClassifier(LinearSVC(dual="auto", random_state=0)) + ecoc = OutputCodeClassifier(LinearSVC(random_state=0)) ecoc.fit(X_sp, y).predict(X_sp) assert len(ecoc.estimators_) == 4 diff --git a/sklearn/tests/test_multioutput.py b/sklearn/tests/test_multioutput.py index 35fe162c0fae7..7c32180c27682 100644 --- a/sklearn/tests/test_multioutput.py +++ b/sklearn/tests/test_multioutput.py @@ -337,7 +337,7 @@ def test_multi_output_classification(): def test_multiclass_multioutput_estimator(): # test to check meta of meta estimators - svc = LinearSVC(dual="auto", random_state=0) + svc = LinearSVC(random_state=0) multi_class_svc = OneVsRestClassifier(svc) multi_target_svc = MultiOutputClassifier(multi_class_svc) @@ -442,7 +442,7 @@ def test_multi_output_classification_partial_fit_sample_weights(): def test_multi_output_exceptions(): # NotFittedError when fit is not done but score, predict and # and predict_proba are called - moc = MultiOutputClassifier(LinearSVC(dual="auto", random_state=0)) + moc = MultiOutputClassifier(LinearSVC(random_state=0)) with pytest.raises(NotFittedError): moc.score(X, y) @@ -478,7 +478,7 @@ def test_multi_output_delegate_predict_proba(): assert hasattr(moc, "predict_proba") # A base estimator without `predict_proba` should raise an AttributeError - moc = MultiOutputClassifier(LinearSVC(dual="auto")) + moc = MultiOutputClassifier(LinearSVC()) assert not hasattr(moc, "predict_proba") outer_msg = "'MultiOutputClassifier' has no attribute 'predict_proba'" @@ -513,7 +513,7 @@ 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(dual="auto"), + LinearSVC(), chain_method=chain_method, ).fit(X, Y) From ed000e13c6f4a5835639004d1af9802c8970dba1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20du=20Boisberranger?= Date: Fri, 12 Apr 2024 10:17:25 +0200 Subject: [PATCH 019/344] MAINT Clean up deprecations for 1.5: in graphical_lasso (#28811) --- sklearn/covariance/_graph_lasso.py | 20 ------------------- .../covariance/tests/test_graphical_lasso.py | 14 ------------- 2 files changed, 34 deletions(-) diff --git a/sklearn/covariance/_graph_lasso.py b/sklearn/covariance/_graph_lasso.py index 75f79bf06ae91..75bfc396340c9 100644 --- a/sklearn/covariance/_graph_lasso.py +++ b/sklearn/covariance/_graph_lasso.py @@ -222,7 +222,6 @@ def alpha_max(emp_cov): @validate_params( { "emp_cov": ["array-like"], - "cov_init": ["array-like", None], "return_costs": ["boolean"], "return_n_iter": ["boolean"], }, @@ -232,7 +231,6 @@ def graphical_lasso( emp_cov, alpha, *, - cov_init=None, mode="cd", tol=1e-4, enet_tol=1e-4, @@ -259,14 +257,6 @@ def graphical_lasso( regularization, the sparser the inverse covariance. Range is (0, inf]. - cov_init : array of shape (n_features, n_features), default=None - The initial guess for the covariance. If None, then the empirical - covariance is used. - - .. deprecated:: 1.3 - `cov_init` is deprecated in 1.3 and will be removed in 1.5. - It currently has no effect. - 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 @@ -347,16 +337,6 @@ def graphical_lasso( [ 0.21..., 0.22..., -0.08...], [-0.20..., -0.08..., 0.23...]]) """ - - if cov_init is not None: - warnings.warn( - ( - "The cov_init parameter is deprecated in 1.3 and will be removed in " - "1.5. It does not have any effect." - ), - FutureWarning, - ) - model = GraphicalLasso( alpha=alpha, mode=mode, diff --git a/sklearn/covariance/tests/test_graphical_lasso.py b/sklearn/covariance/tests/test_graphical_lasso.py index 7be2f3ce95e14..63782a67ebaa8 100644 --- a/sklearn/covariance/tests/test_graphical_lasso.py +++ b/sklearn/covariance/tests/test_graphical_lasso.py @@ -263,20 +263,6 @@ def test_graphical_lasso_cv_scores(): ) -# TODO(1.5): remove in 1.5 -def test_graphical_lasso_cov_init_deprecation(): - """Check that we raise a deprecation warning if providing `cov_init` in - `graphical_lasso`.""" - rng, dim, n_samples = np.random.RandomState(0), 20, 100 - prec = make_sparse_spd_matrix(dim, alpha=0.95, random_state=0) - cov = linalg.inv(prec) - X = rng.multivariate_normal(np.zeros(dim), cov, size=n_samples) - - emp_cov = empirical_covariance(X) - with pytest.warns(FutureWarning, match="cov_init parameter is deprecated"): - graphical_lasso(emp_cov, alpha=0.1, cov_init=emp_cov) - - @pytest.mark.usefixtures("enable_slep006") def test_graphical_lasso_cv_scores_with_routing(global_random_seed): """Check that `GraphicalLassoCV` internally dispatches metadata to From 556e0cf153efe07b35685b1fdeafb06e03b8c3e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20du=20Boisberranger?= Date: Fri, 12 Apr 2024 10:18:27 +0200 Subject: [PATCH 020/344] MAINT Clean up deprecations for 1.5: in inspection._partial_dependence (#28808) --- doc/modules/partial_dependence.rst | 2 +- sklearn/inspection/_partial_dependence.py | 18 +---------- .../tests/test_partial_dependence.py | 30 ------------------- 3 files changed, 2 insertions(+), 48 deletions(-) diff --git a/doc/modules/partial_dependence.rst b/doc/modules/partial_dependence.rst index 6fe5a79b51f63..94f7206140b90 100644 --- a/doc/modules/partial_dependence.rst +++ b/doc/modules/partial_dependence.rst @@ -108,7 +108,7 @@ the plots, you can use the >>> results = partial_dependence(clf, X, [0]) >>> results["average"] array([[ 2.466..., 2.466..., ... - >>> results["values"] + >>> results["grid_values"] [array([-1.624..., -1.592..., ... The values at which the partial dependence should be evaluated are directly diff --git a/sklearn/inspection/_partial_dependence.py b/sklearn/inspection/_partial_dependence.py index 8fbfd6fd5c5f7..b6ca19c407f34 100644 --- a/sklearn/inspection/_partial_dependence.py +++ b/sklearn/inspection/_partial_dependence.py @@ -528,14 +528,6 @@ def partial_dependence( `method` is 'recursion'). Only available when `kind='average'` or `kind='both'`. - values : seq of 1d ndarrays - The values with which the grid has been created. - - .. deprecated:: 1.3 - The key `values` has been deprecated in 1.3 and will be removed - in 1.5 in favor of `grid_values`. See `grid_values` for details - about the `values` attribute. - 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 @@ -716,15 +708,7 @@ def partial_dependence( averaged_predictions = averaged_predictions.reshape( -1, *[val.shape[0] for val in values] ) - pdp_results = Bunch() - - msg = ( - "Key: 'values', is deprecated in 1.3 and will be removed in 1.5. " - "Please use 'grid_values' instead." - ) - pdp_results._set_deprecated( - values, new_key="grid_values", deprecated_key="values", warning_message=msg - ) + pdp_results = Bunch(grid_values=values) if kind == "average": pdp_results["average"] = averaged_predictions diff --git a/sklearn/inspection/tests/test_partial_dependence.py b/sklearn/inspection/tests/test_partial_dependence.py index a98b97fbcbe47..58d71def0252d 100644 --- a/sklearn/inspection/tests/test_partial_dependence.py +++ b/sklearn/inspection/tests/test_partial_dependence.py @@ -2,8 +2,6 @@ Testing for the partial dependence module. """ -import warnings - import numpy as np import pytest @@ -915,34 +913,6 @@ def test_partial_dependence_sample_weight_with_recursion(): ) -# TODO(1.5): Remove when bunch values is deprecated in 1.5 -def test_partial_dependence_bunch_values_deprecated(): - """Test that deprecation warning is raised when values is accessed.""" - - est = LogisticRegression() - (X, y), _ = binary_classification_data - est.fit(X, y) - - pdp_avg = partial_dependence(est, X=X, features=[1, 2], kind="average") - - msg = ( - "Key: 'values', is deprecated in 1.3 and will be " - "removed in 1.5. Please use 'grid_values' instead" - ) - - with warnings.catch_warnings(): - # Does not raise warnings with "grid_values" - warnings.simplefilter("error", FutureWarning) - grid_values = pdp_avg["grid_values"] - - with pytest.warns(FutureWarning, match=msg): - # Warns for "values" - values = pdp_avg["values"] - - # "values" and "grid_values" are the same object - assert values is grid_values - - 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.""" From 3ee60a720aab3598668af3a3d7eb01d6958859be Mon Sep 17 00:00:00 2001 From: Christian Lorentzen Date: Fri, 12 Apr 2024 14:29:54 +0200 Subject: [PATCH 021/344] ENH add verbosity to newton-cg solver (#27526) Co-authored-by: Guillaume Lemaitre Co-authored-by: Olivier Grisel --- doc/whats_new/v1.5.rst | 5 + sklearn/linear_model/_glm/_newton_solver.py | 2 +- sklearn/linear_model/_logistic.py | 9 +- sklearn/linear_model/tests/test_logistic.py | 2 +- sklearn/utils/optimize.py | 98 +++++++++++++-- sklearn/utils/tests/test_optimize.py | 127 ++++++++++++++++++++ 6 files changed, 229 insertions(+), 14 deletions(-) diff --git a/doc/whats_new/v1.5.rst b/doc/whats_new/v1.5.rst index 01f0384af5c1d..1fc651e303e56 100644 --- a/doc/whats_new/v1.5.rst +++ b/doc/whats_new/v1.5.rst @@ -264,6 +264,11 @@ Changelog :mod:`sklearn.linear_model` ........................... +- |Enhancement| Solver `"newton-cg"` in :class:`linear_model.LogisticRegression` and + :class:`linear_model.LogisticRegressionCV` now emits information when `verbose` is + set to positive values. + :pr:`27526` by :user:`Christian Lorentzen `. + - |Fix| :class:`linear_model.ElasticNet`, :class:`linear_model.ElasticNetCV`, :class:`linear_model.Lasso` and :class:`linear_model.LassoCV` now explicitly don't accept large sparse data formats. :pr:`27576` by :user:`Stefanie Senger diff --git a/sklearn/linear_model/_glm/_newton_solver.py b/sklearn/linear_model/_glm/_newton_solver.py index 0b6adbe44e686..20df35e6b48c2 100644 --- a/sklearn/linear_model/_glm/_newton_solver.py +++ b/sklearn/linear_model/_glm/_newton_solver.py @@ -230,7 +230,7 @@ def line_search(self, X, y, sample_weight): is_verbose = self.verbose >= 2 if is_verbose: print(" Backtracking Line Search") - print(f" eps=10 * finfo.eps={eps}") + 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 diff --git a/sklearn/linear_model/_logistic.py b/sklearn/linear_model/_logistic.py index a8ecc29715886..129d3f6cc9494 100644 --- a/sklearn/linear_model/_logistic.py +++ b/sklearn/linear_model/_logistic.py @@ -477,7 +477,14 @@ def _logistic_regression_path( l2_reg_strength = 1.0 / (C * sw_sum) args = (X, target, sample_weight, l2_reg_strength, n_threads) w0, n_iter_i = _newton_cg( - hess, func, grad, w0, args=args, maxiter=max_iter, tol=tol + grad_hess=hess, + func=func, + grad=grad, + x0=w0, + args=args, + maxiter=max_iter, + tol=tol, + verbose=verbose, ) elif solver == "newton-cholesky": l2_reg_strength = 1.0 / (C * sw_sum) diff --git a/sklearn/linear_model/tests/test_logistic.py b/sklearn/linear_model/tests/test_logistic.py index 30625ea9f2bef..deda8a9984048 100644 --- a/sklearn/linear_model/tests/test_logistic.py +++ b/sklearn/linear_model/tests/test_logistic.py @@ -1156,7 +1156,7 @@ def test_logreg_predict_proba_multinomial(): [ ( "newton-cg", - "newton-cg failed to converge. Increase the number of iterations.", + "newton-cg failed to converge.* Increase the number of iterations.", ), ( "liblinear", diff --git a/sklearn/utils/optimize.py b/sklearn/utils/optimize.py index d79f514aae778..5ad2c2daace14 100644 --- a/sklearn/utils/optimize.py +++ b/sklearn/utils/optimize.py @@ -27,7 +27,9 @@ class _LineSearchError(RuntimeError): pass -def _line_search_wolfe12(f, fprime, xk, pk, gfk, old_fval, old_old_fval, **kwargs): +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 @@ -39,24 +41,44 @@ def _line_search_wolfe12(f, fprime, xk, pk, gfk, old_fval, old_old_fval, **kwarg 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) + if is_verbose: + _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) - eps = 16 * np.finfo(np.asarray(old_fval).dtype).eps tiny_loss = np.abs(old_fval * eps) loss_improvement = fval - old_fval check = np.abs(loss_improvement) <= tiny_loss + if is_verbose: + 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 + if is_verbose: + print( + " check sum(|gradient|) < sum(|gradient_old|): " + f"{sum_abs_grad} < {sum_abs_grad_old} {check}" + ) if check: ret = ( 1.0, # step size @@ -72,9 +94,14 @@ def _line_search_wolfe12(f, fprime, xk, pk, gfk, old_fval, old_old_fval, **kwarg # 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. + if is_verbose: + print(" last resort: try line search wolfe2") ret = line_search_wolfe2( f, fprime, xk, pk, gfk, old_fval, old_old_fval, **kwargs ) + if is_verbose: + _not_ = "not " if ret[0] is None else "" + print(" wolfe2 line search was " + _not_ + "successful") if ret[0] is None: raise _LineSearchError() @@ -82,7 +109,7 @@ def _line_search_wolfe12(f, fprime, xk, pk, gfk, old_fval, old_old_fval, **kwarg return ret -def _cg(fhess_p, fgrad, maxiter, tol): +def _cg(fhess_p, fgrad, maxiter, tol, verbose=0): """ Solve iteratively the linear system 'fhess_p . xsupi = fgrad' with a conjugate gradient descent. @@ -107,30 +134,51 @@ def _cg(fhess_p, fgrad, maxiter, tol): 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) + ri = np.copy(fgrad) # residual = fgrad - fhess_p @ xsupi psupi = -ri i = 0 dri0 = np.dot(ri, ri) - # We also track of |p_i|^2. + # We also keep track of |p_i|^2. psupi_norm2 = dri0 + is_verbose = verbose >= 2 while i <= maxiter: if np.sum(np.abs(ri)) <= tol: + if is_verbose: + 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 <= 16 * np.finfo(np.float64).eps * psupi_norm2: + if 0 <= curv <= eps * psupi_norm2: # See https://arxiv.org/abs/1803.02924, Algo 1 Capped Conjugate Gradient. + if is_verbose: + print( + f" Inner CG solver iteration {i} stopped with\n" + f" tiny_|p| = eps * ||p||^2, eps = {eps}, " + f"squred L2 norm ||p||^2 = {psupi_norm2}\n" + f" curvature <= tiny_|p|: {curv} <= {eps * psupi_norm2}" + ) break elif curv < 0: if i > 0: + if is_verbose: + 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 + if is_verbose: + print(" Inner CG solver iteration 0 fell back to steepest descent") break alphai = dri0 / curv xsupi += alphai * psupi @@ -142,7 +190,11 @@ def _cg(fhess_p, fgrad, maxiter, tol): 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 @@ -157,6 +209,7 @@ def _newton_cg( maxinner=200, line_search=True, warn=True, + verbose=0, ): """ Minimization of scalar function of one or more variables using the @@ -210,6 +263,10 @@ def _newton_cg( 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: @@ -218,7 +275,13 @@ def _newton_cg( fgrad, fhess_p = grad_hess(xk, *args) absgrad = np.abs(fgrad) - if np.max(absgrad) <= tol: + max_absgrad = np.max(absgrad) + check = max_absgrad <= tol + if is_verbose: + print(f"Newton-CG iter = {k}") + print(" Check Convergence") + print(f" max |gradient| <= tol: {max_absgrad} <= {tol} {check}") + if check: break maggrad = np.sum(absgrad) @@ -227,14 +290,22 @@ def _newton_cg( # Inner loop: solve the Newton update by conjugate gradient, to # avoid inverting the Hessian - xsupi = _cg(fhess_p, fgrad, maxiter=maxinner, tol=termcond) + 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 + func, + grad, + xk, + xsupi, + fgrad, + old_fval, + old_old_fval, + verbose=verbose, + args=args, ) except _LineSearchError: warnings.warn("Line Search failed") @@ -245,9 +316,14 @@ def _newton_cg( if warn and k >= maxiter: warnings.warn( - "newton-cg failed to converge. Increase the number of iterations.", + ( + f"newton-cg failed to converge at loss = {old_fval}. Increase the" + " number of iterations." + ), ConvergenceWarning, ) + elif is_verbose: + print(f" Solver did converge at loss = {old_fval}.") return xk, k diff --git a/sklearn/utils/tests/test_optimize.py b/sklearn/utils/tests/test_optimize.py index a8bcd1aebf793..5975fe4f9c191 100644 --- a/sklearn/utils/tests/test_optimize.py +++ b/sklearn/utils/tests/test_optimize.py @@ -1,6 +1,8 @@ import numpy as np +import pytest from scipy.optimize import fmin_ncg +from sklearn.exceptions import ConvergenceWarning from sklearn.utils._testing import assert_array_almost_equal from sklearn.utils.optimize import _newton_cg @@ -29,3 +31,128 @@ def grad_hess(x): _newton_cg(grad_hess, func, grad, x0, tol=1e-10)[0], fmin_ncg(f=func, x0=x0, fprime=grad, fhess_p=hess), ) + + +@pytest.mark.parametrize("verbose", [0, 1, 2]) +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]), + verbose=verbose, + ) # returns array([1., 2]) + captured = capsys.readouterr() + + if verbose == 0: + assert captured.out == "" + else: + msg = [ + "Newton-CG iter = 1", + "Check Convergence", + "max |gradient|", + "Solver did converge at loss = ", + ] + for m in msg: + assert m in captured.out + + if verbose >= 2: + msg = [ + "Inner CG solver iteration 1 stopped with", + "sum(|residuals|) <= tol", + "Line Search", + "try line search wolfe1", + "wolfe1 line search was successful", + ] + for m in msg: + assert m in captured.out + + if verbose >= 2: + # 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 + verbose=verbose, + ) + captured = capsys.readouterr() + msg = [ + "wolfe1 line search was not successful", + "check loss |improvement| <= eps * |loss_old|:", + "check sum(|gradient|) < sum(|gradient_old|):", + "last resort: try line search wolfe2", + ] + for m in msg: + assert m in captured.out + + # 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]) + with pytest.warns(ConvergenceWarning): + _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=b, + verbose=verbose, + maxiter=2, + ) + captured = capsys.readouterr() + msg = [ + "tiny_|p| = eps * ||p||^2", + ] + for m in msg: + assert m in captured.out + + # 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) + b = np.array([-2.0, 1]) + with pytest.warns(RuntimeWarning): + _newton_cg( + # Note the wrong sign in the hessian product. + 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([1.0, 1.0]), + verbose=verbose, + maxiter=3, + ) + captured = capsys.readouterr() + msg = [ + "Inner CG solver iteration 0 fell back to steepest descent", + ] + for m in msg: + assert m in captured.out + + A = np.diag([1e-3, 1, 1e3]) + b = np.array([-2.0, 1, 2.0]) + with pytest.warns(ConvergenceWarning): + _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.ones_like(b), + verbose=verbose, + maxiter=2, + maxinner=1, + ) + captured = capsys.readouterr() + msg = [ + "Inner CG solver stopped reaching maxiter=1", + ] + for m in msg: + assert m in captured.out From 8ee2954a8df8d281e17942624252f472663a6101 Mon Sep 17 00:00:00 2001 From: scikit-learn-bot Date: Mon, 15 Apr 2024 08:49:09 +0200 Subject: [PATCH 022/344] :lock: :robot: CI Update lock files for scipy-dev CI build(s) :lock: :robot: (#28832) Co-authored-by: Lock file bot --- build_tools/azure/pylatest_pip_scipy_dev_linux-64_conda.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build_tools/azure/pylatest_pip_scipy_dev_linux-64_conda.lock b/build_tools/azure/pylatest_pip_scipy_dev_linux-64_conda.lock index cc8ed57c847f2..b5f1d7613fa72 100644 --- a/build_tools/azure/pylatest_pip_scipy_dev_linux-64_conda.lock +++ b/build_tools/azure/pylatest_pip_scipy_dev_linux-64_conda.lock @@ -32,8 +32,8 @@ https://repo.anaconda.com/pkgs/main/linux-64/pip-23.3.1-py312h06a4308_0.conda#e1 # pip charset-normalizer @ https://files.pythonhosted.org/packages/ee/fb/14d30eb4956408ee3ae09ad34299131fb383c47df355ddb428a7331cfa1e/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b # pip coverage @ https://files.pythonhosted.org/packages/98/79/185cb42910b6a2b2851980407c8445ac0da0750dff65e420e86f973c8396/coverage-7.4.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51 # pip docutils @ https://files.pythonhosted.org/packages/26/87/f238c0670b94533ac0353a4e2a1a771a0cc73277b88bff23d3ae35a256c1/docutils-0.20.1-py3-none-any.whl#sha256=96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6 -# pip execnet @ https://files.pythonhosted.org/packages/67/14/6582043548cf796408c13454212c26acf781ead50561b6da246bdbc2bb14/execnet-2.1.0-py3-none-any.whl#sha256=ad174d7705410adc9359ba4822bad211d71cdbd59ff70304e1aa41d196b4b4d3 -# pip idna @ https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl#sha256=c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f +# pip execnet @ https://files.pythonhosted.org/packages/43/09/2aea36ff60d16dd8879bdb2f5b3ee0ba8d08cbbdcdfe870e695ce3784385/execnet-2.1.1-py3-none-any.whl#sha256=26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc +# pip idna @ https://files.pythonhosted.org/packages/e5/3e/741d8c82801c347547f8a2a06aa57dbb1992be9e948df2ea0eda2c8b79e8/idna-3.7-py3-none-any.whl#sha256=82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0 # pip imagesize @ https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl#sha256=0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b # pip iniconfig @ https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl#sha256=b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 # pip markupsafe @ https://files.pythonhosted.org/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5 From 0cdb84773e22d2d9ba1a302cb064ee750391d70d Mon Sep 17 00:00:00 2001 From: scikit-learn-bot Date: Mon, 15 Apr 2024 08:49:56 +0200 Subject: [PATCH 023/344] :lock: :robot: CI Update lock files for cirrus-arm CI build(s) :lock: :robot: (#28834) Co-authored-by: Lock file bot --- .../pymin_conda_forge_linux-aarch64_conda.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/build_tools/cirrus/pymin_conda_forge_linux-aarch64_conda.lock b/build_tools/cirrus/pymin_conda_forge_linux-aarch64_conda.lock index a61ce7f58b8bf..62e2d83af5c45 100644 --- a/build_tools/cirrus/pymin_conda_forge_linux-aarch64_conda.lock +++ b/build_tools/cirrus/pymin_conda_forge_linux-aarch64_conda.lock @@ -18,7 +18,7 @@ https://conda.anaconda.org/conda-forge/linux-aarch64/libgfortran5-13.2.0-h582850 https://conda.anaconda.org/conda-forge/linux-aarch64/libjpeg-turbo-3.0.0-h31becfc_1.conda#ed24e702928be089d9ba3f05618515c6 https://conda.anaconda.org/conda-forge/linux-aarch64/libnsl-2.0.1-h31becfc_0.conda#c14f32510f694e3185704d89967ec422 https://conda.anaconda.org/conda-forge/linux-aarch64/libuuid-2.38.1-hb4cce97_0.conda#000e30b09db0b7c775b21695dff30969 -https://conda.anaconda.org/conda-forge/linux-aarch64/libwebp-base-1.3.2-h31becfc_0.conda#1490de434d2a2c06a98af27641a2ffff +https://conda.anaconda.org/conda-forge/linux-aarch64/libwebp-base-1.3.2-h31becfc_1.conda#675c1f4aa320704b899f4eb350a69418 https://conda.anaconda.org/conda-forge/linux-aarch64/libxcrypt-4.4.36-h31becfc_1.conda#b4df5d7d4b63579d081fd3a4cf99740e https://conda.anaconda.org/conda-forge/linux-aarch64/libzlib-1.2.13-h31becfc_5.conda#b213aa87eea9491ef7b129179322e955 https://conda.anaconda.org/conda-forge/linux-aarch64/ncurses-6.4.20240210-h0425590_0.conda#c1a1612ddaee95c83abfa0b2ec858626 @@ -42,7 +42,7 @@ https://conda.anaconda.org/conda-forge/linux-aarch64/freetype-2.12.1-hf0a5ef3_2. https://conda.anaconda.org/conda-forge/linux-aarch64/libhiredis-1.0.2-h05efe27_0.tar.bz2#a87f068744fd20334cd41489eb163bee https://conda.anaconda.org/conda-forge/linux-aarch64/libopenblas-0.3.27-pthreads_h5a5ec62_0.conda#ffecca8f4f31cd50b92c0e6e6bfe4416 https://conda.anaconda.org/conda-forge/linux-aarch64/libtiff-4.6.0-hf980d43_3.conda#b6f3abf5726ae33094bee238b4eb492f -https://conda.anaconda.org/conda-forge/linux-aarch64/llvm-openmp-18.1.2-h8b0cb96_0.conda#8fb9ee33e1a87b5f114ac5c7397386f4 +https://conda.anaconda.org/conda-forge/linux-aarch64/llvm-openmp-18.1.3-h8b0cb96_0.conda#cd4d2b7580dd020814ea34ebbbca8c5e https://conda.anaconda.org/conda-forge/linux-aarch64/python-3.9.19-h4ac3b42_0_cpython.conda#1501507cd9451472ec8900d587ce872f https://conda.anaconda.org/conda-forge/linux-aarch64/brotli-1.1.0-h31becfc_1.conda#e41f5862ac746428407f3fd44d2ed01f https://conda.anaconda.org/conda-forge/linux-aarch64/ccache-4.9.1-h6552966_0.conda#758b202f61f6bbfd2c6adf0fde043276 @@ -51,7 +51,7 @@ https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhd8ed1ab_0.conda#5cd86562580f274031ede6aa6aa24441 https://conda.anaconda.org/conda-forge/linux-aarch64/cython-3.0.10-py39h387a81e_0.conda#0e917a89f77c978d152099357bd75b22 https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.0-pyhd8ed1ab_2.conda#8d652ea2ee8eaee02ed8dc820bc794aa -https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.0-pyhd8ed1ab_0.conda#7a4b32fbe5442e46841ec77695e36d96 +https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_0.conda#15dda3cdbf330abfe9f555d22f66db46 https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_0.conda#f800d2da156d08e289b14e87e43c1ae5 https://conda.anaconda.org/conda-forge/linux-aarch64/kiwisolver-1.4.5-py39had2cf8c_1.conda#ddb99610f7b950fdd5ff2aff19136363 https://conda.anaconda.org/conda-forge/linux-aarch64/lcms2-2.16-h922389a_0.conda#ffdd8267a04c515e7ce69c727b051414 @@ -62,7 +62,7 @@ https://conda.anaconda.org/conda-forge/linux-aarch64/openjpeg-2.5.2-h0d9d63b_0.c https://conda.anaconda.org/conda-forge/noarch/packaging-24.0-pyhd8ed1ab_0.conda#248f521b64ce055e7feae3105e7abeb8 https://conda.anaconda.org/conda-forge/noarch/pluggy-1.4.0-pyhd8ed1ab_0.conda#139e9feb65187e916162917bb2484976 https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.1.2-pyhd8ed1ab_0.conda#b9a4dacf97241704529131a0dfc0494f -https://conda.anaconda.org/conda-forge/noarch/setuptools-69.2.0-pyhd8ed1ab_0.conda#da214ecd521a720a9d521c68047682dc +https://conda.anaconda.org/conda-forge/noarch/setuptools-69.5.1-pyhd8ed1ab_0.conda#7462280d81f639363e6e63c81276bd9e https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2#e5f25f8dbc060e9a8d912e432202afc2 https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.4.0-pyhc1e730c_0.conda#b296278eef667c673bf51de6535bad88 https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2#5844808ffab9ebdb694585b50ba02a96 @@ -72,7 +72,7 @@ https://conda.anaconda.org/conda-forge/noarch/wheel-0.43.0-pyhd8ed1ab_1.conda#0b https://conda.anaconda.org/conda-forge/noarch/zipp-3.17.0-pyhd8ed1ab_0.conda#2e4d6bc0b14e10f895fc6791a7d9b26a https://conda.anaconda.org/conda-forge/linux-aarch64/fonttools-4.51.0-py39h898b7ef_0.conda#7b6a069c66a729454fb4c534ed145dcd https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.4.0-pyhd8ed1ab_0.conda#c5d3907ad8bd7bf557521a1833cf7e6d -https://conda.anaconda.org/conda-forge/noarch/joblib-1.3.2-pyhd8ed1ab_0.conda#4da50d410f553db77e62ab62ffaa1abc +https://conda.anaconda.org/conda-forge/noarch/joblib-1.4.0-pyhd8ed1ab_0.conda#e0ed1bf13ce3a440e022157bf4764465 https://conda.anaconda.org/conda-forge/linux-aarch64/libcblas-3.9.0-22_linuxaarch64_openblas.conda#fbe7fe553f2cc78a0311e009b26f180d https://conda.anaconda.org/conda-forge/linux-aarch64/liblapack-3.9.0-22_linuxaarch64_openblas.conda#8c709d281609792c39b1d5c0241f90f1 https://conda.anaconda.org/conda-forge/noarch/meson-1.4.0-pyhd8ed1ab_0.conda#52a0660cfa40b45bf254ecc3374cb2e0 @@ -90,5 +90,5 @@ https://conda.anaconda.org/conda-forge/linux-aarch64/blas-devel-3.9.0-22_linuxaa https://conda.anaconda.org/conda-forge/linux-aarch64/contourpy-1.2.1-py39hd16970a_0.conda#66b9718539ecdd38876b0176c315bcad https://conda.anaconda.org/conda-forge/linux-aarch64/scipy-1.13.0-py39h91c28bb_0.conda#2b6f1ed053a61c2447304e4b810fc397 https://conda.anaconda.org/conda-forge/linux-aarch64/blas-2.122-openblas.conda#65bc48b3bc85f8eeeab54311443a83aa -https://conda.anaconda.org/conda-forge/linux-aarch64/matplotlib-base-3.8.3-py39h8e43113_0.conda#4feab7c7c4593a67027ab198a90dd86a -https://conda.anaconda.org/conda-forge/linux-aarch64/matplotlib-3.8.3-py39ha65689a_0.conda#a50d805f24faf1a16c494bea4451e3b4 +https://conda.anaconda.org/conda-forge/linux-aarch64/matplotlib-base-3.8.4-py39h8e43113_0.conda#f397ddfe5c551732de61a92106a14cf3 +https://conda.anaconda.org/conda-forge/linux-aarch64/matplotlib-3.8.4-py39ha65689a_0.conda#d501bb96ff505fdd431fd8fdac8efbf9 From 6e03937873eb2017521a1d092020882dcbc93ca0 Mon Sep 17 00:00:00 2001 From: scikit-learn-bot Date: Mon, 15 Apr 2024 08:50:26 +0200 Subject: [PATCH 024/344] :lock: :robot: CI Update lock files for pypy CI build(s) :lock: :robot: (#28833) Co-authored-by: Lock file bot --- build_tools/azure/pypy3_linux-64_conda.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/build_tools/azure/pypy3_linux-64_conda.lock b/build_tools/azure/pypy3_linux-64_conda.lock index 3f898e992efd9..23710cfe35cb8 100644 --- a/build_tools/azure/pypy3_linux-64_conda.lock +++ b/build_tools/azure/pypy3_linux-64_conda.lock @@ -17,7 +17,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.6.2-h59595ed_0.conda# https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.2-h7f98852_5.tar.bz2#d645c6d2ac96843a2bfaccd2d62b3ac3 https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-13.2.0-ha4646dd_5.conda#7a6bd7a12a4bd359e2afe6c0fa1acace https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.0.0-hd590300_1.conda#ea25936bb4080d843790b586850f82b8 -https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.3.2-hd590300_0.conda#30de3fd9b3b602f7473f30e684eeea8c +https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.3.2-hd590300_1.conda#049b7df8bae5e184d1de42cdf64855f8 https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.2.13-hd590300_5.conda#f36c115f1ee199da648e0597ec2047ad https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.4.20240210-h59595ed_0.conda#97da8860a0da5413c7c98a3b3838a645 https://conda.anaconda.org/conda-forge/linux-64/ninja-1.11.1-h924138e_0.conda#73a4953a2d9c115bdc10ff30a52f675f @@ -46,7 +46,7 @@ https://conda.anaconda.org/conda-forge/linux-64/gdbm-1.18-h0a1914f_2.tar.bz2#b77 https://conda.anaconda.org/conda-forge/linux-64/libhiredis-1.0.2-h2cc385e_0.tar.bz2#b34907d3a81a3cd8095ee83d174c074a https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.27-pthreads_h413a1c8_0.conda#a356024784da6dfd4683dc5ecf45b155 https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.6.0-h1dd3fc0_3.conda#66f03896ffbe1a110ffda05c7a856504 -https://conda.anaconda.org/conda-forge/linux-64/llvm-openmp-18.1.2-h4dfa4b3_0.conda#0118c8a03e3dbbb6b348ef71e94ac7af +https://conda.anaconda.org/conda-forge/linux-64/llvm-openmp-18.1.3-h4dfa4b3_0.conda#d39965123dffcad4d750989be65bcb7c https://conda.anaconda.org/conda-forge/linux-64/sqlite-3.45.2-h2c6b66d_0.conda#1423efca06ed343c1da0fc429bae0779 https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.9-h8ee46fc_0.conda#077b6e8ad6a3ddb741fce2496dd01bec https://conda.anaconda.org/conda-forge/linux-64/brotli-1.1.0-hd590300_1.conda#f27a24d46e3ea7b70a1f98e50c62508f @@ -64,7 +64,7 @@ https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhd8ed1ab_0.conda#5cd86562580f274031ede6aa6aa24441 https://conda.anaconda.org/conda-forge/linux-64/cython-3.0.10-py39hc10206b_0.conda#60c2d58b33a21c32f469e3f6a9eb7e4b https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.0-pyhd8ed1ab_2.conda#8d652ea2ee8eaee02ed8dc820bc794aa -https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.0-pyhd8ed1ab_0.conda#7a4b32fbe5442e46841ec77695e36d96 +https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_0.conda#15dda3cdbf330abfe9f555d22f66db46 https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_0.conda#f800d2da156d08e289b14e87e43c1ae5 https://conda.anaconda.org/conda-forge/linux-64/kiwisolver-1.4.5-py39ha90811c_1.conda#25edffabcb0760fc1821597c4ce920db https://conda.anaconda.org/conda-forge/linux-64/liblapacke-3.9.0-22_linux64_openblas.conda#1fd156abd41a4992835952f6f4d951d0 @@ -75,7 +75,7 @@ https://conda.anaconda.org/conda-forge/linux-64/pillow-10.3.0-py39h90a76f3_0.con https://conda.anaconda.org/conda-forge/noarch/pluggy-1.4.0-pyhd8ed1ab_0.conda#139e9feb65187e916162917bb2484976 https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.1.2-pyhd8ed1ab_0.conda#b9a4dacf97241704529131a0dfc0494f https://conda.anaconda.org/conda-forge/noarch/pypy-7.3.15-1_pypy39.conda#a418a6c16bd6f7ed56b92194214791a0 -https://conda.anaconda.org/conda-forge/noarch/setuptools-69.2.0-pyhd8ed1ab_0.conda#da214ecd521a720a9d521c68047682dc +https://conda.anaconda.org/conda-forge/noarch/setuptools-69.5.1-pyhd8ed1ab_0.conda#7462280d81f639363e6e63c81276bd9e https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2#e5f25f8dbc060e9a8d912e432202afc2 https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.4.0-pyhc1e730c_0.conda#b296278eef667c673bf51de6535bad88 https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2#5844808ffab9ebdb694585b50ba02a96 @@ -87,7 +87,7 @@ https://conda.anaconda.org/conda-forge/linux-64/blas-devel-3.9.0-22_linux64_open https://conda.anaconda.org/conda-forge/linux-64/contourpy-1.2.1-py39ha90811c_0.conda#07ed14c8326da42356514bcbc0b04802 https://conda.anaconda.org/conda-forge/linux-64/fonttools-4.51.0-py39hf860d4a_0.conda#63421b4dd7222fad555e34ec9af015a1 https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.4.0-pyhd8ed1ab_0.conda#c5d3907ad8bd7bf557521a1833cf7e6d -https://conda.anaconda.org/conda-forge/noarch/joblib-1.3.2-pyhd8ed1ab_0.conda#4da50d410f553db77e62ab62ffaa1abc +https://conda.anaconda.org/conda-forge/noarch/joblib-1.4.0-pyhd8ed1ab_0.conda#e0ed1bf13ce3a440e022157bf4764465 https://conda.anaconda.org/conda-forge/noarch/meson-1.4.0-pyhd8ed1ab_0.conda#52a0660cfa40b45bf254ecc3374cb2e0 https://conda.anaconda.org/conda-forge/noarch/pip-24.0-pyhd8ed1ab_0.conda#f586ac1e56c8638b64f9c8122a7b8a67 https://conda.anaconda.org/conda-forge/noarch/pyproject-metadata-0.7.1-pyhd8ed1ab_0.conda#dcb27826ffc94d5f04e241322239983b @@ -99,5 +99,5 @@ https://conda.anaconda.org/conda-forge/noarch/importlib-resources-6.4.0-pyhd8ed1 https://conda.anaconda.org/conda-forge/noarch/meson-python-0.15.0-pyh0c530f3_0.conda#3bc64565ca78ce3bb80248d09926d8f9 https://conda.anaconda.org/conda-forge/linux-64/pyamg-5.1.0-py39h5fd064f_0.conda#04676d2a49da3cb608af77e04b796ce1 https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.5.0-pyhd8ed1ab_0.conda#d5f595da2daead898ca958ac62f0307b -https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.8.3-py39h4e7d633_0.conda#0b15e2f7764b1f64a5f4156ba20b090e -https://conda.anaconda.org/conda-forge/linux-64/matplotlib-3.8.3-py39h4162558_0.conda#ccb335b71aedcf24c36b2546741fb5f8 +https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.8.4-py39h4e7d633_0.conda#58272019e595dde98d0844ae3ebf0cfe +https://conda.anaconda.org/conda-forge/linux-64/matplotlib-3.8.4-py39h4162558_0.conda#b0f7702a174422ff1db58190495fd766 From 2108d908ea626e39eef896bbb63c0ae42f52a9eb Mon Sep 17 00:00:00 2001 From: scikit-learn-bot Date: Mon, 15 Apr 2024 08:51:08 +0200 Subject: [PATCH 025/344] :lock: :robot: CI Update lock files for main CI build(s) :lock: :robot: (#28835) Co-authored-by: Lock file bot --- ...latest_conda_forge_mkl_linux-64_conda.lock | 32 +++++++-------- ...pylatest_conda_forge_mkl_osx-64_conda.lock | 20 +++++----- ...st_pip_openblas_pandas_linux-64_conda.lock | 8 ++-- .../pymin_conda_forge_mkl_win-64_conda.lock | 26 ++++++------ ...e_openblas_ubuntu_2204_linux-64_conda.lock | 24 +++++------ build_tools/circle/doc_linux-64_conda.lock | 40 +++++++++---------- .../doc_min_dependencies_linux-64_conda.lock | 26 ++++++------ 7 files changed, 88 insertions(+), 88 deletions(-) diff --git a/build_tools/azure/pylatest_conda_forge_mkl_linux-64_conda.lock b/build_tools/azure/pylatest_conda_forge_mkl_linux-64_conda.lock index 2782588a4bbc7..5f8dda6fa9748 100644 --- a/build_tools/azure/pylatest_conda_forge_mkl_linux-64_conda.lock +++ b/build_tools/azure/pylatest_conda_forge_mkl_linux-64_conda.lock @@ -1,6 +1,6 @@ # Generated by conda-lock. # platform: linux-64 -# input_hash: 8c926fdb4279b181aa6ad88f79c862023c796ec1c3a5cff07cf2ea8dd3a05b0d +# input_hash: 2622dc7361d0af53cfb31534b939a13e48192a3260137ba4ec20083659c2e5fa @EXPLICIT https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2024.2.2-hbcca054_0.conda#2f4327a1cbe7f022401b236e915a5fef @@ -46,7 +46,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libogg-1.3.4-h7f98852_1.tar.bz2# https://conda.anaconda.org/conda-forge/linux-64/libopus-1.3.1-h7f98852_1.tar.bz2#15345e56d527b330e1cacbdf58676e8f https://conda.anaconda.org/conda-forge/linux-64/libutf8proc-2.8.0-h166bdaf_0.tar.bz2#ede4266dc02e875fe1ea77b25dd43747 https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda#40b61aab5c7ba9ff276c41cfffe6b80b -https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.3.2-hd590300_0.conda#30de3fd9b3b602f7473f30e684eeea8c +https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.3.2-hd590300_1.conda#049b7df8bae5e184d1de42cdf64855f8 https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda#5aa797f8787fe7a17d1b0821485b5adc https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.2.13-hd590300_5.conda#f36c115f1ee199da648e0597ec2047ad https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.9.4-hcb278e6_0.conda#318b08df404f9c9be5712aaa5a6f0bb0 @@ -60,7 +60,7 @@ https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-h36c2ea0_1001. https://conda.anaconda.org/conda-forge/linux-64/rdma-core-28.9-h59595ed_1.conda#aeffb7c06b5f65e55e6c637408dc4100 https://conda.anaconda.org/conda-forge/linux-64/re2-2023.03.02-h8c504da_0.conda#206f8fa808748f6e90599c3368a1114e https://conda.anaconda.org/conda-forge/linux-64/sleef-3.5.1-h9b69904_2.tar.bz2#6e016cf4c525d04a7bd038cee53ad3fd -https://conda.anaconda.org/conda-forge/linux-64/snappy-1.1.10-h9fff704_0.conda#e6d228cd0bb74a51dd18f5bfce0b4115 +https://conda.anaconda.org/conda-forge/linux-64/snappy-1.1.10-hdb0a2a9_1.conda#78b8b85bdf1f42b8a2b3cb577d8742d1 https://conda.anaconda.org/conda-forge/linux-64/xorg-kbproto-1.0.7-h7f98852_1002.tar.bz2#4b230e8381279d76131116660f5a241a https://conda.anaconda.org/conda-forge/linux-64/xorg-libice-1.1.1-hd590300_0.conda#b462a33c0be1421532f28bfe8f4a7514 https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.11-hd590300_0.conda#2c80dc38fface310c9bd81b17037fee5 @@ -106,15 +106,15 @@ https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.0.9-h166bdaf_9.cond https://conda.anaconda.org/conda-forge/linux-64/freetype-2.12.1-h267a509_2.conda#9ae35c3d96db2c94ce0cef86efdfa2cb https://conda.anaconda.org/conda-forge/linux-64/gettext-0.22.5-h59595ed_2.conda#219ba82e95d7614cf7140d2a4afc0926 https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.2-h659d440_0.conda#cd95826dbd331ed1be26bdf401432844 -https://conda.anaconda.org/conda-forge/linux-64/libglib-2.80.0-hf2295e7_3.conda#569d25ad54594080778abff56a611dc7 +https://conda.anaconda.org/conda-forge/linux-64/libglib-2.80.0-hf2295e7_4.conda#0269d2b7fa89f4a37cdee5ad6161f6cc https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.54.3-hb20ce57_0.conda#7af7c59ab24db007dfd82e0a3a343f66 https://conda.anaconda.org/conda-forge/linux-64/libhiredis-1.0.2-h2cc385e_0.tar.bz2#b34907d3a81a3cd8095ee83d174c074a -https://conda.anaconda.org/conda-forge/linux-64/libhwloc-2.9.3-default_h554bfaf_1009.conda#f36ddc11ca46958197a45effdd286e45 +https://conda.anaconda.org/conda-forge/linux-64/libhwloc-2.10.0-default_h2fb2949_1000.conda#7e3726e647a619c6ce5939014dfde86d https://conda.anaconda.org/conda-forge/linux-64/libllvm15-15.0.7-hb3ce162_4.conda#8a35df3cbc0c8b12cc8af9473ae75eef https://conda.anaconda.org/conda-forge/linux-64/libllvm18-18.1.3-h2448989_0.conda#927b6d6e80b2c0d4405a58b61ca248a3 https://conda.anaconda.org/conda-forge/linux-64/libthrift-0.18.1-h8fd135c_2.conda#bbf65f7688512872f063810623b755dc https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.6.0-h1dd3fc0_3.conda#66f03896ffbe1a110ffda05c7a856504 -https://conda.anaconda.org/conda-forge/linux-64/llvm-openmp-18.1.2-h4dfa4b3_0.conda#0118c8a03e3dbbb6b348ef71e94ac7af +https://conda.anaconda.org/conda-forge/linux-64/llvm-openmp-18.1.3-h4dfa4b3_0.conda#d39965123dffcad4d750989be65bcb7c https://conda.anaconda.org/conda-forge/linux-64/mysql-libs-8.3.0-hca2cd23_4.conda#1b50eebe2a738a3146c154d2eceaa8b6 https://conda.anaconda.org/conda-forge/linux-64/nss-3.98-h1d7d5a4_0.conda#54b56c2fdf973656b748e0378900ec13 https://conda.anaconda.org/conda-forge/linux-64/orc-1.9.0-h2f23424_1.conda#9571eb3eb0f7fe8b59956a7786babbcd @@ -135,9 +135,9 @@ https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhd8ed1ab_0.conda#5 https://conda.anaconda.org/conda-forge/linux-64/cython-3.0.10-py311hb755f60_0.conda#f3a8a500a2e743ff92f418f0eaf9bf71 https://conda.anaconda.org/conda-forge/linux-64/dbus-1.13.6-h5008d03_3.tar.bz2#ecfff944ba3960ecb334b9a2663d708d https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.0-pyhd8ed1ab_2.conda#8d652ea2ee8eaee02ed8dc820bc794aa -https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.0-pyhd8ed1ab_0.conda#7a4b32fbe5442e46841ec77695e36d96 +https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_0.conda#15dda3cdbf330abfe9f555d22f66db46 https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.14.2-h14ed4e7_0.conda#0f69b688f52ff6da70bccb7ff7001d1d -https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.80.0-hde27a5a_3.conda#d544517494d9008c0b1021213aec4084 +https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.80.0-hde27a5a_4.conda#c9deba4959ea5b5f72f1e3e4a71ae014 https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_0.conda#f800d2da156d08e289b14e87e43c1ae5 https://conda.anaconda.org/conda-forge/linux-64/kiwisolver-1.4.5-py311h9547e67_1.conda#2c65bdf442b0d37aad080c8a4e0d452f https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.16-hb7c19ff_0.conda#51bb7010fc86f70eee639b4bb7a894f5 @@ -156,9 +156,9 @@ https://conda.anaconda.org/conda-forge/noarch/ply-3.11-pyhd8ed1ab_2.conda#18c6de https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.1.2-pyhd8ed1ab_0.conda#b9a4dacf97241704529131a0dfc0494f https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2024.1-pyhd8ed1ab_0.conda#98206ea9954216ee7540f0c773f2104d https://conda.anaconda.org/conda-forge/noarch/pytz-2024.1-pyhd8ed1ab_0.conda#3eeeeb9e4827ace8c0c1419c85d590ad -https://conda.anaconda.org/conda-forge/noarch/setuptools-69.2.0-pyhd8ed1ab_0.conda#da214ecd521a720a9d521c68047682dc +https://conda.anaconda.org/conda-forge/noarch/setuptools-69.5.1-pyhd8ed1ab_0.conda#7462280d81f639363e6e63c81276bd9e https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2#e5f25f8dbc060e9a8d912e432202afc2 -https://conda.anaconda.org/conda-forge/linux-64/tbb-2021.11.0-h00ab1b0_1.conda#4531d2927578e7e254ff3bcf6457518c +https://conda.anaconda.org/conda-forge/linux-64/tbb-2021.12.0-h00ab1b0_0.conda#f1b776cff1b426e7e7461a8502a3b731 https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.4.0-pyhc1e730c_0.conda#b296278eef667c673bf51de6535bad88 https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_0.tar.bz2#f832c45a477c78bebd107098db465095 https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2#5844808ffab9ebdb694585b50ba02a96 @@ -174,8 +174,8 @@ https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.9.3-hb447be9_1.cond https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.0-h3faef2a_0.conda#f907bb958910dc404647326ca80c263e https://conda.anaconda.org/conda-forge/linux-64/coverage-7.4.4-py311h459d7ec_0.conda#1aa22cb84e68841ec206ee066457bdf0 https://conda.anaconda.org/conda-forge/linux-64/fonttools-4.51.0-py311h459d7ec_0.conda#17e1997cc17c571d5ad27bd0159f616c -https://conda.anaconda.org/conda-forge/linux-64/glib-2.80.0-hf2295e7_3.conda#1ade62526144055f05c3eb45ebae3b5b -https://conda.anaconda.org/conda-forge/noarch/joblib-1.3.2-pyhd8ed1ab_0.conda#4da50d410f553db77e62ab62ffaa1abc +https://conda.anaconda.org/conda-forge/linux-64/glib-2.80.0-hf2295e7_4.conda#5521382ee30b96b35eb0037fc3c4f2b4 +https://conda.anaconda.org/conda-forge/noarch/joblib-1.4.0-pyhd8ed1ab_0.conda#e0ed1bf13ce3a440e022157bf4764465 https://conda.anaconda.org/conda-forge/linux-64/libgcrypt-1.10.3-hd590300_0.conda#32d16ad533c59bb0a3c5ffaf16110829 https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-2.12.0-hac9eb74_1.conda#0dee716254497604762957076ac76540 https://conda.anaconda.org/conda-forge/linux-64/libsndfile-1.2.2-hc60ed4a_1.conda#ef1910918dd895516a769ed36b5b3a4e @@ -209,13 +209,13 @@ https://conda.anaconda.org/conda-forge/linux-64/qt-main-5.15.8-hc9dc06e_21.conda https://conda.anaconda.org/conda-forge/noarch/array-api-strict-1.1.1-pyhd8ed1ab_0.conda#941bbcd64d1a7b44aeb497f468fc85b4 https://conda.anaconda.org/conda-forge/linux-64/contourpy-1.2.1-py311h9547e67_0.conda#74ad0ae64f1ef565e27eda87fa749e84 https://conda.anaconda.org/conda-forge/linux-64/libarrow-12.0.1-hb87d912_8_cpu.conda#3f3b11398fe79b578e3c44dd00a44e4a -https://conda.anaconda.org/conda-forge/linux-64/pandas-2.2.1-py311h320fe9a_0.conda#aac8d7137fedc2fd5f8320bf50e4204c -https://conda.anaconda.org/conda-forge/linux-64/polars-0.20.18-py311h78b473b_0.conda#10f68d86f397a539ef2749b057000c74 +https://conda.anaconda.org/conda-forge/linux-64/pandas-2.2.2-py311h320fe9a_0.conda#c79e96ece4110fdaf2657c9f8e16f749 +https://conda.anaconda.org/conda-forge/linux-64/polars-0.20.20-py311h78b473b_0.conda#23bb672152980a11ad74a4af4d987fcf https://conda.anaconda.org/conda-forge/linux-64/pyqt-5.15.9-py311hf0fb5b6_5.conda#ec7e45bc76d9d0b69a74a2075932b8e8 https://conda.anaconda.org/conda-forge/linux-64/pytorch-1.13.1-cpu_py311h410fd25_1.conda#ddd2fadddf89e3dc3d541a2537fce010 https://conda.anaconda.org/conda-forge/linux-64/scipy-1.13.0-py311h64a7726_0.conda#d443c70b4a05f50236c70b9c79beff64 -https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.8.3-py311h54ef318_0.conda#014c115be880802d2372ac6ed665f526 +https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.8.4-py311h54ef318_0.conda#150186110f111b458f86c04361351337 https://conda.anaconda.org/conda-forge/linux-64/pyamg-5.1.0-py311h92ebd52_0.conda#2d415a805458e93fcf5551760fd2d287 https://conda.anaconda.org/conda-forge/linux-64/pyarrow-12.0.1-py311h39c9aba_8_cpu.conda#587370a25bb2c50cce90909ce20d38b8 https://conda.anaconda.org/conda-forge/linux-64/pytorch-cpu-1.13.1-cpu_py311hdb170b5_1.conda#a805d5f103e493f207613283d8acbbe1 -https://conda.anaconda.org/conda-forge/linux-64/matplotlib-3.8.3-py311h38be061_0.conda#0452c2cca94bdda38a16cf7b84edcd27 +https://conda.anaconda.org/conda-forge/linux-64/matplotlib-3.8.4-py311h38be061_0.conda#fd6fc4385d0eb6b00c46c4c0d28f5c48 diff --git a/build_tools/azure/pylatest_conda_forge_mkl_osx-64_conda.lock b/build_tools/azure/pylatest_conda_forge_mkl_osx-64_conda.lock index 813b739ab0091..72289e4ce587a 100644 --- a/build_tools/azure/pylatest_conda_forge_mkl_osx-64_conda.lock +++ b/build_tools/azure/pylatest_conda_forge_mkl_osx-64_conda.lock @@ -13,9 +13,9 @@ https://conda.anaconda.org/conda-forge/osx-64/libffi-3.4.2-h0d85af4_5.tar.bz2#cc https://conda.anaconda.org/conda-forge/noarch/libgfortran-devel_osx-64-12.3.0-h0b6f5ec_3.conda#39eeea5454333825d72202fae2d5e0b8 https://conda.anaconda.org/conda-forge/osx-64/libiconv-1.17-hd75f5a5_2.conda#6c3628d047e151efba7cf08c5e54d1ca https://conda.anaconda.org/conda-forge/osx-64/libjpeg-turbo-3.0.0-h0dc2134_1.conda#72507f8e3961bc968af17435060b6dd6 -https://conda.anaconda.org/conda-forge/osx-64/libwebp-base-1.3.2-h0dc2134_0.conda#4e7e9d244e87d66c18d36894fd6a8ae5 +https://conda.anaconda.org/conda-forge/osx-64/libwebp-base-1.3.2-h10d778d_1.conda#1ff09ca6e85ee516442a6a94cdfc7065 https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.2.13-h8a1eda9_5.conda#4a3ad23f6e16f99c04e166767193d700 -https://conda.anaconda.org/conda-forge/osx-64/llvm-openmp-18.1.2-hb6ac08f_0.conda#e7f7e91cfabd8c7172c9ae405214dd68 +https://conda.anaconda.org/conda-forge/osx-64/llvm-openmp-18.1.3-hb6ac08f_0.conda#506f270f4f00980d27cc1fc127e0ed37 https://conda.anaconda.org/conda-forge/osx-64/mkl-include-2023.2.0-h6bab518_50500.conda#835abb8ded5e26f23ea6996259c7972e https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.4.20240210-h73e2aa4_0.conda#50f28c512e9ad78589e3eab34833f762 https://conda.anaconda.org/conda-forge/osx-64/pthread-stubs-0.4-hc929b4f_1001.tar.bz2#addd19059de62181cd11ae8f4ef26084 @@ -44,7 +44,7 @@ https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.5-h829000d_0.conda#80abc4 https://conda.anaconda.org/conda-forge/osx-64/brotli-bin-1.1.0-h0dc2134_1.conda#ece565c215adcc47fc1db4e651ee094b https://conda.anaconda.org/conda-forge/osx-64/freetype-2.12.1-h60636b9_2.conda#25152fce119320c980e5470e64834b50 https://conda.anaconda.org/conda-forge/osx-64/libgfortran-5.0.0-13_2_0_h97931a8_3.conda#0b6e23a012ee7a9a5f6b244f5a92c1d5 -https://conda.anaconda.org/conda-forge/osx-64/libhwloc-2.9.3-default_h24e0189_1009.conda#22fcbfd2a4cdf941b074a00b773b43dd +https://conda.anaconda.org/conda-forge/osx-64/libhwloc-2.10.0-default_h1321489_1000.conda#6f5fe4374d1003e116e2573022178da6 https://conda.anaconda.org/conda-forge/osx-64/libllvm16-16.0.6-hbedff68_3.conda#8fd56c0adc07a37f93bd44aa61a97c90 https://conda.anaconda.org/conda-forge/osx-64/libtiff-4.6.0-h129831d_3.conda#568593071d2e6cea7b5fc1f75bfa10ca https://conda.anaconda.org/conda-forge/osx-64/mpfr-4.2.1-h4f6b447_1.conda#b90df08f0deb2f58631447c1462c92a7 @@ -56,7 +56,7 @@ https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhd8ed1ab_0.conda#5cd86562580f274031ede6aa6aa24441 https://conda.anaconda.org/conda-forge/osx-64/cython-3.0.10-py312hede676d_0.conda#3008aa88f0dc67e7144734b16e331ee4 https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.0-pyhd8ed1ab_2.conda#8d652ea2ee8eaee02ed8dc820bc794aa -https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.0-pyhd8ed1ab_0.conda#7a4b32fbe5442e46841ec77695e36d96 +https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_0.conda#15dda3cdbf330abfe9f555d22f66db46 https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_0.conda#f800d2da156d08e289b14e87e43c1ae5 https://conda.anaconda.org/conda-forge/osx-64/kiwisolver-1.4.5-py312h49ebfd2_1.conda#21f174a5cfb5964069c374171a979157 https://conda.anaconda.org/conda-forge/osx-64/lcms2-2.16-ha2f27b4_0.conda#1442db8f03517834843666c422238c9b @@ -72,9 +72,9 @@ https://conda.anaconda.org/conda-forge/noarch/pluggy-1.4.0-pyhd8ed1ab_0.conda#13 https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.1.2-pyhd8ed1ab_0.conda#b9a4dacf97241704529131a0dfc0494f https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2024.1-pyhd8ed1ab_0.conda#98206ea9954216ee7540f0c773f2104d https://conda.anaconda.org/conda-forge/noarch/pytz-2024.1-pyhd8ed1ab_0.conda#3eeeeb9e4827ace8c0c1419c85d590ad -https://conda.anaconda.org/conda-forge/noarch/setuptools-69.2.0-pyhd8ed1ab_0.conda#da214ecd521a720a9d521c68047682dc +https://conda.anaconda.org/conda-forge/noarch/setuptools-69.5.1-pyhd8ed1ab_0.conda#7462280d81f639363e6e63c81276bd9e https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2#e5f25f8dbc060e9a8d912e432202afc2 -https://conda.anaconda.org/conda-forge/osx-64/tbb-2021.11.0-h7728843_1.conda#29e29beba9deb0ef66bee015c5bf3c14 +https://conda.anaconda.org/conda-forge/osx-64/tbb-2021.12.0-h7728843_0.conda#e4fb6f4700d8890c36cbf317c2c6d0cb https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.4.0-pyhc1e730c_0.conda#b296278eef667c673bf51de6535bad88 https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_0.tar.bz2#f832c45a477c78bebd107098db465095 https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2#5844808ffab9ebdb694585b50ba02a96 @@ -86,7 +86,7 @@ https://conda.anaconda.org/conda-forge/osx-64/clang-16-16.0.6-default_h7151d67_6 https://conda.anaconda.org/conda-forge/osx-64/coverage-7.4.4-py312h41838bb_0.conda#b0e22bba5fbc3c8d02e25aeb33475fce https://conda.anaconda.org/conda-forge/osx-64/fonttools-4.51.0-py312h41838bb_0.conda#ebe40134b860cf704ddaf81f684f95a5 https://conda.anaconda.org/conda-forge/osx-64/gfortran_impl_osx-64-12.3.0-hc328e78_3.conda#b3d751dc7073bbfdfa9d863e39b9685d -https://conda.anaconda.org/conda-forge/noarch/joblib-1.3.2-pyhd8ed1ab_0.conda#4da50d410f553db77e62ab62ffaa1abc +https://conda.anaconda.org/conda-forge/noarch/joblib-1.4.0-pyhd8ed1ab_0.conda#e0ed1bf13ce3a440e022157bf4764465 https://conda.anaconda.org/conda-forge/osx-64/ld64-711-ha02d983_0.conda#3ae4930ec076735cce481e906f5192e0 https://conda.anaconda.org/conda-forge/noarch/meson-1.4.0-pyhd8ed1ab_0.conda#52a0660cfa40b45bf254ecc3374cb2e0 https://conda.anaconda.org/conda-forge/osx-64/mkl-2023.2.0-h54c2260_50500.conda#0a342ccdc79e4fcd359245ac51941e7b @@ -111,14 +111,14 @@ https://conda.anaconda.org/conda-forge/osx-64/numpy-1.26.4-py312he3a82b2_0.conda https://conda.anaconda.org/conda-forge/osx-64/blas-devel-3.9.0-20_osx64_mkl.conda#cc3260179093918b801e373c6e888e02 https://conda.anaconda.org/conda-forge/osx-64/compiler-rt-16.0.6-ha38d28d_2.conda#3b9e8c5c63b8e86234f499490acd85c2 https://conda.anaconda.org/conda-forge/osx-64/contourpy-1.2.1-py312h9230928_0.conda#079df34ce7c71259cfdd394645370891 -https://conda.anaconda.org/conda-forge/osx-64/pandas-2.2.1-py312h83c8a23_0.conda#c562e07382cdc3194c21b8eca06460ff +https://conda.anaconda.org/conda-forge/osx-64/pandas-2.2.2-py312h83c8a23_0.conda#b422a5d39ff0cd72923aef807f280145 https://conda.anaconda.org/conda-forge/osx-64/scipy-1.13.0-py312h8adb940_0.conda#818232a7807c76970172af9c7698ba4a https://conda.anaconda.org/conda-forge/osx-64/blas-2.120-mkl.conda#b041a7677a412f3d925d8208936cb1e2 https://conda.anaconda.org/conda-forge/osx-64/clang_impl_osx-64-16.0.6-h8787910_11.conda#ed9c90270c77481fc4cfccd0891d62a8 -https://conda.anaconda.org/conda-forge/osx-64/matplotlib-base-3.8.3-py312h1fe5000_0.conda#5f65fc4ce880d4c795e217d563a114ec +https://conda.anaconda.org/conda-forge/osx-64/matplotlib-base-3.8.4-py312h1fe5000_0.conda#3e3097734a5042cb6d2675e69bf1fc5a https://conda.anaconda.org/conda-forge/osx-64/pyamg-5.1.0-py312h3db3e91_0.conda#c6d6248b99fc11b15c9becea581a1462 https://conda.anaconda.org/conda-forge/osx-64/clang_osx-64-16.0.6-hb91bd55_11.conda#24123b15e9c0dad9c0d5fd9da0b4c7a9 -https://conda.anaconda.org/conda-forge/osx-64/matplotlib-3.8.3-py312hb401068_0.conda#7015bf84c9d39284c4746d814da2a0f1 +https://conda.anaconda.org/conda-forge/osx-64/matplotlib-3.8.4-py312hb401068_0.conda#187ee42addd449b4899b55c304012436 https://conda.anaconda.org/conda-forge/osx-64/c-compiler-1.7.0-h282daa2_0.conda#4652f33fe8d895f61177e2783b289377 https://conda.anaconda.org/conda-forge/osx-64/clangxx_impl_osx-64-16.0.6-h6d92fbe_11.conda#a658c595675bde00373347b22a974810 https://conda.anaconda.org/conda-forge/osx-64/gfortran_osx-64-12.3.0-h18f7dce_1.conda#436af2384c47aedb94af78a128e174f1 diff --git a/build_tools/azure/pylatest_pip_openblas_pandas_linux-64_conda.lock b/build_tools/azure/pylatest_pip_openblas_pandas_linux-64_conda.lock index a27e427ff1e88..b2c4d1c5163e4 100644 --- a/build_tools/azure/pylatest_pip_openblas_pandas_linux-64_conda.lock +++ b/build_tools/azure/pylatest_pip_openblas_pandas_linux-64_conda.lock @@ -1,6 +1,6 @@ # Generated by conda-lock. # platform: linux-64 -# input_hash: ec17ebe1c3fd0875fcc62f7df11f43ebdc905b745921603a574f023e92480fe0 +# input_hash: d4063b0b99f7a39e30c5f6e2d9c5dd293d9b206ce326841bf811534ea1be79f0 @EXPLICIT https://repo.anaconda.com/pkgs/main/linux-64/_libgcc_mutex-0.1-main.conda#c3473ff8bdb3d124ed5ff11ec380d6f9 https://repo.anaconda.com/pkgs/main/linux-64/ca-certificates-2024.3.11-h06a4308_0.conda#08529eb3504712baabcbda266a19feb7 @@ -33,10 +33,10 @@ https://repo.anaconda.com/pkgs/main/linux-64/pip-23.3.1-py39h06a4308_0.conda#685 # pip exceptiongroup @ https://files.pythonhosted.org/packages/b8/9a/5028fd52db10e600f1c4674441b968cf2ea4959085bfb5b99fb1250e5f68/exceptiongroup-1.2.0-py3-none-any.whl#sha256=4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14 # pip execnet @ https://files.pythonhosted.org/packages/43/09/2aea36ff60d16dd8879bdb2f5b3ee0ba8d08cbbdcdfe870e695ce3784385/execnet-2.1.1-py3-none-any.whl#sha256=26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc # pip fonttools @ https://files.pythonhosted.org/packages/8b/c6/636f008104908a93b80419f756be755bb91df4b8a0c88d5158bb52c82c3a/fonttools-4.51.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=0d145976194a5242fdd22df18a1b451481a88071feadf251221af110ca8f00ce -# pip idna @ https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl#sha256=c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f +# pip idna @ https://files.pythonhosted.org/packages/e5/3e/741d8c82801c347547f8a2a06aa57dbb1992be9e948df2ea0eda2c8b79e8/idna-3.7-py3-none-any.whl#sha256=82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0 # pip imagesize @ https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl#sha256=0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b # pip iniconfig @ https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl#sha256=b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 -# pip joblib @ https://files.pythonhosted.org/packages/10/40/d551139c85db202f1f384ba8bcf96aca2f329440a844f924c8a0040b6d02/joblib-1.3.2-py3-none-any.whl#sha256=ef4331c65f239985f3f2220ecc87db222f08fd22097a3dd5698f693875f8cbb9 +# pip joblib @ https://files.pythonhosted.org/packages/ae/e2/4dea6313ef2b38442fccbbaf4017e50a6c3c8a50e8ee9b512783e5c90409/joblib-1.4.0-py3-none-any.whl#sha256=42942470d4062537be4d54c83511186da1fc14ba354961a2114da91efa9a4ed7 # pip kiwisolver @ https://files.pythonhosted.org/packages/c0/a8/841594f11d0b88d8aeb26991bc4dac38baa909dc58d0c4262a4f7893bcbf/kiwisolver-1.4.5-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl#sha256=6c3bd3cde54cafb87d74d8db50b909705c62b17c2099b8f2e25b461882e544ff # pip markupsafe @ https://files.pythonhosted.org/packages/5f/5a/360da85076688755ea0cceb92472923086993e86b5613bbae9fbc14136b0/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3 # pip meson @ https://files.pythonhosted.org/packages/33/75/b1a37fa7b2dbca8c0dbb04d5cdd7e2720c8ef6febe41b4a74866350e041c/meson-1.4.0-py3-none-any.whl#sha256=476a458d51fcfa322a6bdc64da5138997c542d08e6b2e49b9fa68c46fd7c4475 @@ -79,7 +79,7 @@ https://repo.anaconda.com/pkgs/main/linux-64/pip-23.3.1-py39h06a4308_0.conda#685 # pip lightgbm @ https://files.pythonhosted.org/packages/ba/11/cb8b67f3cbdca05b59a032bb57963d4fe8c8d18c3870f30bed005b7f174d/lightgbm-4.3.0-py3-none-manylinux_2_28_x86_64.whl#sha256=104496a3404cb2452d3412cbddcfbfadbef9c372ea91e3a9b8794bcc5183bf07 # pip matplotlib @ https://files.pythonhosted.org/packages/5e/2c/513395a63a9e1124a5648addbf73be23cc603f955af026b04416da98dc96/matplotlib-3.8.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=606e3b90897554c989b1e38a258c626d46c873523de432b1462f295db13de6f9 # pip meson-python @ https://files.pythonhosted.org/packages/1f/60/b10b11ab470a690d5777310d6cfd1c9bdbbb0a1313a78c34a1e82e0b9d27/meson_python-0.15.0-py3-none-any.whl#sha256=3ae38253ff02b2e947a05e362a2eaf5a9a09d133c5666b4123399ee5fbf2e591 -# pip pandas @ https://files.pythonhosted.org/packages/1a/5e/71bb0eef0dc543f7516d9ddeca9ee8dc98207043784e3f7e6c08b4a6b3d9/pandas-2.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=f9d3558d263073ed95e46f4650becff0c5e1ffe0fc3a015de3c79283dfbdb3df +# pip pandas @ https://files.pythonhosted.org/packages/bb/30/f6f1f1ac36250f50c421b1b6af08c35e5a8b5a84385ef928625336b93e6f/pandas-2.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=66b479b0bd07204e37583c191535505410daa8df638fd8e75ae1b383851fe921 # pip pyamg @ https://files.pythonhosted.org/packages/68/a9/aed9f557e7eb779d2cb4fa090663f8540979e0c04dadd16e9a0bdc9632c5/pyamg-5.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=5817d4567fb240dab4779bb1630bbb3035b3827731fcdaeb9ecc9c8814319995 # pip pytest-cov @ https://files.pythonhosted.org/packages/78/3a/af5b4fa5961d9a1e6237b530eb87dd04aea6eb83da09d2a4073d81b54ccf/pytest_cov-5.0.0-py3-none-any.whl#sha256=4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652 # pip pytest-xdist @ https://files.pythonhosted.org/packages/50/37/125fe5ec459321e2d48a0c38672cfc2419ad87d580196fd894e5f25230b0/pytest_xdist-3.5.0-py3-none-any.whl#sha256=d075629c7e00b611df89f490a5063944bee7a4362a5ff11c7cc7824a03dfce24 diff --git a/build_tools/azure/pymin_conda_forge_mkl_win-64_conda.lock b/build_tools/azure/pymin_conda_forge_mkl_win-64_conda.lock index 504376f1972aa..c6983ba7a9f51 100644 --- a/build_tools/azure/pymin_conda_forge_mkl_win-64_conda.lock +++ b/build_tools/azure/pymin_conda_forge_mkl_win-64_conda.lock @@ -1,9 +1,9 @@ # Generated by conda-lock. # platform: win-64 -# input_hash: 4a5b218b482447b924e82b14fd1757d34558e8e3486d07feb57c6d50002e37c7 +# input_hash: 4a2ac6360285edd6c1e8182dd51ef698c0041fa9843e4ad9d9bc9dec6a7c8d1d @EXPLICIT https://conda.anaconda.org/conda-forge/win-64/ca-certificates-2024.2.2-h56e8100_0.conda#63da060240ab8087b60d1357051ea7d6 -https://conda.anaconda.org/conda-forge/win-64/intel-openmp-2024.1.0-h57928b3_964.conda#30ebb9fd99666d8b8675fcee541a09f3 +https://conda.anaconda.org/conda-forge/win-64/intel-openmp-2024.1.0-h57928b3_965.conda#c66eb2fd33b999ccc258aef85689758e https://conda.anaconda.org/conda-forge/win-64/libasprintf-0.22.5-h5728263_2.conda#75a6982b9ff0a8db0f53303527b07af8 https://conda.anaconda.org/conda-forge/win-64/mkl-include-2024.1.0-h66d3029_692.conda#60233966dc7c0261c9a443120b43c477 https://conda.anaconda.org/conda-forge/win-64/msys2-conda-epoch-20160418-1.tar.bz2#b0309b72560df66f71a9d5e34a5efdfa @@ -27,7 +27,7 @@ https://conda.anaconda.org/conda-forge/win-64/libiconv-1.17-hcfcfb64_2.conda#e1e https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.0.0-hcfcfb64_1.conda#3f1b948619c45b1ca714d60c7389092c https://conda.anaconda.org/conda-forge/win-64/libogg-1.3.4-h8ffe710_1.tar.bz2#04286d905a0dcb7f7d4a12bdfe02516d https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.45.2-hcfcfb64_0.conda#f95359f8dc5abf7da7776ece9ef10bc5 -https://conda.anaconda.org/conda-forge/win-64/libwebp-base-1.3.2-hcfcfb64_0.conda#dcde8820959e64378d4e06147ffecfdd +https://conda.anaconda.org/conda-forge/win-64/libwebp-base-1.3.2-hcfcfb64_1.conda#fdf80cb33c32d4d002bb89c37cfff5b7 https://conda.anaconda.org/conda-forge/win-64/libzlib-1.2.13-hcfcfb64_5.conda#5fdb9c6a113b6b6cb5e517fd972d5f41 https://conda.anaconda.org/conda-forge/win-64/m2w64-gcc-libgfortran-5.3.0-6.tar.bz2#066552ac6b907ec6d72c0ddab29050dc https://conda.anaconda.org/conda-forge/win-64/ninja-1.11.1-h91493d7_0.conda#44a99ef26178ea98626ff8e027702795 @@ -52,15 +52,15 @@ https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhd8ed1ab_0.conda#5cd86562580f274031ede6aa6aa24441 https://conda.anaconda.org/conda-forge/win-64/cython-3.0.10-py39h99910a6_0.conda#8ebc2fca8a6840d0694f37e698f4e59c https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.0-pyhd8ed1ab_2.conda#8d652ea2ee8eaee02ed8dc820bc794aa -https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.0-pyhd8ed1ab_0.conda#7a4b32fbe5442e46841ec77695e36d96 +https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_0.conda#15dda3cdbf330abfe9f555d22f66db46 https://conda.anaconda.org/conda-forge/win-64/freetype-2.12.1-hdaf720e_2.conda#3761b23693f768dc75a8fd0a73ca053f https://conda.anaconda.org/conda-forge/win-64/gettext-tools-0.22.5-h7d00a51_2.conda#ef1c3bb48c013099c4872640a5f2096c https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_0.conda#f800d2da156d08e289b14e87e43c1ae5 https://conda.anaconda.org/conda-forge/win-64/kiwisolver-1.4.5-py39h1f6ef14_1.conda#4fc5bd0a7b535252028c647cc27d6c87 https://conda.anaconda.org/conda-forge/win-64/libclang13-18.1.3-default_hf64faad_0.conda#9217c37b478ec601af909aafc954a6fc https://conda.anaconda.org/conda-forge/win-64/libgettextpo-0.22.5-h5728263_2.conda#f4c826b19bf1ccee2a63a2c685039728 -https://conda.anaconda.org/conda-forge/win-64/libglib-2.80.0-h39d0aa6_3.conda#6ed359e5ae622059d4d2306328314bf5 -https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.9.3-default_haede6df_1009.conda#87da045f6d26ce9fe20ad76a18f6a18a +https://conda.anaconda.org/conda-forge/win-64/libglib-2.80.0-h39d0aa6_4.conda#9baf04fc7002450a932cf97cb9625ebb +https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.10.0-default_h2fffb23_1000.conda#ee944f0d41d9e2048f9d7492c1623ca3 https://conda.anaconda.org/conda-forge/win-64/libintl-devel-0.22.5-h5728263_2.conda#a2ad82fae23975e4ccbfab2847d31d48 https://conda.anaconda.org/conda-forge/win-64/libtiff-4.6.0-hddb2be6_3.conda#6d1828c9039929e2f185c5fa9d133018 https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyh9f0ad1d_0.tar.bz2#2ba8498c1018c1e9c61eb99b973dfe19 @@ -69,7 +69,7 @@ https://conda.anaconda.org/conda-forge/noarch/pluggy-1.4.0-pyhd8ed1ab_0.conda#13 https://conda.anaconda.org/conda-forge/noarch/ply-3.11-pyhd8ed1ab_2.conda#18c6deb6f9602e32446398203c8f0e91 https://conda.anaconda.org/conda-forge/win-64/pthread-stubs-0.4-hcd874cb_1001.tar.bz2#a1f820480193ea83582b13249a7e7bd9 https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.1.2-pyhd8ed1ab_0.conda#b9a4dacf97241704529131a0dfc0494f -https://conda.anaconda.org/conda-forge/noarch/setuptools-69.2.0-pyhd8ed1ab_0.conda#da214ecd521a720a9d521c68047682dc +https://conda.anaconda.org/conda-forge/noarch/setuptools-69.5.1-pyhd8ed1ab_0.conda#7462280d81f639363e6e63c81276bd9e https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2#e5f25f8dbc060e9a8d912e432202afc2 https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.4.0-pyhc1e730c_0.conda#b296278eef667c673bf51de6535bad88 https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_0.tar.bz2#f832c45a477c78bebd107098db465095 @@ -82,9 +82,9 @@ https://conda.anaconda.org/conda-forge/win-64/xorg-libxdmcp-1.1.3-hcd874cb_0.tar https://conda.anaconda.org/conda-forge/noarch/zipp-3.17.0-pyhd8ed1ab_0.conda#2e4d6bc0b14e10f895fc6791a7d9b26a https://conda.anaconda.org/conda-forge/win-64/brotli-1.1.0-hcfcfb64_1.conda#f47f6db2528e38321fb00ae31674c133 https://conda.anaconda.org/conda-forge/win-64/coverage-7.4.4-py39ha55989b_0.conda#ca4fca57e0e713af82c73a9e6c5b9716 -https://conda.anaconda.org/conda-forge/win-64/glib-tools-2.80.0-h0a98069_3.conda#baef876a13714d6b0c86b25b233d410c +https://conda.anaconda.org/conda-forge/win-64/glib-tools-2.80.0-h0a98069_4.conda#44cd44c004db728bf5788c1d46ce7334 https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.4.0-pyhd8ed1ab_0.conda#c5d3907ad8bd7bf557521a1833cf7e6d -https://conda.anaconda.org/conda-forge/noarch/joblib-1.3.2-pyhd8ed1ab_0.conda#4da50d410f553db77e62ab62ffaa1abc +https://conda.anaconda.org/conda-forge/noarch/joblib-1.4.0-pyhd8ed1ab_0.conda#e0ed1bf13ce3a440e022157bf4764465 https://conda.anaconda.org/conda-forge/win-64/lcms2-2.16-h67d730c_0.conda#d3592435917b62a8becff3a60db674f6 https://conda.anaconda.org/conda-forge/win-64/libgettextpo-devel-0.22.5-h5728263_2.conda#6f42ec61abc6d52a4079800a640319c5 https://conda.anaconda.org/conda-forge/win-64/libxcb-1.15-hcd874cb_0.conda#090d91b69396f14afef450c285f9758c @@ -95,10 +95,10 @@ https://conda.anaconda.org/conda-forge/noarch/pyproject-metadata-0.7.1-pyhd8ed1a https://conda.anaconda.org/conda-forge/noarch/pytest-7.4.4-pyhd8ed1ab_0.conda#a9d145de8c5f064b5fa68fb34725d9f4 https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0-pyhd8ed1ab_0.conda#2cf4264fffb9e6eff6031c5b6884d61c https://conda.anaconda.org/conda-forge/win-64/sip-6.7.12-py39h99910a6_0.conda#0cc5774390ada632ed7975203057c91c -https://conda.anaconda.org/conda-forge/win-64/tbb-2021.11.0-h91493d7_1.conda#21069f3ed16812f9f4f2700667b6ec86 +https://conda.anaconda.org/conda-forge/win-64/tbb-2021.12.0-h91493d7_0.conda#21745fdd12f01b41178596143cbecffd https://conda.anaconda.org/conda-forge/win-64/fonttools-4.51.0-py39ha55989b_0.conda#5d19302bab29e347116b743e793aa7d6 https://conda.anaconda.org/conda-forge/win-64/gettext-0.22.5-h5728263_2.conda#da84216f88a8c89eb943c683ceb34d7d -https://conda.anaconda.org/conda-forge/win-64/glib-2.80.0-h39d0aa6_3.conda#53b689f4e44aaa40923441920fc18114 +https://conda.anaconda.org/conda-forge/win-64/glib-2.80.0-h39d0aa6_4.conda#d0a05e8a76abb68b2beb7b16c6d49213 https://conda.anaconda.org/conda-forge/noarch/importlib-resources-6.4.0-pyhd8ed1ab_0.conda#dcbadab7a68738a028e195ab68ab2d2e https://conda.anaconda.org/conda-forge/noarch/meson-python-0.15.0-pyh0c530f3_0.conda#3bc64565ca78ce3bb80248d09926d8f9 https://conda.anaconda.org/conda-forge/win-64/mkl-2024.1.0-h66d3029_692.conda#b43ec7ed045323edeff31e348eea8652 @@ -120,5 +120,5 @@ https://conda.anaconda.org/conda-forge/win-64/contourpy-1.2.1-py39h1f6ef14_0.con https://conda.anaconda.org/conda-forge/win-64/pyqt-5.15.9-py39hb77abff_5.conda#5ed899124a51958336371ff01482b8fd https://conda.anaconda.org/conda-forge/win-64/scipy-1.13.0-py39hddb5d58_0.conda#cfe749056fb9ed9dbc096b5751becf34 https://conda.anaconda.org/conda-forge/win-64/blas-2.122-mkl.conda#aee642435696de144ddf91dc02101cf8 -https://conda.anaconda.org/conda-forge/win-64/matplotlib-base-3.8.3-py39hf19769e_0.conda#e7a42adb568586ff4035d7ef2d06c4b1 -https://conda.anaconda.org/conda-forge/win-64/matplotlib-3.8.3-py39hcbf5309_0.conda#a4b5946f68ecaed034fa849b8d639e63 +https://conda.anaconda.org/conda-forge/win-64/matplotlib-base-3.8.4-py39hf19769e_0.conda#7836c3dc5814f6d55a7392657c576e88 +https://conda.anaconda.org/conda-forge/win-64/matplotlib-3.8.4-py39hcbf5309_0.conda#cc66c372d5eb745665da06ce56b7d72b diff --git a/build_tools/azure/pymin_conda_forge_openblas_ubuntu_2204_linux-64_conda.lock b/build_tools/azure/pymin_conda_forge_openblas_ubuntu_2204_linux-64_conda.lock index 0b492f9472b7c..7f06496a7e247 100644 --- a/build_tools/azure/pymin_conda_forge_openblas_ubuntu_2204_linux-64_conda.lock +++ b/build_tools/azure/pymin_conda_forge_openblas_ubuntu_2204_linux-64_conda.lock @@ -1,6 +1,6 @@ # Generated by conda-lock. # platform: linux-64 -# input_hash: 10cb97dbddc53d186aedde20d0f8fe610a929d2ebcd3741c4eebdca4bac05de4 +# input_hash: a64ed7d3cc839a12cb1faa238a89d4aec55abc43d335791f0e8422f5722ff662 @EXPLICIT https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2024.2.2-hbcca054_0.conda#2f4327a1cbe7f022401b236e915a5fef @@ -38,7 +38,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda#30 https://conda.anaconda.org/conda-forge/linux-64/libogg-1.3.4-h7f98852_1.tar.bz2#6e8cc2173440d77708196c5b93771680 https://conda.anaconda.org/conda-forge/linux-64/libopus-1.3.1-h7f98852_1.tar.bz2#15345e56d527b330e1cacbdf58676e8f https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda#40b61aab5c7ba9ff276c41cfffe6b80b -https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.3.2-hd590300_0.conda#30de3fd9b3b602f7473f30e684eeea8c +https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.3.2-hd590300_1.conda#049b7df8bae5e184d1de42cdf64855f8 https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda#5aa797f8787fe7a17d1b0821485b5adc https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.2.13-hd590300_5.conda#f36c115f1ee199da648e0597ec2047ad https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.9.4-hcb278e6_0.conda#318b08df404f9c9be5712aaa5a6f0bb0 @@ -83,13 +83,13 @@ https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.1.0-hd590300_1.cond https://conda.anaconda.org/conda-forge/linux-64/freetype-2.12.1-h267a509_2.conda#9ae35c3d96db2c94ce0cef86efdfa2cb https://conda.anaconda.org/conda-forge/linux-64/gettext-0.22.5-h59595ed_2.conda#219ba82e95d7614cf7140d2a4afc0926 https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.2-h659d440_0.conda#cd95826dbd331ed1be26bdf401432844 -https://conda.anaconda.org/conda-forge/linux-64/libglib-2.80.0-hf2295e7_3.conda#569d25ad54594080778abff56a611dc7 +https://conda.anaconda.org/conda-forge/linux-64/libglib-2.80.0-hf2295e7_4.conda#0269d2b7fa89f4a37cdee5ad6161f6cc https://conda.anaconda.org/conda-forge/linux-64/libhiredis-1.0.2-h2cc385e_0.tar.bz2#b34907d3a81a3cd8095ee83d174c074a https://conda.anaconda.org/conda-forge/linux-64/libllvm15-15.0.7-hb3ce162_4.conda#8a35df3cbc0c8b12cc8af9473ae75eef https://conda.anaconda.org/conda-forge/linux-64/libllvm18-18.1.3-h2448989_0.conda#927b6d6e80b2c0d4405a58b61ca248a3 https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.27-pthreads_h413a1c8_0.conda#a356024784da6dfd4683dc5ecf45b155 https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.6.0-h1dd3fc0_3.conda#66f03896ffbe1a110ffda05c7a856504 -https://conda.anaconda.org/conda-forge/linux-64/llvm-openmp-18.1.2-h4dfa4b3_0.conda#0118c8a03e3dbbb6b348ef71e94ac7af +https://conda.anaconda.org/conda-forge/linux-64/llvm-openmp-18.1.3-h4dfa4b3_0.conda#d39965123dffcad4d750989be65bcb7c https://conda.anaconda.org/conda-forge/linux-64/mysql-libs-8.3.0-hca2cd23_4.conda#1b50eebe2a738a3146c154d2eceaa8b6 https://conda.anaconda.org/conda-forge/linux-64/nss-3.98-h1d7d5a4_0.conda#54b56c2fdf973656b748e0378900ec13 https://conda.anaconda.org/conda-forge/linux-64/python-3.9.19-h0755675_0_cpython.conda#d9ee3647fbd9e8595b8df759b2bbefb8 @@ -110,9 +110,9 @@ https://conda.anaconda.org/conda-forge/linux-64/cython-3.0.10-py39h3d6467e_0.con https://conda.anaconda.org/conda-forge/linux-64/dbus-1.13.6-h5008d03_3.tar.bz2#ecfff944ba3960ecb334b9a2663d708d https://conda.anaconda.org/conda-forge/linux-64/docutils-0.20.1-py39hf3d152e_3.conda#09a48956e1c155907fd0d626f3e80f2e https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.0-pyhd8ed1ab_2.conda#8d652ea2ee8eaee02ed8dc820bc794aa -https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.0-pyhd8ed1ab_0.conda#7a4b32fbe5442e46841ec77695e36d96 +https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_0.conda#15dda3cdbf330abfe9f555d22f66db46 https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.14.2-h14ed4e7_0.conda#0f69b688f52ff6da70bccb7ff7001d1d -https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.80.0-hde27a5a_3.conda#d544517494d9008c0b1021213aec4084 +https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.80.0-hde27a5a_4.conda#c9deba4959ea5b5f72f1e3e4a71ae014 https://conda.anaconda.org/conda-forge/noarch/idna-3.6-pyhd8ed1ab_0.conda#1a76f09108576397c41c0b0c5bd84134 https://conda.anaconda.org/conda-forge/noarch/imagesize-1.4.1-pyhd8ed1ab_0.tar.bz2#7de5386c8fea29e76b303f37dde4c352 https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_0.conda#f800d2da156d08e289b14e87e43c1ae5 @@ -137,7 +137,7 @@ https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.1.2-pyhd8ed1ab_0.conda https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha2e5f31_6.tar.bz2#2a7de29fb590ca14b5243c4c812c8025 https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2024.1-pyhd8ed1ab_0.conda#98206ea9954216ee7540f0c773f2104d https://conda.anaconda.org/conda-forge/noarch/pytz-2024.1-pyhd8ed1ab_0.conda#3eeeeb9e4827ace8c0c1419c85d590ad -https://conda.anaconda.org/conda-forge/noarch/setuptools-69.2.0-pyhd8ed1ab_0.conda#da214ecd521a720a9d521c68047682dc +https://conda.anaconda.org/conda-forge/noarch/setuptools-69.5.1-pyhd8ed1ab_0.conda#7462280d81f639363e6e63c81276bd9e https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2#e5f25f8dbc060e9a8d912e432202afc2 https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-2.2.0-pyhd8ed1ab_0.tar.bz2#4d22a9315e78c6827f806065957d566e https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_0.conda#da1d979339e2714c30a8e806a33ec087 @@ -156,11 +156,11 @@ https://conda.anaconda.org/conda-forge/noarch/zipp-3.17.0-pyhd8ed1ab_0.conda#2e4 https://conda.anaconda.org/conda-forge/noarch/babel-2.14.0-pyhd8ed1ab_0.conda#9669586875baeced8fc30c0826c3270e https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.0-h3faef2a_0.conda#f907bb958910dc404647326ca80c263e https://conda.anaconda.org/conda-forge/linux-64/fonttools-4.51.0-py39hd1e30aa_0.conda#79f5dd8778873faa54e8f7b2729fe8a6 -https://conda.anaconda.org/conda-forge/linux-64/glib-2.80.0-hf2295e7_3.conda#1ade62526144055f05c3eb45ebae3b5b +https://conda.anaconda.org/conda-forge/linux-64/glib-2.80.0-hf2295e7_4.conda#5521382ee30b96b35eb0037fc3c4f2b4 https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-7.1.0-pyha770c72_0.conda#0896606848b2dc5cebdf111b6543aa04 https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.4.0-pyhd8ed1ab_0.conda#c5d3907ad8bd7bf557521a1833cf7e6d https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.3-pyhd8ed1ab_0.conda#e7d8df6509ba635247ff9aea31134262 -https://conda.anaconda.org/conda-forge/noarch/joblib-1.3.2-pyhd8ed1ab_0.conda#4da50d410f553db77e62ab62ffaa1abc +https://conda.anaconda.org/conda-forge/noarch/joblib-1.4.0-pyhd8ed1ab_0.conda#e0ed1bf13ce3a440e022157bf4764465 https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-22_linux64_openblas.conda#4b31699e0ec5de64d5896e580389c9a1 https://conda.anaconda.org/conda-forge/linux-64/libgcrypt-1.10.3-hd590300_0.conda#32d16ad533c59bb0a3c5ffaf16110829 https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-22_linux64_openblas.conda#b083767b6c877e24ee597d93b87ab838 @@ -187,15 +187,15 @@ https://conda.anaconda.org/conda-forge/noarch/requests-2.31.0-pyhd8ed1ab_0.conda https://conda.anaconda.org/conda-forge/linux-64/blas-devel-3.9.0-22_linux64_openblas.conda#63ddb593595c9cf5eb08d3de54d66df8 https://conda.anaconda.org/conda-forge/linux-64/contourpy-1.2.1-py39h7633fee_0.conda#bdc188e59857d6efab332714e0d01d93 https://conda.anaconda.org/conda-forge/linux-64/gst-plugins-base-1.24.1-hfa15dee_1.conda#a6dd2bbc684913e2bef0a54ce56fcbfb -https://conda.anaconda.org/conda-forge/linux-64/pandas-2.2.1-py39hddac248_0.conda#85293a042c24a08e71b7608ee66b6134 +https://conda.anaconda.org/conda-forge/linux-64/pandas-2.2.2-py39hddac248_0.conda#259c4e76e6bda8888aefc098ae1ba749 https://conda.anaconda.org/conda-forge/linux-64/pulseaudio-client-17.0-hb77b528_0.conda#07f45f1be1c25345faddb8db0de8039b https://conda.anaconda.org/conda-forge/linux-64/scipy-1.13.0-py39h474f0d3_0.conda#46ae0ecba9726ab4fa44c78fefa522cf https://conda.anaconda.org/conda-forge/linux-64/blas-2.122-openblas.conda#5065468105542a8b23ea47bd8b6fa55f -https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.8.3-py39he9076e7_0.conda#5456bdfe5809ebf5689eda6c808b686e +https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.8.4-py39he9076e7_0.conda#1919384a8420e7bb25f6c3a582e0857c https://conda.anaconda.org/conda-forge/linux-64/pyamg-5.1.0-py39hda80f44_0.conda#f225666c47726329201b604060f1436c https://conda.anaconda.org/conda-forge/linux-64/qt-main-5.15.8-hc9dc06e_21.conda#b325046180590c868ce0dbf267b82eb8 https://conda.anaconda.org/conda-forge/linux-64/pyqt-5.15.9-py39h52134e7_5.conda#e1f148e57d071b09187719df86f513c1 -https://conda.anaconda.org/conda-forge/linux-64/matplotlib-3.8.3-py39hf3d152e_0.conda#983f5b77540eb5aa00238e72ec9b1dfb +https://conda.anaconda.org/conda-forge/linux-64/matplotlib-3.8.4-py39hf3d152e_0.conda#c66d2da2669fddc657b679bccab95775 https://conda.anaconda.org/conda-forge/noarch/numpydoc-1.7.0-pyhd8ed1ab_0.conda#1ad3afced398492586ca1bef70328be4 https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-1.0.8-pyhd8ed1ab_0.conda#611a35a27914fac3aa37611a6fe40bb5 https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-devhelp-1.0.6-pyhd8ed1ab_0.conda#d7e4954df0d3aea2eacc7835ad12671d diff --git a/build_tools/circle/doc_linux-64_conda.lock b/build_tools/circle/doc_linux-64_conda.lock index 5a4b8f07a5f70..984cca332fc7f 100644 --- a/build_tools/circle/doc_linux-64_conda.lock +++ b/build_tools/circle/doc_linux-64_conda.lock @@ -1,6 +1,6 @@ # Generated by conda-lock. # platform: linux-64 -# input_hash: a4de2b553c76a22b14e7e280d371d037c33b6382232c86ae48cc695bbed65852 +# input_hash: b57888763997b08b2f240b5ff1ed6afcf88685f3d8c791ea8eba4d80483c43d0 @EXPLICIT https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2024.2.2-hbcca054_0.conda#2f4327a1cbe7f022401b236e915a5fef @@ -31,7 +31,7 @@ https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hd590300_5.conda#69b https://conda.anaconda.org/conda-forge/linux-64/charls-2.4.2-h59595ed_0.conda#4336bd67920dd504cd8c6761d6a99645 https://conda.anaconda.org/conda-forge/linux-64/dav1d-1.2.1-hd590300_0.conda#418c6ca5929a611cbd69204907a83995 https://conda.anaconda.org/conda-forge/linux-64/gettext-tools-0.22.5-h59595ed_2.conda#985f2f453fb72408d6b6f1be0f324033 -https://conda.anaconda.org/conda-forge/linux-64/giflib-5.2.1-h0b41bf4_3.conda#96f3b11872ef6fad973eac856cd2624f +https://conda.anaconda.org/conda-forge/linux-64/giflib-5.2.2-hd590300_0.conda#3bf7b9fd5a7136126e0234db4b87c8b6 https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.13-h59595ed_1003.conda#f87c7b7c2cb45f323ffbce941c78ab7c https://conda.anaconda.org/conda-forge/linux-64/icu-73.2-h59595ed_0.conda#cc47e1facc155f91abd89b11e48e72ff https://conda.anaconda.org/conda-forge/linux-64/jxrlib-1.1-hd590300_3.conda#5aeabe88534ea4169d4c49998f293d6c @@ -54,7 +54,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libogg-1.3.4-h7f98852_1.tar.bz2# https://conda.anaconda.org/conda-forge/linux-64/libopus-1.3.1-h7f98852_1.tar.bz2#15345e56d527b330e1cacbdf58676e8f https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-12.3.0-h0f45ef3_5.conda#11d1ceacff40054d5a74b12975d76f20 https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda#40b61aab5c7ba9ff276c41cfffe6b80b -https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.3.2-hd590300_0.conda#30de3fd9b3b602f7473f30e684eeea8c +https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.3.2-hd590300_1.conda#049b7df8bae5e184d1de42cdf64855f8 https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda#5aa797f8787fe7a17d1b0821485b5adc https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.2.13-hd590300_5.conda#f36c115f1ee199da648e0597ec2047ad https://conda.anaconda.org/conda-forge/linux-64/libzopfli-1.0.3-h9c3ff4c_0.tar.bz2#c66fe2d123249af7651ebde8984c51c2 @@ -67,7 +67,7 @@ https://conda.anaconda.org/conda-forge/linux-64/openssl-3.2.1-hd590300_1.conda#9 https://conda.anaconda.org/conda-forge/linux-64/pixman-0.43.2-h59595ed_0.conda#71004cbf7924e19c02746ccde9fd7123 https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-h36c2ea0_1001.tar.bz2#22dad4df6e8630e8dff2428f6f6a7036 https://conda.anaconda.org/conda-forge/linux-64/rav1e-0.6.6-he8a937b_2.conda#77d9955b4abddb811cb8ab1aa7d743e4 -https://conda.anaconda.org/conda-forge/linux-64/snappy-1.1.10-h9fff704_0.conda#e6d228cd0bb74a51dd18f5bfce0b4115 +https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.0-hdb0a2a9_1.conda#843bbb8ace1d64ac50d64639ff38b014 https://conda.anaconda.org/conda-forge/linux-64/svt-av1-2.0.0-h59595ed_0.conda#207e01ffa0eb2d2efb83fb6f46365a21 https://conda.anaconda.org/conda-forge/linux-64/xorg-kbproto-1.0.7-h7f98852_1002.tar.bz2#4b230e8381279d76131116660f5a241a https://conda.anaconda.org/conda-forge/linux-64/xorg-libice-1.1.1-hd590300_0.conda#b462a33c0be1421532f28bfe8f4a7514 @@ -103,9 +103,9 @@ https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.con https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.4-h7391055_0.conda#93ee23f12bc2e684548181256edd2cf6 https://conda.anaconda.org/conda-forge/linux-64/zlib-1.2.13-hd590300_5.conda#68c34ec6149623be41a1933ab996a209 https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.5-hfc55251_0.conda#04b88013080254850d6c01ed54810589 -https://conda.anaconda.org/conda-forge/linux-64/blosc-1.21.5-h0f2a231_0.conda#009521b7ed97cca25f8f997f9e745976 +https://conda.anaconda.org/conda-forge/linux-64/blosc-1.21.5-hc2324a3_1.conda#11d76bee958b1989bd1ac6ee7372ea6d https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.1.0-hd590300_1.conda#39f910d205726805a958da408ca194ba -https://conda.anaconda.org/conda-forge/linux-64/c-blosc2-2.14.3-hb4ffafa_0.conda#0673d3714f294406ee458962a212c455 +https://conda.anaconda.org/conda-forge/linux-64/c-blosc2-2.14.4-hb4ffafa_1.conda#84eb54e92644c328e087e1c725773317 https://conda.anaconda.org/conda-forge/linux-64/freetype-2.12.1-h267a509_2.conda#9ae35c3d96db2c94ce0cef86efdfa2cb https://conda.anaconda.org/conda-forge/linux-64/gcc-12.3.0-h95e488c_3.conda#413e326f8a01d041ffbfbb51cea46a93 https://conda.anaconda.org/conda-forge/linux-64/gcc_linux-64-12.3.0-h6477408_3.conda#7a53f84c45bdf4656ba27b9e9ed68b3d @@ -113,13 +113,13 @@ https://conda.anaconda.org/conda-forge/linux-64/gettext-0.22.5-h59595ed_2.conda# https://conda.anaconda.org/conda-forge/linux-64/gfortran_impl_linux-64-12.3.0-hfcedea8_5.conda#4d72ee7c82f8a9b2ecef4fcefa9acd19 https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-12.3.0-he2b93b0_5.conda#cddba8fd94e52012abea1caad722b9c2 https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.2-h659d440_0.conda#cd95826dbd331ed1be26bdf401432844 -https://conda.anaconda.org/conda-forge/linux-64/libglib-2.80.0-hf2295e7_3.conda#569d25ad54594080778abff56a611dc7 +https://conda.anaconda.org/conda-forge/linux-64/libglib-2.80.0-hf2295e7_4.conda#0269d2b7fa89f4a37cdee5ad6161f6cc https://conda.anaconda.org/conda-forge/linux-64/libjxl-0.10.1-hcae5a98_1.conda#ca9532696d031f78d1dc245c413823d4 https://conda.anaconda.org/conda-forge/linux-64/libllvm15-15.0.7-hb3ce162_4.conda#8a35df3cbc0c8b12cc8af9473ae75eef https://conda.anaconda.org/conda-forge/linux-64/libllvm18-18.1.3-h2448989_0.conda#927b6d6e80b2c0d4405a58b61ca248a3 https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.27-pthreads_h413a1c8_0.conda#a356024784da6dfd4683dc5ecf45b155 https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.6.0-h1dd3fc0_3.conda#66f03896ffbe1a110ffda05c7a856504 -https://conda.anaconda.org/conda-forge/linux-64/llvm-openmp-18.1.2-h4dfa4b3_0.conda#0118c8a03e3dbbb6b348ef71e94ac7af +https://conda.anaconda.org/conda-forge/linux-64/llvm-openmp-18.1.3-h4dfa4b3_0.conda#d39965123dffcad4d750989be65bcb7c https://conda.anaconda.org/conda-forge/linux-64/mysql-libs-8.3.0-hca2cd23_4.conda#1b50eebe2a738a3146c154d2eceaa8b6 https://conda.anaconda.org/conda-forge/linux-64/nss-3.98-h1d7d5a4_0.conda#54b56c2fdf973656b748e0378900ec13 https://conda.anaconda.org/conda-forge/linux-64/python-3.9.19-h0755675_0_cpython.conda#d9ee3647fbd9e8595b8df759b2bbefb8 @@ -140,11 +140,11 @@ https://conda.anaconda.org/conda-forge/linux-64/cython-3.0.10-py39h3d6467e_0.con https://conda.anaconda.org/conda-forge/linux-64/dbus-1.13.6-h5008d03_3.tar.bz2#ecfff944ba3960ecb334b9a2663d708d https://conda.anaconda.org/conda-forge/linux-64/docutils-0.20.1-py39hf3d152e_3.conda#09a48956e1c155907fd0d626f3e80f2e https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.0-pyhd8ed1ab_2.conda#8d652ea2ee8eaee02ed8dc820bc794aa -https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.0-pyhd8ed1ab_0.conda#7a4b32fbe5442e46841ec77695e36d96 +https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_0.conda#15dda3cdbf330abfe9f555d22f66db46 https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.14.2-h14ed4e7_0.conda#0f69b688f52ff6da70bccb7ff7001d1d https://conda.anaconda.org/conda-forge/linux-64/gfortran-12.3.0-h7389182_3.conda#6b0b27394cf439d0540f949190556860 https://conda.anaconda.org/conda-forge/linux-64/gfortran_linux-64-12.3.0-h617cb40_3.conda#3a9e5b8a6f651ff14e74d896d8f04ab6 -https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.80.0-hde27a5a_3.conda#d544517494d9008c0b1021213aec4084 +https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.80.0-hde27a5a_4.conda#c9deba4959ea5b5f72f1e3e4a71ae014 https://conda.anaconda.org/conda-forge/linux-64/gxx-12.3.0-h95e488c_3.conda#8c50a4d15a8d4812af563a684d598910 https://conda.anaconda.org/conda-forge/linux-64/gxx_linux-64-12.3.0-h4a1b8e8_3.conda#9ec22c7c544f4a4f6d660f0a3b0fd15c https://conda.anaconda.org/conda-forge/noarch/idna-3.6-pyhd8ed1ab_0.conda#1a76f09108576397c41c0b0c5bd84134 @@ -174,7 +174,7 @@ https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.1.2-pyhd8ed1ab_0.conda https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha2e5f31_6.tar.bz2#2a7de29fb590ca14b5243c4c812c8025 https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2024.1-pyhd8ed1ab_0.conda#98206ea9954216ee7540f0c773f2104d https://conda.anaconda.org/conda-forge/noarch/pytz-2024.1-pyhd8ed1ab_0.conda#3eeeeb9e4827ace8c0c1419c85d590ad -https://conda.anaconda.org/conda-forge/noarch/setuptools-69.2.0-pyhd8ed1ab_0.conda#da214ecd521a720a9d521c68047682dc +https://conda.anaconda.org/conda-forge/noarch/setuptools-69.5.1-pyhd8ed1ab_0.conda#7462280d81f639363e6e63c81276bd9e https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2#e5f25f8dbc060e9a8d912e432202afc2 https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-2.2.0-pyhd8ed1ab_0.tar.bz2#4d22a9315e78c6827f806065957d566e https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_0.conda#da1d979339e2714c30a8e806a33ec087 @@ -198,11 +198,11 @@ https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.0-h3faef2a_0.conda#f9 https://conda.anaconda.org/conda-forge/linux-64/cxx-compiler-1.7.0-h00ab1b0_0.conda#b4537c98cb59f8725b0e1e65816b4a28 https://conda.anaconda.org/conda-forge/linux-64/fonttools-4.51.0-py39hd1e30aa_0.conda#79f5dd8778873faa54e8f7b2729fe8a6 https://conda.anaconda.org/conda-forge/linux-64/fortran-compiler-1.7.0-heb67821_0.conda#7ef7c0f111dad1c8006504a0f1ccd820 -https://conda.anaconda.org/conda-forge/linux-64/glib-2.80.0-hf2295e7_3.conda#1ade62526144055f05c3eb45ebae3b5b +https://conda.anaconda.org/conda-forge/linux-64/glib-2.80.0-hf2295e7_4.conda#5521382ee30b96b35eb0037fc3c4f2b4 https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-7.1.0-pyha770c72_0.conda#0896606848b2dc5cebdf111b6543aa04 https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.4.0-pyhd8ed1ab_0.conda#c5d3907ad8bd7bf557521a1833cf7e6d https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.3-pyhd8ed1ab_0.conda#e7d8df6509ba635247ff9aea31134262 -https://conda.anaconda.org/conda-forge/noarch/joblib-1.3.2-pyhd8ed1ab_0.conda#4da50d410f553db77e62ab62ffaa1abc +https://conda.anaconda.org/conda-forge/noarch/joblib-1.4.0-pyhd8ed1ab_0.conda#e0ed1bf13ce3a440e022157bf4764465 https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-22_linux64_openblas.conda#4b31699e0ec5de64d5896e580389c9a1 https://conda.anaconda.org/conda-forge/linux-64/libgcrypt-1.10.3-hd590300_0.conda#32d16ad533c59bb0a3c5ffaf16110829 https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-22_linux64_openblas.conda#b083767b6c877e24ee597d93b87ab838 @@ -233,17 +233,17 @@ https://conda.anaconda.org/conda-forge/noarch/requests-2.31.0-pyhd8ed1ab_0.conda https://conda.anaconda.org/conda-forge/linux-64/blas-devel-3.9.0-22_linux64_openblas.conda#63ddb593595c9cf5eb08d3de54d66df8 https://conda.anaconda.org/conda-forge/linux-64/contourpy-1.2.1-py39h7633fee_0.conda#bdc188e59857d6efab332714e0d01d93 https://conda.anaconda.org/conda-forge/linux-64/gst-plugins-base-1.24.1-hfa15dee_1.conda#a6dd2bbc684913e2bef0a54ce56fcbfb -https://conda.anaconda.org/conda-forge/linux-64/imagecodecs-2024.1.1-py39h426505d_3.conda#91109406c37fc9c1477d7861614aefa5 +https://conda.anaconda.org/conda-forge/linux-64/imagecodecs-2024.1.1-py39ha98d97a_6.conda#9ada409e8a8202f848abfed8e4e3f6be https://conda.anaconda.org/conda-forge/noarch/imageio-2.34.0-pyh4b66e23_0.conda#b8853659d596f967c661f544dd89ede7 -https://conda.anaconda.org/conda-forge/linux-64/pandas-2.2.1-py39hddac248_0.conda#85293a042c24a08e71b7608ee66b6134 +https://conda.anaconda.org/conda-forge/linux-64/pandas-2.2.2-py39hddac248_0.conda#259c4e76e6bda8888aefc098ae1ba749 https://conda.anaconda.org/conda-forge/noarch/patsy-0.5.6-pyhd8ed1ab_0.conda#a5b55d1cb110cdcedc748b5c3e16e687 -https://conda.anaconda.org/conda-forge/linux-64/polars-0.20.18-py39h87fa3cb_0.conda#1620dcc1eb23e9591e1620390f6bdec2 +https://conda.anaconda.org/conda-forge/linux-64/polars-0.20.20-py39h87fa3cb_0.conda#637e4c3bccd3666e2b71a75c8ec7295b https://conda.anaconda.org/conda-forge/noarch/pooch-1.8.1-pyhd8ed1ab_0.conda#d15917f33140f8d2ac9ca44db7ec8a25 https://conda.anaconda.org/conda-forge/linux-64/pulseaudio-client-17.0-hb77b528_0.conda#07f45f1be1c25345faddb8db0de8039b https://conda.anaconda.org/conda-forge/linux-64/pywavelets-1.4.1-py39h44dd56e_1.conda#d037c20e3da2e85f03ebd20ad480c359 https://conda.anaconda.org/conda-forge/linux-64/scipy-1.13.0-py39h474f0d3_0.conda#46ae0ecba9726ab4fa44c78fefa522cf https://conda.anaconda.org/conda-forge/linux-64/blas-2.122-openblas.conda#5065468105542a8b23ea47bd8b6fa55f -https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.8.3-py39he9076e7_0.conda#5456bdfe5809ebf5689eda6c808b686e +https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.8.4-py39he9076e7_0.conda#1919384a8420e7bb25f6c3a582e0857c https://conda.anaconda.org/conda-forge/linux-64/pyamg-5.1.0-py39hda80f44_0.conda#f225666c47726329201b604060f1436c https://conda.anaconda.org/conda-forge/linux-64/qt-main-5.15.8-hc9dc06e_21.conda#b325046180590c868ce0dbf267b82eb8 https://conda.anaconda.org/conda-forge/linux-64/statsmodels-0.14.1-py39h44dd56e_0.conda#dc565186b972bd87e49b9c35390ddd8c @@ -251,7 +251,7 @@ https://conda.anaconda.org/conda-forge/noarch/tifffile-2024.2.12-pyhd8ed1ab_0.co https://conda.anaconda.org/conda-forge/linux-64/pyqt-5.15.9-py39h52134e7_5.conda#e1f148e57d071b09187719df86f513c1 https://conda.anaconda.org/conda-forge/linux-64/scikit-image-0.22.0-py39hddac248_2.conda#8d502a4d2cbe5a45ff35ca8af8cbec0a https://conda.anaconda.org/conda-forge/noarch/seaborn-base-0.13.2-pyhd8ed1ab_0.conda#0918a9201e824211cdf444dbf8d55752 -https://conda.anaconda.org/conda-forge/linux-64/matplotlib-3.8.3-py39hf3d152e_0.conda#983f5b77540eb5aa00238e72ec9b1dfb +https://conda.anaconda.org/conda-forge/linux-64/matplotlib-3.8.4-py39hf3d152e_0.conda#c66d2da2669fddc657b679bccab95775 https://conda.anaconda.org/conda-forge/noarch/seaborn-0.13.2-hd8ed1ab_0.conda#fd31ebf5867914de597f9961c478e482 https://conda.anaconda.org/conda-forge/noarch/numpydoc-1.7.0-pyhd8ed1ab_0.conda#1ad3afced398492586ca1bef70328be4 https://conda.anaconda.org/conda-forge/noarch/sphinx-copybutton-0.5.2-pyhd8ed1ab_0.conda#ac832cc43adc79118cf6e23f1f9b8995 @@ -269,7 +269,7 @@ https://conda.anaconda.org/conda-forge/noarch/sphinxext-opengraph-0.9.1-pyhd8ed1 # pip defusedxml @ https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl#sha256=a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61 # pip fastjsonschema @ https://files.pythonhosted.org/packages/9c/b9/79691036d4a8f9857e74d1728b23f34f583b81350a27492edda58d5604e1/fastjsonschema-2.19.1-py3-none-any.whl#sha256=3672b47bc94178c9f23dbb654bf47440155d4db9df5f7bc47643315f9c405cd0 # pip fqdn @ https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl#sha256=3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014 -# pip json5 @ https://files.pythonhosted.org/packages/26/2f/f93ccd68858c0005445fbdad053417b7eaab87aaf31bd2b506a9005d0dfd/json5-0.9.24-py3-none-any.whl#sha256=4ca101fd5c7cb47960c055ef8f4d0e31e15a7c6c48c3b6f1473fc83b6c462a13 +# pip json5 @ https://files.pythonhosted.org/packages/8a/3c/4f8791ee53ab9eeb0b022205aa79387119a74cc9429582ce04098e6fc540/json5-0.9.25-py3-none-any.whl#sha256=34ed7d834b1341a86987ed52f3f76cd8ee184394906b6e22a1e0deb9ab294e8f # pip jsonpointer @ https://files.pythonhosted.org/packages/12/f6/0232cc0c617e195f06f810534d00b74d2f348fe71b2118009ad8ad31f878/jsonpointer-2.4-py2.py3-none-any.whl#sha256=15d51bba20eea3165644553647711d150376234112651b4f1811022aecad7d7a # pip jupyterlab-pygments @ https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl#sha256=841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780 # pip mistune @ https://files.pythonhosted.org/packages/f0/74/c95adcdf032956d9ef6c89a9b8a5152bf73915f8c633f3e3d88d06bd699c/mistune-3.0.2-py3-none-any.whl#sha256=71481854c30fdbc938963d3605b72501f5c10a9320ecd412c121c163a1c7d205 @@ -317,6 +317,6 @@ https://conda.anaconda.org/conda-forge/noarch/sphinxext-opengraph-0.9.1-pyhd8ed1 # pip nbformat @ https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl#sha256=3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b # pip nbclient @ https://files.pythonhosted.org/packages/66/e8/00517a23d3eeaed0513e718fbc94aab26eaa1758f5690fc8578839791c79/nbclient-0.10.0-py3-none-any.whl#sha256=f13e3529332a1f1f81d82a53210322476a168bb7090a0289c795fe9cc11c9d3f # pip nbconvert @ https://files.pythonhosted.org/packages/23/8a/8d67cbd984739247e4b205c1143e2f71b25b4f71e180fe70f7cb2cf02633/nbconvert-7.16.3-py3-none-any.whl#sha256=ddeff14beeeedf3dd0bc506623e41e4507e551736de59df69a91f86700292b3b -# pip jupyter-server @ https://files.pythonhosted.org/packages/95/85/483b8e09a897d1bc2194646d30d4ce6ae166106e91ecbd11d6b6d9ccfc36/jupyter_server-2.13.0-py3-none-any.whl#sha256=77b2b49c3831fbbfbdb5048cef4350d12946191f833a24e5f83e5f8f4803e97b +# pip jupyter-server @ https://files.pythonhosted.org/packages/07/46/6bb926b3bf878bf687b952fb6a4c09d014b4575a25960f2cd1a61793763f/jupyter_server-2.14.0-py3-none-any.whl#sha256=fb6be52c713e80e004fac34b35a0990d6d36ba06fd0a2b2ed82b899143a64210 # pip jupyterlab-server @ https://files.pythonhosted.org/packages/6a/c9/b270a875916b18f137bb30ecd05031a2f05c95d47a8e8fbd3f805a72f593/jupyterlab_server-2.26.0-py3-none-any.whl#sha256=54622cbd330526a385ee0c1fdccdff3a1e7219bf3e864a335284a1270a1973df # pip jupyterlite-sphinx @ https://files.pythonhosted.org/packages/38/c9/5f1142c005cf8d75830b10029e53f074324bc85cfca1f1d0f22a207b771c/jupyterlite_sphinx-0.9.3-py3-none-any.whl#sha256=be6332d16490ea2fa90b78187a2c5e1c357195966a25741d60b1790346571041 diff --git a/build_tools/circle/doc_min_dependencies_linux-64_conda.lock b/build_tools/circle/doc_min_dependencies_linux-64_conda.lock index 8d65d424bc3b1..3bc6b95df2fe4 100644 --- a/build_tools/circle/doc_min_dependencies_linux-64_conda.lock +++ b/build_tools/circle/doc_min_dependencies_linux-64_conda.lock @@ -1,6 +1,6 @@ # Generated by conda-lock. # platform: linux-64 -# input_hash: ccbcfb88676b9c5585cb85ec6b62ee679937e2d63c4abcdefc6e79e58539f5a3 +# input_hash: 32601810330a8200864f7908d07d870a3a58931be4f833691b2b5c7937f2d330 @EXPLICIT https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2024.2.2-hbcca054_0.conda#2f4327a1cbe7f022401b236e915a5fef @@ -13,7 +13,7 @@ https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.40-h41732ed_0 https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-12.3.0-h8bca6fd_105.conda#e12ce6b051085b8f27e239f5e5f5bce5 https://conda.anaconda.org/conda-forge/noarch/libstdcxx-devel_linux-64-12.3.0-h8bca6fd_105.conda#b3c6062c84a8e172555ee104ea6a01ab https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-13.2.0-h7e041cc_5.conda#f6f6600d18a4047b54f803cf708b868a -https://conda.anaconda.org/conda-forge/linux-64/mkl-include-2024.1.0-ha957f24_691.conda#bdcce3a990987f46e24418be09ef2b45 +https://conda.anaconda.org/conda-forge/linux-64/mkl-include-2024.1.0-ha957f24_692.conda#b35af3f0f25498f4e9fc4c471910346c https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.9-4_cp39.conda#bfe4b3259a8ac6cdf0037752904da6a7 https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda#161081fc7cec0bfda0d86d7cb595f8d8 https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2#f766549260d6815b0c52253f1fb1bb29 @@ -47,7 +47,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libogg-1.3.4-h7f98852_1.tar.bz2# https://conda.anaconda.org/conda-forge/linux-64/libopus-1.3.1-h7f98852_1.tar.bz2#15345e56d527b330e1cacbdf58676e8f https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-12.3.0-h0f45ef3_5.conda#11d1ceacff40054d5a74b12975d76f20 https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda#40b61aab5c7ba9ff276c41cfffe6b80b -https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.3.2-hd590300_0.conda#30de3fd9b3b602f7473f30e684eeea8c +https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.3.2-hd590300_1.conda#049b7df8bae5e184d1de42cdf64855f8 https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda#5aa797f8787fe7a17d1b0821485b5adc https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.2.13-hd590300_5.conda#f36c115f1ee199da648e0597ec2047ad https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.9.4-hcb278e6_0.conda#318b08df404f9c9be5712aaa5a6f0bb0 @@ -95,12 +95,12 @@ https://conda.anaconda.org/conda-forge/linux-64/gettext-0.22.5-h59595ed_2.conda# https://conda.anaconda.org/conda-forge/linux-64/gfortran_impl_linux-64-12.3.0-hfcedea8_5.conda#4d72ee7c82f8a9b2ecef4fcefa9acd19 https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-12.3.0-he2b93b0_5.conda#cddba8fd94e52012abea1caad722b9c2 https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.2-h659d440_0.conda#cd95826dbd331ed1be26bdf401432844 -https://conda.anaconda.org/conda-forge/linux-64/libglib-2.80.0-hf2295e7_3.conda#569d25ad54594080778abff56a611dc7 -https://conda.anaconda.org/conda-forge/linux-64/libhwloc-2.9.3-default_h554bfaf_1009.conda#f36ddc11ca46958197a45effdd286e45 +https://conda.anaconda.org/conda-forge/linux-64/libglib-2.80.0-hf2295e7_4.conda#0269d2b7fa89f4a37cdee5ad6161f6cc +https://conda.anaconda.org/conda-forge/linux-64/libhwloc-2.10.0-default_h2fb2949_1000.conda#7e3726e647a619c6ce5939014dfde86d https://conda.anaconda.org/conda-forge/linux-64/libllvm15-15.0.7-hb3ce162_4.conda#8a35df3cbc0c8b12cc8af9473ae75eef https://conda.anaconda.org/conda-forge/linux-64/libllvm18-18.1.3-h2448989_0.conda#927b6d6e80b2c0d4405a58b61ca248a3 https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.6.0-h1dd3fc0_3.conda#66f03896ffbe1a110ffda05c7a856504 -https://conda.anaconda.org/conda-forge/linux-64/llvm-openmp-18.1.2-h4dfa4b3_0.conda#0118c8a03e3dbbb6b348ef71e94ac7af +https://conda.anaconda.org/conda-forge/linux-64/llvm-openmp-18.1.3-h4dfa4b3_0.conda#d39965123dffcad4d750989be65bcb7c https://conda.anaconda.org/conda-forge/linux-64/mysql-libs-8.3.0-hca2cd23_4.conda#1b50eebe2a738a3146c154d2eceaa8b6 https://conda.anaconda.org/conda-forge/linux-64/nss-3.98-h1d7d5a4_0.conda#54b56c2fdf973656b748e0378900ec13 https://conda.anaconda.org/conda-forge/linux-64/python-3.9.19-h0755675_0_cpython.conda#d9ee3647fbd9e8595b8df759b2bbefb8 @@ -122,12 +122,12 @@ https://conda.anaconda.org/conda-forge/linux-64/cython-3.0.10-py39h3d6467e_0.con https://conda.anaconda.org/conda-forge/linux-64/dbus-1.13.6-h5008d03_3.tar.bz2#ecfff944ba3960ecb334b9a2663d708d https://conda.anaconda.org/conda-forge/linux-64/docutils-0.19-py39hf3d152e_1.tar.bz2#adb733ec2ee669f6d010758d054da60f https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.0-pyhd8ed1ab_2.conda#8d652ea2ee8eaee02ed8dc820bc794aa -https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.0-pyhd8ed1ab_0.conda#7a4b32fbe5442e46841ec77695e36d96 +https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_0.conda#15dda3cdbf330abfe9f555d22f66db46 https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.14.2-h14ed4e7_0.conda#0f69b688f52ff6da70bccb7ff7001d1d https://conda.anaconda.org/conda-forge/noarch/fsspec-2024.3.1-pyhca7485f_0.conda#b7f0662ef2c9d4404f0af9eef5ed2fde https://conda.anaconda.org/conda-forge/linux-64/gfortran-12.3.0-h7389182_3.conda#6b0b27394cf439d0540f949190556860 https://conda.anaconda.org/conda-forge/linux-64/gfortran_linux-64-12.3.0-h617cb40_3.conda#3a9e5b8a6f651ff14e74d896d8f04ab6 -https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.80.0-hde27a5a_3.conda#d544517494d9008c0b1021213aec4084 +https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.80.0-hde27a5a_4.conda#c9deba4959ea5b5f72f1e3e4a71ae014 https://conda.anaconda.org/conda-forge/linux-64/gxx-12.3.0-h95e488c_3.conda#8c50a4d15a8d4812af563a684d598910 https://conda.anaconda.org/conda-forge/linux-64/gxx_linux-64-12.3.0-h4a1b8e8_3.conda#9ec22c7c544f4a4f6d660f0a3b0fd15c https://conda.anaconda.org/conda-forge/noarch/idna-3.6-pyhd8ed1ab_0.conda#1a76f09108576397c41c0b0c5bd84134 @@ -159,7 +159,7 @@ https://conda.anaconda.org/conda-forge/linux-64/setuptools-59.8.0-py39hf3d152e_1 https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2#e5f25f8dbc060e9a8d912e432202afc2 https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-2.2.0-pyhd8ed1ab_0.tar.bz2#4d22a9315e78c6827f806065957d566e https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_0.conda#da1d979339e2714c30a8e806a33ec087 -https://conda.anaconda.org/conda-forge/linux-64/tbb-2021.11.0-h00ab1b0_1.conda#4531d2927578e7e254ff3bcf6457518c +https://conda.anaconda.org/conda-forge/linux-64/tbb-2021.12.0-h00ab1b0_0.conda#f1b776cff1b426e7e7461a8502a3b731 https://conda.anaconda.org/conda-forge/noarch/tenacity-8.2.3-pyhd8ed1ab_0.conda#1482e77f87c6a702a7e05ef22c9b197b https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.4.0-pyhc1e730c_0.conda#b296278eef667c673bf51de6535bad88 https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_0.tar.bz2#f832c45a477c78bebd107098db465095 @@ -178,16 +178,16 @@ https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.0-h3faef2a_0.conda#f9 https://conda.anaconda.org/conda-forge/linux-64/cxx-compiler-1.7.0-h00ab1b0_0.conda#b4537c98cb59f8725b0e1e65816b4a28 https://conda.anaconda.org/conda-forge/linux-64/cytoolz-0.12.3-py39hd1e30aa_0.conda#dc0fb8e157c7caba4c98f1e1f9d2e5f4 https://conda.anaconda.org/conda-forge/linux-64/fortran-compiler-1.7.0-heb67821_0.conda#7ef7c0f111dad1c8006504a0f1ccd820 -https://conda.anaconda.org/conda-forge/linux-64/glib-2.80.0-hf2295e7_3.conda#1ade62526144055f05c3eb45ebae3b5b +https://conda.anaconda.org/conda-forge/linux-64/glib-2.80.0-hf2295e7_4.conda#5521382ee30b96b35eb0037fc3c4f2b4 https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-7.1.0-pyha770c72_0.conda#0896606848b2dc5cebdf111b6543aa04 https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.3-pyhd8ed1ab_0.conda#e7d8df6509ba635247ff9aea31134262 -https://conda.anaconda.org/conda-forge/noarch/joblib-1.3.2-pyhd8ed1ab_0.conda#4da50d410f553db77e62ab62ffaa1abc +https://conda.anaconda.org/conda-forge/noarch/joblib-1.4.0-pyhd8ed1ab_0.conda#e0ed1bf13ce3a440e022157bf4764465 https://conda.anaconda.org/conda-forge/linux-64/libgcrypt-1.10.3-hd590300_0.conda#32d16ad533c59bb0a3c5ffaf16110829 https://conda.anaconda.org/conda-forge/linux-64/libsndfile-1.2.2-hc60ed4a_1.conda#ef1910918dd895516a769ed36b5b3a4e https://conda.anaconda.org/conda-forge/linux-64/libxkbcommon-1.7.0-h662e7e4_0.conda#b32c0da42b1f24a98577bb3d7fc0b995 https://conda.anaconda.org/conda-forge/noarch/memory_profiler-0.61.0-pyhd8ed1ab_0.tar.bz2#8b45f9f2b2f7a98b0ec179c8991a4a9b https://conda.anaconda.org/conda-forge/noarch/meson-1.4.0-pyhd8ed1ab_0.conda#52a0660cfa40b45bf254ecc3374cb2e0 -https://conda.anaconda.org/conda-forge/linux-64/mkl-2024.1.0-ha957f24_691.conda#1647aafdfa7320bd4668af5aaad692ea +https://conda.anaconda.org/conda-forge/linux-64/mkl-2024.1.0-ha957f24_692.conda#e7f5c5cda17c6f5047db27d44367c19d https://conda.anaconda.org/conda-forge/noarch/partd-1.4.1-pyhd8ed1ab_0.conda#acf4b7c0bcd5fa3b0e05801c4d2accd6 https://conda.anaconda.org/conda-forge/linux-64/pillow-10.3.0-py39h90c7501_0.conda#1e3b6af9592be71ce19f0a6aae05d97b https://conda.anaconda.org/conda-forge/noarch/pip-24.0-pyhd8ed1ab_0.conda#f586ac1e56c8638b64f9c8122a7b8a67 @@ -204,7 +204,7 @@ https://conda.anaconda.org/conda-forge/noarch/importlib_metadata-7.1.0-hd8ed1ab_ https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-22_linux64_mkl.conda#eb6deb4ba6f92ea3f31c09cb8b764738 https://conda.anaconda.org/conda-forge/linux-64/libsystemd0-255-h3516f8a_1.conda#3366af27f0b593544a6cd453c7932ac5 https://conda.anaconda.org/conda-forge/noarch/meson-python-0.15.0-pyh0c530f3_0.conda#3bc64565ca78ce3bb80248d09926d8f9 -https://conda.anaconda.org/conda-forge/linux-64/mkl-devel-2024.1.0-ha770c72_691.conda#25e4569ad4dd6a38c294a0510a60c014 +https://conda.anaconda.org/conda-forge/linux-64/mkl-devel-2024.1.0-ha770c72_692.conda#56142862a71bcfdd6ef2ce95c8e90755 https://conda.anaconda.org/conda-forge/linux-64/pyqt5-sip-12.12.2-py39h3d6467e_5.conda#93aff412f3e49fdb43361c0215cbd72d https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.5.0-pyhd8ed1ab_0.conda#d5f595da2daead898ca958ac62f0307b https://conda.anaconda.org/conda-forge/noarch/requests-2.31.0-pyhd8ed1ab_0.conda#a30144e4156cdbb236f99ebb49828f8b From f0e1605c9f4bfa2eac559852eca442155d73c455 Mon Sep 17 00:00:00 2001 From: Bharat Raghunathan Date: Mon, 15 Apr 2024 03:03:09 -0400 Subject: [PATCH 026/344] DOC Adds Links to plot_svm_scale_c.py Example (#28830) --- doc/modules/svm.rst | 1 + sklearn/svm/_classes.py | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/doc/modules/svm.rst b/doc/modules/svm.rst index 6ef4ed495b03f..e3bc1395819e9 100644 --- a/doc/modules/svm.rst +++ b/doc/modules/svm.rst @@ -520,6 +520,7 @@ is advised to use :class:`~sklearn.model_selection.GridSearchCV` with * :ref:`sphx_glr_auto_examples_svm_plot_rbf_parameters.py` * :ref:`sphx_glr_auto_examples_svm_plot_svm_nonlinear.py` + * :ref:`sphx_glr_auto_examples_svm_plot_svm_scale_c.py` Custom Kernels -------------- diff --git a/sklearn/svm/_classes.py b/sklearn/svm/_classes.py index b8709b43a5a17..5b547fcb98cd6 100644 --- a/sklearn/svm/_classes.py +++ b/sklearn/svm/_classes.py @@ -78,6 +78,9 @@ class LinearSVC(LinearClassifierMixin, SparseCoefMixin, BaseEstimator): 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 @@ -639,7 +642,9 @@ class SVC(BaseSVC): C : float, default=1.0 Regularization parameter. The strength of the regularization is inversely proportional to C. Must be strictly positive. The penalty - is a squared l2 penalty. + is a squared l2 penalty. 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`. kernel : {'linear', 'poly', 'rbf', 'sigmoid', 'precomputed'} or callable, \ default='rbf' @@ -1215,7 +1220,9 @@ class SVR(RegressorMixin, BaseLibSVM): C : float, default=1.0 Regularization parameter. The strength of the regularization is inversely proportional to C. Must be strictly positive. - The penalty is a squared l2 penalty. + The penalty is a squared l2. 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`. epsilon : float, default=0.1 Epsilon in the epsilon-SVR model. It specifies the epsilon-tube @@ -1385,7 +1392,9 @@ class NuSVR(RegressorMixin, BaseLibSVM): default 0.5 will be taken. C : float, default=1.0 - Penalty parameter C of the error term. + Penalty parameter C of the error term. 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`. kernel : {'linear', 'poly', 'rbf', 'sigmoid', 'precomputed'} or callable, \ default='rbf' From af236f6df4f4388f41b59ac5564443822bc5fac2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Est=C3=A8ve?= Date: Mon, 15 Apr 2024 10:34:40 +0200 Subject: [PATCH 027/344] BLD Fix more build dependencies in meson build (#28821) --- sklearn/cluster/_hdbscan/meson.build | 2 +- sklearn/cluster/meson.build | 5 +++-- sklearn/decomposition/meson.build | 2 +- sklearn/feature_extraction/meson.build | 2 +- sklearn/linear_model/meson.build | 4 ++-- sklearn/manifold/meson.build | 2 +- sklearn/meson.build | 4 +++- sklearn/metrics/meson.build | 6 ++++-- sklearn/neighbors/meson.build | 4 ++-- sklearn/preprocessing/meson.build | 4 ++-- sklearn/svm/meson.build | 6 +++--- sklearn/tree/meson.build | 2 +- sklearn/utils/meson.build | 7 +++++-- 13 files changed, 29 insertions(+), 21 deletions(-) diff --git a/sklearn/cluster/_hdbscan/meson.build b/sklearn/cluster/_hdbscan/meson.build index 85a61a73a1ed3..b6a11eda8bb71 100644 --- a/sklearn/cluster/_hdbscan/meson.build +++ b/sklearn/cluster/_hdbscan/meson.build @@ -1,5 +1,5 @@ cluster_hdbscan_extension_metadata = { - '_linkage': {'sources': ['_linkage.pyx'] + metrics_cython_tree}, + '_linkage': {'sources': ['_linkage.pyx', metrics_cython_tree]}, '_reachability': {'sources': ['_reachability.pyx']}, '_tree': {'sources': ['_tree.pyx']} } diff --git a/sklearn/cluster/meson.build b/sklearn/cluster/meson.build index 0e7113f59fd51..afc066797a659 100644 --- a/sklearn/cluster/meson.build +++ b/sklearn/cluster/meson.build @@ -2,7 +2,8 @@ cluster_extension_metadata = { '_dbscan_inner': {'sources': ['_dbscan_inner.pyx'], 'override_options': ['cython_language=cpp']}, '_hierarchical_fast': - {'sources': ['_hierarchical_fast.pyx'] + metrics_cython_tree, 'override_options': ['cython_language=cpp']}, + {'sources': ['_hierarchical_fast.pyx', metrics_cython_tree], + 'override_options': ['cython_language=cpp']}, '_k_means_common': {'sources': ['_k_means_common.pyx']}, '_k_means_lloyd': @@ -16,7 +17,7 @@ cluster_extension_metadata = { foreach ext_name, ext_dict : cluster_extension_metadata py.extension_module( ext_name, - ext_dict.get('sources') + [utils_cython_tree], + [ext_dict.get('sources'), utils_cython_tree], dependencies: [np_dep, openmp_dep], override_options : ext_dict.get('override_options', []), cython_args: cython_args, diff --git a/sklearn/decomposition/meson.build b/sklearn/decomposition/meson.build index e9f6a75e9ec9c..93dc6dff06e90 100644 --- a/sklearn/decomposition/meson.build +++ b/sklearn/decomposition/meson.build @@ -1,6 +1,6 @@ py.extension_module( '_online_lda_fast', - '_online_lda_fast.pyx', + ['_online_lda_fast.pyx', utils_cython_tree], cython_args: cython_args, subdir: 'sklearn/decomposition', install: true diff --git a/sklearn/feature_extraction/meson.build b/sklearn/feature_extraction/meson.build index 5499cea908d79..81732474de3b2 100644 --- a/sklearn/feature_extraction/meson.build +++ b/sklearn/feature_extraction/meson.build @@ -1,6 +1,6 @@ py.extension_module( '_hashing_fast', - '_hashing_fast.pyx', + ['_hashing_fast.pyx', utils_cython_tree], dependencies: [np_dep], override_options: ['cython_language=cpp'], cython_args: cython_args, diff --git a/sklearn/linear_model/meson.build b/sklearn/linear_model/meson.build index 773cdf55bea73..1a40cea39b648 100644 --- a/sklearn/linear_model/meson.build +++ b/sklearn/linear_model/meson.build @@ -6,7 +6,7 @@ linear_model_cython_tree = [ py.extension_module( '_cd_fast', - '_cd_fast.pyx', + ['_cd_fast.pyx', utils_cython_tree], cython_args: cython_args, subdir: 'sklearn/linear_model', install: true @@ -23,7 +23,7 @@ foreach name: name_list ) py.extension_module( name, - [pyx, linear_model_cython_tree], + [pyx, linear_model_cython_tree, utils_cython_tree], cython_args: cython_args, subdir: 'sklearn/linear_model', install: true diff --git a/sklearn/manifold/meson.build b/sklearn/manifold/meson.build index 72ec51d62b164..b112f63dd4f2d 100644 --- a/sklearn/manifold/meson.build +++ b/sklearn/manifold/meson.build @@ -1,6 +1,6 @@ py.extension_module( '_utils', - '_utils.pyx', + ['_utils.pyx', utils_cython_tree], cython_args: cython_args, subdir: 'sklearn/manifold', install: true diff --git a/sklearn/meson.build b/sklearn/meson.build index bd71447597d42..058aad96bb435 100644 --- a/sklearn/meson.build +++ b/sklearn/meson.build @@ -141,7 +141,9 @@ py.extension_module( # Need for Cython cimports across subpackages to work, i.e. avoid errors like # relative cimport from non-package directory is not allowed -fs.copyfile('__init__.py') +sklearn_root_cython_tree = [ + fs.copyfile('__init__.py') +] sklearn_dir = py.get_install_dir() / 'sklearn' diff --git a/sklearn/metrics/meson.build b/sklearn/metrics/meson.build index 8ad49f5787539..24101fb435939 100644 --- a/sklearn/metrics/meson.build +++ b/sklearn/metrics/meson.build @@ -3,6 +3,8 @@ metrics_cython_tree = [ fs.copyfile('__init__.py') ] +# Some metrics code cimports code from utils, we may as well copy all the necessary files +metrics_cython_tree += utils_cython_tree _dist_metrics_pxd = custom_target( '_dist_metrics_pxd', @@ -25,7 +27,7 @@ _dist_metrics_pyx = custom_target( _dist_metrics = py.extension_module( '_dist_metrics', - [_dist_metrics_pyx, utils_cython_tree, metrics_cython_tree], + [_dist_metrics_pyx, metrics_cython_tree], dependencies: [np_dep], cython_args: cython_args, subdir: 'sklearn/metrics', @@ -34,7 +36,7 @@ _dist_metrics = py.extension_module( py.extension_module( '_pairwise_fast', - ['_pairwise_fast.pyx', utils_cython_tree, metrics_cython_tree], + ['_pairwise_fast.pyx', metrics_cython_tree], cython_args: cython_args, subdir: 'sklearn/metrics', install: true diff --git a/sklearn/neighbors/meson.build b/sklearn/neighbors/meson.build index 55b1754c47e8f..b85188cab98be 100644 --- a/sklearn/neighbors/meson.build +++ b/sklearn/neighbors/meson.build @@ -24,7 +24,7 @@ foreach name: name_list ) py.extension_module( name, - [pyx, neighbors_cython_tree], + [pyx, neighbors_cython_tree, utils_cython_tree], dependencies: [np_dep], cython_args: cython_args, subdir: 'sklearn/neighbors', @@ -42,7 +42,7 @@ neighbors_extension_metadata = { foreach ext_name, ext_dict : neighbors_extension_metadata py.extension_module( ext_name, - ext_dict.get('sources'), + [ext_dict.get('sources'), utils_cython_tree], dependencies: ext_dict.get('dependencies'), override_options : ext_dict.get('override_options', []), cython_args: cython_args, diff --git a/sklearn/preprocessing/meson.build b/sklearn/preprocessing/meson.build index 2d280cf5fba39..a8f741ee352b1 100644 --- a/sklearn/preprocessing/meson.build +++ b/sklearn/preprocessing/meson.build @@ -1,6 +1,6 @@ py.extension_module( '_csr_polynomial_expansion', - '_csr_polynomial_expansion.pyx', + ['_csr_polynomial_expansion.pyx', utils_cython_tree], cython_args: cython_args, subdir: 'sklearn/preprocessing', install: true @@ -8,7 +8,7 @@ py.extension_module( py.extension_module( '_target_encoder_fast', - '_target_encoder_fast.pyx', + ['_target_encoder_fast.pyx', utils_cython_tree], override_options: ['cython_language=cpp'], cython_args: cython_args, subdir: 'sklearn/preprocessing', diff --git a/sklearn/svm/meson.build b/sklearn/svm/meson.build index 55c51da12ae42..8372364c429cd 100644 --- a/sklearn/svm/meson.build +++ b/sklearn/svm/meson.build @@ -19,7 +19,7 @@ libsvm_skl = static_library( py.extension_module( '_libsvm', - ['_libsvm.pyx'], + ['_libsvm.pyx', utils_cython_tree], include_directories: [newrand_include, libsvm_include], link_with: libsvm_skl, cython_args: cython_args, @@ -29,7 +29,7 @@ py.extension_module( py.extension_module( '_libsvm_sparse', - ['_libsvm_sparse.pyx'], + ['_libsvm_sparse.pyx', utils_cython_tree], include_directories: [newrand_include, libsvm_include], link_with: libsvm_skl, cython_args: cython_args, @@ -44,7 +44,7 @@ liblinear_skl = static_library( py.extension_module( '_liblinear', - ['_liblinear.pyx'], + ['_liblinear.pyx', utils_cython_tree], include_directories: [newrand_include, liblinear_include], link_with: [liblinear_skl], cython_args: cython_args, diff --git a/sklearn/tree/meson.build b/sklearn/tree/meson.build index 12c1ddcedea98..4bc4e0cf9e464 100644 --- a/sklearn/tree/meson.build +++ b/sklearn/tree/meson.build @@ -16,7 +16,7 @@ tree_extension_metadata = { foreach ext_name, ext_dict : tree_extension_metadata py.extension_module( ext_name, - ext_dict.get('sources'), + [ext_dict.get('sources'), utils_cython_tree], dependencies: [np_dep], override_options : ext_dict.get('override_options', []), cython_args: cython_args, diff --git a/sklearn/utils/meson.build b/sklearn/utils/meson.build index 1cd707beb835f..df74d4c24a411 100644 --- a/sklearn/utils/meson.build +++ b/sklearn/utils/meson.build @@ -1,6 +1,9 @@ # utils is cimported from other subpackages so this is needed for the cimport # to work utils_cython_tree = [ + # We add sklearn_root_cython_tree to make sure sklearn/__init__.py is copied + # early in the build + sklearn_root_cython_tree, fs.copyfile('__init__.py'), fs.copyfile('_cython_blas.pxd'), fs.copyfile('_heap.pxd'), @@ -35,7 +38,7 @@ utils_extension_metadata = { foreach ext_name, ext_dict : utils_extension_metadata py.extension_module( ext_name, - ext_dict.get('sources') + utils_cython_tree, + [ext_dict.get('sources'), utils_cython_tree], dependencies: ext_dict.get('dependencies', []), override_options : ext_dict.get('override_options', []), cython_args: cython_args, @@ -63,7 +66,7 @@ foreach name: util_extension_names ) py.extension_module( name, - [pxd, pyx] + utils_cython_tree, + [pxd, pyx, utils_cython_tree], cython_args: cython_args, subdir: 'sklearn/utils', install: true From 5dbf795dd583119ae44cb91bd6faec3187d16e99 Mon Sep 17 00:00:00 2001 From: Hong Xiang Yue <42855604+hxyue1@users.noreply.github.com> Date: Mon, 15 Apr 2024 06:20:59 -0700 Subject: [PATCH 028/344] DOC Add warnings about causal interpretation of coefficients (#19789) Co-authored-by: Joel Nothman Co-authored-by: adrinjalali --- ...linear_model_coefficient_interpretation.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/examples/inspection/plot_linear_model_coefficient_interpretation.py b/examples/inspection/plot_linear_model_coefficient_interpretation.py index 9ab71dfa500f8..0e11f01937ebc 100644 --- a/examples/inspection/plot_linear_model_coefficient_interpretation.py +++ b/examples/inspection/plot_linear_model_coefficient_interpretation.py @@ -306,6 +306,34 @@ # Also, AGE, EXPERIENCE and EDUCATION are the three variables that most # influence the model. # +# Interpreting coefficients: being cautious about causality +# --------------------------------------------------------- +# +# Linear models are a great tool for measuring statistical association, but we +# should be cautious when making statements about causality, after all +# correlation doesn't always imply causation. This is particularly difficult in +# the social sciences because the variables we observe only function as proxies +# for the underlying causal process. +# +# In our particular case we can think of the EDUCATION of an individual as a +# proxy for their professional aptitude, the real variable we're interested in +# but can't observe. We'd certainly like to think that staying in school for +# longer would increase technical competency, but it's also quite possible that +# causality goes the other way too. That is, those who are technically +# competent tend to stay in school for longer. +# +# An employer is unlikely to care which case it is (or if it's a mix of both), +# as long as they remain convinced that a person with more EDUCATION is better +# suited for the job, they will be happy to pay out a higher WAGE. +# +# This confounding of effects becomes problematic when thinking about some +# form of intervention e.g. government subsidies of university degrees or +# promotional material encouraging individuals to take up higher education. +# The usefulness of these measures could end up being overstated, especially if +# the degree of confounding is strong. Our model predicts a :math:`0.054699` +# increase in hourly wage for each year of education. The actual causal effect +# might be lower because of this confounding. +# # Checking the variability of the coefficients # -------------------------------------------- # @@ -742,6 +770,9 @@ # * Coefficients must be scaled to the same unit of measure to retrieve # feature importance. Scaling them with the standard-deviation of the # feature is a useful proxy. +# * Interpreting causality is difficult when there are confounding effects. If +# the relationship between two variables is also affected by something +# unobserved, we should be careful when making conclusions about causality. # * Coefficients in multivariate linear models represent the dependency # between a given feature and the target, **conditional** on the other # features. From bf62a5006d4ec94ce5d2cf3b18a3d2124e795f30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Est=C3=A8ve?= Date: Tue, 16 Apr 2024 11:42:47 +0200 Subject: [PATCH 029/344] CI Use Meson to build sdist (#28844) --- build_tools/github/build_source.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build_tools/github/build_source.sh b/build_tools/github/build_source.sh index a4d9c7bd05387..ec53284012fa4 100755 --- a/build_tools/github/build_source.sh +++ b/build_tools/github/build_source.sh @@ -11,10 +11,10 @@ python -m venv build_env source build_env/bin/activate python -m pip install numpy scipy cython -python -m pip install twine +python -m pip install twine build cd scikit-learn/scikit-learn -python setup.py sdist +python -m build --sdist # Check whether the source distribution will render correctly twine check dist/*.tar.gz From 481b457a25b1b467b1b7c49089282a9d10d1c7d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20du=20Boisberranger?= Date: Tue, 16 Apr 2024 12:05:04 +0200 Subject: [PATCH 030/344] FIX CalibratedClassifierCV with string targets (#28843) --- doc/whats_new/v1.5.rst | 7 +++++++ sklearn/calibration.py | 4 +--- sklearn/tests/test_calibration.py | 11 +++++++++++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/doc/whats_new/v1.5.rst b/doc/whats_new/v1.5.rst index 1fc651e303e56..adbd64f2f4477 100644 --- a/doc/whats_new/v1.5.rst +++ b/doc/whats_new/v1.5.rst @@ -127,6 +127,13 @@ Changelog :pr:`123456` by :user:`Joe Bloggs `. where 123455 is the *pull request* number, not the issue number. +:mod:`sklearn.calibration` +.......................... + +- |Fix| Fixed a regression in :class:`calibration.CalibratedClassifierCV` where + an error was wrongly raised with string targets. + :pr:`28843` by :user:`Jérémie du Boisberranger `. + :mod:`sklearn.cluster` ...................... diff --git a/sklearn/calibration.py b/sklearn/calibration.py index b02236eb600fe..40d3e5363a7e0 100644 --- a/sklearn/calibration.py +++ b/sklearn/calibration.py @@ -388,9 +388,7 @@ def fit(self, X, y, sample_weight=None, **fit_params): n_folds = self.cv.n_splits else: n_folds = None - if n_folds and np.any( - [np.sum(y == class_) < n_folds for class_ in self.classes_] - ): + if n_folds and np.any(np.unique(y, return_counts=True)[1] < n_folds): raise ValueError( f"Requesting {n_folds}-fold " "cross-validation but provided less than " diff --git a/sklearn/tests/test_calibration.py b/sklearn/tests/test_calibration.py index eb7204b366729..833ef2ea7e558 100644 --- a/sklearn/tests/test_calibration.py +++ b/sklearn/tests/test_calibration.py @@ -1088,3 +1088,14 @@ def predict_proba(self, X): calibrator = CalibratedClassifierCV(model) # Does not raise an error calibrator.fit(*data) + + +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) From 21ee59df578ead368cb1cd062aa3d788832bb334 Mon Sep 17 00:00:00 2001 From: Kaushik Amar Das Date: Tue, 16 Apr 2024 16:04:02 +0530 Subject: [PATCH 031/344] API Deprecate using labels in bytes format (#18555) Co-authored-by: Guillaume Lemaitre Co-authored-by: adrinjalali --- doc/whats_new/v1.5.rst | 9 ++++++++ sklearn/metrics/tests/test_ranking.py | 30 ++++++++++++++++---------- sklearn/utils/multiclass.py | 27 ++++++++++++++++------- sklearn/utils/tests/test_multiclass.py | 16 ++++++++++++++ 4 files changed, 63 insertions(+), 19 deletions(-) diff --git a/doc/whats_new/v1.5.rst b/doc/whats_new/v1.5.rst index adbd64f2f4477..0aa506a71c6fb 100644 --- a/doc/whats_new/v1.5.rst +++ b/doc/whats_new/v1.5.rst @@ -346,6 +346,10 @@ Changelog in favor of `y_proba`. `y_prob` will be removed in version 1.7. :pr:`28092` by :user:`Adam Li `. +- |API| For classifiers and classification metrics, labels encoded as bytes + is deprecated and will raise an error in v1.6. + :pr:`18555` by :user:`Kaushik Amar Das `. + :mod:`sklearn.mixture` ...................... @@ -425,6 +429,11 @@ Changelog - |API| :func:`utils.tosequence` is deprecated and will be removed in version 1.7. :pr:`28763` by :user:`Jérémie du Boisberranger `. +- |API| Raise informative warning message in :func:`type_of_target` when + represented as bytes. For classifiers and classification metrics, labels encoded + as bytes is deprecated and will raise an error in v1.6. + :pr:`18555` by :user:`Kaushik Amar Das `. + - |Fix| :func:`~utils._safe_indexing` now works correctly for polars DataFrame when `axis=0` and supports indexing polars Series. :pr:`28521` by :user:`Yao Xiao `. diff --git a/sklearn/metrics/tests/test_ranking.py b/sklearn/metrics/tests/test_ranking.py index 7b3a71978907a..4beeea23446c8 100644 --- a/sklearn/metrics/tests/test_ranking.py +++ b/sklearn/metrics/tests/test_ranking.py @@ -29,10 +29,12 @@ from sklearn.preprocessing import label_binarize from sklearn.random_projection import _sparse_random_matrix from sklearn.utils._testing import ( + _convert_container, assert_allclose, assert_almost_equal, assert_array_almost_equal, assert_array_equal, + ignore_warnings, ) from sklearn.utils.extmath import softmax from sklearn.utils.fixes import CSR_CONTAINERS @@ -864,17 +866,6 @@ def test_binary_clf_curve_implicit_pos_label(curve_func): with pytest.raises(ValueError, match=msg): curve_func(np.array(["a", "b"], dtype=object), [0.0, 1.0]) - # The error message is slightly different for bytes-encoded - # class labels, but otherwise the behavior is the same: - msg = ( - "y_true takes value in {b'a', b'b'} and pos_label is " - "not specified: either make y_true take " - "value in {0, 1} or {-1, 1} or pass pos_label " - "explicitly." - ) - with pytest.raises(ValueError, match=msg): - curve_func(np.array([b"a", b"b"], dtype=" 2 or (y.ndim == 2 and len(first_row) > 1): + if issparse(first_row_or_val): + first_row_or_val = first_row_or_val.data + if xp.unique_values(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: diff --git a/sklearn/utils/tests/test_multiclass.py b/sklearn/utils/tests/test_multiclass.py index 6603aca206e66..40a13526ab009 100644 --- a/sklearn/utils/tests/test_multiclass.py +++ b/sklearn/utils/tests/test_multiclass.py @@ -10,6 +10,7 @@ from sklearn.utils._array_api import yield_namespace_device_dtype_combinations from sklearn.utils._testing import ( _array_api_for_tests, + _convert_container, assert_allclose, assert_array_almost_equal, assert_array_equal, @@ -595,3 +596,18 @@ def test_ovr_decision_function(): ] assert_allclose(dec_values, dec_values_one, atol=1e-6) + + +# TODO(1.6): Change to ValueError when byte labels is deprecated. +@pytest.mark.parametrize("input_type", ["list", "array"]) +def test_labels_in_bytes_format(input_type): + # check that we raise an error with bytes encoded labels + # non-regression test for: + # 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 deprecated in v1.5 and will" + " error in v1.7. Convert the labels to a string or integer format." + ) + with pytest.warns(FutureWarning, match=err_msg): + type_of_target(target) From ec6749445c695ea307389fa2dceaa47881435577 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20du=20Boisberranger?= Date: Tue, 16 Apr 2024 17:32:39 +0200 Subject: [PATCH 032/344] MAINT Clean up deprecations for 1.5: delayed import path (#28848) --- sklearn/utils/fixes.py | 11 ----------- sklearn/utils/tests/test_fixes.py | 17 +---------------- 2 files changed, 1 insertion(+), 27 deletions(-) diff --git a/sklearn/utils/fixes.py b/sklearn/utils/fixes.py index 1b34a3fe1ffbc..e33519a3154d8 100644 --- a/sklearn/utils/fixes.py +++ b/sklearn/utils/fixes.py @@ -23,7 +23,6 @@ import sklearn from ..externals._packaging.version import parse as parse_version -from .deprecation import deprecated _IS_PYPY = platform.python_implementation() == "PyPy" _IS_32BIT = 8 * struct.calcsize("P") == 32 @@ -134,16 +133,6 @@ def threadpool_info(): threadpool_info.__doc__ = threadpoolctl.threadpool_info.__doc__ -@deprecated( - "The function `delayed` has been moved from `sklearn.utils.fixes` to " - "`sklearn.utils.parallel`. This import path will be removed in 1.5." -) -def delayed(function): - from sklearn.utils.parallel import delayed - - return delayed(function) - - # TODO: Remove when SciPy 1.11 is the minimum supported version def _mode(a, axis=0): if sp_version >= parse_version("1.9.0"): diff --git a/sklearn/utils/tests/test_fixes.py b/sklearn/utils/tests/test_fixes.py index 60c57bbbaaa52..c312b8568c4c6 100644 --- a/sklearn/utils/tests/test_fixes.py +++ b/sklearn/utils/tests/test_fixes.py @@ -7,11 +7,7 @@ import pytest from sklearn.utils._testing import assert_array_equal -from sklearn.utils.fixes import ( - _object_dtype_isnan, - _smallest_admissible_index_dtype, - delayed, -) +from sklearn.utils.fixes import _object_dtype_isnan, _smallest_admissible_index_dtype @pytest.mark.parametrize("dtype, val", ([object, 1], [object, "a"], [float, 1])) @@ -25,17 +21,6 @@ def test_object_dtype_isnan(dtype, val): assert_array_equal(mask, expected_mask) -def test_delayed_deprecation(): - """Check that we issue the FutureWarning regarding the deprecation of delayed.""" - - def func(x): - return x - - warn_msg = "The function `delayed` has been moved from `sklearn.utils.fixes`" - with pytest.warns(FutureWarning, match=warn_msg): - delayed(func) - - @pytest.mark.parametrize( "params, expected_dtype", [ From cb867a7e4631d347222453294028c7aeabfb37de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20du=20Boisberranger?= Date: Tue, 16 Apr 2024 18:19:25 +0200 Subject: [PATCH 033/344] MAINT Clean up deprecations for 1.5: in KMeans.predict (#28849) --- sklearn/cluster/_kmeans.py | 24 ++++-------------------- sklearn/cluster/tests/test_k_means.py | 13 ------------- 2 files changed, 4 insertions(+), 33 deletions(-) diff --git a/sklearn/cluster/_kmeans.py b/sklearn/cluster/_kmeans.py index 178242e60be57..16df12838d91f 100644 --- a/sklearn/cluster/_kmeans.py +++ b/sklearn/cluster/_kmeans.py @@ -1070,7 +1070,7 @@ def fit_predict(self, X, y=None, sample_weight=None): """ return self.fit(X, sample_weight=sample_weight).labels_ - def predict(self, X, sample_weight="deprecated"): + def predict(self, X): """Predict the closest cluster each sample in X belongs to. In the vector quantization literature, `cluster_centers_` is called @@ -1082,14 +1082,6 @@ def predict(self, X, sample_weight="deprecated"): X : {array-like, sparse matrix} of shape (n_samples, n_features) New data to predict. - sample_weight : array-like of shape (n_samples,), default=None - The weights for each observation in X. If None, all observations - are assigned equal weight. - - .. deprecated:: 1.3 - The parameter `sample_weight` is deprecated in version 1.3 - and will be removed in 1.5. - Returns ------- labels : ndarray of shape (n_samples,) @@ -1098,17 +1090,9 @@ def predict(self, X, sample_weight="deprecated"): check_is_fitted(self) X = self._check_test_data(X) - if not (isinstance(sample_weight, str) and sample_weight == "deprecated"): - warnings.warn( - ( - "'sample_weight' was deprecated in version 1.3 and " - "will be removed in 1.5." - ), - FutureWarning, - ) - sample_weight = _check_sample_weight(sample_weight, X, dtype=X.dtype) - else: - sample_weight = _check_sample_weight(None, X, dtype=X.dtype) + + # 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, diff --git a/sklearn/cluster/tests/test_k_means.py b/sklearn/cluster/tests/test_k_means.py index 1f2f8c390c909..b6f718dce03e0 100644 --- a/sklearn/cluster/tests/test_k_means.py +++ b/sklearn/cluster/tests/test_k_means.py @@ -201,19 +201,6 @@ def test_kmeans_convergence(algorithm, global_random_seed): assert km.n_iter_ < max_iter -@pytest.mark.parametrize("Estimator", [KMeans, MiniBatchKMeans]) -def test_predict_sample_weight_deprecation_warning(Estimator): - X = np.random.rand(100, 2) - sample_weight = np.random.uniform(size=100) - kmeans = Estimator() - kmeans.fit(X, sample_weight=sample_weight) - warn_msg = ( - "'sample_weight' was deprecated in version 1.3 and will be removed in 1.5." - ) - with pytest.warns(FutureWarning, match=warn_msg): - kmeans.predict(X, sample_weight=sample_weight) - - @pytest.mark.parametrize("X_csr", X_as_any_csr) def test_minibatch_update_consistency(X_csr, global_random_seed): # Check that dense and sparse minibatch update give the same results From 131109b0037ec71e4a4f262b888e198d58e38f24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20du=20Boisberranger?= Date: Wed, 17 Apr 2024 10:15:19 +0200 Subject: [PATCH 034/344] API Deprecate utils.parallel_backend and utils.register_parallel_backend (#28847) --- build_tools/linting.sh | 2 +- doc/whats_new/v1.5.rst | 5 ++++ .../tests/test_permutation_importance.py | 2 +- sklearn/tests/test_docstring_parameters.py | 1 + sklearn/utils/__init__.py | 17 ++++++----- sklearn/utils/_joblib.py | 2 ++ sklearn/utils/tests/test_utils.py | 29 ++++++++++--------- 7 files changed, 35 insertions(+), 23 deletions(-) diff --git a/build_tools/linting.sh b/build_tools/linting.sh index 58fef42b4dc45..aefabfae7b3f5 100755 --- a/build_tools/linting.sh +++ b/build_tools/linting.sh @@ -89,7 +89,7 @@ else fi # Check for joblib.delayed and joblib.Parallel imports - +# TODO(1.7): remove ":!sklearn/utils/_joblib.py" echo -e "### Checking for joblib imports ###\n" joblib_status=0 joblib_delayed_import="$(git grep -l -A 10 -E "joblib import.+delayed" -- "*.py" ":!sklearn/utils/_joblib.py" ":!sklearn/utils/parallel.py")" diff --git a/doc/whats_new/v1.5.rst b/doc/whats_new/v1.5.rst index 0aa506a71c6fb..3f292323bbcea 100644 --- a/doc/whats_new/v1.5.rst +++ b/doc/whats_new/v1.5.rst @@ -429,6 +429,11 @@ Changelog - |API| :func:`utils.tosequence` is deprecated and will be removed in version 1.7. :pr:`28763` by :user:`Jérémie du Boisberranger `. +- |API| :class:`utils.parallel_backend` and :func:`utils.register_parallel_backend` are + deprecated and will be removed in version 1.7. Use `joblib.parallel_backend` and + `joblib.register_parallel_backend` instead. + :pr:`28847` by :user:`Jérémie du Boisberranger `. + - |API| Raise informative warning message in :func:`type_of_target` when represented as bytes. For classifiers and classification metrics, labels encoded as bytes is deprecated and will raise an error in v1.6. diff --git a/sklearn/inspection/tests/test_permutation_importance.py b/sklearn/inspection/tests/test_permutation_importance.py index 8b3ed78cdd368..478a10515aa01 100644 --- a/sklearn/inspection/tests/test_permutation_importance.py +++ b/sklearn/inspection/tests/test_permutation_importance.py @@ -1,5 +1,6 @@ import numpy as np import pytest +from joblib import parallel_backend from numpy.testing import assert_allclose from sklearn.compose import ColumnTransformer @@ -22,7 +23,6 @@ from sklearn.model_selection import train_test_split from sklearn.pipeline import make_pipeline from sklearn.preprocessing import KBinsDiscretizer, OneHotEncoder, StandardScaler, scale -from sklearn.utils import parallel_backend from sklearn.utils._testing import _convert_container diff --git a/sklearn/tests/test_docstring_parameters.py b/sklearn/tests/test_docstring_parameters.py index bafd1de54f5aa..4f27af18ab4e2 100644 --- a/sklearn/tests/test_docstring_parameters.py +++ b/sklearn/tests/test_docstring_parameters.py @@ -51,6 +51,7 @@ ) # functions to ignore args / docstring of +# TODO(1.7): remove "sklearn.utils._joblib" _DOCSTRING_IGNORES = [ "sklearn.utils.deprecation.load_mlcomp", "sklearn.pipeline.make_pipeline", diff --git a/sklearn/utils/__init__.py b/sklearn/utils/__init__.py index 860972d764482..af02393966cc2 100644 --- a/sklearn/utils/__init__.py +++ b/sklearn/utils/__init__.py @@ -42,13 +42,16 @@ indexable, ) -# Do not deprecate parallel_backend and register_parallel_backend as they are -# needed to tune `scikit-learn` behavior and have different effect if called -# from the vendored version or or the site-package version. The other are -# utilities that are independent of scikit-learn so they are not part of -# scikit-learn public API. -parallel_backend = _joblib.parallel_backend -register_parallel_backend = _joblib.register_parallel_backend +# TODO(1.7): remove parallel_backend and register_parallel_backend +msg = "deprecated in 1.5 to be removed in 1.7. Use joblib.{} instead." +register_parallel_backend = deprecated(msg)(_joblib.register_parallel_backend) + + +# if a class, deprecated will change the object in _joblib module so we need to subclass +@deprecated(msg) +class parallel_backend(_joblib.parallel_backend): + pass + __all__ = [ "murmurhash3_32", diff --git a/sklearn/utils/_joblib.py b/sklearn/utils/_joblib.py index 590fdc6170c64..7638a30e7b5fa 100644 --- a/sklearn/utils/_joblib.py +++ b/sklearn/utils/_joblib.py @@ -1,3 +1,5 @@ +# TODO(1.7): remove this file + import warnings as _warnings with _warnings.catch_warnings(): diff --git a/sklearn/utils/tests/test_utils.py b/sklearn/utils/tests/test_utils.py index d36bdbd99e5f8..8d9af93d216ea 100644 --- a/sklearn/utils/tests/test_utils.py +++ b/sklearn/utils/tests/test_utils.py @@ -1,5 +1,6 @@ import warnings +import joblib import numpy as np import pytest @@ -7,11 +8,13 @@ check_random_state, column_or_1d, deprecated, + parallel_backend, + register_parallel_backend, safe_mask, tosequence, ) from sklearn.utils._missing import is_scalar_nan -from sklearn.utils._testing import assert_array_equal, assert_no_warnings +from sklearn.utils._testing import assert_array_equal from sklearn.utils.fixes import CSR_CONTAINERS from sklearn.utils.validation import _is_polars_df @@ -136,19 +139,6 @@ def dummy_func(): pass -def test_deprecation_joblib_api(tmpdir): - # Only parallel_backend and register_parallel_backend are not deprecated in - # sklearn.utils - from sklearn.utils import parallel_backend, register_parallel_backend - - assert_no_warnings(parallel_backend, "loky", None) - assert_no_warnings(register_parallel_backend, "failing", None) - - from sklearn.utils._joblib import joblib - - del joblib.parallel.BACKENDS["failing"] - - def test__is_polars_df(): """Check that _is_polars_df return False for non-dataframe objects.""" @@ -170,3 +160,14 @@ def test_is_pypy_deprecated(): def test_tosequence_deprecated(): with pytest.warns(FutureWarning, match="tosequence was deprecated in 1.5"): tosequence([1, 2, 3]) + + +# TODO(1.7): remove +def test_parallel_backend_deprecated(): + with pytest.warns(FutureWarning, match="parallel_backend is deprecated"): + parallel_backend("loky", None) + + with pytest.warns(FutureWarning, match="register_parallel_backend is deprecated"): + register_parallel_backend("a_backend", None) + + del joblib.parallel.BACKENDS["a_backend"] From 395761a76397a560303f00d3db24a50a00ca11ed Mon Sep 17 00:00:00 2001 From: Xuefeng Xu Date: Wed, 17 Apr 2024 18:10:25 +0800 Subject: [PATCH 035/344] DOC fix description of sample_weight in KBinsDiscretizer (#28853) --- sklearn/preprocessing/_discretization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sklearn/preprocessing/_discretization.py b/sklearn/preprocessing/_discretization.py index 86e0b870675af..02d144b87f798 100644 --- a/sklearn/preprocessing/_discretization.py +++ b/sklearn/preprocessing/_discretization.py @@ -213,7 +213,7 @@ def fit(self, X, y=None, sample_weight=None): sample_weight : ndarray of shape (n_samples,) Contains weight values to be associated with each sample. - Only possible when `strategy` is set to `"quantile"`. + Cannot be used when `strategy` is set to `"uniform"`. .. versionadded:: 1.3 From beb71774647fe1e7154c546a69c7ce412e5a2027 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20du=20Boisberranger?= Date: Wed, 17 Apr 2024 13:55:00 +0200 Subject: [PATCH 036/344] MAINT Clean up deprecations for 1.5: in make_sparse_coded_signal (#28805) --- sklearn/datasets/_samples_generator.py | 51 ++++--------------- .../datasets/tests/test_samples_generator.py | 36 ------------- 2 files changed, 11 insertions(+), 76 deletions(-) diff --git a/sklearn/datasets/_samples_generator.py b/sklearn/datasets/_samples_generator.py index 224978bd70770..e4fabcd892d7e 100644 --- a/sklearn/datasets/_samples_generator.py +++ b/sklearn/datasets/_samples_generator.py @@ -1466,7 +1466,6 @@ def make_low_rank_matrix( "n_features": [Interval(Integral, 1, None, closed="left")], "n_nonzero_coefs": [Interval(Integral, 1, None, closed="left")], "random_state": ["random_state"], - "data_transposed": ["boolean", Hidden(StrOptions({"deprecated"}))], }, prefer_skip_nested_validation=True, ) @@ -1477,13 +1476,12 @@ def make_sparse_coded_signal( n_features, n_nonzero_coefs, random_state=None, - data_transposed="deprecated", ): """Generate a signal as a sparse combination of dictionary elements. - Returns a matrix `Y = DX`, such that `D` is of shape `(n_features, n_components)`, - `X` is of shape `(n_components, n_samples)` and each column of `X` has exactly - `n_nonzero_coefs` non-zero 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. Read more in the :ref:`User Guide `. @@ -1506,33 +1504,17 @@ def make_sparse_coded_signal( for reproducible output across multiple function calls. See :term:`Glossary `. - data_transposed : bool, default=False - By default, Y, D and X are not transposed. - - .. versionadded:: 1.1 - - .. versionchanged:: 1.3 - Default value changed from True to False. - - .. deprecated:: 1.3 - `data_transposed` is deprecated and will be removed in 1.5. - Returns ------- - data : ndarray of shape (n_features, n_samples) or (n_samples, n_features) - The encoded signal (Y). The shape is `(n_samples, n_features)` if - `data_transposed` is False, otherwise it's `(n_features, n_samples)`. + data : ndarray of shape (n_samples, n_features) + The encoded signal (Y). - dictionary : ndarray of shape (n_features, n_components) or \ - (n_components, n_features) - The dictionary with normalized components (D). The shape is - `(n_components, n_features)` if `data_transposed` is False, otherwise it's - `(n_features, n_components)`. + dictionary : ndarray of shape (n_components, n_features) + The dictionary with normalized components (D). - code : ndarray of shape (n_components, n_samples) or (n_samples, n_components) + 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). The shape is `(n_samples, n_components)` - if `data_transposed` is False, otherwise it's `(n_components, n_samples)`. + n_nonzero_coefs non-zero items (X). Examples -------- @@ -1568,19 +1550,8 @@ def make_sparse_coded_signal( # encode signal Y = np.dot(D, X) - # TODO(1.5) remove data_transposed - # raise warning if data_transposed is not passed explicitly - if data_transposed != "deprecated": - warnings.warn( - "data_transposed was deprecated in version 1.3 and will be removed in 1.5.", - FutureWarning, - ) - else: - data_transposed = False - - # transpose if needed - if not data_transposed: - Y, D, X = Y.T, D.T, X.T + # 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)) diff --git a/sklearn/datasets/tests/test_samples_generator.py b/sklearn/datasets/tests/test_samples_generator.py index 9a9cc41d7229c..a2524fd7561fe 100644 --- a/sklearn/datasets/tests/test_samples_generator.py +++ b/sklearn/datasets/tests/test_samples_generator.py @@ -33,7 +33,6 @@ assert_almost_equal, assert_array_almost_equal, assert_array_equal, - ignore_warnings, ) from sklearn.utils.validation import assert_all_finite @@ -500,41 +499,6 @@ def test_make_sparse_coded_signal(): assert_allclose(np.sqrt((D**2).sum(axis=1)), np.ones(D.shape[0])) -# TODO(1.5): remove -@ignore_warnings(category=FutureWarning) -def test_make_sparse_coded_signal_transposed(): - Y, D, X = make_sparse_coded_signal( - n_samples=5, - n_components=8, - n_features=10, - n_nonzero_coefs=3, - random_state=0, - data_transposed=True, - ) - assert Y.shape == (10, 5), "Y shape mismatch" - assert D.shape == (10, 8), "D shape mismatch" - assert X.shape == (8, 5), "X shape mismatch" - for col in X.T: - assert len(np.flatnonzero(col)) == 3, "Non-zero coefs mismatch" - assert_allclose(Y, D @ X) - assert_allclose(np.sqrt((D**2).sum(axis=0)), np.ones(D.shape[1])) - - -# TODO(1.5): remove -def test_make_sparse_code_signal_deprecation_warning(): - """Check the message for future deprecation.""" - warn_msg = "data_transposed was deprecated in version 1.3" - with pytest.warns(FutureWarning, match=warn_msg): - make_sparse_coded_signal( - n_samples=1, - n_components=1, - n_features=1, - n_nonzero_coefs=1, - random_state=0, - data_transposed=True, - ) - - def test_make_sparse_uncorrelated(): X, y = make_sparse_uncorrelated(n_samples=5, n_features=10, random_state=0) From db9b04519f2f3f25e922b799a17a57f066cffa97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20du=20Boisberranger?= Date: Wed, 17 Apr 2024 16:38:15 +0200 Subject: [PATCH 037/344] CI Update ruff invoke command in comment (#28854) --- build_tools/get_comment.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/build_tools/get_comment.py b/build_tools/get_comment.py index b91e7383e96ea..b357c68f23e3e 100644 --- a/build_tools/get_comment.py +++ b/build_tools/get_comment.py @@ -116,10 +116,10 @@ def get_message(log_file, repo, pr_number, sha, run_id, details, versions): end="Problems detected by ruff", title="`ruff`", message=( - "`ruff` detected issues. Please run `ruff --fix --output-format=full .` " - "locally, fix the remaining issues, and push the changes. " - "Here you can see the detected issues. Note that the installed " - f"`ruff` version is `ruff={versions['ruff']}`." + "`ruff` detected issues. Please run " + "`ruff check --fix --output-format=full .` locally, fix the remaining " + "issues, and push the changes. Here you can see the detected issues. Note " + f"that the installed `ruff` version is `ruff={versions['ruff']}`." ), details=details, ) From 9397ab8958e40baaf419784ec08c9c8cf33f8994 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Duarte=20S=C3=A3o=20Jos=C3=A9?= Date: Wed, 17 Apr 2024 16:06:00 +0100 Subject: [PATCH 038/344] MAINT RFE and RFECV warn when features_to_select larger than n_features (#28764) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jérémie du Boisberranger --- sklearn/feature_selection/_rfe.py | 21 ++++++++++++++++++++- sklearn/feature_selection/tests/test_rfe.py | 17 +++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/sklearn/feature_selection/_rfe.py b/sklearn/feature_selection/_rfe.py index 44764655e988d..7c5cd8d45b8d1 100644 --- a/sklearn/feature_selection/_rfe.py +++ b/sklearn/feature_selection/_rfe.py @@ -6,6 +6,7 @@ """Recursive feature elimination for feature ranking""" +import warnings from numbers import Integral import numpy as np @@ -286,6 +287,14 @@ def _fit(self, X, y, step_score=None, **fit_params): 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: + warnings.warn( + ( + f"Found {n_features_to_select=} > {n_features=}. There will be" + " no feature selection and all features will be kept." + ), + UserWarning, + ) else: # float n_features_to_select = int(n_features * self.n_features_to_select) @@ -729,9 +738,19 @@ def fit(self, X, y, groups=None): # Build an RFE object, which will evaluate and score each possible # feature count, down to self.min_features_to_select + n_features = X.shape[1] + if self.min_features_to_select > n_features: + warnings.warn( + ( + 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." + ), + UserWarning, + ) rfe = RFE( estimator=self.estimator, - n_features_to_select=self.min_features_to_select, + n_features_to_select=min(self.min_features_to_select, n_features), importance_getter=self.importance_getter, step=self.step, verbose=self.verbose, diff --git a/sklearn/feature_selection/tests/test_rfe.py b/sklearn/feature_selection/tests/test_rfe.py index 11ab086f2f010..a0610e990054f 100644 --- a/sklearn/feature_selection/tests/test_rfe.py +++ b/sklearn/feature_selection/tests/test_rfe.py @@ -649,3 +649,20 @@ def test_rfe_estimator_attribute_error(): rfe.fit(iris.data, iris.target).decision_function(iris.data) assert isinstance(exec_info.value.__cause__, AttributeError) assert inner_msg in str(exec_info.value.__cause__) + + +@pytest.mark.parametrize( + "ClsRFE, param", [(RFE, "n_features_to_select"), (RFECV, "min_features_to_select")] +) +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) From 6ffeabd30087d6abefe12cc04290aeeb7e3ec7ea Mon Sep 17 00:00:00 2001 From: myenugula <127900888+myenugula@users.noreply.github.com> Date: Wed, 17 Apr 2024 19:53:02 +0400 Subject: [PATCH 039/344] FIX HDBSCAN allows all pairwise metrics when algorithm="brute"/"auto" (#28664) Co-authored-by: jeremiedbb --- doc/whats_new/v1.5.rst | 4 ++++ sklearn/cluster/_hdbscan/hdbscan.py | 6 +++++- sklearn/cluster/tests/test_hdbscan.py | 20 ++++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/doc/whats_new/v1.5.rst b/doc/whats_new/v1.5.rst index 3f292323bbcea..f313d0c675ceb 100644 --- a/doc/whats_new/v1.5.rst +++ b/doc/whats_new/v1.5.rst @@ -141,6 +141,10 @@ Changelog :class:`~cluster.OPTICS` to avoid in-place modification of the sparse matrix. :pr:`28491` by :user:`Thanh Lam Dang `. +- |Fix| :class:`cluster.HDBSCAN` now supports all metrics supported by + :func:`sklearn.metrics.pairwise_distances` when `algorithm="brute"` or `"auto"`. + :pr:`28664` by :user:`Manideep Yenugula `. + :mod:`sklearn.compose` ...................... diff --git a/sklearn/cluster/_hdbscan/hdbscan.py b/sklearn/cluster/_hdbscan/hdbscan.py index e77baaf4b1146..9933318313cc8 100644 --- a/sklearn/cluster/_hdbscan/hdbscan.py +++ b/sklearn/cluster/_hdbscan/hdbscan.py @@ -45,6 +45,7 @@ from ...base import BaseEstimator, ClusterMixin, _fit_context from ...metrics import pairwise_distances from ...metrics._dist_metrics import DistanceMetric +from ...metrics.pairwise import _VALID_METRICS from ...neighbors import BallTree, KDTree, NearestNeighbors from ...utils._param_validation import Interval, StrOptions from ...utils.validation import _allclose_dense_sparse, _assert_all_finite @@ -647,7 +648,10 @@ class HDBSCAN(ClusterMixin, BaseEstimator): None, Interval(Integral, left=1, right=None, closed="left"), ], - "metric": [StrOptions(FAST_METRICS | {"precomputed"}), callable], + "metric": [ + StrOptions(FAST_METRICS | set(_VALID_METRICS) | {"precomputed"}), + callable, + ], "metric_params": [dict, None], "alpha": [Interval(Real, left=0, right=None, closed="neither")], # TODO(1.6): Remove "kdtree" and "balltree" option diff --git a/sklearn/cluster/tests/test_hdbscan.py b/sklearn/cluster/tests/test_hdbscan.py index d586d203747c2..f5a0cddb0187d 100644 --- a/sklearn/cluster/tests/test_hdbscan.py +++ b/sklearn/cluster/tests/test_hdbscan.py @@ -580,3 +580,23 @@ def test_hdbscan_error_precomputed_and_store_centers(store_centers): err_msg = "Cannot store centers when using a precomputed distance matrix." with pytest.raises(ValueError, match=err_msg): HDBSCAN(metric="precomputed", store_centers=store_centers).fit(X_dist) + + +@pytest.mark.parametrize("valid_algo", ["auto", "brute"]) +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) + + +@pytest.mark.parametrize("invalid_algo", ["kd_tree", "ball_tree"]) +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) From 55378cbc737a518ff1857d79a1f9b7774691ef72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Est=C3=A8ve?= Date: Thu, 18 Apr 2024 09:58:56 +0200 Subject: [PATCH 040/344] BLD Avoid misleading OpenMP warning with Apple Clang (#28839) --- sklearn/meson.build | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/sklearn/meson.build b/sklearn/meson.build index 058aad96bb435..8736669f14cdb 100644 --- a/sklearn/meson.build +++ b/sklearn/meson.build @@ -60,6 +60,27 @@ np_dep = declare_dependency(include_directories: inc_np) openmp_dep = dependency('OpenMP', language: 'c', required: false) if not openmp_dep.found() + warn_about_missing_openmp = true + # On Apple Clang avoid a misleading warning if compiler variables are set. + # See https://github.com/scikit-learn/scikit-learn/issues/28710 for more + # details. This may be removed if the OpenMP detection on Apple Clang improves, + # see https://github.com/mesonbuild/meson/issues/7435#issuecomment-2047585466. + if host_machine.system() == 'darwin' and cc.get_id() == 'clang' + compiler_env_vars_with_openmp = run_command(py, + [ + '-c', + ''' +import os + +compiler_env_vars_to_check = ["CPPFLAGS", "CFLAGS", "CXXFLAGS"] + +compiler_env_vars_with_openmp = [ + var for var in compiler_env_vars_to_check if "-fopenmp" in os.getenv(var, "")] +print(compiler_env_vars_with_openmp) +'''], check: true).stdout().strip() + warn_about_missing_openmp = compiler_env_vars_with_openmp == '[]' + endif + if warn_about_missing_openmp warning( ''' *********** @@ -84,6 +105,13 @@ It seems that scikit-learn cannot be built with OpenMP. *** ''') + else + warning( +'''It looks like compiler environment variables were set to enable OpenMP support. +Check the output of "import sklearn; sklearn.show_versions()" after the build +to make sure that scikit-learn was actually built with OpenMP support. +''') + endif endif # For now, we keep supporting SKLEARN_ENABLE_DEBUG_CYTHON_DIRECTIVES variable @@ -127,7 +155,6 @@ custom_target('write_built_with_meson_file', install: true, install_dir: py.get_install_dir() / 'sklearn' ) -# endif extensions = ['_isotonic'] From 05ee9dd4b5d925c6172d2f21c095051587a84083 Mon Sep 17 00:00:00 2001 From: Arturo Amor <86408019+ArturoAmorQ@users.noreply.github.com> Date: Thu, 18 Apr 2024 12:18:08 +0200 Subject: [PATCH 041/344] FIX Accept d2_absolute_error_score as named scorer (#28836) --- sklearn/metrics/_scorer.py | 3 +++ sklearn/metrics/tests/test_score_objects.py | 1 + 2 files changed, 4 insertions(+) diff --git a/sklearn/metrics/_scorer.py b/sklearn/metrics/_scorer.py index 7c5321b04730f..391d19459a871 100644 --- a/sklearn/metrics/_scorer.py +++ b/sklearn/metrics/_scorer.py @@ -45,6 +45,7 @@ balanced_accuracy_score, brier_score_loss, class_likelihood_ratios, + d2_absolute_error_score, explained_variance_score, f1_score, jaccard_score, @@ -727,6 +728,7 @@ def make_scorer( neg_mean_gamma_deviance_scorer = make_scorer( mean_gamma_deviance, greater_is_better=False ) +d2_absolute_error_scorer = make_scorer(d2_absolute_error_score) # Standard Classification Scores accuracy_scorer = make_scorer(accuracy_score) @@ -819,6 +821,7 @@ def negative_likelihood_ratio(y_true, y_pred): neg_root_mean_squared_log_error=neg_root_mean_squared_log_error_scorer, neg_mean_poisson_deviance=neg_mean_poisson_deviance_scorer, neg_mean_gamma_deviance=neg_mean_gamma_deviance_scorer, + d2_absolute_error_score=d2_absolute_error_scorer, accuracy=accuracy_scorer, top_k_accuracy=top_k_accuracy_scorer, roc_auc=roc_auc_scorer, diff --git a/sklearn/metrics/tests/test_score_objects.py b/sklearn/metrics/tests/test_score_objects.py index 49c671d486550..6ce2e3b30dd8e 100644 --- a/sklearn/metrics/tests/test_score_objects.py +++ b/sklearn/metrics/tests/test_score_objects.py @@ -65,6 +65,7 @@ from sklearn.utils.metadata_routing import MetadataRouter REGRESSION_SCORERS = [ + "d2_absolute_error_score", "explained_variance", "r2", "neg_mean_absolute_error", From 0f701ff83935c35a173d6f9db79f0b5702dfa2af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20du=20Boisberranger?= Date: Fri, 19 Apr 2024 12:37:45 +0200 Subject: [PATCH 042/344] MAINT Fix target versions in TODOs (#28855) --- doc/whats_new/v1.5.rst | 4 ++-- sklearn/metrics/tests/test_ranking.py | 2 +- sklearn/utils/tests/test_multiclass.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/whats_new/v1.5.rst b/doc/whats_new/v1.5.rst index f313d0c675ceb..b30c486d69877 100644 --- a/doc/whats_new/v1.5.rst +++ b/doc/whats_new/v1.5.rst @@ -351,7 +351,7 @@ Changelog :pr:`28092` by :user:`Adam Li `. - |API| For classifiers and classification metrics, labels encoded as bytes - is deprecated and will raise an error in v1.6. + is deprecated and will raise an error in v1.7. :pr:`18555` by :user:`Kaushik Amar Das `. :mod:`sklearn.mixture` @@ -440,7 +440,7 @@ Changelog - |API| Raise informative warning message in :func:`type_of_target` when represented as bytes. For classifiers and classification metrics, labels encoded - as bytes is deprecated and will raise an error in v1.6. + as bytes is deprecated and will raise an error in v1.7. :pr:`18555` by :user:`Kaushik Amar Das `. - |Fix| :func:`~utils._safe_indexing` now works correctly for polars DataFrame when diff --git a/sklearn/metrics/tests/test_ranking.py b/sklearn/metrics/tests/test_ranking.py index 4beeea23446c8..ac3c3855a327e 100644 --- a/sklearn/metrics/tests/test_ranking.py +++ b/sklearn/metrics/tests/test_ranking.py @@ -875,7 +875,7 @@ def test_binary_clf_curve_implicit_pos_label(curve_func): np.testing.assert_allclose(int_curve_part, float_curve_part) -# TODO(1.5): Update test to check for error when bytes support is removed. +# TODO(1.7): Update test to check for error when bytes support is removed. @ignore_warnings(category=FutureWarning) @pytest.mark.parametrize("curve_func", [precision_recall_curve, roc_curve]) @pytest.mark.parametrize("labels_type", ["list", "array"]) diff --git a/sklearn/utils/tests/test_multiclass.py b/sklearn/utils/tests/test_multiclass.py index 40a13526ab009..95a1ea0bb0806 100644 --- a/sklearn/utils/tests/test_multiclass.py +++ b/sklearn/utils/tests/test_multiclass.py @@ -598,7 +598,7 @@ def test_ovr_decision_function(): assert_allclose(dec_values, dec_values_one, atol=1e-6) -# TODO(1.6): Change to ValueError when byte labels is deprecated. +# TODO(1.7): Change to ValueError when byte labels is deprecated. @pytest.mark.parametrize("input_type", ["list", "array"]) def test_labels_in_bytes_format(input_type): # check that we raise an error with bytes encoded labels From 3af257e51f0a213211ac18cbde9175f7ae21579e Mon Sep 17 00:00:00 2001 From: Christian Lorentzen Date: Fri, 19 Apr 2024 12:53:54 +0200 Subject: [PATCH 043/344] DOC minor improvements of Features in Histogram Gradient Boosting Trees (#28858) --- examples/ensemble/plot_hgbt_regression.py | 48 ++++++++++++----------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/examples/ensemble/plot_hgbt_regression.py b/examples/ensemble/plot_hgbt_regression.py index 3bf3762bae406..55ca65ea4a3b8 100644 --- a/examples/ensemble/plot_hgbt_regression.py +++ b/examples/ensemble/plot_hgbt_regression.py @@ -13,10 +13,10 @@ The top usability features of HGBT models are: -1. Several available loss function for mean and quantile regression tasks, see +1. Several available loss functions for mean and quantile regression tasks, see :ref:`Quantile loss `. -2. :ref:`categorical_support_gbdt` (see - :ref:`sphx_glr_auto_examples_ensemble_plot_gradient_boosting_categorical.py`). +2. :ref:`categorical_support_gbdt`, see + :ref:`sphx_glr_auto_examples_ensemble_plot_gradient_boosting_categorical.py`. 3. Early stopping. 4. :ref:`nan_support_hgbt`, which avoids the need for an imputer. 5. :ref:`monotonic_cst_gbdt`. @@ -38,7 +38,7 @@ # set every five minutes. Electricity transfers to/from the neighboring state of # Victoria were done to alleviate fluctuations. # -# The dataset (originally named ELEC2) contains 45,312 instances dated from 7 +# The dataset, originally named ELEC2, contains 45,312 instances dated from 7 # May 1996 to 5 December 1998. Each sample of the dataset refers to a period of # 30 minutes, i.e. there are 48 instances for each time period of one day. Each # sample on the dataset has 7 columns: @@ -48,7 +48,7 @@ # - nswprice/nswdemand: electricity price/demand of New South Wales; # - vicprice/vicdemand: electricity price/demand of Victoria. # -# It is originally a classification task, but here we use it for the regression +# Originally, it is a classification task, but here we use it for the regression # task to predict the scheduled electricity transfer between states. from sklearn.datasets import fetch_openml @@ -86,7 +86,7 @@ _ = ax.legend(handles, ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]) # %% -# Notice energy transfer increases systematically during weekends. +# Notice that energy transfer increases systematically during weekends. # # Effect of number of trees and early stopping # ============================================ @@ -95,7 +95,7 @@ # daily electricity transfer using the whole dataset. Then we visualize its # predictions depending on the `max_iter` parameter. Here we don't try to # evaluate the performance of the model and its capacity to generalize but -# rather its capacity to learn from the training data. +# rather its capability to learn from the training data. from sklearn.ensemble import HistGradientBoostingRegressor from sklearn.model_selection import train_test_split @@ -144,22 +144,24 @@ # With just a few iterations, HGBT models can achieve convergence (see # :ref:`sphx_glr_auto_examples_ensemble_plot_forest_hist_grad_boosting_comparison.py`), # meaning that adding more trees does not improve the model anymore. In the -# figure above, 5 iterations are not enough to be able to predict. With 50 +# figure above, 5 iterations are not enough to get good predictions. With 50 # iterations, we are already able to do a good job. # -# Instead of relying on `max_iter` alone to determine when to stop, the HGBT -# implementation in scikit-learn supports early stopping. With it, the model +# Setting `max_iter` too high might degrade the prediction quality and cost a lot of +# avoidable computing resources. Therefore, the HGBT implementation in scikit-learn +# provides an automatic **early stopping** strategy. With it, the model # uses a fraction of the training data as internal validation set # (`validation_fraction`) and stops training if the validation score does not # improve (or degrades) after `n_iter_no_change` iterations up to a certain -# `tol`. +# tolerance (`tol`). # # Notice that there is a trade-off between `learning_rate` and `max_iter`: # Generally, smaller learning rates are preferable but require more iterations # to converge to the minimum loss, while larger learning rates converge faster # (less iterations/trees needed) but at the cost of a larger minimum loss. # -# Indeed, a good practice is to tune the learning rate along with any other +# Because of this high correlation between the learning rate the number of iterations, +# a good practice is to tune the learning rate along with all (important) other # hyperparameters, fit the HBGT on the training set with a large enough value # for `max_iter` and determine the best `max_iter` via early stopping and some # explicit `validation_fraction`. @@ -204,9 +206,9 @@ # HGBT models have native support of missing values. During training, the tree # grower decides where samples with missing values should go (left or right # child) at each split, based on the potential gain. When predicting, these -# samples are sent to either child accordingly. If a feature had no missing -# values during training, samples with missing values for that feature are sent -# to the child with the most samples. +# samples are sent to the learnt child accordingly. If a feature had no missing +# values during training, then for prediction, samples with missing values for that +# feature are sent to the child with the most samples (as seen during fit). # # The present example shows how HGBT regressions deal with values missing # completely at random (MCAR), i.e. the missingness does not depend on the @@ -313,10 +315,10 @@ def generate_missing_values(X, missing_fraction): # model. One can still improve the quality of such estimations by: # # - collecting more data-points; -# - better tuning of the model hyperparameters (see -# :ref:`sphx_glr_auto_examples_ensemble_plot_gradient_boosting_quantile.py`); -# - engineering more predictive features from the same data (see -# :ref:`sphx_glr_auto_examples_applications_plot_cyclical_feature_engineering.py`). +# - better tuning of the model hyperparameters, see +# :ref:`sphx_glr_auto_examples_ensemble_plot_gradient_boosting_quantile.py`; +# - engineering more predictive features from the same data, see +# :ref:`sphx_glr_auto_examples_applications_plot_cyclical_feature_engineering.py`. # # Monotonic constraints # ===================== @@ -342,7 +344,8 @@ def generate_missing_values(X, missing_fraction): # - 0: no constraint # - -1: monotonic decrease # -# Else, one can pass an array-like encoding the above convention by position. +# Alternatively, one can pass an array-like object encoding the above convention by +# position. from sklearn.inspection import PartialDependenceDisplay @@ -394,8 +397,9 @@ def generate_missing_values(X, missing_fraction): _ = plt.legend() # %% -# Observe that `nswdemand` seems already monotonic without constraint. This is a -# good example to show that the model is "overconstraining". +# Observe that `nswdemand` and `vicdemand` seem already monotonic without constraint. +# This is a good example to show that the model with monotonicity constraints is +# "overconstraining". # # Additionally, we can verify that the predictive quality of the model is not # significantly degraded by introducing the monotonic constraints. For such From 51fca394aa5236503c8abf1f056f50f823ec3d67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20du=20Boisberranger?= Date: Mon, 22 Apr 2024 09:25:15 +0200 Subject: [PATCH 044/344] Fix ColumnTransformer in parallel with joblib's auto memmapping (#28822) --- doc/whats_new/v1.5.rst | 4 +++ sklearn/compose/_column_transformer.py | 9 ++++--- .../compose/tests/test_column_transformer.py | 27 ++++++++++++++++++- sklearn/pipeline.py | 24 ++++++++++++++--- 4 files changed, 55 insertions(+), 9 deletions(-) diff --git a/doc/whats_new/v1.5.rst b/doc/whats_new/v1.5.rst index b30c486d69877..24893cdebc7cc 100644 --- a/doc/whats_new/v1.5.rst +++ b/doc/whats_new/v1.5.rst @@ -156,6 +156,10 @@ Changelog being explicitly set as well. :pr:`28483` by :user:`Stefanie Senger `. +- |Fix| Fixed an bug in :class:`compose.ColumnTransformer` with `n_jobs > 1`, where the + intermediate selected columns were passed to the transformers as read-only arrays. + :pr:`28822` by :user:`Jérémie du Boisberranger `. + :mod:`sklearn.cross_decomposition` .................................. diff --git a/sklearn/compose/_column_transformer.py b/sklearn/compose/_column_transformer.py index 96b0ff527b1f8..69c6effc835b2 100644 --- a/sklearn/compose/_column_transformer.py +++ b/sklearn/compose/_column_transformer.py @@ -18,7 +18,7 @@ from ..base import TransformerMixin, _fit_context, clone from ..pipeline import _fit_transform_one, _name_estimators, _transform_one from ..preprocessing import FunctionTransformer -from ..utils import Bunch, _safe_indexing +from ..utils import Bunch from ..utils._estimator_html_repr import _VisualBlock from ..utils._indexing import _get_column_indices from ..utils._metadata_requests import METHODS @@ -389,7 +389,7 @@ def set_params(self, **kwargs): def _iter(self, fitted, column_as_labels, skip_drop, skip_empty_columns): """ - Generate (name, trans, column, weight) tuples. + Generate (name, trans, columns, weight) tuples. Parameters @@ -794,7 +794,7 @@ def _call_func_on_transformers(self, X, y, func, column_as_labels, routed_params ) try: jobs = [] - for idx, (name, trans, column, weight) in enumerate(transformers, start=1): + 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) @@ -813,9 +813,10 @@ def _call_func_on_transformers(self, X, y, func, column_as_labels, routed_params jobs.append( delayed(func)( transformer=clone(trans) if not fitted else trans, - X=_safe_indexing(X, column, axis=1), + X=X, y=y, weight=weight, + columns=columns, **extra_args, params=routed_params[name], ) diff --git a/sklearn/compose/tests/test_column_transformer.py b/sklearn/compose/tests/test_column_transformer.py index 56c4cd459aab5..d2c7b920d0e1d 100644 --- a/sklearn/compose/tests/test_column_transformer.py +++ b/sklearn/compose/tests/test_column_transformer.py @@ -6,6 +6,7 @@ import re import warnings +import joblib import numpy as np import pytest from numpy.testing import assert_allclose @@ -36,7 +37,7 @@ assert_almost_equal, assert_array_equal, ) -from sklearn.utils.fixes import CSR_CONTAINERS +from sklearn.utils.fixes import CSR_CONTAINERS, parse_version class Trans(TransformerMixin, BaseEstimator): @@ -2447,6 +2448,30 @@ def test_column_transformer_error_with_duplicated_columns(dataframe_lib): transformer.fit_transform(df) +@pytest.mark.skipif( + parse_version(joblib.__version__) < parse_version("1.3"), + reason="requires joblib >= 1.3", +) +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) + + transformer = ColumnTransformer( + 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]])) + + # Metadata Routing Tests # ====================== diff --git a/sklearn/pipeline.py b/sklearn/pipeline.py index 1b17599068d7a..020491eb413fe 100644 --- a/sklearn/pipeline.py +++ b/sklearn/pipeline.py @@ -19,7 +19,7 @@ from .base import TransformerMixin, _fit_context, clone from .exceptions import NotFittedError from .preprocessing import FunctionTransformer -from .utils import Bunch +from .utils import Bunch, _safe_indexing from .utils._estimator_html_repr import _VisualBlock from .utils._metadata_requests import METHODS from .utils._param_validation import HasMethods, Hidden @@ -1258,7 +1258,7 @@ def make_pipeline(*steps, memory=None, verbose=False): return Pipeline(_name_estimators(steps), memory=memory, verbose=verbose) -def _transform_one(transformer, X, y, weight, params): +def _transform_one(transformer, X, y, weight, columns=None, params=None): """Call transform and apply weight to output. Parameters @@ -1275,11 +1275,17 @@ def _transform_one(transformer, X, y, weight, params): weight : float Weight to be applied to the output of the transformation. + columns : str, array-like of str, int, array-like of int, array-like of bool, slice + Columns to select before transforming. + params : dict Parameters to be passed to the transformer's ``transform`` method. This should be of the form ``process_routing()["step_name"]``. """ + if columns is not None: + X = _safe_indexing(X, columns, axis=1) + res = transformer.transform(X, **params.transform) # if we have a weight for this transformer, multiply output if weight is None: @@ -1288,7 +1294,14 @@ def _transform_one(transformer, X, y, weight, params): def _fit_transform_one( - transformer, X, y, weight, message_clsname="", message=None, params=None + transformer, + X, + y, + weight, + columns=None, + message_clsname="", + message=None, + params=None, ): """ Fits ``transformer`` to ``X`` and ``y``. The transformed result is returned @@ -1297,6 +1310,9 @@ def _fit_transform_one( ``params`` needs to be of the form ``process_routing()["step_name"]``. """ + if columns is not None: + X = _safe_indexing(X, columns, axis=1) + params = params or {} with _print_elapsed_time(message_clsname, message): if hasattr(transformer, "fit_transform"): @@ -1792,7 +1808,7 @@ def transform(self, X, **params): routed_params[name] = Bunch(transform={}) Xs = Parallel(n_jobs=self.n_jobs)( - delayed(_transform_one)(trans, X, None, weight, routed_params[name]) + delayed(_transform_one)(trans, X, None, weight, params=routed_params[name]) for name, trans, weight in self._iter() ) if not Xs: From 8d456d555ef208a1585159421bfc782e93cb275f Mon Sep 17 00:00:00 2001 From: Christian Lorentzen Date: Mon, 22 Apr 2024 10:14:16 +0200 Subject: [PATCH 045/344] MNT replace cdef extern from by cimport libc... (#28866) --- sklearn/manifold/_barnes_hut_tsne.pyx | 11 +---------- sklearn/neighbors/_quad_tree.pyx | 4 +--- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/sklearn/manifold/_barnes_hut_tsne.pyx b/sklearn/manifold/_barnes_hut_tsne.pyx index 485a14edce6c5..f0906fbf2bec8 100644 --- a/sklearn/manifold/_barnes_hut_tsne.pyx +++ b/sklearn/manifold/_barnes_hut_tsne.pyx @@ -10,6 +10,7 @@ cimport numpy as cnp from libc.stdio cimport printf from libc.math cimport log from libc.stdlib cimport malloc, free +from libc.time cimport clock, clock_t from cython.parallel cimport prange, parallel from ..neighbors._quad_tree cimport _QuadTree @@ -19,9 +20,6 @@ cnp.import_array() cdef char* EMPTY_STRING = "" -cdef extern from "math.h": - float fabsf(float x) nogil - # Smallest strictly positive value that can be represented by floating # point numbers for different precision levels. This is useful to avoid # taking the log of zero when computing the KL divergence. @@ -36,13 +34,6 @@ cdef float FLOAT64_EPS = np.finfo(np.float64).eps cdef enum: DEBUGFLAG = 0 -cdef extern from "time.h": - # Declare only what is necessary from `tm` structure. - ctypedef long clock_t - clock_t clock() nogil - double CLOCKS_PER_SEC - - cdef float compute_gradient(float[:] val_P, float[:, :] pos_reference, cnp.int64_t[:] neighbors, diff --git a/sklearn/neighbors/_quad_tree.pyx b/sklearn/neighbors/_quad_tree.pyx index e481e41ca65e4..f1ef4e64f30fe 100644 --- a/sklearn/neighbors/_quad_tree.pyx +++ b/sklearn/neighbors/_quad_tree.pyx @@ -4,6 +4,7 @@ from cpython cimport Py_INCREF, PyObject, PyTypeObject +from libc.math cimport fabsf from libc.stdlib cimport free from libc.string cimport memcpy from libc.stdio cimport printf @@ -15,9 +16,6 @@ import numpy as np cimport numpy as cnp cnp.import_array() -cdef extern from "math.h": - float fabsf(float x) nogil - cdef extern from "numpy/arrayobject.h": object PyArray_NewFromDescr(PyTypeObject* subtype, cnp.dtype descr, int nd, cnp.npy_intp* dims, From 02faf57fe6477bf1c2e977ccdf9a644ebb4ea0b4 Mon Sep 17 00:00:00 2001 From: scikit-learn-bot Date: Mon, 22 Apr 2024 10:38:17 +0200 Subject: [PATCH 046/344] :lock: :robot: CI Update lock files for main CI build(s) :lock: :robot: (#28872) Co-authored-by: Lock file bot --- build_tools/azure/debian_atlas_32bit_lock.txt | 7 +- ...latest_conda_forge_mkl_linux-64_conda.lock | 34 ++++----- ...pylatest_conda_forge_mkl_osx-64_conda.lock | 16 ++-- ...test_conda_mkl_no_openmp_osx-64_conda.lock | 12 +-- ...st_pip_openblas_pandas_linux-64_conda.lock | 18 ++--- ...onda_defaults_openblas_linux-64_conda.lock | 4 +- .../pymin_conda_forge_mkl_win-64_conda.lock | 20 ++--- ...e_openblas_ubuntu_2204_linux-64_conda.lock | 36 ++++----- build_tools/azure/ubuntu_atlas_lock.txt | 9 ++- build_tools/circle/doc_linux-64_conda.lock | 74 +++++++++---------- .../doc_min_dependencies_linux-64_conda.lock | 60 +++++++-------- 11 files changed, 146 insertions(+), 144 deletions(-) diff --git a/build_tools/azure/debian_atlas_32bit_lock.txt b/build_tools/azure/debian_atlas_32bit_lock.txt index 40e0ff4e25cb8..a6bb0485d1933 100644 --- a/build_tools/azure/debian_atlas_32bit_lock.txt +++ b/build_tools/azure/debian_atlas_32bit_lock.txt @@ -16,19 +16,20 @@ joblib==1.2.0 # via -r build_tools/azure/debian_atlas_32bit_requirements.txt meson==1.4.0 # via meson-python -meson-python==0.15.0 +meson-python==0.16.0 # via -r build_tools/azure/debian_atlas_32bit_requirements.txt ninja==1.11.1.1 # via -r build_tools/azure/debian_atlas_32bit_requirements.txt packaging==24.0 # via + # meson-python # pyproject-metadata # pytest -pluggy==1.4.0 +pluggy==1.5.0 # via pytest py==1.11.0 # via pytest -pyproject-metadata==0.7.1 +pyproject-metadata==0.8.0 # via meson-python pytest==7.1.2 # via diff --git a/build_tools/azure/pylatest_conda_forge_mkl_linux-64_conda.lock b/build_tools/azure/pylatest_conda_forge_mkl_linux-64_conda.lock index 5f8dda6fa9748..aa525371df1ef 100644 --- a/build_tools/azure/pylatest_conda_forge_mkl_linux-64_conda.lock +++ b/build_tools/azure/pylatest_conda_forge_mkl_linux-64_conda.lock @@ -8,14 +8,14 @@ https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2#34893075a5c9e55cdafac56607368fc6 https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2#4d59c254e01d9cde7957100457e2d5fb https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_1.conda#6185f640c43843e5ad6fd1c5372c3f80 -https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.40-h41732ed_0.conda#7aca3059a1729aa76c597603f10b0dd3 -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-13.2.0-h7e041cc_5.conda#f6f6600d18a4047b54f803cf708b868a +https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.40-h55db66e_0.conda#10569984e7db886e4f1abc2b47ad79a1 +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-13.2.0-h95c4c6d_6.conda#3cfab3e709f77e9f1b3d380eb622494a https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.11-4_cp311.conda#d786502c97404c94d7d58d258a445a65 https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda#161081fc7cec0bfda0d86d7cb595f8d8 https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2#f766549260d6815b0c52253f1fb1bb29 https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2#fee5683a3f04bd15cbd8318b096a27ab https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_kmp_llvm.tar.bz2#562b26ba2e19059551a811e72ab7f793 -https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-h807b86a_5.conda#d4ff227c46917d3b4565302a2bbb276b +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-hc881cc4_6.conda#df88796bd09a0d2ed292e59101478ad8 https://conda.anaconda.org/conda-forge/linux-64/alsa-lib-1.2.11-hd590300_1.conda#0bb492cca54017ea314b809b1ee3a176 https://conda.anaconda.org/conda-forge/linux-64/attr-2.5.1-h166bdaf_1.tar.bz2#d9c69a24ad678ffce24c6543a0176b00 https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.9.0-hd590300_0.conda#71b89db63b5b504e7afc8ad901172e1e @@ -37,7 +37,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda#172b https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.6.2-h59595ed_0.conda#e7ba12deb7020dd080c6c70e7b6f6a3d https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.2-h7f98852_5.tar.bz2#d645c6d2ac96843a2bfaccd2d62b3ac3 https://conda.anaconda.org/conda-forge/linux-64/libgettextpo-0.22.5-h59595ed_2.conda#172bcc51059416e7ce99e7b528cede83 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-13.2.0-ha4646dd_5.conda#7a6bd7a12a4bd359e2afe6c0fa1acace +https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-13.2.0-h43f5ff8_6.conda#e54a5ddc67e673f9105cf2a2e9c070b0 https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.17-hd590300_2.conda#d66573916ffcf376178462f1b61c941e https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.0.0-hd590300_1.conda#ea25936bb4080d843790b586850f82b8 https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda#30fd6e37fe21f86f4bd26d6ee73eeec7 @@ -46,13 +46,13 @@ https://conda.anaconda.org/conda-forge/linux-64/libogg-1.3.4-h7f98852_1.tar.bz2# https://conda.anaconda.org/conda-forge/linux-64/libopus-1.3.1-h7f98852_1.tar.bz2#15345e56d527b330e1cacbdf58676e8f https://conda.anaconda.org/conda-forge/linux-64/libutf8proc-2.8.0-h166bdaf_0.tar.bz2#ede4266dc02e875fe1ea77b25dd43747 https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda#40b61aab5c7ba9ff276c41cfffe6b80b -https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.3.2-hd590300_1.conda#049b7df8bae5e184d1de42cdf64855f8 +https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.4.0-hd590300_0.conda#b26e8aa824079e1be0294e7152ca4559 https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda#5aa797f8787fe7a17d1b0821485b5adc https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.2.13-hd590300_5.conda#f36c115f1ee199da648e0597ec2047ad https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.9.4-hcb278e6_0.conda#318b08df404f9c9be5712aaa5a6f0bb0 https://conda.anaconda.org/conda-forge/linux-64/mpg123-1.32.6-h59595ed_0.conda#9160cdeb523a1b20cf8d2a0bf821f45d https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.4.20240210-h59595ed_0.conda#97da8860a0da5413c7c98a3b3838a645 -https://conda.anaconda.org/conda-forge/linux-64/ninja-1.11.1-h924138e_0.conda#73a4953a2d9c115bdc10ff30a52f675f +https://conda.anaconda.org/conda-forge/linux-64/ninja-1.12.0-h00ab1b0_0.conda#b048701d52e7cbb5f59ddd4d3b17bbf5 https://conda.anaconda.org/conda-forge/linux-64/nspr-4.35-h27087fc_0.conda#da0ec11a6454ae19bff5b02ed881a2b1 https://conda.anaconda.org/conda-forge/linux-64/openssl-3.2.1-hd590300_1.conda#9d731343cff6ee2e5a25c4a091bf8e2a https://conda.anaconda.org/conda-forge/linux-64/pixman-0.43.2-h59595ed_0.conda#71004cbf7924e19c02746ccde9fd7123 @@ -83,15 +83,15 @@ https://conda.anaconda.org/conda-forge/linux-64/libcap-2.69-h0f662aa_0.conda#25c https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20191231-he28a2e2_2.tar.bz2#4d331e44109e3f0e19b4cb8f9b82f3e1 https://conda.anaconda.org/conda-forge/linux-64/libevent-2.1.12-hf998b51_1.conda#a1cfcc585f0c42bf8d5546bb1dfb668d https://conda.anaconda.org/conda-forge/linux-64/libgettextpo-devel-0.22.5-h59595ed_2.conda#b63d9b6da3653179a278077f0de20014 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran-ng-13.2.0-h69a702a_5.conda#e73e9cfd1191783392131e6238bdb3e9 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-ng-13.2.0-h69a702a_6.conda#3666a850342f8f3be88f9a93d948d027 https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.58.0-h47da74e_1.conda#700ac6ea6d53d5510591c4344d5c989a https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.43-h2797004_0.conda#009981dd9cfcaa4dbfa25ffaed86bcae https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-3.21.12-hfc55251_2.conda#e3a7d4ba09b8dc939b98fef55f539220 -https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.45.2-h2797004_0.conda#866983a220e27a80cb75e85cb30466a1 +https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.45.3-h2797004_0.conda#b3316cbe90249da4f8e84cd66e1cc55b https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.0-h0841786_0.conda#1f5a58e686b13bcfde88b93f547d23fe https://conda.anaconda.org/conda-forge/linux-64/libvorbis-1.3.7-h9c3ff4c_0.tar.bz2#309dec04b70a3cc0f1e84a4013683bc0 https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.15-h0b41bf4_0.conda#33277193f5b92bad9fdd230eb700929c -https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.12.6-h232c23b_1.conda#6853448e9ca1cfd5f15382afd2a6d123 +https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.12.6-h232c23b_2.conda#9a3a42df8a95f65334dfc7b80da1195d https://conda.anaconda.org/conda-forge/linux-64/mysql-common-8.3.0-hf1915f5_4.conda#784a4df6676c581ca624fbe460703a6d https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.43-hcad00b1_0.conda#8292dea9e022d9610a11fce5e0896ed8 https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda#47d31b792659ce70f470b5c82fdfb7a4 @@ -106,7 +106,7 @@ https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.0.9-h166bdaf_9.cond https://conda.anaconda.org/conda-forge/linux-64/freetype-2.12.1-h267a509_2.conda#9ae35c3d96db2c94ce0cef86efdfa2cb https://conda.anaconda.org/conda-forge/linux-64/gettext-0.22.5-h59595ed_2.conda#219ba82e95d7614cf7140d2a4afc0926 https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.2-h659d440_0.conda#cd95826dbd331ed1be26bdf401432844 -https://conda.anaconda.org/conda-forge/linux-64/libglib-2.80.0-hf2295e7_4.conda#0269d2b7fa89f4a37cdee5ad6161f6cc +https://conda.anaconda.org/conda-forge/linux-64/libglib-2.80.0-hf2295e7_6.conda#9342e7c44c38bea649490f72d92c382d https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.54.3-hb20ce57_0.conda#7af7c59ab24db007dfd82e0a3a343f66 https://conda.anaconda.org/conda-forge/linux-64/libhiredis-1.0.2-h2cc385e_0.tar.bz2#b34907d3a81a3cd8095ee83d174c074a https://conda.anaconda.org/conda-forge/linux-64/libhwloc-2.10.0-default_h2fb2949_1000.conda#7e3726e647a619c6ce5939014dfde86d @@ -118,7 +118,7 @@ https://conda.anaconda.org/conda-forge/linux-64/llvm-openmp-18.1.3-h4dfa4b3_0.co https://conda.anaconda.org/conda-forge/linux-64/mysql-libs-8.3.0-hca2cd23_4.conda#1b50eebe2a738a3146c154d2eceaa8b6 https://conda.anaconda.org/conda-forge/linux-64/nss-3.98-h1d7d5a4_0.conda#54b56c2fdf973656b748e0378900ec13 https://conda.anaconda.org/conda-forge/linux-64/orc-1.9.0-h2f23424_1.conda#9571eb3eb0f7fe8b59956a7786babbcd -https://conda.anaconda.org/conda-forge/linux-64/python-3.11.8-hab00c5b_0_cpython.conda#2fdc314ee058eda0114738a9309d3683 +https://conda.anaconda.org/conda-forge/linux-64/python-3.11.9-hb806964_0_cpython.conda#ac68acfa8b558ed406c75e98d3428d7b https://conda.anaconda.org/conda-forge/linux-64/xcb-util-0.4.0-hd590300_1.conda#9bfac7ccd94d54fd21a0501296d60424 https://conda.anaconda.org/conda-forge/linux-64/xcb-util-keysyms-0.4.0-h8ee46fc_1.conda#632413adcd8bc16b515cab87a2932913 https://conda.anaconda.org/conda-forge/linux-64/xcb-util-renderutil-0.3.9-hd590300_1.conda#e995b155d938b6779da6ace6c6b13816 @@ -137,7 +137,7 @@ https://conda.anaconda.org/conda-forge/linux-64/dbus-1.13.6-h5008d03_3.tar.bz2#e https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.0-pyhd8ed1ab_2.conda#8d652ea2ee8eaee02ed8dc820bc794aa https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_0.conda#15dda3cdbf330abfe9f555d22f66db46 https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.14.2-h14ed4e7_0.conda#0f69b688f52ff6da70bccb7ff7001d1d -https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.80.0-hde27a5a_4.conda#c9deba4959ea5b5f72f1e3e4a71ae014 +https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.80.0-hde27a5a_6.conda#a9d23c02485c5cf055f9ac90eb9c9c63 https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_0.conda#f800d2da156d08e289b14e87e43c1ae5 https://conda.anaconda.org/conda-forge/linux-64/kiwisolver-1.4.5-py311h9547e67_1.conda#2c65bdf442b0d37aad080c8a4e0d452f https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.16-hb7c19ff_0.conda#51bb7010fc86f70eee639b4bb7a894f5 @@ -151,7 +151,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libpq-16.2-h33b98f1_1.conda#9e49 https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyh9f0ad1d_0.tar.bz2#2ba8498c1018c1e9c61eb99b973dfe19 https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.2-h488ebb8_0.conda#7f2e286780f072ed750df46dc2631138 https://conda.anaconda.org/conda-forge/noarch/packaging-24.0-pyhd8ed1ab_0.conda#248f521b64ce055e7feae3105e7abeb8 -https://conda.anaconda.org/conda-forge/noarch/pluggy-1.4.0-pyhd8ed1ab_0.conda#139e9feb65187e916162917bb2484976 +https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_0.conda#d3483c8fc2dc2cc3f5cf43e26d60cabf https://conda.anaconda.org/conda-forge/noarch/ply-3.11-pyhd8ed1ab_2.conda#18c6deb6f9602e32446398203c8f0e91 https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.1.2-pyhd8ed1ab_0.conda#b9a4dacf97241704529131a0dfc0494f https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2024.1-pyhd8ed1ab_0.conda#98206ea9954216ee7540f0c773f2104d @@ -174,7 +174,7 @@ https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.9.3-hb447be9_1.cond https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.0-h3faef2a_0.conda#f907bb958910dc404647326ca80c263e https://conda.anaconda.org/conda-forge/linux-64/coverage-7.4.4-py311h459d7ec_0.conda#1aa22cb84e68841ec206ee066457bdf0 https://conda.anaconda.org/conda-forge/linux-64/fonttools-4.51.0-py311h459d7ec_0.conda#17e1997cc17c571d5ad27bd0159f616c -https://conda.anaconda.org/conda-forge/linux-64/glib-2.80.0-hf2295e7_4.conda#5521382ee30b96b35eb0037fc3c4f2b4 +https://conda.anaconda.org/conda-forge/linux-64/glib-2.80.0-hf2295e7_6.conda#a1e026a82a562b443845db5614ca568a https://conda.anaconda.org/conda-forge/noarch/joblib-1.4.0-pyhd8ed1ab_0.conda#e0ed1bf13ce3a440e022157bf4764465 https://conda.anaconda.org/conda-forge/linux-64/libgcrypt-1.10.3-hd590300_0.conda#32d16ad533c59bb0a3c5ffaf16110829 https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-2.12.0-hac9eb74_1.conda#0dee716254497604762957076ac76540 @@ -184,7 +184,7 @@ https://conda.anaconda.org/conda-forge/noarch/meson-1.4.0-pyhd8ed1ab_0.conda#52a https://conda.anaconda.org/conda-forge/linux-64/mkl-2022.2.1-h84fe81f_16997.conda#a7ce56d5757f5b57e7daabe703ade5bb https://conda.anaconda.org/conda-forge/linux-64/pillow-10.3.0-py311h18e6fac_0.conda#6c520a9d36c9d7270988c7a6c360d6d4 https://conda.anaconda.org/conda-forge/noarch/pip-24.0-pyhd8ed1ab_0.conda#f586ac1e56c8638b64f9c8122a7b8a67 -https://conda.anaconda.org/conda-forge/noarch/pyproject-metadata-0.7.1-pyhd8ed1ab_0.conda#dcb27826ffc94d5f04e241322239983b +https://conda.anaconda.org/conda-forge/noarch/pyproject-metadata-0.8.0-pyhd8ed1ab_0.conda#573fe09d7bd0cd4bcc210d8369b5ca47 https://conda.anaconda.org/conda-forge/noarch/pytest-7.4.4-pyhd8ed1ab_0.conda#a9d145de8c5f064b5fa68fb34725d9f4 https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0-pyhd8ed1ab_0.conda#2cf4264fffb9e6eff6031c5b6884d61c https://conda.anaconda.org/conda-forge/linux-64/sip-6.7.12-py311hb755f60_0.conda#02336abab4cb5dd794010ef53c54bd09 @@ -194,7 +194,7 @@ https://conda.anaconda.org/conda-forge/linux-64/gstreamer-1.24.1-h98fc4e7_1.cond https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-8.3.0-h3d44ed6_0.conda#5a6f6c00ef982a9bc83558d9ac8f64a0 https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-16_linux64_mkl.tar.bz2#85f61af03fd291dae33150ffe89dc09a https://conda.anaconda.org/conda-forge/linux-64/libsystemd0-255-h3516f8a_1.conda#3366af27f0b593544a6cd453c7932ac5 -https://conda.anaconda.org/conda-forge/noarch/meson-python-0.15.0-pyh0c530f3_0.conda#3bc64565ca78ce3bb80248d09926d8f9 +https://conda.anaconda.org/conda-forge/noarch/meson-python-0.16.0-pyh0c530f3_0.conda#e16f0dbf502da873be9f9adb0dc52547 https://conda.anaconda.org/conda-forge/linux-64/pyqt5-sip-12.12.2-py311hb755f60_5.conda#e4d262cc3600e70b505a6761d29f6207 https://conda.anaconda.org/conda-forge/noarch/pytest-cov-5.0.0-pyhd8ed1ab_0.conda#c54c0107057d67ddf077751339ec2c63 https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.5.0-pyhd8ed1ab_0.conda#d5f595da2daead898ca958ac62f0307b @@ -210,7 +210,7 @@ https://conda.anaconda.org/conda-forge/noarch/array-api-strict-1.1.1-pyhd8ed1ab_ https://conda.anaconda.org/conda-forge/linux-64/contourpy-1.2.1-py311h9547e67_0.conda#74ad0ae64f1ef565e27eda87fa749e84 https://conda.anaconda.org/conda-forge/linux-64/libarrow-12.0.1-hb87d912_8_cpu.conda#3f3b11398fe79b578e3c44dd00a44e4a https://conda.anaconda.org/conda-forge/linux-64/pandas-2.2.2-py311h320fe9a_0.conda#c79e96ece4110fdaf2657c9f8e16f749 -https://conda.anaconda.org/conda-forge/linux-64/polars-0.20.20-py311h78b473b_0.conda#23bb672152980a11ad74a4af4d987fcf +https://conda.anaconda.org/conda-forge/linux-64/polars-0.20.21-py311h78b473b_1.conda#f5731dac66c9ee9f861679523807f47d https://conda.anaconda.org/conda-forge/linux-64/pyqt-5.15.9-py311hf0fb5b6_5.conda#ec7e45bc76d9d0b69a74a2075932b8e8 https://conda.anaconda.org/conda-forge/linux-64/pytorch-1.13.1-cpu_py311h410fd25_1.conda#ddd2fadddf89e3dc3d541a2537fce010 https://conda.anaconda.org/conda-forge/linux-64/scipy-1.13.0-py311h64a7726_0.conda#d443c70b4a05f50236c70b9c79beff64 diff --git a/build_tools/azure/pylatest_conda_forge_mkl_osx-64_conda.lock b/build_tools/azure/pylatest_conda_forge_mkl_osx-64_conda.lock index 72289e4ce587a..801ac1192a038 100644 --- a/build_tools/azure/pylatest_conda_forge_mkl_osx-64_conda.lock +++ b/build_tools/azure/pylatest_conda_forge_mkl_osx-64_conda.lock @@ -13,7 +13,7 @@ https://conda.anaconda.org/conda-forge/osx-64/libffi-3.4.2-h0d85af4_5.tar.bz2#cc https://conda.anaconda.org/conda-forge/noarch/libgfortran-devel_osx-64-12.3.0-h0b6f5ec_3.conda#39eeea5454333825d72202fae2d5e0b8 https://conda.anaconda.org/conda-forge/osx-64/libiconv-1.17-hd75f5a5_2.conda#6c3628d047e151efba7cf08c5e54d1ca https://conda.anaconda.org/conda-forge/osx-64/libjpeg-turbo-3.0.0-h0dc2134_1.conda#72507f8e3961bc968af17435060b6dd6 -https://conda.anaconda.org/conda-forge/osx-64/libwebp-base-1.3.2-h10d778d_1.conda#1ff09ca6e85ee516442a6a94cdfc7065 +https://conda.anaconda.org/conda-forge/osx-64/libwebp-base-1.4.0-h10d778d_0.conda#b2c0047ea73819d992484faacbbe1c24 https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.2.13-h8a1eda9_5.conda#4a3ad23f6e16f99c04e166767193d700 https://conda.anaconda.org/conda-forge/osx-64/llvm-openmp-18.1.3-hb6ac08f_0.conda#506f270f4f00980d27cc1fc127e0ed37 https://conda.anaconda.org/conda-forge/osx-64/mkl-include-2023.2.0-h6bab518_50500.conda#835abb8ded5e26f23ea6996259c7972e @@ -31,10 +31,10 @@ https://conda.anaconda.org/conda-forge/osx-64/libbrotlidec-1.1.0-h0dc2134_1.cond https://conda.anaconda.org/conda-forge/osx-64/libbrotlienc-1.1.0-h0dc2134_1.conda#8a421fe09c6187f0eb5e2338a8a8be6d https://conda.anaconda.org/conda-forge/osx-64/libgfortran5-13.2.0-h2873a65_3.conda#e4fb4d23ec2870ff3c40d10afe305aec https://conda.anaconda.org/conda-forge/osx-64/libpng-1.6.43-h92b6c6a_0.conda#65dcddb15965c9de2c0365cb14910532 -https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.45.2-h92b6c6a_0.conda#086f56e13a96a6cfb1bf640505ae6b70 +https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.45.3-h92b6c6a_0.conda#68e462226209f35182ef66eda0f794ff https://conda.anaconda.org/conda-forge/osx-64/libxcb-1.15-hb7f2c08_0.conda#5513f57e0238c87c12dffedbcc9c1a4a -https://conda.anaconda.org/conda-forge/osx-64/libxml2-2.12.6-hc0ae0f7_1.conda#bd85e0ca9e1ffaadc3b56079fd956035 -https://conda.anaconda.org/conda-forge/osx-64/ninja-1.11.1-hb8565cd_0.conda#49ad513efe39447aa51affd47e3aa68f +https://conda.anaconda.org/conda-forge/osx-64/libxml2-2.12.6-hc0ae0f7_2.conda#50b997370584f2c83ca0c38e9028eab9 +https://conda.anaconda.org/conda-forge/osx-64/ninja-1.12.0-h7728843_0.conda#1ac079f6ecddd2c336f3acb7b371851f https://conda.anaconda.org/conda-forge/osx-64/openssl-3.2.1-hd75f5a5_1.conda#570a6f04802df580be529f3a72d2bbf7 https://conda.anaconda.org/conda-forge/osx-64/readline-8.2-h9e318b2_1.conda#f17f77f2acf4d344734bda76829ce14e https://conda.anaconda.org/conda-forge/osx-64/tapi-1100.0.11-h9ce4665_0.tar.bz2#f9ff42ccf809a21ba6f8607f8de36108 @@ -48,7 +48,7 @@ https://conda.anaconda.org/conda-forge/osx-64/libhwloc-2.10.0-default_h1321489_1 https://conda.anaconda.org/conda-forge/osx-64/libllvm16-16.0.6-hbedff68_3.conda#8fd56c0adc07a37f93bd44aa61a97c90 https://conda.anaconda.org/conda-forge/osx-64/libtiff-4.6.0-h129831d_3.conda#568593071d2e6cea7b5fc1f75bfa10ca https://conda.anaconda.org/conda-forge/osx-64/mpfr-4.2.1-h4f6b447_1.conda#b90df08f0deb2f58631447c1462c92a7 -https://conda.anaconda.org/conda-forge/osx-64/python-3.12.2-h9f0c242_0_cpython.conda#0179b8007ba008cf5bec11f3b3853902 +https://conda.anaconda.org/conda-forge/osx-64/python-3.12.3-h1411813_0_cpython.conda#df1448ec6cbf8eceb03d29003cf72ae6 https://conda.anaconda.org/conda-forge/osx-64/sigtool-0.1.3-h88f4db0_0.tar.bz2#fbfb84b9de9a6939cb165c02c69b1865 https://conda.anaconda.org/conda-forge/osx-64/brotli-1.1.0-h0dc2134_1.conda#9272dd3b19c4e8212f8542cefd5c3d67 https://conda.anaconda.org/conda-forge/noarch/certifi-2024.2.2-pyhd8ed1ab_0.conda#0876280e409658fc6f9e75d035960333 @@ -68,7 +68,7 @@ https://conda.anaconda.org/conda-forge/osx-64/mpc-1.3.1-h81bd1dd_0.conda#c752c0e https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyh9f0ad1d_0.tar.bz2#2ba8498c1018c1e9c61eb99b973dfe19 https://conda.anaconda.org/conda-forge/osx-64/openjpeg-2.5.2-h7310d3a_0.conda#05a14cc9d725dd74995927968d6547e3 https://conda.anaconda.org/conda-forge/noarch/packaging-24.0-pyhd8ed1ab_0.conda#248f521b64ce055e7feae3105e7abeb8 -https://conda.anaconda.org/conda-forge/noarch/pluggy-1.4.0-pyhd8ed1ab_0.conda#139e9feb65187e916162917bb2484976 +https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_0.conda#d3483c8fc2dc2cc3f5cf43e26d60cabf https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.1.2-pyhd8ed1ab_0.conda#b9a4dacf97241704529131a0dfc0494f https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2024.1-pyhd8ed1ab_0.conda#98206ea9954216ee7540f0c773f2104d https://conda.anaconda.org/conda-forge/noarch/pytz-2024.1-pyhd8ed1ab_0.conda#3eeeeb9e4827ace8c0c1419c85d590ad @@ -92,13 +92,13 @@ https://conda.anaconda.org/conda-forge/noarch/meson-1.4.0-pyhd8ed1ab_0.conda#52a https://conda.anaconda.org/conda-forge/osx-64/mkl-2023.2.0-h54c2260_50500.conda#0a342ccdc79e4fcd359245ac51941e7b https://conda.anaconda.org/conda-forge/osx-64/pillow-10.3.0-py312h0c923fa_0.conda#6f0591ae972e9b815739da3392fbb3c3 https://conda.anaconda.org/conda-forge/noarch/pip-24.0-pyhd8ed1ab_0.conda#f586ac1e56c8638b64f9c8122a7b8a67 -https://conda.anaconda.org/conda-forge/noarch/pyproject-metadata-0.7.1-pyhd8ed1ab_0.conda#dcb27826ffc94d5f04e241322239983b +https://conda.anaconda.org/conda-forge/noarch/pyproject-metadata-0.8.0-pyhd8ed1ab_0.conda#573fe09d7bd0cd4bcc210d8369b5ca47 https://conda.anaconda.org/conda-forge/noarch/pytest-7.4.4-pyhd8ed1ab_0.conda#a9d145de8c5f064b5fa68fb34725d9f4 https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0-pyhd8ed1ab_0.conda#2cf4264fffb9e6eff6031c5b6884d61c https://conda.anaconda.org/conda-forge/osx-64/cctools-986-h40f6528_0.conda#b7a2ca0062a6ee8bc4e83ec887bef942 https://conda.anaconda.org/conda-forge/osx-64/clang-16.0.6-hdae98eb_6.conda#884e7b24306e4f21b7ee08dabadb2ecc https://conda.anaconda.org/conda-forge/osx-64/libblas-3.9.0-20_osx64_mkl.conda#160fdc97a51d66d51dc782fb67d35205 -https://conda.anaconda.org/conda-forge/noarch/meson-python-0.15.0-pyh0c530f3_0.conda#3bc64565ca78ce3bb80248d09926d8f9 +https://conda.anaconda.org/conda-forge/noarch/meson-python-0.16.0-pyh0c530f3_0.conda#e16f0dbf502da873be9f9adb0dc52547 https://conda.anaconda.org/conda-forge/osx-64/mkl-devel-2023.2.0-h694c41f_50500.conda#1b4d0235ef253a1e19459351badf4f9f https://conda.anaconda.org/conda-forge/noarch/pytest-cov-5.0.0-pyhd8ed1ab_0.conda#c54c0107057d67ddf077751339ec2c63 https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.5.0-pyhd8ed1ab_0.conda#d5f595da2daead898ca958ac62f0307b diff --git a/build_tools/azure/pylatest_conda_mkl_no_openmp_osx-64_conda.lock b/build_tools/azure/pylatest_conda_mkl_no_openmp_osx-64_conda.lock index aa946a23c4650..a9c9b364426a7 100644 --- a/build_tools/azure/pylatest_conda_mkl_no_openmp_osx-64_conda.lock +++ b/build_tools/azure/pylatest_conda_mkl_no_openmp_osx-64_conda.lock @@ -17,7 +17,7 @@ https://repo.anaconda.com/pkgs/main/noarch/tzdata-2024a-h04d1e81_0.conda#452af53 https://repo.anaconda.com/pkgs/main/osx-64/xz-5.4.6-h6c40b1e_0.conda#412bf13f273c0e086da65f86567cfe80 https://repo.anaconda.com/pkgs/main/osx-64/zlib-1.2.13-h4dc903c_0.conda#d0202dd912bfb45d3422786531717882 https://repo.anaconda.com/pkgs/main/osx-64/ccache-3.7.9-hf120daa_0.conda#a01515a32e721c51d631283f991bc8ea -https://repo.anaconda.com/pkgs/main/osx-64/expat-2.5.0-hcec6c5f_0.conda#ce90fd42031d3c01944146f089a9130b +https://repo.anaconda.com/pkgs/main/osx-64/expat-2.6.2-hcec6c5f_0.conda#c748234dd7e242784198ab038372cb0c https://repo.anaconda.com/pkgs/main/osx-64/intel-openmp-2023.1.0-ha357a0b_43548.conda#ba8a89ffe593eb88e4c01334753c40c3 https://repo.anaconda.com/pkgs/main/osx-64/lerc-3.0-he9d5cce_0.conda#aec2c3dbef836849c9260f05be04f3db https://repo.anaconda.com/pkgs/main/osx-64/libbrotlidec-1.0.9-hca72f7f_7.conda#b85983951745cc666d9a1b42894210b2 @@ -38,7 +38,7 @@ https://repo.anaconda.com/pkgs/main/osx-64/sqlite-3.41.2-h6c40b1e_0.conda#6947a5 https://repo.anaconda.com/pkgs/main/osx-64/zstd-1.5.5-hc035e20_0.conda#5e0b7ddb1b7dc6b630e1f9a03499c19c https://repo.anaconda.com/pkgs/main/osx-64/brotli-1.0.9-hca72f7f_7.conda#68e54d12ec67591deb2ffd70348fb00f https://repo.anaconda.com/pkgs/main/osx-64/libtiff-4.5.1-hcec6c5f_0.conda#e127a800ffd9d300ed7d5e1b026944ec -https://repo.anaconda.com/pkgs/main/osx-64/python-3.12.2-hd58486a_0.conda#21efba1355d32906d082aaff16698961 +https://repo.anaconda.com/pkgs/main/osx-64/python-3.12.3-hd58486a_0.conda#1a287cfa37c5a92972f5f527b6af7eed https://repo.anaconda.com/pkgs/main/osx-64/coverage-7.2.2-py312h6c40b1e_0.conda#b6e4b9fba325047c07f3c9211ae91d1c https://repo.anaconda.com/pkgs/main/noarch/cycler-0.11.0-pyhd3eb1b0_0.conda#f5e365d2cdb66d547eb8c3ab93843aab https://repo.anaconda.com/pkgs/main/noarch/execnet-1.9.0-pyhd3eb1b0_0.conda#f895937671af67cebb8af617494b3513 @@ -47,7 +47,6 @@ https://repo.anaconda.com/pkgs/main/osx-64/joblib-1.2.0-py312hecd8cb5_0.conda#ae https://repo.anaconda.com/pkgs/main/osx-64/kiwisolver-1.4.4-py312hcec6c5f_0.conda#2ba6561ddd1d05936fe74f5d118ce7dd https://repo.anaconda.com/pkgs/main/osx-64/lcms2-2.12-hf1fd2bf_0.conda#697aba7a3308226df7a93ccfeae16ffa https://repo.anaconda.com/pkgs/main/osx-64/mkl-service-2.4.0-py312h6c40b1e_1.conda#b1ef860be9043b35c5e8d9388b858514 -https://repo.anaconda.com/pkgs/main/noarch/munkres-1.1.4-py_0.conda#148362ba07f92abab76999a680c80084 https://repo.anaconda.com/pkgs/main/osx-64/ninja-1.10.2-hecd8cb5_5.conda#a0043b325fb08db82477ae433668e684 https://repo.anaconda.com/pkgs/main/osx-64/openjpeg-2.4.0-h66ea3da_0.conda#882833bd7befc5e60e6fba9c518c1b79 https://repo.anaconda.com/pkgs/main/osx-64/packaging-23.2-py312hecd8cb5_0.conda#2b4e331c8f6df5d95a5dd3af37a34d89 @@ -60,8 +59,9 @@ https://repo.anaconda.com/pkgs/main/noarch/six-1.16.0-pyhd3eb1b0_1.conda#3458682 https://repo.anaconda.com/pkgs/main/noarch/threadpoolctl-2.2.0-pyh0d69192_0.conda#bbfdbae4934150b902f97daaf287efe2 https://repo.anaconda.com/pkgs/main/noarch/toml-0.10.2-pyhd3eb1b0_0.conda#cda05f5f6d8509529d1a2743288d197a https://repo.anaconda.com/pkgs/main/osx-64/tornado-6.3.3-py312h6c40b1e_0.conda#49173b5a36c9134865221f29d4a73fb6 +https://repo.anaconda.com/pkgs/main/osx-64/unicodedata2-15.1.0-py312h6c40b1e_0.conda#65bd2cb787fc99662d9bb6e6520c5826 https://repo.anaconda.com/pkgs/main/osx-64/wheel-0.41.2-py312hecd8cb5_0.conda#e7aea266d81142e2bb0bbc2280e64526 -https://repo.anaconda.com/pkgs/main/noarch/fonttools-4.25.0-pyhd3eb1b0_0.conda#bb9c5b5a6d892fca5efe4bf0203b6a48 +https://repo.anaconda.com/pkgs/main/osx-64/fonttools-4.51.0-py312h6c40b1e_0.conda#8f55fa86b73e8a7f4403503f9b7a9959 https://repo.anaconda.com/pkgs/main/osx-64/meson-1.3.1-py312hecd8cb5_0.conda#43963a2b38becce4caa95434b8c96837 https://repo.anaconda.com/pkgs/main/osx-64/numpy-base-1.26.4-py312h6f81483_0.conda#87f73efbf26ab2e2ea7c32481a71bd47 https://repo.anaconda.com/pkgs/main/osx-64/pillow-10.2.0-py312h6c40b1e_0.conda#5a44bd28cf26fff2d6219e76a86db126 @@ -74,8 +74,8 @@ https://repo.anaconda.com/pkgs/main/osx-64/pytest-cov-4.1.0-py312hecd8cb5_1.cond https://repo.anaconda.com/pkgs/main/osx-64/pytest-xdist-3.5.0-py312hecd8cb5_0.conda#d1ecfb3691cceecb1f16bcfdf0b67bb5 https://repo.anaconda.com/pkgs/main/osx-64/bottleneck-1.3.7-py312h32608ca_0.conda#f96a01eba5ea542cf9c7cc8d77447627 https://repo.anaconda.com/pkgs/main/osx-64/contourpy-1.2.0-py312ha357a0b_0.conda#57d384ad07152375b40a6293f79e3f0c -https://repo.anaconda.com/pkgs/main/osx-64/matplotlib-3.8.0-py312hecd8cb5_0.conda#64ffa3462aace0fc2d5fa5bff15f63f6 -https://repo.anaconda.com/pkgs/main/osx-64/matplotlib-base-3.8.0-py312h7f12edd_0.conda#bda389e5a1ff69f763911cf90102893b +https://repo.anaconda.com/pkgs/main/osx-64/matplotlib-3.8.4-py312hecd8cb5_0.conda#6886c230c2ec2f47621b5cca4c7d493a +https://repo.anaconda.com/pkgs/main/osx-64/matplotlib-base-3.8.4-py312h7f12edd_0.conda#a4eee14a4dcaa89b306ca33d2d479fa4 https://repo.anaconda.com/pkgs/main/osx-64/mkl_fft-1.3.8-py312h6c40b1e_0.conda#d59d01b940493f2b6a84aac922fd0c76 https://repo.anaconda.com/pkgs/main/osx-64/mkl_random-1.2.4-py312ha357a0b_0.conda#c1ea9c8eee79a5af3399f3c31be0e9c6 https://repo.anaconda.com/pkgs/main/osx-64/numpy-1.26.4-py312hac873b0_0.conda#3150bac1e382156f82a153229e1ebd06 diff --git a/build_tools/azure/pylatest_pip_openblas_pandas_linux-64_conda.lock b/build_tools/azure/pylatest_pip_openblas_pandas_linux-64_conda.lock index b2c4d1c5163e4..58bdbdad5044d 100644 --- a/build_tools/azure/pylatest_pip_openblas_pandas_linux-64_conda.lock +++ b/build_tools/azure/pylatest_pip_openblas_pandas_linux-64_conda.lock @@ -29,8 +29,8 @@ https://repo.anaconda.com/pkgs/main/linux-64/pip-23.3.1-py39h06a4308_0.conda#685 # pip charset-normalizer @ https://files.pythonhosted.org/packages/98/69/5d8751b4b670d623aa7a47bef061d69c279e9f922f6705147983aa76c3ce/charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796 # pip cycler @ https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl#sha256=85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30 # pip cython @ https://files.pythonhosted.org/packages/a7/f5/3dde4d96076888ceaa981827b098274c2b45ddd4b20d75a8cfaa92b91eec/Cython-3.0.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=651a15a8534ebfb9b58cb0b87c269c70984b6f9c88bfe65e4f635f0e3f07dfcd -# pip docutils @ https://files.pythonhosted.org/packages/26/87/f238c0670b94533ac0353a4e2a1a771a0cc73277b88bff23d3ae35a256c1/docutils-0.20.1-py3-none-any.whl#sha256=96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6 -# pip exceptiongroup @ https://files.pythonhosted.org/packages/b8/9a/5028fd52db10e600f1c4674441b968cf2ea4959085bfb5b99fb1250e5f68/exceptiongroup-1.2.0-py3-none-any.whl#sha256=4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14 +# pip docutils @ https://files.pythonhosted.org/packages/ea/6a/1c934b70bc12be40be886060709317c2ad5a5fe2180469410bd9344f5235/docutils-0.21.1-py3-none-any.whl#sha256=14c8d34a55b46c88f9f714adb29cefbdd69fb82f3fef825e59c5faab935390d8 +# pip exceptiongroup @ https://files.pythonhosted.org/packages/01/90/79fe92dd413a9cab314ef5c591b5aa9b9ba787ae4cadab75055b0ae00b33/exceptiongroup-1.2.1-py3-none-any.whl#sha256=5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad # pip execnet @ https://files.pythonhosted.org/packages/43/09/2aea36ff60d16dd8879bdb2f5b3ee0ba8d08cbbdcdfe870e695ce3784385/execnet-2.1.1-py3-none-any.whl#sha256=26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc # pip fonttools @ https://files.pythonhosted.org/packages/8b/c6/636f008104908a93b80419f756be755bb91df4b8a0c88d5158bb52c82c3a/fonttools-4.51.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=0d145976194a5242fdd22df18a1b451481a88071feadf251221af110ca8f00ce # pip idna @ https://files.pythonhosted.org/packages/e5/3e/741d8c82801c347547f8a2a06aa57dbb1992be9e948df2ea0eda2c8b79e8/idna-3.7-py3-none-any.whl#sha256=82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0 @@ -45,7 +45,7 @@ https://repo.anaconda.com/pkgs/main/linux-64/pip-23.3.1-py39h06a4308_0.conda#685 # pip numpy @ https://files.pythonhosted.org/packages/54/30/c2a907b9443cf42b90c17ad10c1e8fa801975f01cb9764f3f8eb8aea638b/numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3 # pip packaging @ https://files.pythonhosted.org/packages/49/df/1fceb2f8900f8639e278b056416d49134fb8d84c5942ffaa01ad34782422/packaging-24.0-py3-none-any.whl#sha256=2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5 # pip pillow @ https://files.pythonhosted.org/packages/f5/6d/52e82352670e850f468de9e6bccced4202a09f58e7ea5ecdbf08283d85cb/pillow-10.3.0-cp39-cp39-manylinux_2_28_x86_64.whl#sha256=1dfc94946bc60ea375cc39cff0b8da6c7e5f8fcdc1d946beb8da5c216156ddd8 -# pip pluggy @ https://files.pythonhosted.org/packages/a5/5b/0cc789b59e8cc1bf288b38111d002d8c5917123194d45b29dcdac64723cc/pluggy-1.4.0-py3-none-any.whl#sha256=7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981 +# pip pluggy @ https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl#sha256=44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 # pip pygments @ https://files.pythonhosted.org/packages/97/9c/372fef8377a6e340b1704768d20daaded98bf13282b5327beb2e2fe2c7ef/pygments-2.17.2-py3-none-any.whl#sha256=b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c # pip pyparsing @ https://files.pythonhosted.org/packages/9d/ea/6d76df31432a0e6fdf81681a895f009a4bb47b3c39036db3e1b528191d52/pyparsing-3.1.2-py3-none-any.whl#sha256=f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742 # pip pytz @ https://files.pythonhosted.org/packages/9c/3d/a121f284241f08268b21359bd425f7d4825cffc5ac5cd0e1b3d82ffd2b10/pytz-2024.1-py2.py3-none-any.whl#sha256=328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319 @@ -65,24 +65,24 @@ https://repo.anaconda.com/pkgs/main/linux-64/pip-23.3.1-py39h06a4308_0.conda#685 # pip zipp @ https://files.pythonhosted.org/packages/c2/0a/ba9d0ee9536d3ef73a3448e931776e658b36f128d344e175bc32b092a8bf/zipp-3.18.1-py3-none-any.whl#sha256=206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b # pip contourpy @ https://files.pythonhosted.org/packages/31/a2/2f12e3a6e45935ff694654b710961b03310b0e1ec997ee9f416d3c873f87/contourpy-1.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=e1d59258c3c67c865435d8fbeb35f8c59b8bef3d6f46c1f29f6123556af28445 # pip coverage @ https://files.pythonhosted.org/packages/5b/ec/9bd500128995e9eec2ab50361ce8b853bab2b4839316ddcfd6a34f5bbfed/coverage-7.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4 -# pip imageio @ https://files.pythonhosted.org/packages/02/25/66533a8390e3763cf8254dee143dbf8a830391ea60d2762512ba7f9ddfbe/imageio-2.34.0-py3-none-any.whl#sha256=08082bf47ccb54843d9c73fe9fc8f3a88c72452ab676b58aca74f36167e8ccba +# pip imageio @ https://files.pythonhosted.org/packages/a3/b6/39c7dad203d9984225f47e0aa39ac3ba3a47c77a02d0ef2a7be691855a06/imageio-2.34.1-py3-none-any.whl#sha256=408c1d4d62f72c9e8347e7d1ca9bc11d8673328af3913868db3b828e28b40a4c # pip importlib-metadata @ https://files.pythonhosted.org/packages/2d/0a/679461c511447ffaf176567d5c496d1de27cbe34a87df6677d7171b2fbd4/importlib_metadata-7.1.0-py3-none-any.whl#sha256=30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570 # pip importlib-resources @ https://files.pythonhosted.org/packages/75/06/4df55e1b7b112d183f65db9503bff189e97179b256e1ea450a3c365241e0/importlib_resources-6.4.0-py3-none-any.whl#sha256=50d10f043df931902d4194ea07ec57960f66a80449ff867bfe782b4c486ba78c # pip jinja2 @ https://files.pythonhosted.org/packages/30/6d/6de6be2d02603ab56e72997708809e8a5b0fbfee080735109b40a3564843/Jinja2-3.1.3-py3-none-any.whl#sha256=7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa # pip lazy-loader @ https://files.pythonhosted.org/packages/83/60/d497a310bde3f01cb805196ac61b7ad6dc5dcf8dce66634dc34364b20b4f/lazy_loader-0.4-py3-none-any.whl#sha256=342aa8e14d543a154047afb4ba8ef17f5563baad3fc610d7b15b213b0f119efc -# pip pyproject-metadata @ https://files.pythonhosted.org/packages/c4/cb/4678dfd70cd2f2d8969e571cdc1bb1e9293c698f8d1cf428fadcf48d6e9f/pyproject_metadata-0.7.1-py3-none-any.whl#sha256=28691fbb36266a819ec56c9fa1ecaf36f879d6944dfde5411e87fc4ff793aa60 +# pip pyproject-metadata @ https://files.pythonhosted.org/packages/aa/5f/bb5970d3d04173b46c9037109f7f05fc8904ff5be073ee49bb6ff00301bc/pyproject_metadata-0.8.0-py3-none-any.whl#sha256=ad858d448e1d3a1fb408ac5bac9ea7743e7a8bbb472f2693aaa334d2db42f526 # pip pytest @ https://files.pythonhosted.org/packages/51/ff/f6e8b8f39e08547faece4bd80f89d5a8de68a38b2d179cc1c4490ffa3286/pytest-7.4.4-py3-none-any.whl#sha256=b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8 # pip python-dateutil @ https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl#sha256=a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 # pip requests @ https://files.pythonhosted.org/packages/70/8e/0e2d847013cb52cd35b38c009bb167a1a26b2ce6cd6965bf26b47bc0bf44/requests-2.31.0-py3-none-any.whl#sha256=58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f # pip scipy @ https://files.pythonhosted.org/packages/c6/ba/a778e6c0020d728c119b0379805a357135fe8c9bc87fdb7e0750ca11319f/scipy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=28e286bf9ac422d6beb559bc61312c348ca9b0f0dae0d7c5afde7f722d6ea13d -# pip tifffile @ https://files.pythonhosted.org/packages/cd/0b/33610b4d0d1bb83a6bfd20ed838f52e02a44e9b439116cd4f3d424e81a80/tifffile-2024.2.12-py3-none-any.whl#sha256=870998f82fbc94ff7c3528884c1b0ae54863504ff51dbebea431ac3fa8fb7c21 +# pip tifffile @ https://files.pythonhosted.org/packages/50/d8/870c966ced9aae912d113f2ca6a651bdc9db69d1ddf0afb5a87840ce0011/tifffile-2024.4.18-py3-none-any.whl#sha256=72643b5c9ef886669a00a659c9fd60a81f220d2fb6572d184c3e147435ccec43 # pip lightgbm @ https://files.pythonhosted.org/packages/ba/11/cb8b67f3cbdca05b59a032bb57963d4fe8c8d18c3870f30bed005b7f174d/lightgbm-4.3.0-py3-none-manylinux_2_28_x86_64.whl#sha256=104496a3404cb2452d3412cbddcfbfadbef9c372ea91e3a9b8794bcc5183bf07 # pip matplotlib @ https://files.pythonhosted.org/packages/5e/2c/513395a63a9e1124a5648addbf73be23cc603f955af026b04416da98dc96/matplotlib-3.8.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=606e3b90897554c989b1e38a258c626d46c873523de432b1462f295db13de6f9 -# pip meson-python @ https://files.pythonhosted.org/packages/1f/60/b10b11ab470a690d5777310d6cfd1c9bdbbb0a1313a78c34a1e82e0b9d27/meson_python-0.15.0-py3-none-any.whl#sha256=3ae38253ff02b2e947a05e362a2eaf5a9a09d133c5666b4123399ee5fbf2e591 +# pip meson-python @ https://files.pythonhosted.org/packages/91/c0/104cb6244c83fe6bc3886f144cc433db0c0c78efac5dc00e409a5a08c87d/meson_python-0.16.0-py3-none-any.whl#sha256=842dc9f5dc29e55fc769ff1b6fe328412fe6c870220fc321060a1d2d395e69e8 # pip pandas @ https://files.pythonhosted.org/packages/bb/30/f6f1f1ac36250f50c421b1b6af08c35e5a8b5a84385ef928625336b93e6f/pandas-2.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=66b479b0bd07204e37583c191535505410daa8df638fd8e75ae1b383851fe921 # pip pyamg @ https://files.pythonhosted.org/packages/68/a9/aed9f557e7eb779d2cb4fa090663f8540979e0c04dadd16e9a0bdc9632c5/pyamg-5.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=5817d4567fb240dab4779bb1630bbb3035b3827731fcdaeb9ecc9c8814319995 # pip pytest-cov @ https://files.pythonhosted.org/packages/78/3a/af5b4fa5961d9a1e6237b530eb87dd04aea6eb83da09d2a4073d81b54ccf/pytest_cov-5.0.0-py3-none-any.whl#sha256=4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652 -# pip pytest-xdist @ https://files.pythonhosted.org/packages/50/37/125fe5ec459321e2d48a0c38672cfc2419ad87d580196fd894e5f25230b0/pytest_xdist-3.5.0-py3-none-any.whl#sha256=d075629c7e00b611df89f490a5063944bee7a4362a5ff11c7cc7824a03dfce24 +# pip pytest-xdist @ https://files.pythonhosted.org/packages/e0/df/585742c204227526baf641e45a95d3c0a94978b85a1414622c1017ebfcf8/pytest_xdist-3.6.0-py3-none-any.whl#sha256=958e08f38472e1b3a83450d8d3e682e90fdbffee39a97dd0f27185a3bd9074d1 # pip scikit-image @ https://files.pythonhosted.org/packages/a3/7e/4cd853a855ac34b4ef3ef6a5c3d1c2e96eaca1154fc6be75db55ffa87393/scikit_image-0.22.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=3b7a6c89e8d6252332121b58f50e1625c35f7d6a85489c0b6b7ee4f5155d547a -# pip sphinx @ https://files.pythonhosted.org/packages/b2/b6/8ed35256aa530a9d3da15d20bdc0ba888d5364441bb50a5a83ee7827affe/sphinx-7.2.6-py3-none-any.whl#sha256=1e09160a40b956dc623c910118fa636da93bd3ca0b9876a7b3df90f07d691560 +# pip sphinx @ https://files.pythonhosted.org/packages/b4/fa/130c32ed94cf270e3d0b9ded16fb7b2c8fea86fa7263c29a696a30c1dde7/sphinx-7.3.7-py3-none-any.whl#sha256=413f75440be4cacf328f580b4274ada4565fb2187d696a84970c23f77b64d8c3 # pip numpydoc @ https://files.pythonhosted.org/packages/f0/fa/dcfe0f65660661db757ee9ebd84e170ff98edd5d80235f62457d9088f85f/numpydoc-1.7.0-py3-none-any.whl#sha256=5a56419d931310d79a06cfc2a126d1558700feeb9b4f3d8dcae1a8134be829c9 diff --git a/build_tools/azure/pymin_conda_defaults_openblas_linux-64_conda.lock b/build_tools/azure/pymin_conda_defaults_openblas_linux-64_conda.lock index 4e64af1960718..00797394e4253 100644 --- a/build_tools/azure/pymin_conda_defaults_openblas_linux-64_conda.lock +++ b/build_tools/azure/pymin_conda_defaults_openblas_linux-64_conda.lock @@ -14,7 +14,7 @@ https://repo.anaconda.com/pkgs/main/linux-64/libstdcxx-ng-11.2.0-h1234567_1.cond https://repo.anaconda.com/pkgs/main/linux-64/_openmp_mutex-5.1-1_gnu.conda#71d281e9c2192cb3fa425655a8defb85 https://repo.anaconda.com/pkgs/main/linux-64/libgcc-ng-11.2.0-h1234567_1.conda#a87728dabf3151fb9cfa990bd2eb0464 https://repo.anaconda.com/pkgs/main/linux-64/bzip2-1.0.8-h5eee18b_5.conda#9c8dec113089c4aca7392c6a3864f505 -https://repo.anaconda.com/pkgs/main/linux-64/expat-2.5.0-h6a678d5_0.conda#9a21d99d49a0a556cf9590430dec8ec0 +https://repo.anaconda.com/pkgs/main/linux-64/expat-2.6.2-h6a678d5_0.conda#55049db2772dae035f6b8a95f72b5970 https://repo.anaconda.com/pkgs/main/linux-64/fftw-3.3.9-h5eee18b_2.conda#db1df41113accc18ec59a99f1631bfcd https://repo.anaconda.com/pkgs/main/linux-64/icu-73.1-h6a678d5_0.conda#6d09df641fc23f7d277a04dc7ea32dd4 https://repo.anaconda.com/pkgs/main/linux-64/jpeg-9e-h5eee18b_1.conda#ac373800fda872108412d1ccfe3fa572 @@ -36,7 +36,7 @@ https://repo.anaconda.com/pkgs/main/linux-64/libcups-2.4.2-h2d74bed_1.conda#3f26 https://repo.anaconda.com/pkgs/main/linux-64/libedit-3.1.20230828-h5eee18b_0.conda#850eb5a9d2d7d3c66cce12e84406ca08 https://repo.anaconda.com/pkgs/main/linux-64/libllvm14-14.0.6-hdb19cb5_3.conda#aefea2b45cf32f12b4f1ffaa70aa3201 https://repo.anaconda.com/pkgs/main/linux-64/libpng-1.6.39-h5eee18b_0.conda#f6aee38184512eb05b06c2e94d39ab22 -https://repo.anaconda.com/pkgs/main/linux-64/libxml2-2.10.4-hf1b16e4_1.conda#e87849ce513f9968794f20bba620e6a4 +https://repo.anaconda.com/pkgs/main/linux-64/libxml2-2.10.4-hfdd30dd_2.conda#ff7a0e3b92afb3c99b82c9f0ba8b5670 https://repo.anaconda.com/pkgs/main/linux-64/pcre2-10.42-hebb0a14_0.conda#fca6dea6ce1eddd0876a024f62c5097a https://repo.anaconda.com/pkgs/main/linux-64/readline-8.2-h5eee18b_0.conda#be42180685cce6e6b0329201d9f48efb https://repo.anaconda.com/pkgs/main/linux-64/tk-8.6.12-h1ccaba5_0.conda#fa10ff4aa631fa4aa090a6234d7770b9 diff --git a/build_tools/azure/pymin_conda_forge_mkl_win-64_conda.lock b/build_tools/azure/pymin_conda_forge_mkl_win-64_conda.lock index c6983ba7a9f51..52f7233deb707 100644 --- a/build_tools/azure/pymin_conda_forge_mkl_win-64_conda.lock +++ b/build_tools/azure/pymin_conda_forge_mkl_win-64_conda.lock @@ -26,11 +26,11 @@ https://conda.anaconda.org/conda-forge/win-64/libffi-3.4.2-h8ffe710_5.tar.bz2#2c https://conda.anaconda.org/conda-forge/win-64/libiconv-1.17-hcfcfb64_2.conda#e1eb10b1cca179f2baa3601e4efc8712 https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.0.0-hcfcfb64_1.conda#3f1b948619c45b1ca714d60c7389092c https://conda.anaconda.org/conda-forge/win-64/libogg-1.3.4-h8ffe710_1.tar.bz2#04286d905a0dcb7f7d4a12bdfe02516d -https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.45.2-hcfcfb64_0.conda#f95359f8dc5abf7da7776ece9ef10bc5 -https://conda.anaconda.org/conda-forge/win-64/libwebp-base-1.3.2-hcfcfb64_1.conda#fdf80cb33c32d4d002bb89c37cfff5b7 +https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.45.3-hcfcfb64_0.conda#73f5dc8e2d55d9a1e14b11f49c3b4a28 +https://conda.anaconda.org/conda-forge/win-64/libwebp-base-1.4.0-hcfcfb64_0.conda#abd61d0ab127ec5cd68f62c2969e6f34 https://conda.anaconda.org/conda-forge/win-64/libzlib-1.2.13-hcfcfb64_5.conda#5fdb9c6a113b6b6cb5e517fd972d5f41 https://conda.anaconda.org/conda-forge/win-64/m2w64-gcc-libgfortran-5.3.0-6.tar.bz2#066552ac6b907ec6d72c0ddab29050dc -https://conda.anaconda.org/conda-forge/win-64/ninja-1.11.1-h91493d7_0.conda#44a99ef26178ea98626ff8e027702795 +https://conda.anaconda.org/conda-forge/win-64/ninja-1.12.0-h91493d7_0.conda#e67ab00f4d2c089864c2b8dcccf4dc58 https://conda.anaconda.org/conda-forge/win-64/openssl-3.2.1-hcfcfb64_1.conda#958e0418e93e50c575bff70fbcaa12d8 https://conda.anaconda.org/conda-forge/win-64/pthreads-win32-2.9.1-hfa6e2cd_3.tar.bz2#e2da8758d7d51ff6aa78a14dfb9dbed4 https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h5226925_1.conda#fc048363eb8f03cd1737600a5d08aafe @@ -41,7 +41,7 @@ https://conda.anaconda.org/conda-forge/win-64/libbrotlienc-1.1.0-hcfcfb64_1.cond https://conda.anaconda.org/conda-forge/win-64/libintl-0.22.5-h5728263_2.conda#aa622c938af057adc119f8b8eecada01 https://conda.anaconda.org/conda-forge/win-64/libpng-1.6.43-h19919ed_0.conda#77e398acc32617a0384553aea29e866b https://conda.anaconda.org/conda-forge/win-64/libvorbis-1.3.7-h0e60522_0.tar.bz2#e1a22282de0169c93e4ffe6ce6acc212 -https://conda.anaconda.org/conda-forge/win-64/libxml2-2.12.6-hc3477c8_1.conda#eb9f59dd51f50f5aa369813fa63ba569 +https://conda.anaconda.org/conda-forge/win-64/libxml2-2.12.6-hc3477c8_2.conda#ac7af7a949db01dae61ddc48f4a93d79 https://conda.anaconda.org/conda-forge/win-64/m2w64-gcc-libs-5.3.0-7.tar.bz2#fe759119b8b3bfa720b8762c6fdc35de https://conda.anaconda.org/conda-forge/win-64/pcre2-10.43-h17e33f8_0.conda#d0485b8aa2cedb141a7bd27b4efa4c9c https://conda.anaconda.org/conda-forge/win-64/python-3.9.19-h4de0772_0_cpython.conda#b6999bc275e0e6beae7b1c8ea0be1e85 @@ -59,13 +59,13 @@ https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_0.conda https://conda.anaconda.org/conda-forge/win-64/kiwisolver-1.4.5-py39h1f6ef14_1.conda#4fc5bd0a7b535252028c647cc27d6c87 https://conda.anaconda.org/conda-forge/win-64/libclang13-18.1.3-default_hf64faad_0.conda#9217c37b478ec601af909aafc954a6fc https://conda.anaconda.org/conda-forge/win-64/libgettextpo-0.22.5-h5728263_2.conda#f4c826b19bf1ccee2a63a2c685039728 -https://conda.anaconda.org/conda-forge/win-64/libglib-2.80.0-h39d0aa6_4.conda#9baf04fc7002450a932cf97cb9625ebb +https://conda.anaconda.org/conda-forge/win-64/libglib-2.80.0-h39d0aa6_6.conda#cd5c6efbe213c089f78575c98ab9a0ed https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.10.0-default_h2fffb23_1000.conda#ee944f0d41d9e2048f9d7492c1623ca3 https://conda.anaconda.org/conda-forge/win-64/libintl-devel-0.22.5-h5728263_2.conda#a2ad82fae23975e4ccbfab2847d31d48 https://conda.anaconda.org/conda-forge/win-64/libtiff-4.6.0-hddb2be6_3.conda#6d1828c9039929e2f185c5fa9d133018 https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyh9f0ad1d_0.tar.bz2#2ba8498c1018c1e9c61eb99b973dfe19 https://conda.anaconda.org/conda-forge/noarch/packaging-24.0-pyhd8ed1ab_0.conda#248f521b64ce055e7feae3105e7abeb8 -https://conda.anaconda.org/conda-forge/noarch/pluggy-1.4.0-pyhd8ed1ab_0.conda#139e9feb65187e916162917bb2484976 +https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_0.conda#d3483c8fc2dc2cc3f5cf43e26d60cabf https://conda.anaconda.org/conda-forge/noarch/ply-3.11-pyhd8ed1ab_2.conda#18c6deb6f9602e32446398203c8f0e91 https://conda.anaconda.org/conda-forge/win-64/pthread-stubs-0.4-hcd874cb_1001.tar.bz2#a1f820480193ea83582b13249a7e7bd9 https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.1.2-pyhd8ed1ab_0.conda#b9a4dacf97241704529131a0dfc0494f @@ -82,7 +82,7 @@ https://conda.anaconda.org/conda-forge/win-64/xorg-libxdmcp-1.1.3-hcd874cb_0.tar https://conda.anaconda.org/conda-forge/noarch/zipp-3.17.0-pyhd8ed1ab_0.conda#2e4d6bc0b14e10f895fc6791a7d9b26a https://conda.anaconda.org/conda-forge/win-64/brotli-1.1.0-hcfcfb64_1.conda#f47f6db2528e38321fb00ae31674c133 https://conda.anaconda.org/conda-forge/win-64/coverage-7.4.4-py39ha55989b_0.conda#ca4fca57e0e713af82c73a9e6c5b9716 -https://conda.anaconda.org/conda-forge/win-64/glib-tools-2.80.0-h0a98069_4.conda#44cd44c004db728bf5788c1d46ce7334 +https://conda.anaconda.org/conda-forge/win-64/glib-tools-2.80.0-h0a98069_6.conda#40d452e4012c00f644b1dd6319fcdbcf https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.4.0-pyhd8ed1ab_0.conda#c5d3907ad8bd7bf557521a1833cf7e6d https://conda.anaconda.org/conda-forge/noarch/joblib-1.4.0-pyhd8ed1ab_0.conda#e0ed1bf13ce3a440e022157bf4764465 https://conda.anaconda.org/conda-forge/win-64/lcms2-2.16-h67d730c_0.conda#d3592435917b62a8becff3a60db674f6 @@ -91,16 +91,16 @@ https://conda.anaconda.org/conda-forge/win-64/libxcb-1.15-hcd874cb_0.conda#090d9 https://conda.anaconda.org/conda-forge/noarch/meson-1.4.0-pyhd8ed1ab_0.conda#52a0660cfa40b45bf254ecc3374cb2e0 https://conda.anaconda.org/conda-forge/win-64/openjpeg-2.5.2-h3d672ee_0.conda#7e7099ad94ac3b599808950cec30ad4e https://conda.anaconda.org/conda-forge/noarch/pip-24.0-pyhd8ed1ab_0.conda#f586ac1e56c8638b64f9c8122a7b8a67 -https://conda.anaconda.org/conda-forge/noarch/pyproject-metadata-0.7.1-pyhd8ed1ab_0.conda#dcb27826ffc94d5f04e241322239983b +https://conda.anaconda.org/conda-forge/noarch/pyproject-metadata-0.8.0-pyhd8ed1ab_0.conda#573fe09d7bd0cd4bcc210d8369b5ca47 https://conda.anaconda.org/conda-forge/noarch/pytest-7.4.4-pyhd8ed1ab_0.conda#a9d145de8c5f064b5fa68fb34725d9f4 https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0-pyhd8ed1ab_0.conda#2cf4264fffb9e6eff6031c5b6884d61c https://conda.anaconda.org/conda-forge/win-64/sip-6.7.12-py39h99910a6_0.conda#0cc5774390ada632ed7975203057c91c https://conda.anaconda.org/conda-forge/win-64/tbb-2021.12.0-h91493d7_0.conda#21745fdd12f01b41178596143cbecffd https://conda.anaconda.org/conda-forge/win-64/fonttools-4.51.0-py39ha55989b_0.conda#5d19302bab29e347116b743e793aa7d6 https://conda.anaconda.org/conda-forge/win-64/gettext-0.22.5-h5728263_2.conda#da84216f88a8c89eb943c683ceb34d7d -https://conda.anaconda.org/conda-forge/win-64/glib-2.80.0-h39d0aa6_4.conda#d0a05e8a76abb68b2beb7b16c6d49213 +https://conda.anaconda.org/conda-forge/win-64/glib-2.80.0-h39d0aa6_6.conda#a4036d0bc6f499ebe9fef7b887f3ca0f https://conda.anaconda.org/conda-forge/noarch/importlib-resources-6.4.0-pyhd8ed1ab_0.conda#dcbadab7a68738a028e195ab68ab2d2e -https://conda.anaconda.org/conda-forge/noarch/meson-python-0.15.0-pyh0c530f3_0.conda#3bc64565ca78ce3bb80248d09926d8f9 +https://conda.anaconda.org/conda-forge/noarch/meson-python-0.16.0-pyh0c530f3_0.conda#e16f0dbf502da873be9f9adb0dc52547 https://conda.anaconda.org/conda-forge/win-64/mkl-2024.1.0-h66d3029_692.conda#b43ec7ed045323edeff31e348eea8652 https://conda.anaconda.org/conda-forge/win-64/pillow-10.3.0-py39h9ee4981_0.conda#6d69d57c41867acc162ef0205a8efaef https://conda.anaconda.org/conda-forge/win-64/pyqt5-sip-12.12.2-py39h99910a6_5.conda#dffbcea794c524c471772a5f697c2aea diff --git a/build_tools/azure/pymin_conda_forge_openblas_ubuntu_2204_linux-64_conda.lock b/build_tools/azure/pymin_conda_forge_openblas_ubuntu_2204_linux-64_conda.lock index 7f06496a7e247..1525847b0153f 100644 --- a/build_tools/azure/pymin_conda_forge_openblas_ubuntu_2204_linux-64_conda.lock +++ b/build_tools/azure/pymin_conda_forge_openblas_ubuntu_2204_linux-64_conda.lock @@ -8,14 +8,14 @@ https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2#34893075a5c9e55cdafac56607368fc6 https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2#4d59c254e01d9cde7957100457e2d5fb https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_1.conda#6185f640c43843e5ad6fd1c5372c3f80 -https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.40-h41732ed_0.conda#7aca3059a1729aa76c597603f10b0dd3 -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-13.2.0-h7e041cc_5.conda#f6f6600d18a4047b54f803cf708b868a +https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.40-h55db66e_0.conda#10569984e7db886e4f1abc2b47ad79a1 +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-13.2.0-h95c4c6d_6.conda#3cfab3e709f77e9f1b3d380eb622494a https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.9-4_cp39.conda#bfe4b3259a8ac6cdf0037752904da6a7 https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda#161081fc7cec0bfda0d86d7cb595f8d8 https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2#f766549260d6815b0c52253f1fb1bb29 https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2#fee5683a3f04bd15cbd8318b096a27ab https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_kmp_llvm.tar.bz2#562b26ba2e19059551a811e72ab7f793 -https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-h807b86a_5.conda#d4ff227c46917d3b4565302a2bbb276b +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-hc881cc4_6.conda#df88796bd09a0d2ed292e59101478ad8 https://conda.anaconda.org/conda-forge/linux-64/alsa-lib-1.2.11-hd590300_1.conda#0bb492cca54017ea314b809b1ee3a176 https://conda.anaconda.org/conda-forge/linux-64/attr-2.5.1-h166bdaf_1.tar.bz2#d9c69a24ad678ffce24c6543a0176b00 https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hd590300_5.conda#69b8b6202a07720f448be700e300ccf4 @@ -31,20 +31,20 @@ https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.20-hd590300_0.conda https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.6.2-h59595ed_0.conda#e7ba12deb7020dd080c6c70e7b6f6a3d https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.2-h7f98852_5.tar.bz2#d645c6d2ac96843a2bfaccd2d62b3ac3 https://conda.anaconda.org/conda-forge/linux-64/libgettextpo-0.22.5-h59595ed_2.conda#172bcc51059416e7ce99e7b528cede83 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-13.2.0-ha4646dd_5.conda#7a6bd7a12a4bd359e2afe6c0fa1acace +https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-13.2.0-h43f5ff8_6.conda#e54a5ddc67e673f9105cf2a2e9c070b0 https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.17-hd590300_2.conda#d66573916ffcf376178462f1b61c941e https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.0.0-hd590300_1.conda#ea25936bb4080d843790b586850f82b8 https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda#30fd6e37fe21f86f4bd26d6ee73eeec7 https://conda.anaconda.org/conda-forge/linux-64/libogg-1.3.4-h7f98852_1.tar.bz2#6e8cc2173440d77708196c5b93771680 https://conda.anaconda.org/conda-forge/linux-64/libopus-1.3.1-h7f98852_1.tar.bz2#15345e56d527b330e1cacbdf58676e8f https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda#40b61aab5c7ba9ff276c41cfffe6b80b -https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.3.2-hd590300_1.conda#049b7df8bae5e184d1de42cdf64855f8 +https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.4.0-hd590300_0.conda#b26e8aa824079e1be0294e7152ca4559 https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda#5aa797f8787fe7a17d1b0821485b5adc https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.2.13-hd590300_5.conda#f36c115f1ee199da648e0597ec2047ad https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.9.4-hcb278e6_0.conda#318b08df404f9c9be5712aaa5a6f0bb0 https://conda.anaconda.org/conda-forge/linux-64/mpg123-1.32.6-h59595ed_0.conda#9160cdeb523a1b20cf8d2a0bf821f45d https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.4.20240210-h59595ed_0.conda#97da8860a0da5413c7c98a3b3838a645 -https://conda.anaconda.org/conda-forge/linux-64/ninja-1.11.1-h924138e_0.conda#73a4953a2d9c115bdc10ff30a52f675f +https://conda.anaconda.org/conda-forge/linux-64/ninja-1.12.0-h00ab1b0_0.conda#b048701d52e7cbb5f59ddd4d3b17bbf5 https://conda.anaconda.org/conda-forge/linux-64/nspr-4.35-h27087fc_0.conda#da0ec11a6454ae19bff5b02ed881a2b1 https://conda.anaconda.org/conda-forge/linux-64/openssl-3.2.1-hd590300_1.conda#9d731343cff6ee2e5a25c4a091bf8e2a https://conda.anaconda.org/conda-forge/linux-64/pixman-0.43.2-h59595ed_0.conda#71004cbf7924e19c02746ccde9fd7123 @@ -66,12 +66,12 @@ https://conda.anaconda.org/conda-forge/linux-64/libcap-2.69-h0f662aa_0.conda#25c https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20191231-he28a2e2_2.tar.bz2#4d331e44109e3f0e19b4cb8f9b82f3e1 https://conda.anaconda.org/conda-forge/linux-64/libevent-2.1.12-hf998b51_1.conda#a1cfcc585f0c42bf8d5546bb1dfb668d https://conda.anaconda.org/conda-forge/linux-64/libgettextpo-devel-0.22.5-h59595ed_2.conda#b63d9b6da3653179a278077f0de20014 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran-ng-13.2.0-h69a702a_5.conda#e73e9cfd1191783392131e6238bdb3e9 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-ng-13.2.0-h69a702a_6.conda#3666a850342f8f3be88f9a93d948d027 https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.43-h2797004_0.conda#009981dd9cfcaa4dbfa25ffaed86bcae -https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.45.2-h2797004_0.conda#866983a220e27a80cb75e85cb30466a1 +https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.45.3-h2797004_0.conda#b3316cbe90249da4f8e84cd66e1cc55b https://conda.anaconda.org/conda-forge/linux-64/libvorbis-1.3.7-h9c3ff4c_0.tar.bz2#309dec04b70a3cc0f1e84a4013683bc0 https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.15-h0b41bf4_0.conda#33277193f5b92bad9fdd230eb700929c -https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.12.6-h232c23b_1.conda#6853448e9ca1cfd5f15382afd2a6d123 +https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.12.6-h232c23b_2.conda#9a3a42df8a95f65334dfc7b80da1195d https://conda.anaconda.org/conda-forge/linux-64/mysql-common-8.3.0-hf1915f5_4.conda#784a4df6676c581ca624fbe460703a6d https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.43-hcad00b1_0.conda#8292dea9e022d9610a11fce5e0896ed8 https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda#47d31b792659ce70f470b5c82fdfb7a4 @@ -83,7 +83,7 @@ https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.1.0-hd590300_1.cond https://conda.anaconda.org/conda-forge/linux-64/freetype-2.12.1-h267a509_2.conda#9ae35c3d96db2c94ce0cef86efdfa2cb https://conda.anaconda.org/conda-forge/linux-64/gettext-0.22.5-h59595ed_2.conda#219ba82e95d7614cf7140d2a4afc0926 https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.2-h659d440_0.conda#cd95826dbd331ed1be26bdf401432844 -https://conda.anaconda.org/conda-forge/linux-64/libglib-2.80.0-hf2295e7_4.conda#0269d2b7fa89f4a37cdee5ad6161f6cc +https://conda.anaconda.org/conda-forge/linux-64/libglib-2.80.0-hf2295e7_6.conda#9342e7c44c38bea649490f72d92c382d https://conda.anaconda.org/conda-forge/linux-64/libhiredis-1.0.2-h2cc385e_0.tar.bz2#b34907d3a81a3cd8095ee83d174c074a https://conda.anaconda.org/conda-forge/linux-64/libllvm15-15.0.7-hb3ce162_4.conda#8a35df3cbc0c8b12cc8af9473ae75eef https://conda.anaconda.org/conda-forge/linux-64/libllvm18-18.1.3-h2448989_0.conda#927b6d6e80b2c0d4405a58b61ca248a3 @@ -108,12 +108,12 @@ https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhd8ed1ab_0.conda#5cd86562580f274031ede6aa6aa24441 https://conda.anaconda.org/conda-forge/linux-64/cython-3.0.10-py39h3d6467e_0.conda#76b5d215fb735a6dc43010ffbe78040e https://conda.anaconda.org/conda-forge/linux-64/dbus-1.13.6-h5008d03_3.tar.bz2#ecfff944ba3960ecb334b9a2663d708d -https://conda.anaconda.org/conda-forge/linux-64/docutils-0.20.1-py39hf3d152e_3.conda#09a48956e1c155907fd0d626f3e80f2e +https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.1-pyhd8ed1ab_0.conda#04ffd8609a7483e8b262aa0f121e7360 https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.0-pyhd8ed1ab_2.conda#8d652ea2ee8eaee02ed8dc820bc794aa https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_0.conda#15dda3cdbf330abfe9f555d22f66db46 https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.14.2-h14ed4e7_0.conda#0f69b688f52ff6da70bccb7ff7001d1d -https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.80.0-hde27a5a_4.conda#c9deba4959ea5b5f72f1e3e4a71ae014 -https://conda.anaconda.org/conda-forge/noarch/idna-3.6-pyhd8ed1ab_0.conda#1a76f09108576397c41c0b0c5bd84134 +https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.80.0-hde27a5a_6.conda#a9d23c02485c5cf055f9ac90eb9c9c63 +https://conda.anaconda.org/conda-forge/noarch/idna-3.7-pyhd8ed1ab_0.conda#c0cc1420498b17414d8617d0b9f506ca https://conda.anaconda.org/conda-forge/noarch/imagesize-1.4.1-pyhd8ed1ab_0.tar.bz2#7de5386c8fea29e76b303f37dde4c352 https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_0.conda#f800d2da156d08e289b14e87e43c1ae5 https://conda.anaconda.org/conda-forge/linux-64/kiwisolver-1.4.5-py39h7633fee_1.conda#c9f74d717e5a2847a9f8b779c54130f2 @@ -130,7 +130,7 @@ https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyh9f0ad1d_0.tar.bz2 https://conda.anaconda.org/conda-forge/linux-64/openblas-0.3.27-pthreads_h7a3da1a_0.conda#4b422ebe8fc6a5320d0c1c22e5a46032 https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.2-h488ebb8_0.conda#7f2e286780f072ed750df46dc2631138 https://conda.anaconda.org/conda-forge/noarch/packaging-24.0-pyhd8ed1ab_0.conda#248f521b64ce055e7feae3105e7abeb8 -https://conda.anaconda.org/conda-forge/noarch/pluggy-1.4.0-pyhd8ed1ab_0.conda#139e9feb65187e916162917bb2484976 +https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_0.conda#d3483c8fc2dc2cc3f5cf43e26d60cabf https://conda.anaconda.org/conda-forge/noarch/ply-3.11-pyhd8ed1ab_2.conda#18c6deb6f9602e32446398203c8f0e91 https://conda.anaconda.org/conda-forge/noarch/pygments-2.17.2-pyhd8ed1ab_0.conda#140a7f159396547e9799aa98f9f0742e https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.1.2-pyhd8ed1ab_0.conda#b9a4dacf97241704529131a0dfc0494f @@ -156,7 +156,7 @@ https://conda.anaconda.org/conda-forge/noarch/zipp-3.17.0-pyhd8ed1ab_0.conda#2e4 https://conda.anaconda.org/conda-forge/noarch/babel-2.14.0-pyhd8ed1ab_0.conda#9669586875baeced8fc30c0826c3270e https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.0-h3faef2a_0.conda#f907bb958910dc404647326ca80c263e https://conda.anaconda.org/conda-forge/linux-64/fonttools-4.51.0-py39hd1e30aa_0.conda#79f5dd8778873faa54e8f7b2729fe8a6 -https://conda.anaconda.org/conda-forge/linux-64/glib-2.80.0-hf2295e7_4.conda#5521382ee30b96b35eb0037fc3c4f2b4 +https://conda.anaconda.org/conda-forge/linux-64/glib-2.80.0-hf2295e7_6.conda#a1e026a82a562b443845db5614ca568a https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-7.1.0-pyha770c72_0.conda#0896606848b2dc5cebdf111b6543aa04 https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.4.0-pyhd8ed1ab_0.conda#c5d3907ad8bd7bf557521a1833cf7e6d https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.3-pyhd8ed1ab_0.conda#e7d8df6509ba635247ff9aea31134262 @@ -169,7 +169,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libxkbcommon-1.7.0-h662e7e4_0.co https://conda.anaconda.org/conda-forge/noarch/meson-1.4.0-pyhd8ed1ab_0.conda#52a0660cfa40b45bf254ecc3374cb2e0 https://conda.anaconda.org/conda-forge/linux-64/pillow-10.3.0-py39h90c7501_0.conda#1e3b6af9592be71ce19f0a6aae05d97b https://conda.anaconda.org/conda-forge/noarch/pip-24.0-pyhd8ed1ab_0.conda#f586ac1e56c8638b64f9c8122a7b8a67 -https://conda.anaconda.org/conda-forge/noarch/pyproject-metadata-0.7.1-pyhd8ed1ab_0.conda#dcb27826ffc94d5f04e241322239983b +https://conda.anaconda.org/conda-forge/noarch/pyproject-metadata-0.8.0-pyhd8ed1ab_0.conda#573fe09d7bd0cd4bcc210d8369b5ca47 https://conda.anaconda.org/conda-forge/noarch/pytest-7.4.4-pyhd8ed1ab_0.conda#a9d145de8c5f064b5fa68fb34725d9f4 https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0-pyhd8ed1ab_0.conda#2cf4264fffb9e6eff6031c5b6884d61c https://conda.anaconda.org/conda-forge/linux-64/sip-6.7.12-py39h3d6467e_0.conda#e667a3ab0df62c54e60e1843d2e6defb @@ -179,7 +179,7 @@ https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-8.3.0-h3d44ed6_0.conda# https://conda.anaconda.org/conda-forge/noarch/importlib-resources-6.4.0-pyhd8ed1ab_0.conda#dcbadab7a68738a028e195ab68ab2d2e https://conda.anaconda.org/conda-forge/linux-64/liblapacke-3.9.0-22_linux64_openblas.conda#1fd156abd41a4992835952f6f4d951d0 https://conda.anaconda.org/conda-forge/linux-64/libsystemd0-255-h3516f8a_1.conda#3366af27f0b593544a6cd453c7932ac5 -https://conda.anaconda.org/conda-forge/noarch/meson-python-0.15.0-pyh0c530f3_0.conda#3bc64565ca78ce3bb80248d09926d8f9 +https://conda.anaconda.org/conda-forge/noarch/meson-python-0.16.0-pyh0c530f3_0.conda#e16f0dbf502da873be9f9adb0dc52547 https://conda.anaconda.org/conda-forge/linux-64/numpy-1.26.4-py39h474f0d3_0.conda#aa265f5697237aa13cc10f53fa8acc4f https://conda.anaconda.org/conda-forge/linux-64/pyqt5-sip-12.12.2-py39h3d6467e_5.conda#93aff412f3e49fdb43361c0215cbd72d https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.5.0-pyhd8ed1ab_0.conda#d5f595da2daead898ca958ac62f0307b @@ -201,5 +201,5 @@ https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-1.0.8-pyhd https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-devhelp-1.0.6-pyhd8ed1ab_0.conda#d7e4954df0d3aea2eacc7835ad12671d https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-htmlhelp-2.0.5-pyhd8ed1ab_0.conda#7e1e7437273682ada2ed5e9e9714b140 https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-qthelp-1.0.7-pyhd8ed1ab_0.conda#26acae54b06f178681bfb551760f5dd1 -https://conda.anaconda.org/conda-forge/noarch/sphinx-7.2.6-pyhd8ed1ab_0.conda#bbfd1120d1824d2d073bc65935f0e4c0 +https://conda.anaconda.org/conda-forge/noarch/sphinx-7.3.7-pyhd8ed1ab_0.conda#7b1465205e28d75d2c0e1a868ee00a67 https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-serializinghtml-1.1.10-pyhd8ed1ab_0.conda#e507335cb4ca9cff4c3d0fa9cdab255e diff --git a/build_tools/azure/ubuntu_atlas_lock.txt b/build_tools/azure/ubuntu_atlas_lock.txt index aa17f49e75936..c8e94493ccf28 100644 --- a/build_tools/azure/ubuntu_atlas_lock.txt +++ b/build_tools/azure/ubuntu_atlas_lock.txt @@ -6,7 +6,7 @@ # cython==3.0.10 # via -r build_tools/azure/ubuntu_atlas_requirements.txt -exceptiongroup==1.2.0 +exceptiongroup==1.2.1 # via pytest execnet==2.1.1 # via pytest-xdist @@ -16,17 +16,18 @@ joblib==1.2.0 # via -r build_tools/azure/ubuntu_atlas_requirements.txt meson==1.4.0 # via meson-python -meson-python==0.15.0 +meson-python==0.16.0 # via -r build_tools/azure/ubuntu_atlas_requirements.txt ninja==1.11.1.1 # via -r build_tools/azure/ubuntu_atlas_requirements.txt packaging==24.0 # via + # meson-python # pyproject-metadata # pytest -pluggy==1.4.0 +pluggy==1.5.0 # via pytest -pyproject-metadata==0.7.1 +pyproject-metadata==0.8.0 # via meson-python pytest==7.4.4 # via diff --git a/build_tools/circle/doc_linux-64_conda.lock b/build_tools/circle/doc_linux-64_conda.lock index 984cca332fc7f..3f05468863ae6 100644 --- a/build_tools/circle/doc_linux-64_conda.lock +++ b/build_tools/circle/doc_linux-64_conda.lock @@ -9,21 +9,21 @@ https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed3 https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2#4d59c254e01d9cde7957100457e2d5fb https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_1.conda#6185f640c43843e5ad6fd1c5372c3f80 https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-2.6.32-he073ed8_17.conda#d731b543793afc0433c4fd593e693fce -https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.40-h41732ed_0.conda#7aca3059a1729aa76c597603f10b0dd3 -https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-12.3.0-h8bca6fd_105.conda#e12ce6b051085b8f27e239f5e5f5bce5 -https://conda.anaconda.org/conda-forge/noarch/libstdcxx-devel_linux-64-12.3.0-h8bca6fd_105.conda#b3c6062c84a8e172555ee104ea6a01ab -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-13.2.0-h7e041cc_5.conda#f6f6600d18a4047b54f803cf708b868a +https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.40-h55db66e_0.conda#10569984e7db886e4f1abc2b47ad79a1 +https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-12.3.0-h2af2641_106.conda#b97e137a252f112b8d5fadb313bd8ec9 +https://conda.anaconda.org/conda-forge/noarch/libstdcxx-devel_linux-64-12.3.0-h2af2641_106.conda#647bd9d44ad216d410329e659c898d8f +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-13.2.0-h95c4c6d_6.conda#3cfab3e709f77e9f1b3d380eb622494a https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.9-4_cp39.conda#bfe4b3259a8ac6cdf0037752904da6a7 https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda#161081fc7cec0bfda0d86d7cb595f8d8 https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2#f766549260d6815b0c52253f1fb1bb29 -https://conda.anaconda.org/conda-forge/linux-64/libgomp-13.2.0-h807b86a_5.conda#d211c42b9ce49aee3734fdc828731689 +https://conda.anaconda.org/conda-forge/linux-64/libgomp-13.2.0-hc881cc4_6.conda#aae89d3736661c36a5591788aebd0817 https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.12-he073ed8_17.conda#595db67e32b276298ff3d94d07d47fbf -https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.40-hf600244_0.conda#33084421a8c0af6aef1b439707f7662a +https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.40-ha885e6a_0.conda#800a4c872b5bc06fa83888d112fe6c4f https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2#fee5683a3f04bd15cbd8318b096a27ab -https://conda.anaconda.org/conda-forge/linux-64/binutils-2.40-hdd6e379_0.conda#ccc940fddbc3fcd3d79cd4c654c4b5c4 +https://conda.anaconda.org/conda-forge/linux-64/binutils-2.40-h4852527_0.conda#a05c7712be80622934f7011e0a1d43fc https://conda.anaconda.org/conda-forge/linux-64/binutils_linux-64-2.40-hdade7a5_3.conda#2d9a60578bc28469d9aeef9aea5520c3 https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_kmp_llvm.tar.bz2#562b26ba2e19059551a811e72ab7f793 -https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-h807b86a_5.conda#d4ff227c46917d3b4565302a2bbb276b +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-hc881cc4_6.conda#df88796bd09a0d2ed292e59101478ad8 https://conda.anaconda.org/conda-forge/linux-64/alsa-lib-1.2.11-hd590300_1.conda#0bb492cca54017ea314b809b1ee3a176 https://conda.anaconda.org/conda-forge/linux-64/aom-3.8.2-h59595ed_0.conda#625e1fed28a5139aed71b3a76117ef84 https://conda.anaconda.org/conda-forge/linux-64/attr-2.5.1-h166bdaf_1.tar.bz2#d9c69a24ad678ffce24c6543a0176b00 @@ -45,23 +45,23 @@ https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.20-hd590300_0.conda https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.6.2-h59595ed_0.conda#e7ba12deb7020dd080c6c70e7b6f6a3d https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.2-h7f98852_5.tar.bz2#d645c6d2ac96843a2bfaccd2d62b3ac3 https://conda.anaconda.org/conda-forge/linux-64/libgettextpo-0.22.5-h59595ed_2.conda#172bcc51059416e7ce99e7b528cede83 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-13.2.0-ha4646dd_5.conda#7a6bd7a12a4bd359e2afe6c0fa1acace +https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-13.2.0-h43f5ff8_6.conda#e54a5ddc67e673f9105cf2a2e9c070b0 https://conda.anaconda.org/conda-forge/linux-64/libhwy-1.1.0-h00ab1b0_0.conda#88928158ccfe797eac29ef5e03f7d23d https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.17-hd590300_2.conda#d66573916ffcf376178462f1b61c941e https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.0.0-hd590300_1.conda#ea25936bb4080d843790b586850f82b8 https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda#30fd6e37fe21f86f4bd26d6ee73eeec7 https://conda.anaconda.org/conda-forge/linux-64/libogg-1.3.4-h7f98852_1.tar.bz2#6e8cc2173440d77708196c5b93771680 https://conda.anaconda.org/conda-forge/linux-64/libopus-1.3.1-h7f98852_1.tar.bz2#15345e56d527b330e1cacbdf58676e8f -https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-12.3.0-h0f45ef3_5.conda#11d1ceacff40054d5a74b12975d76f20 +https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-12.3.0-h2af2641_6.conda#1cf0b420341bb1a7b7f34f6e0f4bbf2b https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda#40b61aab5c7ba9ff276c41cfffe6b80b -https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.3.2-hd590300_1.conda#049b7df8bae5e184d1de42cdf64855f8 +https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.4.0-hd590300_0.conda#b26e8aa824079e1be0294e7152ca4559 https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda#5aa797f8787fe7a17d1b0821485b5adc https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.2.13-hd590300_5.conda#f36c115f1ee199da648e0597ec2047ad https://conda.anaconda.org/conda-forge/linux-64/libzopfli-1.0.3-h9c3ff4c_0.tar.bz2#c66fe2d123249af7651ebde8984c51c2 https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.9.4-hcb278e6_0.conda#318b08df404f9c9be5712aaa5a6f0bb0 https://conda.anaconda.org/conda-forge/linux-64/mpg123-1.32.6-h59595ed_0.conda#9160cdeb523a1b20cf8d2a0bf821f45d https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.4.20240210-h59595ed_0.conda#97da8860a0da5413c7c98a3b3838a645 -https://conda.anaconda.org/conda-forge/linux-64/ninja-1.11.1-h924138e_0.conda#73a4953a2d9c115bdc10ff30a52f675f +https://conda.anaconda.org/conda-forge/linux-64/ninja-1.12.0-h00ab1b0_0.conda#b048701d52e7cbb5f59ddd4d3b17bbf5 https://conda.anaconda.org/conda-forge/linux-64/nspr-4.35-h27087fc_0.conda#da0ec11a6454ae19bff5b02ed881a2b1 https://conda.anaconda.org/conda-forge/linux-64/openssl-3.2.1-hd590300_1.conda#9d731343cff6ee2e5a25c4a091bf8e2a https://conda.anaconda.org/conda-forge/linux-64/pixman-0.43.2-h59595ed_0.conda#71004cbf7924e19c02746ccde9fd7123 @@ -81,7 +81,7 @@ https://conda.anaconda.org/conda-forge/linux-64/xz-5.2.6-h166bdaf_0.tar.bz2#2161 https://conda.anaconda.org/conda-forge/linux-64/zfp-1.0.1-h59595ed_0.conda#fd486bffbf0d6841cf1456a8f2e3a995 https://conda.anaconda.org/conda-forge/linux-64/zlib-ng-2.0.7-h0b41bf4_0.conda#49e8329110001f04923fe7e864990b0c https://conda.anaconda.org/conda-forge/linux-64/expat-2.6.2-h59595ed_0.conda#53fb86322bdb89496d7579fe3f02fd61 -https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-12.3.0-he2b93b0_5.conda#e89827619e73df59496c708b94f6f3d5 +https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-12.3.0-h1562d66_6.conda#5e4e8358a4ab43498e0ac3b6776d1c94 https://conda.anaconda.org/conda-forge/linux-64/libasprintf-devel-0.22.5-h661eb56_2.conda#02e41ab5834dcdcc8590cf29d9526f50 https://conda.anaconda.org/conda-forge/linux-64/libavif16-1.0.4-hd9d6309_2.conda#a8c65cba5f77abc1f2e85ab9a0e614aa https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.1.0-hd590300_1.conda#f07002e225d7a60a694d42a7bf5ff53f @@ -90,12 +90,12 @@ https://conda.anaconda.org/conda-forge/linux-64/libcap-2.69-h0f662aa_0.conda#25c https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20191231-he28a2e2_2.tar.bz2#4d331e44109e3f0e19b4cb8f9b82f3e1 https://conda.anaconda.org/conda-forge/linux-64/libevent-2.1.12-hf998b51_1.conda#a1cfcc585f0c42bf8d5546bb1dfb668d https://conda.anaconda.org/conda-forge/linux-64/libgettextpo-devel-0.22.5-h59595ed_2.conda#b63d9b6da3653179a278077f0de20014 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran-ng-13.2.0-h69a702a_5.conda#e73e9cfd1191783392131e6238bdb3e9 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-ng-13.2.0-h69a702a_6.conda#3666a850342f8f3be88f9a93d948d027 https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.43-h2797004_0.conda#009981dd9cfcaa4dbfa25ffaed86bcae -https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.45.2-h2797004_0.conda#866983a220e27a80cb75e85cb30466a1 +https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.45.3-h2797004_0.conda#b3316cbe90249da4f8e84cd66e1cc55b https://conda.anaconda.org/conda-forge/linux-64/libvorbis-1.3.7-h9c3ff4c_0.tar.bz2#309dec04b70a3cc0f1e84a4013683bc0 https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.15-h0b41bf4_0.conda#33277193f5b92bad9fdd230eb700929c -https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.12.6-h232c23b_1.conda#6853448e9ca1cfd5f15382afd2a6d123 +https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.12.6-h232c23b_2.conda#9a3a42df8a95f65334dfc7b80da1195d https://conda.anaconda.org/conda-forge/linux-64/mysql-common-8.3.0-hf1915f5_4.conda#784a4df6676c581ca624fbe460703a6d https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.43-hcad00b1_0.conda#8292dea9e022d9610a11fce5e0896ed8 https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda#47d31b792659ce70f470b5c82fdfb7a4 @@ -107,14 +107,14 @@ https://conda.anaconda.org/conda-forge/linux-64/blosc-1.21.5-hc2324a3_1.conda#11 https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.1.0-hd590300_1.conda#39f910d205726805a958da408ca194ba https://conda.anaconda.org/conda-forge/linux-64/c-blosc2-2.14.4-hb4ffafa_1.conda#84eb54e92644c328e087e1c725773317 https://conda.anaconda.org/conda-forge/linux-64/freetype-2.12.1-h267a509_2.conda#9ae35c3d96db2c94ce0cef86efdfa2cb -https://conda.anaconda.org/conda-forge/linux-64/gcc-12.3.0-h95e488c_3.conda#413e326f8a01d041ffbfbb51cea46a93 +https://conda.anaconda.org/conda-forge/linux-64/gcc-12.3.0-h915e2ae_6.conda#ec683e084ea08ef94528f15d30fa1e03 https://conda.anaconda.org/conda-forge/linux-64/gcc_linux-64-12.3.0-h6477408_3.conda#7a53f84c45bdf4656ba27b9e9ed68b3d https://conda.anaconda.org/conda-forge/linux-64/gettext-0.22.5-h59595ed_2.conda#219ba82e95d7614cf7140d2a4afc0926 -https://conda.anaconda.org/conda-forge/linux-64/gfortran_impl_linux-64-12.3.0-hfcedea8_5.conda#4d72ee7c82f8a9b2ecef4fcefa9acd19 -https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-12.3.0-he2b93b0_5.conda#cddba8fd94e52012abea1caad722b9c2 +https://conda.anaconda.org/conda-forge/linux-64/gfortran_impl_linux-64-12.3.0-h6d6b2fb_6.conda#d6c441226a4bd0af4c024e8c0f4a47cf +https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-12.3.0-h1562d66_6.conda#5ad72ddd14e13d589dea2afe6e626619 https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.2-h659d440_0.conda#cd95826dbd331ed1be26bdf401432844 -https://conda.anaconda.org/conda-forge/linux-64/libglib-2.80.0-hf2295e7_4.conda#0269d2b7fa89f4a37cdee5ad6161f6cc -https://conda.anaconda.org/conda-forge/linux-64/libjxl-0.10.1-hcae5a98_1.conda#ca9532696d031f78d1dc245c413823d4 +https://conda.anaconda.org/conda-forge/linux-64/libglib-2.80.0-hf2295e7_6.conda#9342e7c44c38bea649490f72d92c382d +https://conda.anaconda.org/conda-forge/linux-64/libjxl-0.10.2-hcae5a98_0.conda#901db891e1e21afd8524cd636a8c8e3b https://conda.anaconda.org/conda-forge/linux-64/libllvm15-15.0.7-hb3ce162_4.conda#8a35df3cbc0c8b12cc8af9473ae75eef https://conda.anaconda.org/conda-forge/linux-64/libllvm18-18.1.3-h2448989_0.conda#927b6d6e80b2c0d4405a58b61ca248a3 https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.27-pthreads_h413a1c8_0.conda#a356024784da6dfd4683dc5ecf45b155 @@ -138,16 +138,16 @@ https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhd8ed1ab_0.conda#5cd86562580f274031ede6aa6aa24441 https://conda.anaconda.org/conda-forge/linux-64/cython-3.0.10-py39h3d6467e_0.conda#76b5d215fb735a6dc43010ffbe78040e https://conda.anaconda.org/conda-forge/linux-64/dbus-1.13.6-h5008d03_3.tar.bz2#ecfff944ba3960ecb334b9a2663d708d -https://conda.anaconda.org/conda-forge/linux-64/docutils-0.20.1-py39hf3d152e_3.conda#09a48956e1c155907fd0d626f3e80f2e +https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.1-pyhd8ed1ab_0.conda#04ffd8609a7483e8b262aa0f121e7360 https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.0-pyhd8ed1ab_2.conda#8d652ea2ee8eaee02ed8dc820bc794aa https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_0.conda#15dda3cdbf330abfe9f555d22f66db46 https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.14.2-h14ed4e7_0.conda#0f69b688f52ff6da70bccb7ff7001d1d -https://conda.anaconda.org/conda-forge/linux-64/gfortran-12.3.0-h7389182_3.conda#6b0b27394cf439d0540f949190556860 +https://conda.anaconda.org/conda-forge/linux-64/gfortran-12.3.0-h915e2ae_6.conda#84b517f4f53e56256dbd65133aae04ac https://conda.anaconda.org/conda-forge/linux-64/gfortran_linux-64-12.3.0-h617cb40_3.conda#3a9e5b8a6f651ff14e74d896d8f04ab6 -https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.80.0-hde27a5a_4.conda#c9deba4959ea5b5f72f1e3e4a71ae014 -https://conda.anaconda.org/conda-forge/linux-64/gxx-12.3.0-h95e488c_3.conda#8c50a4d15a8d4812af563a684d598910 +https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.80.0-hde27a5a_6.conda#a9d23c02485c5cf055f9ac90eb9c9c63 +https://conda.anaconda.org/conda-forge/linux-64/gxx-12.3.0-h915e2ae_6.conda#0d977804df65082e17c860600ca2894b https://conda.anaconda.org/conda-forge/linux-64/gxx_linux-64-12.3.0-h4a1b8e8_3.conda#9ec22c7c544f4a4f6d660f0a3b0fd15c -https://conda.anaconda.org/conda-forge/noarch/idna-3.6-pyhd8ed1ab_0.conda#1a76f09108576397c41c0b0c5bd84134 +https://conda.anaconda.org/conda-forge/noarch/idna-3.7-pyhd8ed1ab_0.conda#c0cc1420498b17414d8617d0b9f506ca https://conda.anaconda.org/conda-forge/noarch/imagesize-1.4.1-pyhd8ed1ab_0.tar.bz2#7de5386c8fea29e76b303f37dde4c352 https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_0.conda#f800d2da156d08e289b14e87e43c1ae5 https://conda.anaconda.org/conda-forge/linux-64/kiwisolver-1.4.5-py39h7633fee_1.conda#c9f74d717e5a2847a9f8b779c54130f2 @@ -166,7 +166,7 @@ https://conda.anaconda.org/conda-forge/linux-64/openblas-0.3.27-pthreads_h7a3da1 https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.2-h488ebb8_0.conda#7f2e286780f072ed750df46dc2631138 https://conda.anaconda.org/conda-forge/noarch/packaging-24.0-pyhd8ed1ab_0.conda#248f521b64ce055e7feae3105e7abeb8 https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.2.0-pyhd8ed1ab_0.conda#a0bc3eec34b0fab84be6b2da94e98e20 -https://conda.anaconda.org/conda-forge/noarch/pluggy-1.4.0-pyhd8ed1ab_0.conda#139e9feb65187e916162917bb2484976 +https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_0.conda#d3483c8fc2dc2cc3f5cf43e26d60cabf https://conda.anaconda.org/conda-forge/noarch/ply-3.11-pyhd8ed1ab_2.conda#18c6deb6f9602e32446398203c8f0e91 https://conda.anaconda.org/conda-forge/linux-64/psutil-5.9.8-py39hd1e30aa_0.conda#ec86403fde8793ac1c36f8afa3d15902 https://conda.anaconda.org/conda-forge/noarch/pygments-2.17.2-pyhd8ed1ab_0.conda#140a7f159396547e9799aa98f9f0742e @@ -198,7 +198,7 @@ https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.0-h3faef2a_0.conda#f9 https://conda.anaconda.org/conda-forge/linux-64/cxx-compiler-1.7.0-h00ab1b0_0.conda#b4537c98cb59f8725b0e1e65816b4a28 https://conda.anaconda.org/conda-forge/linux-64/fonttools-4.51.0-py39hd1e30aa_0.conda#79f5dd8778873faa54e8f7b2729fe8a6 https://conda.anaconda.org/conda-forge/linux-64/fortran-compiler-1.7.0-heb67821_0.conda#7ef7c0f111dad1c8006504a0f1ccd820 -https://conda.anaconda.org/conda-forge/linux-64/glib-2.80.0-hf2295e7_4.conda#5521382ee30b96b35eb0037fc3c4f2b4 +https://conda.anaconda.org/conda-forge/linux-64/glib-2.80.0-hf2295e7_6.conda#a1e026a82a562b443845db5614ca568a https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-7.1.0-pyha770c72_0.conda#0896606848b2dc5cebdf111b6543aa04 https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.4.0-pyhd8ed1ab_0.conda#c5d3907ad8bd7bf557521a1833cf7e6d https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.3-pyhd8ed1ab_0.conda#e7d8df6509ba635247ff9aea31134262 @@ -212,8 +212,8 @@ https://conda.anaconda.org/conda-forge/noarch/memory_profiler-0.61.0-pyhd8ed1ab_ https://conda.anaconda.org/conda-forge/noarch/meson-1.4.0-pyhd8ed1ab_0.conda#52a0660cfa40b45bf254ecc3374cb2e0 https://conda.anaconda.org/conda-forge/linux-64/pillow-10.3.0-py39h90c7501_0.conda#1e3b6af9592be71ce19f0a6aae05d97b https://conda.anaconda.org/conda-forge/noarch/pip-24.0-pyhd8ed1ab_0.conda#f586ac1e56c8638b64f9c8122a7b8a67 -https://conda.anaconda.org/conda-forge/noarch/plotly-5.19.0-pyhd8ed1ab_0.conda#669cd7065794633b9e64e6a9612ec700 -https://conda.anaconda.org/conda-forge/noarch/pyproject-metadata-0.7.1-pyhd8ed1ab_0.conda#dcb27826ffc94d5f04e241322239983b +https://conda.anaconda.org/conda-forge/noarch/plotly-5.21.0-pyhd8ed1ab_0.conda#c8f5835e6c3a850d9a000d23056d780b +https://conda.anaconda.org/conda-forge/noarch/pyproject-metadata-0.8.0-pyhd8ed1ab_0.conda#573fe09d7bd0cd4bcc210d8369b5ca47 https://conda.anaconda.org/conda-forge/noarch/pytest-7.4.4-pyhd8ed1ab_0.conda#a9d145de8c5f064b5fa68fb34725d9f4 https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0-pyhd8ed1ab_0.conda#2cf4264fffb9e6eff6031c5b6884d61c https://conda.anaconda.org/conda-forge/linux-64/sip-6.7.12-py39h3d6467e_0.conda#e667a3ab0df62c54e60e1843d2e6defb @@ -225,7 +225,7 @@ https://conda.anaconda.org/conda-forge/noarch/importlib-resources-6.4.0-pyhd8ed1 https://conda.anaconda.org/conda-forge/noarch/lazy_loader-0.4-pyhd8ed1ab_0.conda#a284ff318fbdb0dd83928275b4b6087c https://conda.anaconda.org/conda-forge/linux-64/liblapacke-3.9.0-22_linux64_openblas.conda#1fd156abd41a4992835952f6f4d951d0 https://conda.anaconda.org/conda-forge/linux-64/libsystemd0-255-h3516f8a_1.conda#3366af27f0b593544a6cd453c7932ac5 -https://conda.anaconda.org/conda-forge/noarch/meson-python-0.15.0-pyh0c530f3_0.conda#3bc64565ca78ce3bb80248d09926d8f9 +https://conda.anaconda.org/conda-forge/noarch/meson-python-0.16.0-pyh0c530f3_0.conda#e16f0dbf502da873be9f9adb0dc52547 https://conda.anaconda.org/conda-forge/linux-64/numpy-1.26.4-py39h474f0d3_0.conda#aa265f5697237aa13cc10f53fa8acc4f https://conda.anaconda.org/conda-forge/linux-64/pyqt5-sip-12.12.2-py39h3d6467e_5.conda#93aff412f3e49fdb43361c0215cbd72d https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.5.0-pyhd8ed1ab_0.conda#d5f595da2daead898ca958ac62f0307b @@ -234,10 +234,10 @@ https://conda.anaconda.org/conda-forge/linux-64/blas-devel-3.9.0-22_linux64_open https://conda.anaconda.org/conda-forge/linux-64/contourpy-1.2.1-py39h7633fee_0.conda#bdc188e59857d6efab332714e0d01d93 https://conda.anaconda.org/conda-forge/linux-64/gst-plugins-base-1.24.1-hfa15dee_1.conda#a6dd2bbc684913e2bef0a54ce56fcbfb https://conda.anaconda.org/conda-forge/linux-64/imagecodecs-2024.1.1-py39ha98d97a_6.conda#9ada409e8a8202f848abfed8e4e3f6be -https://conda.anaconda.org/conda-forge/noarch/imageio-2.34.0-pyh4b66e23_0.conda#b8853659d596f967c661f544dd89ede7 +https://conda.anaconda.org/conda-forge/noarch/imageio-2.34.1-pyh4b66e23_0.conda#bcf6a6f4c6889ca083e8d33afbafb8d5 https://conda.anaconda.org/conda-forge/linux-64/pandas-2.2.2-py39hddac248_0.conda#259c4e76e6bda8888aefc098ae1ba749 https://conda.anaconda.org/conda-forge/noarch/patsy-0.5.6-pyhd8ed1ab_0.conda#a5b55d1cb110cdcedc748b5c3e16e687 -https://conda.anaconda.org/conda-forge/linux-64/polars-0.20.20-py39h87fa3cb_0.conda#637e4c3bccd3666e2b71a75c8ec7295b +https://conda.anaconda.org/conda-forge/linux-64/polars-0.20.21-py39h87fa3cb_1.conda#67b1ece36573ad3e1c195bcd580277ba https://conda.anaconda.org/conda-forge/noarch/pooch-1.8.1-pyhd8ed1ab_0.conda#d15917f33140f8d2ac9ca44db7ec8a25 https://conda.anaconda.org/conda-forge/linux-64/pulseaudio-client-17.0-hb77b528_0.conda#07f45f1be1c25345faddb8db0de8039b https://conda.anaconda.org/conda-forge/linux-64/pywavelets-1.4.1-py39h44dd56e_1.conda#d037c20e3da2e85f03ebd20ad480c359 @@ -247,7 +247,7 @@ https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.8.4-py39he9076 https://conda.anaconda.org/conda-forge/linux-64/pyamg-5.1.0-py39hda80f44_0.conda#f225666c47726329201b604060f1436c https://conda.anaconda.org/conda-forge/linux-64/qt-main-5.15.8-hc9dc06e_21.conda#b325046180590c868ce0dbf267b82eb8 https://conda.anaconda.org/conda-forge/linux-64/statsmodels-0.14.1-py39h44dd56e_0.conda#dc565186b972bd87e49b9c35390ddd8c -https://conda.anaconda.org/conda-forge/noarch/tifffile-2024.2.12-pyhd8ed1ab_0.conda#d5c8bef52be4e70c48b1400eec3eecc8 +https://conda.anaconda.org/conda-forge/noarch/tifffile-2024.4.18-pyhd8ed1ab_0.conda#9640ec921dce12e87e589ac634c7bd8a https://conda.anaconda.org/conda-forge/linux-64/pyqt-5.15.9-py39h52134e7_5.conda#e1f148e57d071b09187719df86f513c1 https://conda.anaconda.org/conda-forge/linux-64/scikit-image-0.22.0-py39hddac248_2.conda#8d502a4d2cbe5a45ff35ca8af8cbec0a https://conda.anaconda.org/conda-forge/noarch/seaborn-base-0.13.2-pyhd8ed1ab_0.conda#0918a9201e824211cdf444dbf8d55752 @@ -261,7 +261,7 @@ https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-1.0.8-pyhd https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-devhelp-1.0.6-pyhd8ed1ab_0.conda#d7e4954df0d3aea2eacc7835ad12671d https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-htmlhelp-2.0.5-pyhd8ed1ab_0.conda#7e1e7437273682ada2ed5e9e9714b140 https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-qthelp-1.0.7-pyhd8ed1ab_0.conda#26acae54b06f178681bfb551760f5dd1 -https://conda.anaconda.org/conda-forge/noarch/sphinx-7.2.6-pyhd8ed1ab_0.conda#bbfd1120d1824d2d073bc65935f0e4c0 +https://conda.anaconda.org/conda-forge/noarch/sphinx-7.3.7-pyhd8ed1ab_0.conda#7b1465205e28d75d2c0e1a868ee00a67 https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-serializinghtml-1.1.10-pyhd8ed1ab_0.conda#e507335cb4ca9cff4c3d0fa9cdab255e https://conda.anaconda.org/conda-forge/noarch/sphinxext-opengraph-0.9.1-pyhd8ed1ab_0.conda#286283e05a1eff606f55e7cd70f6d7f7 # pip attrs @ https://files.pythonhosted.org/packages/e0/44/827b2a91a5816512fcaf3cc4ebc465ccd5d598c45cefa6703fcf4a79018f/attrs-23.2.0-py3-none-any.whl#sha256=99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1 @@ -286,7 +286,7 @@ https://conda.anaconda.org/conda-forge/noarch/sphinxext-opengraph-0.9.1-pyhd8ed1 # pip send2trash @ https://files.pythonhosted.org/packages/40/b0/4562db6223154aa4e22f939003cb92514c79f3d4dccca3444253fd17f902/Send2Trash-1.8.3-py3-none-any.whl#sha256=0c31227e0bd08961c7665474a3d1ef7193929fedda4233843689baa056be46c9 # pip sniffio @ https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl#sha256=2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 # pip soupsieve @ https://files.pythonhosted.org/packages/4c/f3/038b302fdfbe3be7da016777069f26ceefe11a681055ea1f7817546508e3/soupsieve-2.5-py3-none-any.whl#sha256=eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7 -# pip traitlets @ https://files.pythonhosted.org/packages/7c/c4/366a09036c07f46eb8c9b2af39c97f502ef24f11f2a6e4d763655d9f2708/traitlets-5.14.2-py3-none-any.whl#sha256=fcdf85684a772ddeba87db2f398ce00b40ff550d1528c03c14dbf6a02003cd80 +# pip traitlets @ https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl#sha256=b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f # pip types-python-dateutil @ https://files.pythonhosted.org/packages/c7/1b/af4f4c4f3f7339a4b7eb3c0ab13416db98f8ac09de3399129ee5fdfa282b/types_python_dateutil-2.9.0.20240316-py3-none-any.whl#sha256=6b8cb66d960771ce5ff974e9dd45e38facb81718cc1e208b10b1baccbfdbee3b # pip uri-template @ https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl#sha256=a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363 # pip webcolors @ https://files.pythonhosted.org/packages/d5/e1/3e9013159b4cbb71df9bd7611cbf90dc2c621c8aeeb677fc41dad72f2261/webcolors-1.13-py3-none-any.whl#sha256=29bc7e8752c0a1bd4a1f03c14d6e6a72e93d82193738fa860cbff59d0fcc11bf @@ -308,7 +308,7 @@ https://conda.anaconda.org/conda-forge/noarch/sphinxext-opengraph-0.9.1-pyhd8ed1 # pip jsonschema-specifications @ https://files.pythonhosted.org/packages/ee/07/44bd408781594c4d0a027666ef27fab1e441b109dc3b76b4f836f8fd04fe/jsonschema_specifications-2023.12.1-py3-none-any.whl#sha256=87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c # pip jupyter-server-terminals @ https://files.pythonhosted.org/packages/07/2d/2b32cdbe8d2a602f697a649798554e4f072115438e92249624e532e8aca6/jupyter_server_terminals-0.5.3-py3-none-any.whl#sha256=41ee0d7dc0ebf2809c668e0fc726dfaf258fcd3e769568996ca731b6194ae9aa # pip jupyterlite-core @ https://files.pythonhosted.org/packages/05/d2/1d59d9a70d684b1eb3eb3a0b80a36b4e1d691e94af5d53aee56b1ad5240b/jupyterlite_core-0.3.0-py3-none-any.whl#sha256=247cc34ae6fedda41b15ce4778997164508b2039bc92480665cadfe955193467 -# pip pyzmq @ https://files.pythonhosted.org/packages/76/8b/6fca99e22c6316917de32b17be299dea431544209d619da16b6d9ec85c83/pyzmq-25.1.2-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl#sha256=c0b5ca88a8928147b7b1e2dfa09f3b6c256bc1135a1338536cbc9ea13d3b7add +# pip pyzmq @ https://files.pythonhosted.org/packages/2c/1f/044aafe62c85d579f87846f9cfd2cfce12a08ae72426ec92986171421d9f/pyzmq-26.0.2-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl#sha256=c40b09b7e184d6e3e1be1c8af2cc320c0f9f610d8a5df3dd866e6e6e4e32b235 # pip argon2-cffi @ https://files.pythonhosted.org/packages/a4/6a/e8a041599e78b6b3752da48000b14c8d1e8a04ded09c88c714ba047f34f5/argon2_cffi-23.1.0-py3-none-any.whl#sha256=c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea # pip jsonschema @ https://files.pythonhosted.org/packages/39/9d/b035d024c62c85f2e2d4806a59ca7b8520307f34e0932fbc8cc75fe7b2d9/jsonschema-4.21.1-py3-none-any.whl#sha256=7996507afae316306f9e2290407761157c6f78002dcf7419acb99822143d1c6f # pip jupyter-client @ https://files.pythonhosted.org/packages/75/6d/d7b55b9c1ac802ab066b3e5015e90faab1fffbbd67a2af498ffc6cc81c97/jupyter_client-8.6.1-py3-none-any.whl#sha256=3b7bd22f058434e3b9a7ea4b1500ed47de2713872288c0d511d19926f99b459f diff --git a/build_tools/circle/doc_min_dependencies_linux-64_conda.lock b/build_tools/circle/doc_min_dependencies_linux-64_conda.lock index 3bc6b95df2fe4..ac92112d663fc 100644 --- a/build_tools/circle/doc_min_dependencies_linux-64_conda.lock +++ b/build_tools/circle/doc_min_dependencies_linux-64_conda.lock @@ -9,22 +9,22 @@ https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed3 https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2#4d59c254e01d9cde7957100457e2d5fb https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_1.conda#6185f640c43843e5ad6fd1c5372c3f80 https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-2.6.32-he073ed8_17.conda#d731b543793afc0433c4fd593e693fce -https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.40-h41732ed_0.conda#7aca3059a1729aa76c597603f10b0dd3 -https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-12.3.0-h8bca6fd_105.conda#e12ce6b051085b8f27e239f5e5f5bce5 -https://conda.anaconda.org/conda-forge/noarch/libstdcxx-devel_linux-64-12.3.0-h8bca6fd_105.conda#b3c6062c84a8e172555ee104ea6a01ab -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-13.2.0-h7e041cc_5.conda#f6f6600d18a4047b54f803cf708b868a +https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.40-h55db66e_0.conda#10569984e7db886e4f1abc2b47ad79a1 +https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-12.3.0-h2af2641_106.conda#b97e137a252f112b8d5fadb313bd8ec9 +https://conda.anaconda.org/conda-forge/noarch/libstdcxx-devel_linux-64-12.3.0-h2af2641_106.conda#647bd9d44ad216d410329e659c898d8f +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-13.2.0-h95c4c6d_6.conda#3cfab3e709f77e9f1b3d380eb622494a https://conda.anaconda.org/conda-forge/linux-64/mkl-include-2024.1.0-ha957f24_692.conda#b35af3f0f25498f4e9fc4c471910346c https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.9-4_cp39.conda#bfe4b3259a8ac6cdf0037752904da6a7 https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda#161081fc7cec0bfda0d86d7cb595f8d8 https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2#f766549260d6815b0c52253f1fb1bb29 -https://conda.anaconda.org/conda-forge/linux-64/libgomp-13.2.0-h807b86a_5.conda#d211c42b9ce49aee3734fdc828731689 +https://conda.anaconda.org/conda-forge/linux-64/libgomp-13.2.0-hc881cc4_6.conda#aae89d3736661c36a5591788aebd0817 https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.12-he073ed8_17.conda#595db67e32b276298ff3d94d07d47fbf -https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.40-hf600244_0.conda#33084421a8c0af6aef1b439707f7662a +https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.40-ha885e6a_0.conda#800a4c872b5bc06fa83888d112fe6c4f https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2#fee5683a3f04bd15cbd8318b096a27ab -https://conda.anaconda.org/conda-forge/linux-64/binutils-2.40-hdd6e379_0.conda#ccc940fddbc3fcd3d79cd4c654c4b5c4 +https://conda.anaconda.org/conda-forge/linux-64/binutils-2.40-h4852527_0.conda#a05c7712be80622934f7011e0a1d43fc https://conda.anaconda.org/conda-forge/linux-64/binutils_linux-64-2.40-hdade7a5_3.conda#2d9a60578bc28469d9aeef9aea5520c3 https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_kmp_llvm.tar.bz2#562b26ba2e19059551a811e72ab7f793 -https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-h807b86a_5.conda#d4ff227c46917d3b4565302a2bbb276b +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-hc881cc4_6.conda#df88796bd09a0d2ed292e59101478ad8 https://conda.anaconda.org/conda-forge/linux-64/alsa-lib-1.2.11-hd590300_1.conda#0bb492cca54017ea314b809b1ee3a176 https://conda.anaconda.org/conda-forge/linux-64/attr-2.5.1-h166bdaf_1.tar.bz2#d9c69a24ad678ffce24c6543a0176b00 https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hd590300_5.conda#69b8b6202a07720f448be700e300ccf4 @@ -39,21 +39,21 @@ https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.20-hd590300_0.conda https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.6.2-h59595ed_0.conda#e7ba12deb7020dd080c6c70e7b6f6a3d https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.2-h7f98852_5.tar.bz2#d645c6d2ac96843a2bfaccd2d62b3ac3 https://conda.anaconda.org/conda-forge/linux-64/libgettextpo-0.22.5-h59595ed_2.conda#172bcc51059416e7ce99e7b528cede83 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-13.2.0-ha4646dd_5.conda#7a6bd7a12a4bd359e2afe6c0fa1acace +https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-13.2.0-h43f5ff8_6.conda#e54a5ddc67e673f9105cf2a2e9c070b0 https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.17-hd590300_2.conda#d66573916ffcf376178462f1b61c941e https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.0.0-hd590300_1.conda#ea25936bb4080d843790b586850f82b8 https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda#30fd6e37fe21f86f4bd26d6ee73eeec7 https://conda.anaconda.org/conda-forge/linux-64/libogg-1.3.4-h7f98852_1.tar.bz2#6e8cc2173440d77708196c5b93771680 https://conda.anaconda.org/conda-forge/linux-64/libopus-1.3.1-h7f98852_1.tar.bz2#15345e56d527b330e1cacbdf58676e8f -https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-12.3.0-h0f45ef3_5.conda#11d1ceacff40054d5a74b12975d76f20 +https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-12.3.0-h2af2641_6.conda#1cf0b420341bb1a7b7f34f6e0f4bbf2b https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda#40b61aab5c7ba9ff276c41cfffe6b80b -https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.3.2-hd590300_1.conda#049b7df8bae5e184d1de42cdf64855f8 +https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.4.0-hd590300_0.conda#b26e8aa824079e1be0294e7152ca4559 https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda#5aa797f8787fe7a17d1b0821485b5adc https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.2.13-hd590300_5.conda#f36c115f1ee199da648e0597ec2047ad https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.9.4-hcb278e6_0.conda#318b08df404f9c9be5712aaa5a6f0bb0 https://conda.anaconda.org/conda-forge/linux-64/mpg123-1.32.6-h59595ed_0.conda#9160cdeb523a1b20cf8d2a0bf821f45d https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.4.20240210-h59595ed_0.conda#97da8860a0da5413c7c98a3b3838a645 -https://conda.anaconda.org/conda-forge/linux-64/ninja-1.11.1-h924138e_0.conda#73a4953a2d9c115bdc10ff30a52f675f +https://conda.anaconda.org/conda-forge/linux-64/ninja-1.12.0-h00ab1b0_0.conda#b048701d52e7cbb5f59ddd4d3b17bbf5 https://conda.anaconda.org/conda-forge/linux-64/nspr-4.35-h27087fc_0.conda#da0ec11a6454ae19bff5b02ed881a2b1 https://conda.anaconda.org/conda-forge/linux-64/openssl-3.2.1-hd590300_1.conda#9d731343cff6ee2e5a25c4a091bf8e2a https://conda.anaconda.org/conda-forge/linux-64/pixman-0.43.2-h59595ed_0.conda#71004cbf7924e19c02746ccde9fd7123 @@ -69,18 +69,18 @@ https://conda.anaconda.org/conda-forge/linux-64/xorg-xproto-7.0.31-h7f98852_1007 https://conda.anaconda.org/conda-forge/linux-64/xz-5.2.6-h166bdaf_0.tar.bz2#2161070d867d1b1204ea749c8eec4ef0 https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h7f98852_2.tar.bz2#4cb3ad778ec2d5a7acbdf254eb1c42ae https://conda.anaconda.org/conda-forge/linux-64/expat-2.6.2-h59595ed_0.conda#53fb86322bdb89496d7579fe3f02fd61 -https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-12.3.0-he2b93b0_5.conda#e89827619e73df59496c708b94f6f3d5 +https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-12.3.0-h1562d66_6.conda#5e4e8358a4ab43498e0ac3b6776d1c94 https://conda.anaconda.org/conda-forge/linux-64/libasprintf-devel-0.22.5-h661eb56_2.conda#02e41ab5834dcdcc8590cf29d9526f50 https://conda.anaconda.org/conda-forge/linux-64/libcap-2.69-h0f662aa_0.conda#25cb5999faa414e5ccb2c1388f62d3d5 https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20191231-he28a2e2_2.tar.bz2#4d331e44109e3f0e19b4cb8f9b82f3e1 https://conda.anaconda.org/conda-forge/linux-64/libevent-2.1.12-hf998b51_1.conda#a1cfcc585f0c42bf8d5546bb1dfb668d https://conda.anaconda.org/conda-forge/linux-64/libgettextpo-devel-0.22.5-h59595ed_2.conda#b63d9b6da3653179a278077f0de20014 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran-ng-13.2.0-h69a702a_5.conda#e73e9cfd1191783392131e6238bdb3e9 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-ng-13.2.0-h69a702a_6.conda#3666a850342f8f3be88f9a93d948d027 https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.43-h2797004_0.conda#009981dd9cfcaa4dbfa25ffaed86bcae -https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.45.2-h2797004_0.conda#866983a220e27a80cb75e85cb30466a1 +https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.45.3-h2797004_0.conda#b3316cbe90249da4f8e84cd66e1cc55b https://conda.anaconda.org/conda-forge/linux-64/libvorbis-1.3.7-h9c3ff4c_0.tar.bz2#309dec04b70a3cc0f1e84a4013683bc0 https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.15-h0b41bf4_0.conda#33277193f5b92bad9fdd230eb700929c -https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.12.6-h232c23b_1.conda#6853448e9ca1cfd5f15382afd2a6d123 +https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.12.6-h232c23b_2.conda#9a3a42df8a95f65334dfc7b80da1195d https://conda.anaconda.org/conda-forge/linux-64/mysql-common-8.3.0-hf1915f5_4.conda#784a4df6676c581ca624fbe460703a6d https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.43-hcad00b1_0.conda#8292dea9e022d9610a11fce5e0896ed8 https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda#47d31b792659ce70f470b5c82fdfb7a4 @@ -89,13 +89,13 @@ https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.4-h7391055_0.cond https://conda.anaconda.org/conda-forge/linux-64/zlib-1.2.13-hd590300_5.conda#68c34ec6149623be41a1933ab996a209 https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.5-hfc55251_0.conda#04b88013080254850d6c01ed54810589 https://conda.anaconda.org/conda-forge/linux-64/freetype-2.12.1-h267a509_2.conda#9ae35c3d96db2c94ce0cef86efdfa2cb -https://conda.anaconda.org/conda-forge/linux-64/gcc-12.3.0-h95e488c_3.conda#413e326f8a01d041ffbfbb51cea46a93 +https://conda.anaconda.org/conda-forge/linux-64/gcc-12.3.0-h915e2ae_6.conda#ec683e084ea08ef94528f15d30fa1e03 https://conda.anaconda.org/conda-forge/linux-64/gcc_linux-64-12.3.0-h6477408_3.conda#7a53f84c45bdf4656ba27b9e9ed68b3d https://conda.anaconda.org/conda-forge/linux-64/gettext-0.22.5-h59595ed_2.conda#219ba82e95d7614cf7140d2a4afc0926 -https://conda.anaconda.org/conda-forge/linux-64/gfortran_impl_linux-64-12.3.0-hfcedea8_5.conda#4d72ee7c82f8a9b2ecef4fcefa9acd19 -https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-12.3.0-he2b93b0_5.conda#cddba8fd94e52012abea1caad722b9c2 +https://conda.anaconda.org/conda-forge/linux-64/gfortran_impl_linux-64-12.3.0-h6d6b2fb_6.conda#d6c441226a4bd0af4c024e8c0f4a47cf +https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-12.3.0-h1562d66_6.conda#5ad72ddd14e13d589dea2afe6e626619 https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.2-h659d440_0.conda#cd95826dbd331ed1be26bdf401432844 -https://conda.anaconda.org/conda-forge/linux-64/libglib-2.80.0-hf2295e7_4.conda#0269d2b7fa89f4a37cdee5ad6161f6cc +https://conda.anaconda.org/conda-forge/linux-64/libglib-2.80.0-hf2295e7_6.conda#9342e7c44c38bea649490f72d92c382d https://conda.anaconda.org/conda-forge/linux-64/libhwloc-2.10.0-default_h2fb2949_1000.conda#7e3726e647a619c6ce5939014dfde86d https://conda.anaconda.org/conda-forge/linux-64/libllvm15-15.0.7-hb3ce162_4.conda#8a35df3cbc0c8b12cc8af9473ae75eef https://conda.anaconda.org/conda-forge/linux-64/libllvm18-18.1.3-h2448989_0.conda#927b6d6e80b2c0d4405a58b61ca248a3 @@ -125,12 +125,12 @@ https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.0-pyhd8ed1ab_2. https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_0.conda#15dda3cdbf330abfe9f555d22f66db46 https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.14.2-h14ed4e7_0.conda#0f69b688f52ff6da70bccb7ff7001d1d https://conda.anaconda.org/conda-forge/noarch/fsspec-2024.3.1-pyhca7485f_0.conda#b7f0662ef2c9d4404f0af9eef5ed2fde -https://conda.anaconda.org/conda-forge/linux-64/gfortran-12.3.0-h7389182_3.conda#6b0b27394cf439d0540f949190556860 +https://conda.anaconda.org/conda-forge/linux-64/gfortran-12.3.0-h915e2ae_6.conda#84b517f4f53e56256dbd65133aae04ac https://conda.anaconda.org/conda-forge/linux-64/gfortran_linux-64-12.3.0-h617cb40_3.conda#3a9e5b8a6f651ff14e74d896d8f04ab6 -https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.80.0-hde27a5a_4.conda#c9deba4959ea5b5f72f1e3e4a71ae014 -https://conda.anaconda.org/conda-forge/linux-64/gxx-12.3.0-h95e488c_3.conda#8c50a4d15a8d4812af563a684d598910 +https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.80.0-hde27a5a_6.conda#a9d23c02485c5cf055f9ac90eb9c9c63 +https://conda.anaconda.org/conda-forge/linux-64/gxx-12.3.0-h915e2ae_6.conda#0d977804df65082e17c860600ca2894b https://conda.anaconda.org/conda-forge/linux-64/gxx_linux-64-12.3.0-h4a1b8e8_3.conda#9ec22c7c544f4a4f6d660f0a3b0fd15c -https://conda.anaconda.org/conda-forge/noarch/idna-3.6-pyhd8ed1ab_0.conda#1a76f09108576397c41c0b0c5bd84134 +https://conda.anaconda.org/conda-forge/noarch/idna-3.7-pyhd8ed1ab_0.conda#c0cc1420498b17414d8617d0b9f506ca https://conda.anaconda.org/conda-forge/noarch/imagesize-1.4.1-pyhd8ed1ab_0.tar.bz2#7de5386c8fea29e76b303f37dde4c352 https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_0.conda#f800d2da156d08e289b14e87e43c1ae5 https://conda.anaconda.org/conda-forge/linux-64/kiwisolver-1.4.5-py39h7633fee_1.conda#c9f74d717e5a2847a9f8b779c54130f2 @@ -147,7 +147,7 @@ https://conda.anaconda.org/conda-forge/noarch/networkx-3.2-pyhd8ed1ab_0.conda#ce https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.2-h488ebb8_0.conda#7f2e286780f072ed750df46dc2631138 https://conda.anaconda.org/conda-forge/noarch/packaging-24.0-pyhd8ed1ab_0.conda#248f521b64ce055e7feae3105e7abeb8 https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.2.0-pyhd8ed1ab_0.conda#a0bc3eec34b0fab84be6b2da94e98e20 -https://conda.anaconda.org/conda-forge/noarch/pluggy-1.4.0-pyhd8ed1ab_0.conda#139e9feb65187e916162917bb2484976 +https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_0.conda#d3483c8fc2dc2cc3f5cf43e26d60cabf https://conda.anaconda.org/conda-forge/noarch/ply-3.11-pyhd8ed1ab_2.conda#18c6deb6f9602e32446398203c8f0e91 https://conda.anaconda.org/conda-forge/linux-64/psutil-5.9.8-py39hd1e30aa_0.conda#ec86403fde8793ac1c36f8afa3d15902 https://conda.anaconda.org/conda-forge/noarch/pygments-2.17.2-pyhd8ed1ab_0.conda#140a7f159396547e9799aa98f9f0742e @@ -178,7 +178,7 @@ https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.0-h3faef2a_0.conda#f9 https://conda.anaconda.org/conda-forge/linux-64/cxx-compiler-1.7.0-h00ab1b0_0.conda#b4537c98cb59f8725b0e1e65816b4a28 https://conda.anaconda.org/conda-forge/linux-64/cytoolz-0.12.3-py39hd1e30aa_0.conda#dc0fb8e157c7caba4c98f1e1f9d2e5f4 https://conda.anaconda.org/conda-forge/linux-64/fortran-compiler-1.7.0-heb67821_0.conda#7ef7c0f111dad1c8006504a0f1ccd820 -https://conda.anaconda.org/conda-forge/linux-64/glib-2.80.0-hf2295e7_4.conda#5521382ee30b96b35eb0037fc3c4f2b4 +https://conda.anaconda.org/conda-forge/linux-64/glib-2.80.0-hf2295e7_6.conda#a1e026a82a562b443845db5614ca568a https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-7.1.0-pyha770c72_0.conda#0896606848b2dc5cebdf111b6543aa04 https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.3-pyhd8ed1ab_0.conda#e7d8df6509ba635247ff9aea31134262 https://conda.anaconda.org/conda-forge/noarch/joblib-1.4.0-pyhd8ed1ab_0.conda#e0ed1bf13ce3a440e022157bf4764465 @@ -192,7 +192,7 @@ https://conda.anaconda.org/conda-forge/noarch/partd-1.4.1-pyhd8ed1ab_0.conda#acf https://conda.anaconda.org/conda-forge/linux-64/pillow-10.3.0-py39h90c7501_0.conda#1e3b6af9592be71ce19f0a6aae05d97b https://conda.anaconda.org/conda-forge/noarch/pip-24.0-pyhd8ed1ab_0.conda#f586ac1e56c8638b64f9c8122a7b8a67 https://conda.anaconda.org/conda-forge/noarch/plotly-5.14.0-pyhd8ed1ab_0.conda#6a7bcc42ef58dd6cf3da9333ea102433 -https://conda.anaconda.org/conda-forge/noarch/pyproject-metadata-0.7.1-pyhd8ed1ab_0.conda#dcb27826ffc94d5f04e241322239983b +https://conda.anaconda.org/conda-forge/noarch/pyproject-metadata-0.8.0-pyhd8ed1ab_0.conda#573fe09d7bd0cd4bcc210d8369b5ca47 https://conda.anaconda.org/conda-forge/noarch/pytest-7.4.4-pyhd8ed1ab_0.conda#a9d145de8c5f064b5fa68fb34725d9f4 https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0-pyhd8ed1ab_0.conda#2cf4264fffb9e6eff6031c5b6884d61c https://conda.anaconda.org/conda-forge/linux-64/sip-6.7.12-py39h3d6467e_0.conda#e667a3ab0df62c54e60e1843d2e6defb @@ -203,12 +203,12 @@ https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-8.3.0-h3d44ed6_0.conda# https://conda.anaconda.org/conda-forge/noarch/importlib_metadata-7.1.0-hd8ed1ab_0.conda#6ef2b72d291b39e479d7694efa2b2b98 https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-22_linux64_mkl.conda#eb6deb4ba6f92ea3f31c09cb8b764738 https://conda.anaconda.org/conda-forge/linux-64/libsystemd0-255-h3516f8a_1.conda#3366af27f0b593544a6cd453c7932ac5 -https://conda.anaconda.org/conda-forge/noarch/meson-python-0.15.0-pyh0c530f3_0.conda#3bc64565ca78ce3bb80248d09926d8f9 +https://conda.anaconda.org/conda-forge/noarch/meson-python-0.16.0-pyh0c530f3_0.conda#e16f0dbf502da873be9f9adb0dc52547 https://conda.anaconda.org/conda-forge/linux-64/mkl-devel-2024.1.0-ha770c72_692.conda#56142862a71bcfdd6ef2ce95c8e90755 https://conda.anaconda.org/conda-forge/linux-64/pyqt5-sip-12.12.2-py39h3d6467e_5.conda#93aff412f3e49fdb43361c0215cbd72d https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.5.0-pyhd8ed1ab_0.conda#d5f595da2daead898ca958ac62f0307b https://conda.anaconda.org/conda-forge/noarch/requests-2.31.0-pyhd8ed1ab_0.conda#a30144e4156cdbb236f99ebb49828f8b -https://conda.anaconda.org/conda-forge/noarch/dask-core-2024.4.1-pyhd8ed1ab_0.conda#52387f00fee8dcd5cf75f8886025293f +https://conda.anaconda.org/conda-forge/noarch/dask-core-2024.4.2-pyhd8ed1ab_0.conda#bb4e6c52855aa64a5443ca4eedaa6cfe https://conda.anaconda.org/conda-forge/linux-64/gst-plugins-base-1.24.1-hfa15dee_1.conda#a6dd2bbc684913e2bef0a54ce56fcbfb https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-22_linux64_mkl.conda#d6f942423116553f068b2f2d93ffea2e https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-22_linux64_mkl.conda#4edf2e7ce63920e4f539d12e32fb478e @@ -219,7 +219,7 @@ https://conda.anaconda.org/conda-forge/linux-64/numpy-1.19.5-py39hd249d9e_3.tar. https://conda.anaconda.org/conda-forge/linux-64/qt-main-5.15.8-hc9dc06e_21.conda#b325046180590c868ce0dbf267b82eb8 https://conda.anaconda.org/conda-forge/linux-64/blas-devel-3.9.0-22_linux64_mkl.conda#3cb0e51433c88d2f4cdfb50c5c08a683 https://conda.anaconda.org/conda-forge/linux-64/imagecodecs-lite-2019.12.3-py39hd257fcd_5.tar.bz2#32dba66d6abc2b4b5b019c9e54307312 -https://conda.anaconda.org/conda-forge/noarch/imageio-2.34.0-pyh4b66e23_0.conda#b8853659d596f967c661f544dd89ede7 +https://conda.anaconda.org/conda-forge/noarch/imageio-2.34.1-pyh4b66e23_0.conda#bcf6a6f4c6889ca083e8d33afbafb8d5 https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.3.4-py39h2fa2bec_0.tar.bz2#9ec0b2186fab9121c54f4844f93ee5b7 https://conda.anaconda.org/conda-forge/linux-64/pandas-1.1.5-py39hde0f152_0.tar.bz2#79fc4b5b3a865b90dd3701cecf1ad33c https://conda.anaconda.org/conda-forge/noarch/patsy-0.5.6-pyhd8ed1ab_0.conda#a5b55d1cb110cdcedc748b5c3e16e687 From 0c72e5448b9da3b0d9202aa5a2abbd9bff4c238a Mon Sep 17 00:00:00 2001 From: scikit-learn-bot Date: Mon, 22 Apr 2024 10:39:23 +0200 Subject: [PATCH 047/344] :lock: :robot: CI Update lock files for cirrus-arm CI build(s) :lock: :robot: (#28870) Co-authored-by: Lock file bot --- .../pymin_conda_forge_linux-aarch64_conda.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/build_tools/cirrus/pymin_conda_forge_linux-aarch64_conda.lock b/build_tools/cirrus/pymin_conda_forge_linux-aarch64_conda.lock index 62e2d83af5c45..d9fa69b319d28 100644 --- a/build_tools/cirrus/pymin_conda_forge_linux-aarch64_conda.lock +++ b/build_tools/cirrus/pymin_conda_forge_linux-aarch64_conda.lock @@ -3,7 +3,7 @@ # input_hash: 80459c6003cbcd22780a22a62ed5cc116e951d5c2c14602af1281434263b9138 @EXPLICIT https://conda.anaconda.org/conda-forge/linux-aarch64/ca-certificates-2024.2.2-hcefe29a_0.conda#57c226edb90c4e973b9b7503537dd339 -https://conda.anaconda.org/conda-forge/linux-aarch64/ld_impl_linux-aarch64-2.40-h2d8c526_0.conda#16246d69e945d0b1969a6099e7c5d457 +https://conda.anaconda.org/conda-forge/linux-aarch64/ld_impl_linux-aarch64-2.40-hba4e955_0.conda#b55c1cb33c63d23b542fa53f24541e56 https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-ng-13.2.0-h9a76618_5.conda#1b79d37dce0fad96bdf3de03925f43b4 https://conda.anaconda.org/conda-forge/linux-aarch64/python_abi-3.9-4_cp39.conda#c191905a08694e4a5cb1238e90233878 https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda#161081fc7cec0bfda0d86d7cb595f8d8 @@ -18,11 +18,11 @@ https://conda.anaconda.org/conda-forge/linux-aarch64/libgfortran5-13.2.0-h582850 https://conda.anaconda.org/conda-forge/linux-aarch64/libjpeg-turbo-3.0.0-h31becfc_1.conda#ed24e702928be089d9ba3f05618515c6 https://conda.anaconda.org/conda-forge/linux-aarch64/libnsl-2.0.1-h31becfc_0.conda#c14f32510f694e3185704d89967ec422 https://conda.anaconda.org/conda-forge/linux-aarch64/libuuid-2.38.1-hb4cce97_0.conda#000e30b09db0b7c775b21695dff30969 -https://conda.anaconda.org/conda-forge/linux-aarch64/libwebp-base-1.3.2-h31becfc_1.conda#675c1f4aa320704b899f4eb350a69418 +https://conda.anaconda.org/conda-forge/linux-aarch64/libwebp-base-1.4.0-h31becfc_0.conda#5fd7ab3e5f382c70607fbac6335e6e19 https://conda.anaconda.org/conda-forge/linux-aarch64/libxcrypt-4.4.36-h31becfc_1.conda#b4df5d7d4b63579d081fd3a4cf99740e https://conda.anaconda.org/conda-forge/linux-aarch64/libzlib-1.2.13-h31becfc_5.conda#b213aa87eea9491ef7b129179322e955 https://conda.anaconda.org/conda-forge/linux-aarch64/ncurses-6.4.20240210-h0425590_0.conda#c1a1612ddaee95c83abfa0b2ec858626 -https://conda.anaconda.org/conda-forge/linux-aarch64/ninja-1.11.1-hdd96247_0.conda#58f4c67113cda9171e3c03d3e62731e1 +https://conda.anaconda.org/conda-forge/linux-aarch64/ninja-1.12.0-h2a328a1_0.conda#c0f3f508baf69c8db8142466beaa0ccc https://conda.anaconda.org/conda-forge/linux-aarch64/openssl-3.2.1-h31becfc_1.conda#e95eb18d256edc72058e0dc9be5338a0 https://conda.anaconda.org/conda-forge/linux-aarch64/pthread-stubs-0.4-hb9de7d4_1001.tar.bz2#d0183ec6ce0b5aaa3486df25fa5f0ded https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxau-1.0.11-h31becfc_0.conda#13de34f69cb73165dbe08c1e9148bedb @@ -32,7 +32,7 @@ https://conda.anaconda.org/conda-forge/linux-aarch64/libbrotlidec-1.1.0-h31becfc https://conda.anaconda.org/conda-forge/linux-aarch64/libbrotlienc-1.1.0-h31becfc_1.conda#ad3d3a826b5848d99936e4466ebbaa26 https://conda.anaconda.org/conda-forge/linux-aarch64/libgfortran-ng-13.2.0-he9431aa_5.conda#fab7c6a8c84492e18cbe578820e97a56 https://conda.anaconda.org/conda-forge/linux-aarch64/libpng-1.6.43-h194ca79_0.conda#1123e504d9254dd9494267ab9aba95f0 -https://conda.anaconda.org/conda-forge/linux-aarch64/libsqlite-3.45.2-h194ca79_0.conda#bf4c96a21fbfc6a6ef6a7781a534a4e0 +https://conda.anaconda.org/conda-forge/linux-aarch64/libsqlite-3.45.3-h194ca79_0.conda#fb35b8afbe9e92467ac7b5608d60b775 https://conda.anaconda.org/conda-forge/linux-aarch64/libxcb-1.15-h2a766a3_0.conda#eb3d8c8170e3d03f2564ed2024aa00c8 https://conda.anaconda.org/conda-forge/linux-aarch64/readline-8.2-h8fc344f_1.conda#105eb1e16bf83bfb2eb380a48032b655 https://conda.anaconda.org/conda-forge/linux-aarch64/tk-8.6.13-h194ca79_0.conda#f75105e0585851f818e0009dd1dde4dc @@ -60,7 +60,7 @@ https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyh9f0ad1d_0.tar.bz2 https://conda.anaconda.org/conda-forge/linux-aarch64/openblas-0.3.27-pthreads_h339cbfa_0.conda#cb06c34a3056f59e9e244c20836add8a https://conda.anaconda.org/conda-forge/linux-aarch64/openjpeg-2.5.2-h0d9d63b_0.conda#fd2898519e839d5ceb778343f39a3176 https://conda.anaconda.org/conda-forge/noarch/packaging-24.0-pyhd8ed1ab_0.conda#248f521b64ce055e7feae3105e7abeb8 -https://conda.anaconda.org/conda-forge/noarch/pluggy-1.4.0-pyhd8ed1ab_0.conda#139e9feb65187e916162917bb2484976 +https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_0.conda#d3483c8fc2dc2cc3f5cf43e26d60cabf https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.1.2-pyhd8ed1ab_0.conda#b9a4dacf97241704529131a0dfc0494f https://conda.anaconda.org/conda-forge/noarch/setuptools-69.5.1-pyhd8ed1ab_0.conda#7462280d81f639363e6e63c81276bd9e https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2#e5f25f8dbc060e9a8d912e432202afc2 @@ -78,12 +78,12 @@ https://conda.anaconda.org/conda-forge/linux-aarch64/liblapack-3.9.0-22_linuxaar https://conda.anaconda.org/conda-forge/noarch/meson-1.4.0-pyhd8ed1ab_0.conda#52a0660cfa40b45bf254ecc3374cb2e0 https://conda.anaconda.org/conda-forge/linux-aarch64/pillow-10.3.0-py39h71661b1_0.conda#dae548b7b537d7ef796d1d4c38a55319 https://conda.anaconda.org/conda-forge/noarch/pip-24.0-pyhd8ed1ab_0.conda#f586ac1e56c8638b64f9c8122a7b8a67 -https://conda.anaconda.org/conda-forge/noarch/pyproject-metadata-0.7.1-pyhd8ed1ab_0.conda#dcb27826ffc94d5f04e241322239983b +https://conda.anaconda.org/conda-forge/noarch/pyproject-metadata-0.8.0-pyhd8ed1ab_0.conda#573fe09d7bd0cd4bcc210d8369b5ca47 https://conda.anaconda.org/conda-forge/noarch/pytest-7.4.4-pyhd8ed1ab_0.conda#a9d145de8c5f064b5fa68fb34725d9f4 https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0-pyhd8ed1ab_0.conda#2cf4264fffb9e6eff6031c5b6884d61c https://conda.anaconda.org/conda-forge/noarch/importlib-resources-6.4.0-pyhd8ed1ab_0.conda#dcbadab7a68738a028e195ab68ab2d2e https://conda.anaconda.org/conda-forge/linux-aarch64/liblapacke-3.9.0-22_linuxaarch64_openblas.conda#5acf669e0be669f30f4b813d2ecda7b8 -https://conda.anaconda.org/conda-forge/noarch/meson-python-0.15.0-pyh0c530f3_0.conda#3bc64565ca78ce3bb80248d09926d8f9 +https://conda.anaconda.org/conda-forge/noarch/meson-python-0.16.0-pyh0c530f3_0.conda#e16f0dbf502da873be9f9adb0dc52547 https://conda.anaconda.org/conda-forge/linux-aarch64/numpy-1.26.4-py39h91c28bb_0.conda#d88e195f11a9f27e649aea408b54cb48 https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.5.0-pyhd8ed1ab_0.conda#d5f595da2daead898ca958ac62f0307b https://conda.anaconda.org/conda-forge/linux-aarch64/blas-devel-3.9.0-22_linuxaarch64_openblas.conda#a5b77b6c6807661afd716f33e85814b3 From 1210b0685b4b321ecee34a9a0fb449dd9d9bafb1 Mon Sep 17 00:00:00 2001 From: Randolf Scholz Date: Mon, 22 Apr 2024 11:07:34 +0200 Subject: [PATCH 048/344] FIX parametrizing estimator checks allows custom estimators that implements `__call__` (#28860) Co-authored-by: Adrin Jalali Co-authored-by: jeremiedbb --- sklearn/tests/test_common.py | 12 ++++++++++++ sklearn/utils/estimator_checks.py | 10 ++++------ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/sklearn/tests/test_common.py b/sklearn/tests/test_common.py index 4564eddee410c..9ff83953f4b0e 100644 --- a/sklearn/tests/test_common.py +++ b/sklearn/tests/test_common.py @@ -20,6 +20,7 @@ import pytest import sklearn +from sklearn.base import BaseEstimator from sklearn.cluster import ( OPTICS, AffinityPropagation, @@ -103,6 +104,16 @@ def _sample_func(x, y=1): pass +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 + + @pytest.mark.parametrize( "val, expected", [ @@ -122,6 +133,7 @@ def _sample_func(x, y=1): "solver='newton-cg',warm_start=True)" ), ), + (CallableEstimator(), "CallableEstimator()"), ], ) def test_get_check_estimator_ids(val, expected): diff --git a/sklearn/utils/estimator_checks.py b/sklearn/utils/estimator_checks.py index fb60f206bea66..daacac9398f96 100644 --- a/sklearn/utils/estimator_checks.py +++ b/sklearn/utils/estimator_checks.py @@ -9,7 +9,7 @@ from contextlib import nullcontext from copy import deepcopy from functools import partial, wraps -from inspect import signature +from inspect import isfunction, signature from numbers import Integral, Real import joblib @@ -405,13 +405,11 @@ def _get_check_estimator_ids(obj): -------- check_estimator """ - if callable(obj): - if not isinstance(obj, partial): - return obj.__name__ - + 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"): From 70ca21f106b603b611da73012c9ade7cd8e438b8 Mon Sep 17 00:00:00 2001 From: Olivier Grisel Date: Mon, 22 Apr 2024 15:10:46 +0200 Subject: [PATCH 049/344] FIX remove the computed stop_words_ attribute of text vectorizer (#28823) --- doc/whats_new/v1.5.rst | 18 ++++++++ sklearn/feature_extraction/tests/test_text.py | 42 ------------------- sklearn/feature_extraction/text.py | 36 +--------------- 3 files changed, 20 insertions(+), 76 deletions(-) diff --git a/doc/whats_new/v1.5.rst b/doc/whats_new/v1.5.rst index 24893cdebc7cc..974ecc4bf3479 100644 --- a/doc/whats_new/v1.5.rst +++ b/doc/whats_new/v1.5.rst @@ -22,6 +22,24 @@ Version 1.5.0 **In Development** +Security +-------- + +- |Fix| :class:`feature_extraction.text.CountVectorizer` and + :class:`feature_extraction.text.TfidfVectorizer` no longer store discarded + tokens from the training set in their `stop_words_` attribute. This attribute + would hold too frequent (above `max_df`) but also too rare tokens (below + `min_df`). This fixes a potential security issue (data leak) if the discarded + rare tokens hold sensitive information from the training set without the + model developer's knowledge. + + Note: users of those classes are encouraged to either retrain their pipelines + with the new scikit-learn version or to manually clear the `stop_words_` + attribute from previously trained instances of those transformers. This + attribute was designed only for model inspection purposes and has no impact + on the behavior of the transformers. + :pr:`28823` by :user:`Olivier Grisel `. + Changed models -------------- diff --git a/sklearn/feature_extraction/tests/test_text.py b/sklearn/feature_extraction/tests/test_text.py index a513b94436d93..6b14d0dd8f271 100644 --- a/sklearn/feature_extraction/tests/test_text.py +++ b/sklearn/feature_extraction/tests/test_text.py @@ -756,21 +756,11 @@ def test_feature_names(): @pytest.mark.parametrize("Vectorizer", (CountVectorizer, TfidfVectorizer)) def test_vectorizer_max_features(Vectorizer): expected_vocabulary = {"burger", "beer", "salad", "pizza"} - expected_stop_words = { - "celeri", - "tomato", - "copyright", - "coke", - "sparkling", - "water", - "the", - } # 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 - assert vectorizer.stop_words_ == expected_stop_words def test_count_vectorizer_max_features(): @@ -805,21 +795,16 @@ def test_vectorizer_max_df(): vect.fit(test_data) assert "a" in vect.vocabulary_.keys() assert len(vect.vocabulary_.keys()) == 6 - assert len(vect.stop_words_) == 0 vect.max_df = 0.5 # 0.5 * 3 documents -> max_doc_count == 1.5 vect.fit(test_data) assert "a" not in vect.vocabulary_.keys() # {ae} ignored assert len(vect.vocabulary_.keys()) == 4 # {bcdt} remain - assert "a" in vect.stop_words_ - assert len(vect.stop_words_) == 2 vect.max_df = 1 vect.fit(test_data) assert "a" not in vect.vocabulary_.keys() # {ae} ignored assert len(vect.vocabulary_.keys()) == 4 # {bcdt} remain - assert "a" in vect.stop_words_ - assert len(vect.stop_words_) == 2 def test_vectorizer_min_df(): @@ -828,21 +813,16 @@ def test_vectorizer_min_df(): vect.fit(test_data) assert "a" in vect.vocabulary_.keys() assert len(vect.vocabulary_.keys()) == 6 - assert len(vect.stop_words_) == 0 vect.min_df = 2 vect.fit(test_data) assert "c" not in vect.vocabulary_.keys() # {bcdt} ignored assert len(vect.vocabulary_.keys()) == 2 # {ae} remain - assert "c" in vect.stop_words_ - assert len(vect.stop_words_) == 4 vect.min_df = 0.8 # 0.8 * 3 documents -> min_doc_count == 2.4 vect.fit(test_data) assert "c" not in vect.vocabulary_.keys() # {bcdet} ignored assert len(vect.vocabulary_.keys()) == 1 # {a} remains - assert "c" in vect.stop_words_ - assert len(vect.stop_words_) == 5 def test_count_binary_occurrences(): @@ -1155,28 +1135,6 @@ def test_countvectorizer_vocab_dicts_when_pickling(): ) -def test_stop_words_removal(): - # Ensure that deleting the stop_words_ attribute doesn't affect transform - - fitted_vectorizers = ( - TfidfVectorizer().fit(JUNK_FOOD_DOCS), - CountVectorizer(preprocessor=strip_tags).fit(JUNK_FOOD_DOCS), - CountVectorizer(strip_accents=strip_eacute).fit(JUNK_FOOD_DOCS), - ) - - for vect in fitted_vectorizers: - vect_transform = vect.transform(JUNK_FOOD_DOCS).toarray() - - vect.stop_words_ = None - stop_None_transform = vect.transform(JUNK_FOOD_DOCS).toarray() - - delattr(vect, "stop_words_") - stop_del_transform = vect.transform(JUNK_FOOD_DOCS).toarray() - - assert_array_equal(stop_None_transform, vect_transform) - assert_array_equal(stop_del_transform, vect_transform) - - def test_pickling_transformer(): X = CountVectorizer().fit_transform(JUNK_FOOD_DOCS) orig = TfidfTransformer().fit(X) diff --git a/sklearn/feature_extraction/text.py b/sklearn/feature_extraction/text.py index c8cf37ac322e9..756a5222c437d 100644 --- a/sklearn/feature_extraction/text.py +++ b/sklearn/feature_extraction/text.py @@ -1079,15 +1079,6 @@ class CountVectorizer(_VectorizerMixin, BaseEstimator): True if a fixed vocabulary of term to indices mapping is provided by the user. - stop_words_ : set - Terms that were ignored because they either: - - - occurred in too many documents (`max_df`) - - occurred in too few documents (`min_df`) - - were cut off by feature selection (`max_features`). - - This is only available if no vocabulary was given. - See Also -------- HashingVectorizer : Convert a collection of text documents to a @@ -1096,12 +1087,6 @@ class CountVectorizer(_VectorizerMixin, BaseEstimator): TfidfVectorizer : Convert a collection of raw documents to a matrix of TF-IDF features. - Notes - ----- - The ``stop_words_`` attribute can get large and increase the model size - when pickling. This attribute is provided only for introspection and can - be safely removed using delattr or set to None before pickling. - Examples -------- >>> from sklearn.feature_extraction.text import CountVectorizer @@ -1240,19 +1225,17 @@ def _limit_features(self, X, vocabulary, high=None, low=None, limit=None): mask = new_mask new_indices = np.cumsum(mask) - 1 # maps old indices to new - removed_terms = set() for term, old_index in list(vocabulary.items()): if mask[old_index]: vocabulary[term] = new_indices[old_index] else: del vocabulary[term] - removed_terms.add(term) kept_indices = np.where(mask)[0] if len(kept_indices) == 0: raise ValueError( "After pruning, no terms remain. Try a lower min_df or a higher max_df." ) - return X[:, kept_indices], removed_terms + return X[:, kept_indices] def _count_vocab(self, raw_documents, fixed_vocab): """Create sparse feature matrix, and vocabulary where fixed_vocab=False""" @@ -1397,7 +1380,7 @@ def fit_transform(self, raw_documents, y=None): raise ValueError("max_df corresponds to < documents than min_df") if max_features is not None: X = self._sort_features(X, vocabulary) - X, self.stop_words_ = self._limit_features( + X = self._limit_features( X, vocabulary, max_doc_count, min_doc_count, max_features ) if max_features is None: @@ -1911,15 +1894,6 @@ class TfidfVectorizer(CountVectorizer): The inverse document frequency (IDF) vector; only defined if ``use_idf`` is True. - stop_words_ : set - Terms that were ignored because they either: - - - occurred in too many documents (`max_df`) - - occurred in too few documents (`min_df`) - - were cut off by feature selection (`max_features`). - - This is only available if no vocabulary was given. - See Also -------- CountVectorizer : Transforms text into a sparse matrix of n-gram counts. @@ -1927,12 +1901,6 @@ class TfidfVectorizer(CountVectorizer): TfidfTransformer : Performs the TF-IDF transformation from a provided matrix of counts. - Notes - ----- - The ``stop_words_`` attribute can get large and increase the model size - when pickling. This attribute is provided only for introspection and can - be safely removed using delattr or set to None before pickling. - Examples -------- >>> from sklearn.feature_extraction.text import TfidfVectorizer From 79e14a77565e28fc716cd292a80f35a92efa1ca4 Mon Sep 17 00:00:00 2001 From: Priyash Shah <89684873+plon-Susk7@users.noreply.github.com> Date: Mon, 22 Apr 2024 18:49:41 +0530 Subject: [PATCH 050/344] DOC Enhanced example visualization to RFE (#28862) --- examples/feature_selection/plot_rfe_digits.py | 36 ++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/examples/feature_selection/plot_rfe_digits.py b/examples/feature_selection/plot_rfe_digits.py index 553f38f9c674f..198a3d6f3af90 100644 --- a/examples/feature_selection/plot_rfe_digits.py +++ b/examples/feature_selection/plot_rfe_digits.py @@ -3,8 +3,14 @@ Recursive feature elimination ============================= -A recursive feature elimination example showing the relevance of pixels in -a digit classification task. +This example demonstrates how Recursive Feature Elimination +(:class:`~sklearn.feature_selection.RFE`) can be used to determine the +importance of individual pixels for classifying handwritten digits. +:class:`~sklearn.feature_selection.RFE` recursively removes the least +significant features, assigning ranks based on their importance, where higher +`ranking_` values denote lower importance. The ranking is visualized using both +shades of blue and pixel annotations for clarity. As expected, pixels positioned +at the center of the image tend to be more predictive than those near the edges. .. note:: @@ -16,21 +22,33 @@ from sklearn.datasets import load_digits from sklearn.feature_selection import RFE -from sklearn.svm import SVC +from sklearn.linear_model import LogisticRegression +from sklearn.pipeline import Pipeline +from sklearn.preprocessing import MinMaxScaler # Load the digits dataset digits = load_digits() X = digits.images.reshape((len(digits.images), -1)) y = digits.target -# Create the RFE object and rank each pixel -svc = SVC(kernel="linear", C=1) -rfe = RFE(estimator=svc, n_features_to_select=1, step=1) -rfe.fit(X, y) -ranking = rfe.ranking_.reshape(digits.images[0].shape) +pipe = Pipeline( + [ + ("scaler", MinMaxScaler()), + ("rfe", RFE(estimator=LogisticRegression(), n_features_to_select=1, step=1)), + ] +) + +pipe.fit(X, y) +ranking = pipe.named_steps["rfe"].ranking_.reshape(digits.images[0].shape) # Plot pixel ranking plt.matshow(ranking, cmap=plt.cm.Blues) + +# Add annotations for pixel numbers +for i in range(ranking.shape[0]): + for j in range(ranking.shape[1]): + plt.text(j, i, str(ranking[i, j]), ha="center", va="center", color="black") + plt.colorbar() -plt.title("Ranking of pixels with RFE") +plt.title("Ranking of pixels with RFE\n(Logistic Regression)") plt.show() From 153e7969b0d89646a8139f709a792298ef506ae5 Mon Sep 17 00:00:00 2001 From: Xiao Yuan Date: Tue, 23 Apr 2024 18:56:32 +0800 Subject: [PATCH 051/344] DOC add "polars" to options of transform in `set_output` (#28875) --- sklearn/compose/_column_transformer.py | 2 +- sklearn/pipeline.py | 5 +++-- sklearn/preprocessing/_function_transformer.py | 2 +- sklearn/utils/_set_output.py | 4 ++-- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/sklearn/compose/_column_transformer.py b/sklearn/compose/_column_transformer.py index 69c6effc835b2..364cdbe932197 100644 --- a/sklearn/compose/_column_transformer.py +++ b/sklearn/compose/_column_transformer.py @@ -314,7 +314,7 @@ def set_output(self, *, transform=None): Parameters ---------- - transform : {"default", "pandas"}, default=None + transform : {"default", "pandas", "polars"}, default=None Configure output of `transform` and `fit_transform`. - `"default"`: Default output format of a transformer diff --git a/sklearn/pipeline.py b/sklearn/pipeline.py index 020491eb413fe..93f9ef09fc40a 100644 --- a/sklearn/pipeline.py +++ b/sklearn/pipeline.py @@ -178,7 +178,7 @@ def set_output(self, *, transform=None): Parameters ---------- - transform : {"default", "pandas"}, default=None + transform : {"default", "pandas", "polars"}, default=None Configure output of `transform` and `fit_transform`. - `"default"`: Default output format of a transformer @@ -1463,11 +1463,12 @@ def set_output(self, *, transform=None): Parameters ---------- - transform : {"default", "pandas"}, default=None + 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 Returns diff --git a/sklearn/preprocessing/_function_transformer.py b/sklearn/preprocessing/_function_transformer.py index 0442e75346fed..c49684d0ebfbc 100644 --- a/sklearn/preprocessing/_function_transformer.py +++ b/sklearn/preprocessing/_function_transformer.py @@ -393,7 +393,7 @@ def set_output(self, *, transform=None): Parameters ---------- - transform : {"default", "pandas"}, default=None + transform : {"default", "pandas", "polars"}, default=None Configure output of `transform` and `fit_transform`. - `"default"`: Default output format of a transformer diff --git a/sklearn/utils/_set_output.py b/sklearn/utils/_set_output.py index d5c23a4c7c6f9..42757dbb00fae 100644 --- a/sklearn/utils/_set_output.py +++ b/sklearn/utils/_set_output.py @@ -392,7 +392,7 @@ def set_output(self, *, transform=None): Parameters ---------- - transform : {"default", "pandas"}, default=None + transform : {"default", "pandas", "polars"}, default=None Configure output of `transform` and `fit_transform`. - `"default"`: Default output format of a transformer @@ -428,7 +428,7 @@ def _safe_set_output(estimator, *, transform=None): estimator : estimator instance Estimator instance. - transform : {"default", "pandas"}, default=None + transform : {"default", "pandas", "polars"}, default=None Configure output of the following estimator's methods: - `"transform"` From 78675d16b53651b53259704ff4c11c5cca1165d4 Mon Sep 17 00:00:00 2001 From: Stefanie Senger <91849487+StefanieSenger@users.noreply.github.com> Date: Tue, 23 Apr 2024 13:50:28 +0200 Subject: [PATCH 052/344] FIX `RecursionError` bug with metadata routing in metaestimators with scoring (#28712) LGTM. Thanks @StefanieSenger , @adrinjalali --- doc/whats_new/v1.5.rst | 4 ++ sklearn/linear_model/_ridge.py | 15 +++--- sklearn/linear_model/tests/test_ridge.py | 17 +++++++ sklearn/metrics/_scorer.py | 52 +++++++++++++++++---- sklearn/metrics/tests/test_score_objects.py | 43 +++++++++++------ 5 files changed, 100 insertions(+), 31 deletions(-) diff --git a/doc/whats_new/v1.5.rst b/doc/whats_new/v1.5.rst index 974ecc4bf3479..e60846cb33a7e 100644 --- a/doc/whats_new/v1.5.rst +++ b/doc/whats_new/v1.5.rst @@ -131,6 +131,10 @@ more details. methods, which can happen if one tries to decorate them. :pr:`28651` by `Adrin Jalali`_. +- |FIX| Prevent a `RecursionError` when estimators with the default `scoring` + param (`None`) route metadata. :pr:`28712` by :user:`Stefanie Senger + `. + Changelog --------- diff --git a/sklearn/linear_model/_ridge.py b/sklearn/linear_model/_ridge.py index 584302f927ab6..71760efd056fe 100644 --- a/sklearn/linear_model/_ridge.py +++ b/sklearn/linear_model/_ridge.py @@ -2394,12 +2394,10 @@ class RidgeCV(MultiOutputMixin, RegressorMixin, _BaseRidgeCV): (i.e. data is expected to be centered). scoring : str, callable, default=None - A string (see model evaluation documentation) or - a scorer callable object / function with signature - ``scorer(estimator, X, y)``. - If None, the negative mean squared error if cv is 'auto' or None - (i.e. when using leave-one-out cross-validation), and r2 score - otherwise. + A string (see :ref:`scoring_parameter`) or a scorer callable object / + function with signature ``scorer(estimator, X, y)``. If None, the + negative mean squared error if cv is 'auto' or None (i.e. when using + leave-one-out cross-validation), and r2 score otherwise. cv : int, cross-validation generator or an iterable, default=None Determines the cross-validation splitting strategy. @@ -2570,9 +2568,8 @@ class RidgeClassifierCV(_RidgeClassifierMixin, _BaseRidgeCV): (i.e. data is expected to be centered). scoring : str, callable, default=None - A string (see model evaluation documentation) or - a scorer callable object / function with signature - ``scorer(estimator, X, y)``. + A string (see :ref:`scoring_parameter`) or a scorer callable object / + function with signature ``scorer(estimator, X, y)``. cv : int, cross-validation generator or an iterable, default=None Determines the cross-validation splitting strategy. diff --git a/sklearn/linear_model/tests/test_ridge.py b/sklearn/linear_model/tests/test_ridge.py index 3a015defd59b1..187ef3a324741 100644 --- a/sklearn/linear_model/tests/test_ridge.py +++ b/sklearn/linear_model/tests/test_ridge.py @@ -2085,3 +2085,20 @@ def test_ridge_sample_weight_consistency( assert_allclose(reg1.coef_, reg2.coef_) if fit_intercept: assert_allclose(reg1.intercept_, reg2.intercept_) + + +# Metadata Routing Tests +# ====================== + + +@pytest.mark.usefixtures("enable_slep006") +@pytest.mark.parametrize("metaestimator", [RidgeCV, RidgeClassifierCV]) +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() + + +# End of Metadata Routing Tests +# ============================= diff --git a/sklearn/metrics/_scorer.py b/sklearn/metrics/_scorer.py index 391d19459a871..19ca055a81b95 100644 --- a/sklearn/metrics/_scorer.py +++ b/sklearn/metrics/_scorer.py @@ -411,14 +411,31 @@ def get_scorer(scoring): return scorer -class _PassthroughScorer: +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) + except AttributeError: + try: + requests.score = copy.deepcopy(estimator._get_default_requests().score) + except AttributeError: + pass + + self._metadata_request = requests + def __call__(self, estimator, *args, **kwargs): """Method that wraps estimator.score""" return estimator.score(*args, **kwargs) + def __repr__(self): + return f"{self._estimator.__class__}.score" + def get_metadata_routing(self): """Get requested data properties. @@ -433,13 +450,32 @@ def get_metadata_routing(self): A :class:`~utils.metadata_routing.MetadataRouter` encapsulating routing information. """ - # This scorer doesn't do any validation or routing, it only exposes the - # requests of the given estimator. This object behaves as a consumer - # rather than a router. Ideally it only exposes the score requests to - # the parent object; however, that requires computing the routing for - # meta-estimators, which would be more time consuming than simply - # returning the child object's requests. - return get_routing_for_object(self._estimator) + return get_routing_for_object(self._metadata_request) + + def set_score_request(self, **kwargs): + """Set requested parameters by the scorer. + + Please see :ref:`User Guide ` on how the routing + mechanism works. + + .. versionadded:: 1.5 + + Parameters + ---------- + 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(): + raise RuntimeError( + "This method is only available when metadata routing is enabled." + " You can enable it using" + " sklearn.set_config(enable_metadata_routing=True)." + ) + + for param, alias in kwargs.items(): + self._metadata_request.score.add_request(param=param, alias=alias) + return self def _check_multimetric_scoring(estimator, scoring): diff --git a/sklearn/metrics/tests/test_score_objects.py b/sklearn/metrics/tests/test_score_objects.py index 6ce2e3b30dd8e..ac10e0413af0c 100644 --- a/sklearn/metrics/tests/test_score_objects.py +++ b/sklearn/metrics/tests/test_score_objects.py @@ -53,7 +53,6 @@ from sklearn.pipeline import make_pipeline from sklearn.svm import LinearSVC from sklearn.tests.metadata_routing_common import ( - assert_request_equal, assert_request_is_empty, ) from sklearn.tree import DecisionTreeClassifier, DecisionTreeRegressor @@ -1276,24 +1275,40 @@ def test_metadata_kwarg_conflict(): @pytest.mark.usefixtures("enable_slep006") -def test_PassthroughScorer_metadata_request(): - """Test that _PassthroughScorer properly routes metadata. +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" + ) - _PassthroughScorer should behave like a consumer, mirroring whatever is the - underlying score method. - """ - scorer = _PassthroughScorer( - estimator=LinearSVC() - .set_score_request(sample_weight="alias") - .set_fit_request(sample_weight=True) + scorer.set_score_request(sample_weight="scorer_weights") + assert ( + scorer.get_metadata_routing().score.requests["sample_weight"] + == "scorer_weights" ) - # Test that _PassthroughScorer doesn't change estimator's routing. - assert_request_equal( - scorer.get_metadata_routing(), - {"fit": {"sample_weight": True}, "score": {"sample_weight": "alias"}}, + + # making sure changing the passthrough object doesn't affect the estimator. + assert ( + est.get_metadata_routing().score.requests["sample_weight"] + == "estimator_weights" ) +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") + + @pytest.mark.usefixtures("enable_slep006") def test_multimetric_scoring_metadata_routing(): # Test that _MultimetricScorer properly routes metadata. From 9985c0b91506dff8e4c156798093179a2c4f396c Mon Sep 17 00:00:00 2001 From: Stefanie Senger <91849487+StefanieSenger@users.noreply.github.com> Date: Tue, 23 Apr 2024 14:40:58 +0200 Subject: [PATCH 053/344] DOC improve metadata routing example (#27357) Co-authored-by: Adrin Jalali Co-authored-by: Guillaume Lemaitre Co-authored-by: Guillaume Lemaitre --- .../miscellaneous/plot_metadata_routing.py | 415 ++++++++++-------- sklearn/utils/_metadata_requests.py | 26 +- 2 files changed, 258 insertions(+), 183 deletions(-) diff --git a/examples/miscellaneous/plot_metadata_routing.py b/examples/miscellaneous/plot_metadata_routing.py index b9683d114e257..ef7012fb61aab 100644 --- a/examples/miscellaneous/plot_metadata_routing.py +++ b/examples/miscellaneous/plot_metadata_routing.py @@ -6,17 +6,23 @@ .. currentmodule:: sklearn This document shows how you can use the :ref:`metadata routing mechanism -` in scikit-learn to route metadata through meta-estimators -to the estimators consuming them. To better understand the rest of the -document, we need to introduce two concepts: routers and consumers. A router is -an object, in most cases a meta-estimator, which forwards given data and -metadata to other objects and estimators. A consumer, on the other hand, is an -object which accepts and uses a certain given metadata. For instance, an -estimator taking into account ``sample_weight`` in its :term:`fit` method is a -consumer of ``sample_weight``. It is possible for an object to be both a router -and a consumer. For instance, a meta-estimator may take into account -``sample_weight`` in certain calculations, but it may also route it to the -underlying estimator. +` in scikit-learn to route metadata to the estimators, +scorers, and CV splitters consuming them. + +To better understand the following document, we need to introduce two concepts: +routers and consumers. A router is an object which forwards some given data and +metadata to other objects. In most cases, a router is a :term:`meta-estimator`, +i.e. an estimator which takes another estimator as a parameter. A function such +as :func:`sklearn.model_selection.cross_validate` which takes an estimator as a +parameter and forwards data and metadata, is also a router. + +A consumer, on the other hand, is an object which accepts and uses some given +metadata. For instance, an estimator taking into account ``sample_weight`` in +its :term:`fit` method is a consumer of ``sample_weight``. + +It is possible for an object to be both a router and a consumer. For instance, +a meta-estimator may take into account ``sample_weight`` in certain +calculations, but it may also route it to the underlying estimator. First a few imports and some random data for the rest of the script. """ @@ -56,13 +62,12 @@ my_other_weights = rng.rand(n_samples) # %% -# This feature is only available if explicitly enabled: +# Metadata routing is only available if explicitly enabled: set_config(enable_metadata_routing=True) -# %% -# This utility function is a dummy to check if a metadata is passed. - +# %% +# This utility function is a dummy to check if a metadata is passed: def check_metadata(obj, **kwargs): for key, value in kwargs.items(): if value is not None: @@ -74,14 +79,14 @@ def check_metadata(obj, **kwargs): # %% -# A utility function to nicely print the routing information of an object +# A utility function to nicely print the routing information of an object: def print_routing(obj): pprint(obj.get_metadata_routing()._serialize()) # %% -# Estimators -# ---------- +# Consuming Estimator +# ------------------- # Here we demonstrate how an estimator can expose the required API to support # metadata routing as a consumer. Imagine a simple classifier accepting # ``sample_weight`` as a metadata on its ``fit`` and ``groups`` in its @@ -117,11 +122,11 @@ def predict(self, X, groups=None): # %% # The above output means that ``sample_weight`` and ``groups`` are not -# requested, but if a router is given those metadata, it should raise an error, -# since the user has not explicitly set whether they are required or not. The -# same is true for ``sample_weight`` in the ``score`` method, which is -# inherited from :class:`~base.ClassifierMixin`. In order to explicitly set -# request values for those metadata, we can use these methods: +# requested by `ExampleClassifier`, and if a router is given those metadata, it +# should raise an error, since the user has not explicitly set whether they are +# required or not. The same is true for ``sample_weight`` in the ``score`` +# method, which is inherited from :class:`~base.ClassifierMixin`. In order to +# explicitly set request values for those metadata, we can use these methods: est = ( ExampleClassifier() @@ -133,7 +138,7 @@ def predict(self, X, groups=None): # %% # .. note :: -# Please note that as long as the above estimator is not used in another +# Please note that as long as the above estimator is not used in a # meta-estimator, the user does not need to set any requests for the # metadata and the set values are ignored, since a consumer does not # validate or route given metadata. A simple usage of the above estimator @@ -144,8 +149,11 @@ def predict(self, X, groups=None): est.predict(X[:3, :], groups=my_groups) # %% -# Now let's have a meta-estimator, which doesn't do much other than routing the -# metadata. +# Routing Meta-Estimator +# ---------------------- +# Now, we show how to design a meta-estimator to be a router. As a simplified +# example, here is a meta-estimator, which doesn't do much other than routing +# the metadata. class MetaClassifier(MetaEstimatorMixin, ClassifierMixin, BaseEstimator): @@ -155,35 +163,43 @@ def __init__(self, estimator): def get_metadata_routing(self): # This method defines the routing for this meta-estimator. # In order to do so, a `MetadataRouter` instance is created, and the - # right routing is added to it. More explanations follow. + # routing is added to it. More explanations follow below. router = MetadataRouter(owner=self.__class__.__name__).add( - estimator=self.estimator, method_mapping="one-to-one" + estimator=self.estimator, + method_mapping=MethodMapping() + .add(callee="fit", caller="fit") + .add(callee="predict", caller="predict") + .add(callee="score", caller="score"), ) return router def fit(self, X, y, **fit_params): - # meta-estimators are responsible for validating the given metadata. - # `get_routing_for_object` is a safe way to construct a - # `MetadataRouter` or a `MetadataRequest` from the given object. + # `get_routing_for_object` returns a copy of the `MetadataRouter` + # constructed by the above `get_metadata_routing` method, that is + # internally called. request_router = get_routing_for_object(self) + # Meta-estimators are responsible for validating the given metadata. + # `method` refers to the parent's method, i.e. `fit` in this example. request_router.validate_metadata(params=fit_params, method="fit") - # we can use provided utility methods to map the given metadata to what - # is required by the underlying estimator. Here `method` refers to the - # parent's method, i.e. `fit` in this example. + # `MetadataRouter.route_params` maps the given metadata to the metadata + # required by the underlying estimator based on the routing information + # defined by the MetadataRouter. The output of type `Bunch` has a key + # for each consuming object and those hold keys for their consuming + # methods, which then contain key for the metadata which should be + # routed to them. routed_params = request_router.route_params(params=fit_params, caller="fit") - # the output has a key for each object's method which is used here, - # i.e. parent's `fit` method, containing the metadata which should be - # routed to them, based on the information provided in - # `get_metadata_routing`. + # A sub-estimator is fitted and its classes are attributed to the + # meta-estimator. self.estimator_ = clone(self.estimator).fit(X, y, **routed_params.estimator.fit) self.classes_ = self.estimator_.classes_ return self def predict(self, X, **predict_params): check_is_fitted(self) - # same as in `fit`, we validate the given metadata + # As in `fit`, we get a copy of the object's MetadataRouter, request_router = get_routing_for_object(self) + # then we validate the given metadata, request_router.validate_metadata(params=predict_params, method="predict") # and then prepare the input to the underlying `predict` method. routed_params = request_router.route_params( @@ -195,77 +211,82 @@ def predict(self, X, **predict_params): # %% # Let's break down different parts of the above code. # -# First, the :meth:`~utils.metadata_routing.get_routing_for_object` takes an -# estimator (``self``) and returns a -# :class:`~utils.metadata_routing.MetadataRouter` or a -# :class:`~utils.metadata_routing.MetadataRequest` based on the output of the -# estimator's ``get_metadata_routing`` method. +# First, the :meth:`~utils.metadata_routing.get_routing_for_object` takes our +# meta-estimator (``self``) and returns a +# :class:`~utils.metadata_routing.MetadataRouter` or, a +# :class:`~utils.metadata_routing.MetadataRequest` if the object is a consumer, +# based on the output of the estimator's ``get_metadata_routing`` method. # # Then in each method, we use the ``route_params`` method to construct a # dictionary of the form ``{"object_name": {"method_name": {"metadata": # value}}}`` to pass to the underlying estimator's method. The ``object_name`` # (``estimator`` in the above ``routed_params.estimator.fit`` example) is the # same as the one added in the ``get_metadata_routing``. ``validate_metadata`` -# makes sure all given metadata are requested to avoid silent bugs. Now, we -# illustrate the different behaviors and notably the type of errors raised: +# makes sure all given metadata are requested to avoid silent bugs. +# +# Next, we illustrate the different behaviors and notably the type of errors +# raised. -est = MetaClassifier(estimator=ExampleClassifier().set_fit_request(sample_weight=True)) -est.fit(X, y, sample_weight=my_weights) +meta_est = MetaClassifier( + estimator=ExampleClassifier().set_fit_request(sample_weight=True) +) +meta_est.fit(X, y, sample_weight=my_weights) # %% -# Note that the above example checks that ``sample_weight`` is correctly passed -# to ``ExampleClassifier``, or else it would print that ``sample_weight`` is -# ``None``: +# Note that the above example is calling our utility function +# `check_metadata()` via the `ExampleClassifier`. It checks that +# ``sample_weight`` is correctly passed to it. If it is not, like in the +# following example, it would print that ``sample_weight`` is ``None``: -est.fit(X, y) +meta_est.fit(X, y) # %% # If we pass an unknown metadata, an error is raised: try: - est.fit(X, y, test=my_weights) + meta_est.fit(X, y, test=my_weights) except TypeError as e: print(e) # %% # And if we pass a metadata which is not explicitly requested: try: - est.fit(X, y, sample_weight=my_weights).predict(X, groups=my_groups) + meta_est.fit(X, y, sample_weight=my_weights).predict(X, groups=my_groups) except ValueError as e: print(e) # %% # Also, if we explicitly set it as not requested, but it is provided: -est = MetaClassifier( +meta_est = MetaClassifier( estimator=ExampleClassifier() .set_fit_request(sample_weight=True) .set_predict_request(groups=False) ) try: - est.fit(X, y, sample_weight=my_weights).predict(X[:3, :], groups=my_groups) + meta_est.fit(X, y, sample_weight=my_weights).predict(X[:3, :], groups=my_groups) except TypeError as e: print(e) # %% -# Another concept to introduce is **aliased metadata**. This is when an estimator -# requests a metadata with a different name than the default value. For -# instance, in a setting where there are two estimators in a pipeline, one -# could request ``sample_weight1`` and the other ``sample_weight2``. Note that -# this doesn't change what the estimator expects, it only tells the -# meta-estimator how to map the provided metadata to what's required. Here's an -# example, where we pass ``aliased_sample_weight`` to the meta-estimator, but -# the meta-estimator understands that ``aliased_sample_weight`` is an alias for -# ``sample_weight``, and passes it as ``sample_weight`` to the underlying -# estimator: -est = MetaClassifier( +# Another concept to introduce is **aliased metadata**. This is when an +# estimator requests a metadata with a different variable name than the default +# variable name. For instance, in a setting where there are two estimators in a +# pipeline, one could request ``sample_weight1`` and the other +# ``sample_weight2``. Note that this doesn't change what the estimator expects, +# it only tells the meta-estimator how to map the provided metadata to what is +# required. Here's an example, where we pass ``aliased_sample_weight`` to the +# meta-estimator, but the meta-estimator understands that +# ``aliased_sample_weight`` is an alias for ``sample_weight``, and passes it as +# ``sample_weight`` to the underlying estimator: +meta_est = MetaClassifier( estimator=ExampleClassifier().set_fit_request(sample_weight="aliased_sample_weight") ) -est.fit(X, y, aliased_sample_weight=my_weights) +meta_est.fit(X, y, aliased_sample_weight=my_weights) # %% -# And passing ``sample_weight`` here will fail since it is requested with an +# Passing ``sample_weight`` here will fail since it is requested with an # alias and ``sample_weight`` with that name is not requested: try: - est.fit(X, y, sample_weight=my_weights) + meta_est.fit(X, y, sample_weight=my_weights) except TypeError as e: print(e) @@ -280,41 +301,46 @@ def predict(self, X, **predict_params): # corresponding method routings, i.e. which method of a sub-estimator is used # in which method of a meta-estimator: -print_routing(est) +print_routing(meta_est) # %% # As you can see, the only metadata requested for method ``fit`` is # ``"sample_weight"`` with ``"aliased_sample_weight"`` as the alias. The # ``~utils.metadata_routing.MetadataRouter`` class enables us to easily create # the routing object which would create the output we need for our -# ``get_metadata_routing``. In the above implementation, -# ``mapping="one-to-one"`` means there is a one to one mapping between -# sub-estimator's methods and meta-estimator's ones, i.e. ``fit`` used in -# ``fit`` and so on. In order to understand how aliases work in -# meta-estimators, imagine our meta-estimator inside another one: +# ``get_metadata_routing``. +# +# In order to understand how aliases work in meta-estimators, imagine our +# meta-estimator inside another one: -meta_est = MetaClassifier(estimator=est).fit(X, y, aliased_sample_weight=my_weights) +meta_meta_est = MetaClassifier(estimator=meta_est).fit( + X, y, aliased_sample_weight=my_weights +) # %% -# In the above example, this is how each ``fit`` method will call the -# sub-estimator's ``fit``:: +# In the above example, this is how the ``fit`` method of `meta_meta_est` +# will call their sub-estimator's ``fit`` methods:: +# +# # user feeds `my_weights` as `aliased_sample_weight` into `meta_meta_est`: +# meta_meta_est.fit(X, y, aliased_sample_weight=my_weights): +# ... # -# meta_est.fit(X, y, aliased_sample_weight=my_weights): -# ... # this estimator (est), expects aliased_sample_weight as seen above +# # the first sub-estimator (`meta_est`) expects `aliased_sample_weight` # self.estimator_.fit(X, y, aliased_sample_weight=aliased_sample_weight): -# ... # now est passes aliased_sample_weight's value as sample_weight, -# # which is expected by the sub-estimator -# self.estimator_.fit(X, y, sample_weight=aliased_sample_weight) -# ... +# ... +# +# # the second sub-estimator (`est`) expects `sample_weight` +# self.estimator_.fit(X, y, sample_weight=aliased_sample_weight): +# ... # %% -# Router and Consumer -# ------------------- -# To show how a slightly more complex case would work, consider a case -# where a meta-estimator uses some metadata, but it also routes them to an -# underlying estimator. In this case, this meta-estimator is a consumer and a -# router at the same time. This is how we can implement one, and it is very -# similar to what we had before, with a few tweaks. +# Consuming and routing Meta-Estimator +# ------------------------------------ +# For a slightly more complex example, consider a meta-estimator that routes +# metadata to an underlying estimator as before, but it also uses some metadata +# in its own methods. This meta-estimator is a consumer and a router at the +# same time. Implementing one is very similar to what we had before, but with a +# few tweaks. class RouterConsumerClassifier(MetaEstimatorMixin, ClassifierMixin, BaseEstimator): @@ -324,47 +350,59 @@ def __init__(self, estimator): def get_metadata_routing(self): router = ( MetadataRouter(owner=self.__class__.__name__) + # defining metadata routing request values for usage in the meta-estimator .add_self_request(self) - .add(estimator=self.estimator, method_mapping="one-to-one") + # defining metadata routing request values for usage in the sub-estimator + .add( + estimator=self.estimator, + method_mapping=MethodMapping() + .add(callee="fit", caller="fit") + .add(callee="predict", caller="predict") + .add(callee="score", caller="score"), + ) ) return router + # Since `sample_weight` is used and consumed here, it should be defined as + # an explicit argument in the method's signature. All other metadata which + # are only routed, will be passed as `**fit_params`: def fit(self, X, y, sample_weight, **fit_params): if self.estimator is None: raise ValueError("estimator cannot be None!") check_metadata(self, sample_weight=sample_weight) + # We add `sample_weight` to the `fit_params` dictionary. if sample_weight is not None: fit_params["sample_weight"] = sample_weight - # meta-estimators are responsible for validating the given metadata request_router = get_routing_for_object(self) request_router.validate_metadata(params=fit_params, method="fit") - # we can use provided utility methods to map the given metadata to what - # is required by the underlying estimator - params = request_router.route_params(params=fit_params, caller="fit") - self.estimator_ = clone(self.estimator).fit(X, y, **params.estimator.fit) + routed_params = request_router.route_params(params=fit_params, caller="fit") + self.estimator_ = clone(self.estimator).fit(X, y, **routed_params.estimator.fit) self.classes_ = self.estimator_.classes_ return self def predict(self, X, **predict_params): check_is_fitted(self) - # same as in ``fit``, we validate the given metadata + # As in `fit`, we get a copy of the object's MetadataRouter, request_router = get_routing_for_object(self) + # we validate the given metadata, request_router.validate_metadata(params=predict_params, method="predict") # and then prepare the input to the underlying ``predict`` method. - params = request_router.route_params(params=predict_params, caller="predict") - return self.estimator_.predict(X, **params.estimator.predict) + routed_params = request_router.route_params( + params=predict_params, caller="predict" + ) + return self.estimator_.predict(X, **routed_params.estimator.predict) # %% -# The key parts where the above estimator differs from our previous +# The key parts where the above meta-estimator differs from our previous # meta-estimator is accepting ``sample_weight`` explicitly in ``fit`` and -# including it in ``fit_params``. Making ``sample_weight`` an explicit argument -# makes sure ``set_fit_request(sample_weight=...)`` is present for this class. -# In a sense, this means the estimator is both a consumer, as well as a router -# of ``sample_weight``. +# including it in ``fit_params``. Since ``sample_weight`` is an explicit +# argument, we can be sure that ``set_fit_request(sample_weight=...)`` is +# present for this method. The meta-estimator is both a consumer, as well as a +# router of ``sample_weight``. # # In ``get_metadata_routing``, we add ``self`` to the routing using # ``add_self_request`` to indicate this estimator is consuming @@ -374,61 +412,68 @@ def predict(self, X, **predict_params): # %% # - No metadata requested -est = RouterConsumerClassifier(estimator=ExampleClassifier()) -print_routing(est) +meta_est = RouterConsumerClassifier(estimator=ExampleClassifier()) +print_routing(meta_est) # %% -# - ``sample_weight`` requested by underlying estimator -est = RouterConsumerClassifier( +# - ``sample_weight`` requested by sub-estimator +meta_est = RouterConsumerClassifier( estimator=ExampleClassifier().set_fit_request(sample_weight=True) ) -print_routing(est) +print_routing(meta_est) # %% # - ``sample_weight`` requested by meta-estimator -est = RouterConsumerClassifier(estimator=ExampleClassifier()).set_fit_request( +meta_est = RouterConsumerClassifier(estimator=ExampleClassifier()).set_fit_request( sample_weight=True ) -print_routing(est) +print_routing(meta_est) # %% # Note the difference in the requested metadata representations above. # -# - We can also alias the metadata to pass different values to them: +# - We can also alias the metadata to pass different values to the fit methods +# of the meta- and the sub-estimator: -est = RouterConsumerClassifier( +meta_est = RouterConsumerClassifier( estimator=ExampleClassifier().set_fit_request(sample_weight="clf_sample_weight"), ).set_fit_request(sample_weight="meta_clf_sample_weight") -print_routing(est) +print_routing(meta_est) # %% # However, ``fit`` of the meta-estimator only needs the alias for the -# sub-estimator, since it doesn't validate and route its own required metadata: -est.fit(X, y, sample_weight=my_weights, clf_sample_weight=my_other_weights) +# sub-estimator and addresses their own sample weight as `sample_weight`, since +# it doesn't validate and route its own required metadata: +meta_est.fit(X, y, sample_weight=my_weights, clf_sample_weight=my_other_weights) # %% -# - Alias only on the sub-estimator. This is useful if we don't want the -# meta-estimator to use the metadata, and we only want the metadata to be used -# by the sub-estimator. -est = RouterConsumerClassifier( +# - Alias only on the sub-estimator: +# +# This is useful when we don't want the meta-estimator to use the metadata, but +# the sub-estimator should. +meta_est = RouterConsumerClassifier( estimator=ExampleClassifier().set_fit_request(sample_weight="aliased_sample_weight") -).set_fit_request(sample_weight=True) -print_routing(est) - +) +print_routing(meta_est) +# %% +# The meta-estimator cannot use `aliased_sample_weight`, because it expects +# it passed as `sample_weight`. This would apply even if +# `set_fit_request(sample_weight=True)` was set on it. # %% # Simple Pipeline # --------------- -# A slightly more complicated use-case is a meta-estimator which does something -# similar to the :class:`~pipeline.Pipeline`. Here is a meta-estimator, which -# accepts a transformer and a classifier, and applies the transformer before -# running the classifier. +# A slightly more complicated use-case is a meta-estimator resembling a +# :class:`~pipeline.Pipeline`. Here is a meta-estimator, which accepts a +# transformer and a classifier. When calling its `fit` method, it applies the +# transformer's `fit` and `transform` before running the classifier on the +# transformed data. Upon `predict`, it applies the transformer's `transform` +# before predicting with the classifier's `predict` method on the transformed +# new data. class SimplePipeline(ClassifierMixin, BaseEstimator): - _required_parameters = ["estimator"] - def __init__(self, transformer, classifier): self.transformer = transformer self.classifier = classifier @@ -436,48 +481,66 @@ def __init__(self, transformer, classifier): def get_metadata_routing(self): router = ( MetadataRouter(owner=self.__class__.__name__) + # We add the routing for the transformer. .add( transformer=self.transformer, method_mapping=MethodMapping() + # The metadata is routed such that it retraces how + # `SimplePipeline` internally calls the transformer's `fit` and + # `transform` methods in its own methods (`fit` and `predict`). .add(callee="fit", caller="fit") .add(callee="transform", caller="fit") .add(callee="transform", caller="predict"), ) - .add(classifier=self.classifier, method_mapping="one-to-one") + # We add the routing for the classifier. + .add( + classifier=self.classifier, + method_mapping=MethodMapping() + .add(callee="fit", caller="fit") + .add(callee="predict", caller="predict"), + ) ) return router def fit(self, X, y, **fit_params): - params = process_routing(self, "fit", **fit_params) + routed_params = process_routing(self, "fit", **fit_params) - self.transformer_ = clone(self.transformer).fit(X, y, **params.transformer.fit) - X_transformed = self.transformer_.transform(X, **params.transformer.transform) + self.transformer_ = clone(self.transformer).fit( + X, y, **routed_params.transformer.fit + ) + X_transformed = self.transformer_.transform( + X, **routed_params.transformer.transform + ) self.classifier_ = clone(self.classifier).fit( - X_transformed, y, **params.classifier.fit + X_transformed, y, **routed_params.classifier.fit ) return self def predict(self, X, **predict_params): - params = process_routing(self, "predict", **predict_params) + routed_params = process_routing(self, "predict", **predict_params) - X_transformed = self.transformer_.transform(X, **params.transformer.transform) - return self.classifier_.predict(X_transformed, **params.classifier.predict) + X_transformed = self.transformer_.transform( + X, **routed_params.transformer.transform + ) + return self.classifier_.predict( + X_transformed, **routed_params.classifier.predict + ) # %% -# Note the usage of :class:`~utils.metadata_routing.MethodMapping` to declare -# which methods of the child estimator (callee) are used in which methods of -# the meta estimator (caller). As you can see, we use the transformer's -# ``transform`` and ``fit`` methods in ``fit``, and its ``transform`` method in -# ``predict``, and that's what you see implemented in the routing structure of -# the pipeline class. +# Note the usage of :class:`~utils.metadata_routing.MethodMapping` to +# declare which methods of the child estimator (callee) are used in which +# methods of the meta estimator (caller). As you can see, `SimplePipeline` uses +# the transformer's ``transform`` and ``fit`` methods in ``fit``, and its +# ``transform`` method in ``predict``, and that's what you see implemented in +# the routing structure of the pipeline class. # # Another difference in the above example with the previous ones is the usage # of :func:`~utils.metadata_routing.process_routing`, which processes the input -# parameters, does the required validation, and returns the `params` which we -# had created in previous examples. This reduces the boilerplate code a -# developer needs to write in each meta-estimator's method. Developers are +# parameters, does the required validation, and returns the `routed_params` +# which we had created in previous examples. This reduces the boilerplate code +# a developer needs to write in each meta-estimator's method. Developers are # strongly recommended to use this function unless there is a good reason # against it. # @@ -505,14 +568,14 @@ def fit_transform(self, X, y, sample_weight=None, groups=None): # ``transform``. # # Now we can test our pipeline, and see if metadata is correctly passed around. -# This example uses our simple pipeline, and our transformer, and our -# consumer+router estimator which uses our simple classifier. +# This example uses our `SimplePipeline`, our `ExampleTransformer`, and our +# `RouterConsumerClassifier` which uses our `ExampleClassifier`. -est = SimplePipeline( +pipe = SimplePipeline( transformer=ExampleTransformer() - # we transformer's fit to receive sample_weight + # we set transformer's fit to receive sample_weight .set_fit_request(sample_weight=True) - # we want transformer's transform to receive groups + # we set transformer's transform to receive groups .set_transform_request(groups=True), classifier=RouterConsumerClassifier( estimator=ExampleClassifier() @@ -520,12 +583,11 @@ def fit_transform(self, X, y, sample_weight=None, groups=None): .set_fit_request(sample_weight=True) # but not groups in predict .set_predict_request(groups=False), - ).set_fit_request( - # and we want the meta-estimator to receive sample_weight as well - sample_weight=True - ), + ) + # and we want the meta-estimator to receive sample_weight as well + .set_fit_request(sample_weight=True), ) -est.fit(X, y, sample_weight=my_weights, groups=my_groups).predict( +pipe.fit(X, y, sample_weight=my_weights, groups=my_groups).predict( X[:3], groups=my_groups ) @@ -544,18 +606,20 @@ def __init__(self, estimator): self.estimator = estimator def fit(self, X, y, **fit_params): - params = process_routing(self, "fit", **fit_params) - self.estimator_ = clone(self.estimator).fit(X, y, **params.estimator.fit) + routed_params = process_routing(self, "fit", **fit_params) + self.estimator_ = clone(self.estimator).fit(X, y, **routed_params.estimator.fit) def get_metadata_routing(self): router = MetadataRouter(owner=self.__class__.__name__).add( - estimator=self.estimator, method_mapping="one-to-one" + estimator=self.estimator, + method_mapping=MethodMapping().add(callee="fit", caller="fit"), ) return router # %% -# As explained above, this is now a valid usage: +# As explained above, this is a valid usage if `my_weights` aren't supposed +# to be passed as `sample_weight` to `MetaRegressor`: reg = MetaRegressor(estimator=LinearRegression().set_fit_request(sample_weight=True)) reg.fit(X, y, sample_weight=my_weights) @@ -567,29 +631,36 @@ def get_metadata_routing(self): class WeightedMetaRegressor(MetaEstimatorMixin, RegressorMixin, BaseEstimator): + # show warning to remind user to explicitly set the value with + # `.set_{method}_request(sample_weight={boolean})` __metadata_request__fit = {"sample_weight": metadata_routing.WARN} def __init__(self, estimator): self.estimator = estimator def fit(self, X, y, sample_weight=None, **fit_params): - params = process_routing(self, "fit", sample_weight=sample_weight, **fit_params) + routed_params = process_routing( + self, "fit", sample_weight=sample_weight, **fit_params + ) check_metadata(self, sample_weight=sample_weight) - self.estimator_ = clone(self.estimator).fit(X, y, **params.estimator.fit) + self.estimator_ = clone(self.estimator).fit(X, y, **routed_params.estimator.fit) def get_metadata_routing(self): router = ( MetadataRouter(owner=self.__class__.__name__) .add_self_request(self) - .add(estimator=self.estimator, method_mapping="one-to-one") + .add( + estimator=self.estimator, + method_mapping=MethodMapping().add(callee="fit", caller="fit"), + ) ) return router # %% -# The above implementation is almost no different than ``MetaRegressor``, and +# The above implementation is almost the same as ``MetaRegressor``, and # because of the default request value defined in ``__metadata_request__fit`` -# there is a warning raised. +# there is a warning raised when fitted. with warnings.catch_warnings(record=True) as record: WeightedMetaRegressor( @@ -600,7 +671,7 @@ def get_metadata_routing(self): # %% -# When an estimator supports a metadata which wasn't supported before, the +# When an estimator consumes a metadata which it didn't consume before, the # following pattern can be used to warn the users about it. @@ -634,12 +705,12 @@ def predict(self, X): # :class:`~utils.metadata_routing.MetadataRouter`. It is strongly not advised, # but possible to vendor the tools related to metadata-routing if you strictly # want to have a scikit-learn compatible estimator, without depending on the -# scikit-learn package. If the following conditions are met, you do NOT need to -# modify your code at all: +# scikit-learn package. If all of the following conditions are met, you do NOT +# need to modify your code at all: # # - your estimator inherits from :class:`~base.BaseEstimator` # - the parameters consumed by your estimator's methods, e.g. ``fit``, are # explicitly defined in the method's signature, as opposed to being # ``*args`` or ``*kwargs``. -# - you do not route any metadata to the underlying objects, i.e. you're not a -# *router*. +# - your estimator does not route any metadata to the underlying objects, i.e. +# it's not a *router*. diff --git a/sklearn/utils/_metadata_requests.py b/sklearn/utils/_metadata_requests.py index 075fe196ae01f..4acac0f9fd22f 100644 --- a/sklearn/utils/_metadata_requests.py +++ b/sklearn/utils/_metadata_requests.py @@ -399,8 +399,10 @@ def _check_warnings(self, *, params): warn( f"Support for {param} has recently been added to this class. " "To maintain backward compatibility, it is ignored now. " - "You can set the request value to False to silence this " - "warning, or to True to consume and use the metadata." + 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): @@ -1031,9 +1033,8 @@ def _route_params(self, *, params, method, parent, caller): def route_params(self, *, caller, params): """Return the input parameters requested by child objects. - The output of this method is a bunch, which includes the inputs for all - methods of each child object that are used in the router's `caller` - method. + The output of this method is a 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. @@ -1052,7 +1053,7 @@ def route_params(self, *, caller, params): ------- params : Bunch A :class:`~sklearn.utils.Bunch` of the form - ``{"object_name": {"method_name": {prop: value}}}`` which can be + ``{"object_name": {"method_name": {params: value}}}`` which can be used to pass the required metadata to corresponding methods or corresponding child objects. """ @@ -1159,12 +1160,12 @@ def get_routing_for_object(obj=None): Parameters ---------- 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. - - If the object provides a `get_metadata_routing` method, return a copy - of the output of that method. - Returns an empty :class:`~sklearn.utils.metadata_routing.MetadataRequest` otherwise. @@ -1557,9 +1558,10 @@ def process_routing(_obj, _method, /, **kwargs): This function is used inside a router's method, e.g. :term:`fit`, to validate the metadata and handle the routing. - Assuming this signature: ``fit(self, X, y, sample_weight=None, **fit_params)``, + 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, sample_weight=sample_weight, **fit_params)``. + ``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`` @@ -1582,8 +1584,10 @@ def process_routing(_obj, _method, /, **kwargs): 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": - {prop: value}}}`` which can be used to pass the required metadata to + {params: value}}}`` which can be used to pass the required metadata to corresponding methods or corresponding child objects. The object names are those defined in `obj.get_metadata_routing()`. """ From 040347a16048999b0319dc8e0d42798cfb0e654b Mon Sep 17 00:00:00 2001 From: Christian Lorentzen Date: Tue, 23 Apr 2024 15:56:24 +0200 Subject: [PATCH 054/344] DOC document cython typedefs (#28865) Co-authored-by: Tim Head --- doc/developers/cython.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/doc/developers/cython.rst b/doc/developers/cython.rst index e98501879d50e..82022ddcbcc56 100644 --- a/doc/developers/cython.rst +++ b/doc/developers/cython.rst @@ -141,3 +141,16 @@ must be ``cimported`` from this module and not from the OpenMP library directly: The parallel loop, `prange`, is already protected by cython and can be used directly from `cython.parallel`. + +Types +~~~~~ + +Cython code requires to use explicit types. This is one of the reasons you get a +performance boost. In order to avoid code duplication, we have a central place +for the most used types in +`sklearn/utils/_typedefs.pyd `_. +Ideally you start by having a look there and `cimport` types you need, for example + +.. code-block:: cython + + from sklear.utils._typedefs cimport float32, float64 From 08c948afd246b827810d5b78c8054fdb279a87a3 Mon Sep 17 00:00:00 2001 From: Rahil Parikh <75483881+rprkh@users.noreply.github.com> Date: Tue, 23 Apr 2024 21:00:39 +0530 Subject: [PATCH 055/344] DOC update model persistence page to provide clearer recommendations (#28189) Co-authored-by: Arturo Amor <86408019+ArturoAmorQ@users.noreply.github.com> Co-authored-by: Adrin Jalali --- doc/model_persistence.rst | 155 ++++++++++++++++++++++++++++++++------ 1 file changed, 133 insertions(+), 22 deletions(-) diff --git a/doc/model_persistence.rst b/doc/model_persistence.rst index 0f775c774465a..afd492d805e58 100644 --- a/doc/model_persistence.rst +++ b/doc/model_persistence.rst @@ -9,31 +9,118 @@ Model persistence ================= After training a scikit-learn model, it is desirable to have a way to persist -the model for future use without having to retrain. The following sections give -you some hints on how to persist a scikit-learn model. - -Python specific serialization ------------------------------ - -It is possible to save a model in scikit-learn by using Python's built-in -persistence model, namely `pickle -`_:: +the model for future use without having to retrain. This can be accomplished +using `pickle `_, `joblib +`_, `skops +`_, `ONNX `_, +or `PMML `_. In most cases +`pickle` can be used to persist a trained scikit-learn model. Once all +transitive scikit-learn dependencies have been pinned, the trained model can +then be loaded and executed under conditions similar to those in which it was +originally pinned. The following sections will give you some hints on how to +persist a scikit-learn model and will provide details on what each alternative +can offer. + +Workflow Overview +----------------- + +In this section we present a general workflow on how to persist a +scikit-learn model. We will demonstrate this with a simple example using +Python's built-in persistence module, namely `pickle +`_. + +Storing the model in an artifact +................................ + +Once the model training process in completed, the trained model can be stored +as an artifact with the help of `pickle`. The model can be saved using the +process of serialization, where the Python object hierarchy is converted into +a byte stream. We can persist a trained model in the following manner:: >>> from sklearn import svm >>> from sklearn import datasets + >>> import pickle >>> clf = svm.SVC() - >>> X, y= datasets.load_iris(return_X_y=True) + >>> X, y = datasets.load_iris(return_X_y=True) >>> clf.fit(X, y) SVC() - - >>> import pickle >>> s = pickle.dumps(clf) - >>> clf2 = pickle.loads(s) - >>> clf2.predict(X[0:1]) + +Replicating the training environment in production +.................................................. + +The versions of the dependencies used may differ from training to production. +This may result in unexpected behaviour and errors while using the trained +model. To prevent such situations it is recommended to use the same +dependencies and versions in both the training and production environment. +These transitive dependencies can be pinned with the help of `pip`, `conda`, +`poetry`, `conda-lock`, `pixi`, etc. + +.. note:: + + To execute a pickled scikit-learn model in a reproducible environment it is + advisable to pin all transitive scikit-learn dependencies. This prevents + any incompatibility issues that may arise while trying to load the pickled + model. You can read more about persisting models with `pickle` over + :ref:`here `. + +Loading the model artifact +.......................... + +The saved scikit-learn model can be loaded using `pickle` for future use +without having to re-train the entire model from scratch. The saved model +artifact can be unpickled by converting the byte stream into an object +hierarchy. This can be done with the help of `pickle` as follows:: + + >>> clf2 = pickle.loads(s) # doctest:+SKIP + >>> clf2.predict(X[0:1]) # doctest:+SKIP + array([0]) + >>> y[0] # doctest:+SKIP + 0 + +Serving the model artifact +.......................... + +The last step after training a scikit-learn model is serving the model. +Once the trained model is successfully loaded it can be served to manage +different prediction requests. This can involve deploying the model as a +web service using containerization, or other model deployment strategies, +according to the specifications. In the next sections, we will explore +different approaches to persist a trained scikit-learn model. + +.. _persisting_models_with_pickle: + +Persisting models with pickle +----------------------------- + +As demonstrated in the previous section, `pickle` uses serialization and +deserialization to persist scikit-learn models. Instead of using `dumps` and +`loads`, `dump` and `load` can also be used in the following way:: + + >>> from sklearn.tree import DecisionTreeClassifier + >>> from sklearn import datasets + >>> clf = DecisionTreeClassifier() + >>> X, y = datasets.load_iris(return_X_y=True) + >>> clf.fit(X, y) + DecisionTreeClassifier() + >>> from pickle import dump, load + >>> with open('filename.pkl', 'wb') as f: dump(clf, f) # doctest:+SKIP + >>> with open('filename.pkl', 'rb') as f: clf2 = load(f) # doctest:+SKIP + >>> clf2.predict(X[0:1]) # doctest:+SKIP array([0]) >>> y[0] 0 +For applications that involve writing and loading the serialized object to or +from a file, `dump` and `load` can be used instead of `dumps` and `loads`. When +file operations are not required the pickled representation of the object can +be returned as a bytes object with the help of the `dumps` function. The +reconstituted object hierarchy of the pickled data can then be returned using +the `loads` function. + +Persisting models with joblib +----------------------------- + In the specific case of scikit-learn, it may be better to use joblib's replacement of pickle (``dump`` & ``load``), which is more efficient on objects that carry large numpy arrays internally as is often the case for @@ -41,7 +128,7 @@ fitted scikit-learn estimators, but can only pickle to the disk and not to a string:: >>> from joblib import dump, load - >>> dump(clf, 'filename.joblib') # doctest: +SKIP + >>> dump(clf, 'filename.joblib') # doctest:+SKIP Later you can load back the pickled model (possibly in another Python process) with:: @@ -76,8 +163,8 @@ can be caught to obtain the original version the estimator was pickled with:: .. _persistence_limitations: -Security & maintainability limitations -...................................... +Security & maintainability limitations for pickle and joblib +------------------------------------------------------------ pickle (and joblib by extension), has some issues regarding maintainability and security. Because of this, @@ -111,9 +198,8 @@ serialization methods, please refer to this `talk by Alex Gaynor `_. - -A more secure format: `skops` -............................. +Persisting models with a more secure format using skops +------------------------------------------------------- `skops `__ provides a more secure format via the :mod:`skops.io` module. It avoids using :mod:`pickle` and only @@ -150,8 +236,8 @@ issue tracker `__. |details-end| -Interoperable formats ---------------------- +Persisting models with interoperable formats +-------------------------------------------- For reproducibility and quality control needs, when different architectures and environments should be taken into account, exporting the model in @@ -181,3 +267,28 @@ not help in production when performance is critical. To convert scikit-learn model to PMML you can use for example `sklearn2pmml `_ distributed under the Affero GPLv3 license. + +Summarizing the keypoints +------------------------- + +Based on the different approaches for model persistence, the keypoints for each +approach can be summarized as follows: + +* `pickle`: It is native to Python and any Python object can be serialized and + deserialized using `pickle`, including custom Python classes and objects. + While `pickle` can be used to easily save and load scikit-learn models, + unpickling of untrusted data might lead to security issues. +* `joblib`: Efficient storage and memory mapping techniques make it faster + when working with large machine learning models or large numpy arrays. However, + it may trigger the execution of malicious code while loading untrusted data. +* `skops`: Trained scikit-learn models can be easily shared and put into + production using `skops`. It is more secure compared to alternate approaches + as it allows users to load data from trusted sources. It however, does not + allow for persistence of arbitrary Python code. +* `ONNX`: It provides a uniform format for persisting any machine learning + or deep learning model (other than scikit-learn) and is useful + for model inference. It can however, result in compatibility issues with + different frameworks. +* `PMML`: Platform independent format that can be used to persist models + and reduce the risk of vendor lock-ins. The complexity and verbosity of + this format might make it harder to use for larger models. \ No newline at end of file From 46af70759590779ce7c6564557087ba6728fcf1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20du=20Boisberranger?= Date: Tue, 23 Apr 2024 18:19:13 +0200 Subject: [PATCH 056/344] MAINT Bump threadpoolctl min version for 1.5 (#28838) --- README.rst | 2 +- build_tools/azure/debian_atlas_32bit_lock.txt | 2 +- .../azure/debian_atlas_32bit_requirements.txt | 2 +- ...latest_conda_mkl_no_openmp_environment.yml | 2 +- ...test_conda_mkl_no_openmp_osx-64_conda.lock | 4 +- ...pylatest_pip_scipy_dev_linux-64_conda.lock | 16 +-- ...in_conda_defaults_openblas_environment.yml | 2 +- ...onda_defaults_openblas_linux-64_conda.lock | 4 +- build_tools/azure/pypy3_linux-64_conda.lock | 22 ++-- build_tools/azure/ubuntu_atlas_lock.txt | 2 +- .../azure/ubuntu_atlas_requirements.txt | 2 +- build_tools/circle/doc_linux-64_conda.lock | 2 +- .../update_environments_and_lock_files.py | 23 ++-- pyproject.toml | 4 +- sklearn/__init__.py | 15 +++ sklearn/_min_dependencies.py | 2 +- sklearn/cluster/_kmeans.py | 112 +++++++++--------- sklearn/cluster/tests/test_k_means.py | 7 +- .../_argkmin.pyx.tp | 4 +- .../_argkmin_classmode.pyx.tp | 4 +- .../_radius_neighbors.pyx.tp | 4 +- .../_radius_neighbors_classmode.pyx.tp | 4 +- .../test_pairwise_distances_reduction.py | 4 +- sklearn/utils/_show_versions.py | 3 +- sklearn/utils/fixes.py | 39 +----- sklearn/utils/tests/test_show_versions.py | 3 +- 26 files changed, 135 insertions(+), 155 deletions(-) diff --git a/README.rst b/README.rst index 3f9a4ad726806..4ac297063c26e 100644 --- a/README.rst +++ b/README.rst @@ -36,7 +36,7 @@ .. |NumPyMinVersion| replace:: 1.19.5 .. |SciPyMinVersion| replace:: 1.6.0 .. |JoblibMinVersion| replace:: 1.2.0 -.. |ThreadpoolctlMinVersion| replace:: 2.0.0 +.. |ThreadpoolctlMinVersion| replace:: 3.1.0 .. |MatplotlibMinVersion| replace:: 3.3.4 .. |Scikit-ImageMinVersion| replace:: 0.17.2 .. |PandasMinVersion| replace:: 1.1.5 diff --git a/build_tools/azure/debian_atlas_32bit_lock.txt b/build_tools/azure/debian_atlas_32bit_lock.txt index a6bb0485d1933..5bff365bb1eea 100644 --- a/build_tools/azure/debian_atlas_32bit_lock.txt +++ b/build_tools/azure/debian_atlas_32bit_lock.txt @@ -37,7 +37,7 @@ pytest==7.1.2 # pytest-cov pytest-cov==2.9.0 # via -r build_tools/azure/debian_atlas_32bit_requirements.txt -threadpoolctl==2.2.0 +threadpoolctl==3.1.0 # via -r build_tools/azure/debian_atlas_32bit_requirements.txt tomli==2.0.1 # via diff --git a/build_tools/azure/debian_atlas_32bit_requirements.txt b/build_tools/azure/debian_atlas_32bit_requirements.txt index d1bc22529d4f4..615193a71fc6b 100644 --- a/build_tools/azure/debian_atlas_32bit_requirements.txt +++ b/build_tools/azure/debian_atlas_32bit_requirements.txt @@ -3,7 +3,7 @@ # build_tools/update_environments_and_lock_files.py cython==3.0.10 # min joblib==1.2.0 # min -threadpoolctl==2.2.0 +threadpoolctl==3.1.0 pytest==7.1.2 # min pytest-cov==2.9.0 # min ninja diff --git a/build_tools/azure/pylatest_conda_mkl_no_openmp_environment.yml b/build_tools/azure/pylatest_conda_mkl_no_openmp_environment.yml index 9c46400c2d3c6..01bd378aa121a 100644 --- a/build_tools/azure/pylatest_conda_mkl_no_openmp_environment.yml +++ b/build_tools/azure/pylatest_conda_mkl_no_openmp_environment.yml @@ -9,7 +9,6 @@ dependencies: - blas[build=mkl] - scipy<1.12 - joblib - - threadpoolctl - matplotlib - pandas - pyamg @@ -25,3 +24,4 @@ dependencies: - pip - pip: - cython + - threadpoolctl diff --git a/build_tools/azure/pylatest_conda_mkl_no_openmp_osx-64_conda.lock b/build_tools/azure/pylatest_conda_mkl_no_openmp_osx-64_conda.lock index a9c9b364426a7..7da0e513ac143 100644 --- a/build_tools/azure/pylatest_conda_mkl_no_openmp_osx-64_conda.lock +++ b/build_tools/azure/pylatest_conda_mkl_no_openmp_osx-64_conda.lock @@ -1,6 +1,6 @@ # Generated by conda-lock. # platform: osx-64 -# input_hash: d3fadab6d5d5d715660beb53286e2687b018b5195ef7ce47928bb79a96ee851b +# input_hash: e0d2cf2593df1f2c6969d68cf849136bee785b51f6cfc50ea1bdca2143d4a051 @EXPLICIT https://repo.anaconda.com/pkgs/main/osx-64/blas-1.0-mkl.conda#cb2c87e85ac8e0ceae776d26d4214c8a https://repo.anaconda.com/pkgs/main/osx-64/bzip2-1.0.8-h6c40b1e_5.conda#0f51dde96c82dcf58a788787fed4c5b9 @@ -56,7 +56,6 @@ https://repo.anaconda.com/pkgs/main/noarch/python-tzdata-2023.3-pyhd3eb1b0_0.con https://repo.anaconda.com/pkgs/main/osx-64/pytz-2023.3.post1-py312hecd8cb5_0.conda#2636382c9a424f69cbc36b1c5dc1f2fc https://repo.anaconda.com/pkgs/main/osx-64/setuptools-68.2.2-py312hecd8cb5_0.conda#64235f0c451427d86808c70c1c31cb8b https://repo.anaconda.com/pkgs/main/noarch/six-1.16.0-pyhd3eb1b0_1.conda#34586824d411d36af2fa40e799c172d0 -https://repo.anaconda.com/pkgs/main/noarch/threadpoolctl-2.2.0-pyh0d69192_0.conda#bbfdbae4934150b902f97daaf287efe2 https://repo.anaconda.com/pkgs/main/noarch/toml-0.10.2-pyhd3eb1b0_0.conda#cda05f5f6d8509529d1a2743288d197a https://repo.anaconda.com/pkgs/main/osx-64/tornado-6.3.3-py312h6c40b1e_0.conda#49173b5a36c9134865221f29d4a73fb6 https://repo.anaconda.com/pkgs/main/osx-64/unicodedata2-15.1.0-py312h6c40b1e_0.conda#65bd2cb787fc99662d9bb6e6520c5826 @@ -84,3 +83,4 @@ https://repo.anaconda.com/pkgs/main/osx-64/scipy-1.11.4-py312h81688c2_0.conda#7d https://repo.anaconda.com/pkgs/main/osx-64/pandas-2.2.1-py312he282a81_0.conda#021b70a1e40efb75b89eb8ebdb347132 https://repo.anaconda.com/pkgs/main/osx-64/pyamg-4.2.3-py312h44cbcf4_0.conda#3bdc7be74087b3a5a83c520a74e1e8eb # pip cython @ https://files.pythonhosted.org/packages/d5/6d/06c08d75adb98cdf72af18801e193d22580cc86ca553610f430f18ea26b3/Cython-3.0.10-cp312-cp312-macosx_10_9_x86_64.whl#sha256=8f2864ab5fcd27a346f0b50f901ebeb8f60b25a60a575ccfd982e7f3e9674914 +# pip threadpoolctl @ https://files.pythonhosted.org/packages/1e/84/ccd9b08653022b7785b6e3ee070ffb2825841e0dc119be22f0840b2b35cb/threadpoolctl-3.4.0-py3-none-any.whl#sha256=8f4c689a65b23e5ed825c8436a92b818aac005e0f3715f6a1664d7c7ee29d262 diff --git a/build_tools/azure/pylatest_pip_scipy_dev_linux-64_conda.lock b/build_tools/azure/pylatest_pip_scipy_dev_linux-64_conda.lock index b5f1d7613fa72..a4d18dc37716a 100644 --- a/build_tools/azure/pylatest_pip_scipy_dev_linux-64_conda.lock +++ b/build_tools/azure/pylatest_pip_scipy_dev_linux-64_conda.lock @@ -11,7 +11,7 @@ https://repo.anaconda.com/pkgs/main/linux-64/libstdcxx-ng-11.2.0-h1234567_1.cond https://repo.anaconda.com/pkgs/main/linux-64/_openmp_mutex-5.1-1_gnu.conda#71d281e9c2192cb3fa425655a8defb85 https://repo.anaconda.com/pkgs/main/linux-64/libgcc-ng-11.2.0-h1234567_1.conda#a87728dabf3151fb9cfa990bd2eb0464 https://repo.anaconda.com/pkgs/main/linux-64/bzip2-1.0.8-h5eee18b_5.conda#9c8dec113089c4aca7392c6a3864f505 -https://repo.anaconda.com/pkgs/main/linux-64/expat-2.5.0-h6a678d5_0.conda#9a21d99d49a0a556cf9590430dec8ec0 +https://repo.anaconda.com/pkgs/main/linux-64/expat-2.6.2-h6a678d5_0.conda#55049db2772dae035f6b8a95f72b5970 https://repo.anaconda.com/pkgs/main/linux-64/libffi-3.4.4-h6a678d5_0.conda#06e288f9250abef59b9a367d151fc339 https://repo.anaconda.com/pkgs/main/linux-64/libuuid-1.41.5-h5eee18b_0.conda#4a6a2354414c9080327274aa514e5299 https://repo.anaconda.com/pkgs/main/linux-64/ncurses-6.4-h6a678d5_0.conda#5558eec6e2191741a92f832ea826251c @@ -22,7 +22,7 @@ https://repo.anaconda.com/pkgs/main/linux-64/ccache-3.7.9-hfe4627d_0.conda#bef6f https://repo.anaconda.com/pkgs/main/linux-64/readline-8.2-h5eee18b_0.conda#be42180685cce6e6b0329201d9f48efb https://repo.anaconda.com/pkgs/main/linux-64/tk-8.6.12-h1ccaba5_0.conda#fa10ff4aa631fa4aa090a6234d7770b9 https://repo.anaconda.com/pkgs/main/linux-64/sqlite-3.41.2-h5eee18b_0.conda#c7086c9ceb6cfe1c4c729a774a2d88a5 -https://repo.anaconda.com/pkgs/main/linux-64/python-3.12.2-h996f2a0_0.conda#bc4748d0d26253c8499a3abeca289469 +https://repo.anaconda.com/pkgs/main/linux-64/python-3.12.3-h996f2a0_0.conda#77af2bd351a8311d1e780bcfa7819bb8 https://repo.anaconda.com/pkgs/main/linux-64/setuptools-68.2.2-py312h06a4308_0.conda#83ba634cde4f30d9e0b88e4ac9716ca4 https://repo.anaconda.com/pkgs/main/linux-64/wheel-0.41.2-py312h06a4308_0.conda#b2c4f82880d58d679f3982370d80c0e2 https://repo.anaconda.com/pkgs/main/linux-64/pip-23.3.1-py312h06a4308_0.conda#e1d44bca4a257e84af33503233491107 @@ -31,7 +31,7 @@ https://repo.anaconda.com/pkgs/main/linux-64/pip-23.3.1-py312h06a4308_0.conda#e1 # pip certifi @ https://files.pythonhosted.org/packages/ba/06/a07f096c664aeb9f01624f858c3add0a4e913d6c96257acb4fce61e7de14/certifi-2024.2.2-py3-none-any.whl#sha256=dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1 # pip charset-normalizer @ https://files.pythonhosted.org/packages/ee/fb/14d30eb4956408ee3ae09ad34299131fb383c47df355ddb428a7331cfa1e/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b # pip coverage @ https://files.pythonhosted.org/packages/98/79/185cb42910b6a2b2851980407c8445ac0da0750dff65e420e86f973c8396/coverage-7.4.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51 -# pip docutils @ https://files.pythonhosted.org/packages/26/87/f238c0670b94533ac0353a4e2a1a771a0cc73277b88bff23d3ae35a256c1/docutils-0.20.1-py3-none-any.whl#sha256=96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6 +# pip docutils @ https://files.pythonhosted.org/packages/ea/6a/1c934b70bc12be40be886060709317c2ad5a5fe2180469410bd9344f5235/docutils-0.21.1-py3-none-any.whl#sha256=14c8d34a55b46c88f9f714adb29cefbdd69fb82f3fef825e59c5faab935390d8 # pip execnet @ https://files.pythonhosted.org/packages/43/09/2aea36ff60d16dd8879bdb2f5b3ee0ba8d08cbbdcdfe870e695ce3784385/execnet-2.1.1-py3-none-any.whl#sha256=26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc # pip idna @ https://files.pythonhosted.org/packages/e5/3e/741d8c82801c347547f8a2a06aa57dbb1992be9e948df2ea0eda2c8b79e8/idna-3.7-py3-none-any.whl#sha256=82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0 # pip imagesize @ https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl#sha256=0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b @@ -41,7 +41,7 @@ https://repo.anaconda.com/pkgs/main/linux-64/pip-23.3.1-py312h06a4308_0.conda#e1 # pip ninja @ https://files.pythonhosted.org/packages/6d/92/8d7aebd4430ab5ff65df2bfee6d5745f95c004284db2d8ca76dcbfd9de47/ninja-1.11.1.1-py2.py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl#sha256=84502ec98f02a037a169c4b0d5d86075eaf6afc55e1879003d6cab51ced2ea4b # pip packaging @ https://files.pythonhosted.org/packages/49/df/1fceb2f8900f8639e278b056416d49134fb8d84c5942ffaa01ad34782422/packaging-24.0-py3-none-any.whl#sha256=2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5 # pip platformdirs @ https://files.pythonhosted.org/packages/55/72/4898c44ee9ea6f43396fbc23d9bfaf3d06e01b83698bdf2e4c919deceb7c/platformdirs-4.2.0-py3-none-any.whl#sha256=0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068 -# pip pluggy @ https://files.pythonhosted.org/packages/a5/5b/0cc789b59e8cc1bf288b38111d002d8c5917123194d45b29dcdac64723cc/pluggy-1.4.0-py3-none-any.whl#sha256=7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981 +# pip pluggy @ https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl#sha256=44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 # pip pygments @ https://files.pythonhosted.org/packages/97/9c/372fef8377a6e340b1704768d20daaded98bf13282b5327beb2e2fe2c7ef/pygments-2.17.2-py3-none-any.whl#sha256=b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c # pip six @ https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl#sha256=8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 # pip snowballstemmer @ https://files.pythonhosted.org/packages/ed/dc/c02e01294f7265e63a7315fe086dd1df7dacb9f840a804da846b96d01b96/snowballstemmer-2.2.0-py2.py3-none-any.whl#sha256=c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a @@ -55,13 +55,13 @@ https://repo.anaconda.com/pkgs/main/linux-64/pip-23.3.1-py312h06a4308_0.conda#e1 # pip threadpoolctl @ https://files.pythonhosted.org/packages/1e/84/ccd9b08653022b7785b6e3ee070ffb2825841e0dc119be22f0840b2b35cb/threadpoolctl-3.4.0-py3-none-any.whl#sha256=8f4c689a65b23e5ed825c8436a92b818aac005e0f3715f6a1664d7c7ee29d262 # pip urllib3 @ https://files.pythonhosted.org/packages/a2/73/a68704750a7679d0b6d3ad7aa8d4da8e14e151ae82e6fee774e6e0d05ec8/urllib3-2.2.1-py3-none-any.whl#sha256=450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d # pip jinja2 @ https://files.pythonhosted.org/packages/30/6d/6de6be2d02603ab56e72997708809e8a5b0fbfee080735109b40a3564843/Jinja2-3.1.3-py3-none-any.whl#sha256=7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa -# pip pyproject-metadata @ https://files.pythonhosted.org/packages/c4/cb/4678dfd70cd2f2d8969e571cdc1bb1e9293c698f8d1cf428fadcf48d6e9f/pyproject_metadata-0.7.1-py3-none-any.whl#sha256=28691fbb36266a819ec56c9fa1ecaf36f879d6944dfde5411e87fc4ff793aa60 +# pip pyproject-metadata @ https://files.pythonhosted.org/packages/aa/5f/bb5970d3d04173b46c9037109f7f05fc8904ff5be073ee49bb6ff00301bc/pyproject_metadata-0.8.0-py3-none-any.whl#sha256=ad858d448e1d3a1fb408ac5bac9ea7743e7a8bbb472f2693aaa334d2db42f526 # pip pytest @ https://files.pythonhosted.org/packages/51/ff/f6e8b8f39e08547faece4bd80f89d5a8de68a38b2d179cc1c4490ffa3286/pytest-7.4.4-py3-none-any.whl#sha256=b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8 # pip python-dateutil @ https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl#sha256=a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 # pip requests @ https://files.pythonhosted.org/packages/70/8e/0e2d847013cb52cd35b38c009bb167a1a26b2ce6cd6965bf26b47bc0bf44/requests-2.31.0-py3-none-any.whl#sha256=58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f -# pip meson-python @ https://files.pythonhosted.org/packages/1f/60/b10b11ab470a690d5777310d6cfd1c9bdbbb0a1313a78c34a1e82e0b9d27/meson_python-0.15.0-py3-none-any.whl#sha256=3ae38253ff02b2e947a05e362a2eaf5a9a09d133c5666b4123399ee5fbf2e591 +# pip meson-python @ https://files.pythonhosted.org/packages/91/c0/104cb6244c83fe6bc3886f144cc433db0c0c78efac5dc00e409a5a08c87d/meson_python-0.16.0-py3-none-any.whl#sha256=842dc9f5dc29e55fc769ff1b6fe328412fe6c870220fc321060a1d2d395e69e8 # pip pooch @ https://files.pythonhosted.org/packages/f4/72/8ae0f1ba4ce6a4f6d4d01a60a9fdf690fde188c45c1872b0b4ddb0607ace/pooch-1.8.1-py3-none-any.whl#sha256=6b56611ac320c239faece1ac51a60b25796792599ce5c0b1bb87bf01df55e0a9 # pip pytest-cov @ https://files.pythonhosted.org/packages/78/3a/af5b4fa5961d9a1e6237b530eb87dd04aea6eb83da09d2a4073d81b54ccf/pytest_cov-5.0.0-py3-none-any.whl#sha256=4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652 -# pip pytest-xdist @ https://files.pythonhosted.org/packages/50/37/125fe5ec459321e2d48a0c38672cfc2419ad87d580196fd894e5f25230b0/pytest_xdist-3.5.0-py3-none-any.whl#sha256=d075629c7e00b611df89f490a5063944bee7a4362a5ff11c7cc7824a03dfce24 -# pip sphinx @ https://files.pythonhosted.org/packages/b2/b6/8ed35256aa530a9d3da15d20bdc0ba888d5364441bb50a5a83ee7827affe/sphinx-7.2.6-py3-none-any.whl#sha256=1e09160a40b956dc623c910118fa636da93bd3ca0b9876a7b3df90f07d691560 +# pip pytest-xdist @ https://files.pythonhosted.org/packages/e0/df/585742c204227526baf641e45a95d3c0a94978b85a1414622c1017ebfcf8/pytest_xdist-3.6.0-py3-none-any.whl#sha256=958e08f38472e1b3a83450d8d3e682e90fdbffee39a97dd0f27185a3bd9074d1 +# pip sphinx @ https://files.pythonhosted.org/packages/b4/fa/130c32ed94cf270e3d0b9ded16fb7b2c8fea86fa7263c29a696a30c1dde7/sphinx-7.3.7-py3-none-any.whl#sha256=413f75440be4cacf328f580b4274ada4565fb2187d696a84970c23f77b64d8c3 # pip numpydoc @ https://files.pythonhosted.org/packages/f0/fa/dcfe0f65660661db757ee9ebd84e170ff98edd5d80235f62457d9088f85f/numpydoc-1.7.0-py3-none-any.whl#sha256=5a56419d931310d79a06cfc2a126d1558700feeb9b4f3d8dcae1a8134be829c9 diff --git a/build_tools/azure/pymin_conda_defaults_openblas_environment.yml b/build_tools/azure/pymin_conda_defaults_openblas_environment.yml index a422a0a539c53..17824c9b97074 100644 --- a/build_tools/azure/pymin_conda_defaults_openblas_environment.yml +++ b/build_tools/azure/pymin_conda_defaults_openblas_environment.yml @@ -9,7 +9,6 @@ dependencies: - blas[build=openblas] - scipy=1.7 - joblib - - threadpoolctl=2.2.0 - matplotlib=3.3.4 # min - pyamg - pytest<8 @@ -21,3 +20,4 @@ dependencies: - pip - pip: - cython==3.0.10 # min + - threadpoolctl diff --git a/build_tools/azure/pymin_conda_defaults_openblas_linux-64_conda.lock b/build_tools/azure/pymin_conda_defaults_openblas_linux-64_conda.lock index 00797394e4253..fef1871be2088 100644 --- a/build_tools/azure/pymin_conda_defaults_openblas_linux-64_conda.lock +++ b/build_tools/azure/pymin_conda_defaults_openblas_linux-64_conda.lock @@ -1,6 +1,6 @@ # Generated by conda-lock. # platform: linux-64 -# input_hash: 3d6bcb087065974114c1567c8dccd947a7376a7503b3514d82904299b651692d +# input_hash: 4f71ddcce93c9279161f5b016be417469aa5df726d06e4a1447c9270f60179e4 @EXPLICIT https://repo.anaconda.com/pkgs/main/linux-64/_libgcc_mutex-0.1-main.conda#c3473ff8bdb3d124ed5ff11ec380d6f9 https://repo.anaconda.com/pkgs/main/linux-64/blas-1.0-openblas.conda#9ddfcaef10d79366c90128f5dc444be8 @@ -74,7 +74,6 @@ https://repo.anaconda.com/pkgs/main/linux-64/pyparsing-3.0.9-py39h06a4308_0.cond https://repo.anaconda.com/pkgs/main/linux-64/pyqt5-sip-12.13.0-py39h5eee18b_0.conda#256840c3841b52346ea5743be8490ede https://repo.anaconda.com/pkgs/main/linux-64/setuptools-68.2.2-py39h06a4308_0.conda#5b42cae5548732ae5c167bb1066085de https://repo.anaconda.com/pkgs/main/noarch/six-1.16.0-pyhd3eb1b0_1.conda#34586824d411d36af2fa40e799c172d0 -https://repo.anaconda.com/pkgs/main/noarch/threadpoolctl-2.2.0-pyh0d69192_0.conda#bbfdbae4934150b902f97daaf287efe2 https://repo.anaconda.com/pkgs/main/noarch/toml-0.10.2-pyhd3eb1b0_0.conda#cda05f5f6d8509529d1a2743288d197a https://repo.anaconda.com/pkgs/main/linux-64/tomli-2.0.1-py39h06a4308_0.conda#b06dffe7ddca2645ed72f5116f0a087d https://repo.anaconda.com/pkgs/main/linux-64/tornado-6.3.3-py39h5eee18b_0.conda#9c4bd985bb8adcd12f47e790e95a9333 @@ -97,3 +96,4 @@ https://repo.anaconda.com/pkgs/main/linux-64/qt-main-5.15.2-h53bd1ea_10.conda#bd https://repo.anaconda.com/pkgs/main/linux-64/pyqt-5.15.10-py39h6a678d5_0.conda#52da5ff9b1144b078d2f41bab0b213f2 https://repo.anaconda.com/pkgs/main/linux-64/matplotlib-3.3.4-py39h06a4308_0.conda#384fc5e01ebfcf30e7161119d3029b5a # pip cython @ https://files.pythonhosted.org/packages/a7/f5/3dde4d96076888ceaa981827b098274c2b45ddd4b20d75a8cfaa92b91eec/Cython-3.0.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=651a15a8534ebfb9b58cb0b87c269c70984b6f9c88bfe65e4f635f0e3f07dfcd +# pip threadpoolctl @ https://files.pythonhosted.org/packages/1e/84/ccd9b08653022b7785b6e3ee070ffb2825841e0dc119be22f0840b2b35cb/threadpoolctl-3.4.0-py3-none-any.whl#sha256=8f4c689a65b23e5ed825c8436a92b818aac005e0f3715f6a1664d7c7ee29d262 diff --git a/build_tools/azure/pypy3_linux-64_conda.lock b/build_tools/azure/pypy3_linux-64_conda.lock index 23710cfe35cb8..342bd1af08406 100644 --- a/build_tools/azure/pypy3_linux-64_conda.lock +++ b/build_tools/azure/pypy3_linux-64_conda.lock @@ -4,23 +4,23 @@ @EXPLICIT https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2024.2.2-hbcca054_0.conda#2f4327a1cbe7f022401b236e915a5fef -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-13.2.0-h7e041cc_5.conda#f6f6600d18a4047b54f803cf708b868a +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-13.2.0-h95c4c6d_6.conda#3cfab3e709f77e9f1b3d380eb622494a https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.9-4_pypy39_pp73.conda#c1b2f29111681a4036ed21eaa3f44620 https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda#161081fc7cec0bfda0d86d7cb595f8d8 https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_kmp_llvm.tar.bz2#562b26ba2e19059551a811e72ab7f793 -https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-h807b86a_5.conda#d4ff227c46917d3b4565302a2bbb276b +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-hc881cc4_6.conda#df88796bd09a0d2ed292e59101478ad8 https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hd590300_5.conda#69b8b6202a07720f448be700e300ccf4 https://conda.anaconda.org/conda-forge/linux-64/lerc-4.0.0-h27087fc_0.tar.bz2#76bbff344f0134279f225174e9064c8f https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.1.0-hd590300_1.conda#aec6c91c7371c26392a06708a73c70e5 https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.20-hd590300_0.conda#8e88f9389f1165d7c0936fe40d9a9a79 https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.6.2-h59595ed_0.conda#e7ba12deb7020dd080c6c70e7b6f6a3d https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.2-h7f98852_5.tar.bz2#d645c6d2ac96843a2bfaccd2d62b3ac3 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-13.2.0-ha4646dd_5.conda#7a6bd7a12a4bd359e2afe6c0fa1acace +https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-13.2.0-h43f5ff8_6.conda#e54a5ddc67e673f9105cf2a2e9c070b0 https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.0.0-hd590300_1.conda#ea25936bb4080d843790b586850f82b8 -https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.3.2-hd590300_1.conda#049b7df8bae5e184d1de42cdf64855f8 +https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.4.0-hd590300_0.conda#b26e8aa824079e1be0294e7152ca4559 https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.2.13-hd590300_5.conda#f36c115f1ee199da648e0597ec2047ad https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.4.20240210-h59595ed_0.conda#97da8860a0da5413c7c98a3b3838a645 -https://conda.anaconda.org/conda-forge/linux-64/ninja-1.11.1-h924138e_0.conda#73a4953a2d9c115bdc10ff30a52f675f +https://conda.anaconda.org/conda-forge/linux-64/ninja-1.12.0-h00ab1b0_0.conda#b048701d52e7cbb5f59ddd4d3b17bbf5 https://conda.anaconda.org/conda-forge/linux-64/openssl-3.2.1-hd590300_1.conda#9d731343cff6ee2e5a25c4a091bf8e2a https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-h36c2ea0_1001.tar.bz2#22dad4df6e8630e8dff2428f6f6a7036 https://conda.anaconda.org/conda-forge/linux-64/xorg-kbproto-1.0.7-h7f98852_1002.tar.bz2#4b230e8381279d76131116660f5a241a @@ -32,9 +32,9 @@ https://conda.anaconda.org/conda-forge/linux-64/xz-5.2.6-h166bdaf_0.tar.bz2#2161 https://conda.anaconda.org/conda-forge/linux-64/expat-2.6.2-h59595ed_0.conda#53fb86322bdb89496d7579fe3f02fd61 https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.1.0-hd590300_1.conda#f07002e225d7a60a694d42a7bf5ff53f https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.1.0-hd590300_1.conda#5fc11c6020d421960607d821310fcd4d -https://conda.anaconda.org/conda-forge/linux-64/libgfortran-ng-13.2.0-h69a702a_5.conda#e73e9cfd1191783392131e6238bdb3e9 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-ng-13.2.0-h69a702a_6.conda#3666a850342f8f3be88f9a93d948d027 https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.43-h2797004_0.conda#009981dd9cfcaa4dbfa25ffaed86bcae -https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.45.2-h2797004_0.conda#866983a220e27a80cb75e85cb30466a1 +https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.45.3-h2797004_0.conda#b3316cbe90249da4f8e84cd66e1cc55b https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.15-h0b41bf4_0.conda#33277193f5b92bad9fdd230eb700929c https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda#47d31b792659ce70f470b5c82fdfb7a4 https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda#d453b98d9c83e71da0741bb0ff4d76bc @@ -47,7 +47,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libhiredis-1.0.2-h2cc385e_0.tar. https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.27-pthreads_h413a1c8_0.conda#a356024784da6dfd4683dc5ecf45b155 https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.6.0-h1dd3fc0_3.conda#66f03896ffbe1a110ffda05c7a856504 https://conda.anaconda.org/conda-forge/linux-64/llvm-openmp-18.1.3-h4dfa4b3_0.conda#d39965123dffcad4d750989be65bcb7c -https://conda.anaconda.org/conda-forge/linux-64/sqlite-3.45.2-h2c6b66d_0.conda#1423efca06ed343c1da0fc429bae0779 +https://conda.anaconda.org/conda-forge/linux-64/sqlite-3.45.3-h2c6b66d_0.conda#be7d70f2db41b674733667bdd69bd000 https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.9-h8ee46fc_0.conda#077b6e8ad6a3ddb741fce2496dd01bec https://conda.anaconda.org/conda-forge/linux-64/brotli-1.1.0-hd590300_1.conda#f27a24d46e3ea7b70a1f98e50c62508f https://conda.anaconda.org/conda-forge/linux-64/ccache-4.9.1-h1fcd64f_0.conda#3620f564bcf28c3524951b6f64f5c5ac @@ -72,7 +72,7 @@ https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyh9f0ad1d_0.tar.bz2 https://conda.anaconda.org/conda-forge/linux-64/numpy-1.26.4-py39h6dedee3_0.conda#557d64563e84ff21b14f586c7f662b7f https://conda.anaconda.org/conda-forge/noarch/packaging-24.0-pyhd8ed1ab_0.conda#248f521b64ce055e7feae3105e7abeb8 https://conda.anaconda.org/conda-forge/linux-64/pillow-10.3.0-py39h90a76f3_0.conda#799e6519cfffe2784db27b1db2ef33f3 -https://conda.anaconda.org/conda-forge/noarch/pluggy-1.4.0-pyhd8ed1ab_0.conda#139e9feb65187e916162917bb2484976 +https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_0.conda#d3483c8fc2dc2cc3f5cf43e26d60cabf https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.1.2-pyhd8ed1ab_0.conda#b9a4dacf97241704529131a0dfc0494f https://conda.anaconda.org/conda-forge/noarch/pypy-7.3.15-1_pypy39.conda#a418a6c16bd6f7ed56b92194214791a0 https://conda.anaconda.org/conda-forge/noarch/setuptools-69.5.1-pyhd8ed1ab_0.conda#7462280d81f639363e6e63c81276bd9e @@ -90,13 +90,13 @@ https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.4.0-pyhd8ed1 https://conda.anaconda.org/conda-forge/noarch/joblib-1.4.0-pyhd8ed1ab_0.conda#e0ed1bf13ce3a440e022157bf4764465 https://conda.anaconda.org/conda-forge/noarch/meson-1.4.0-pyhd8ed1ab_0.conda#52a0660cfa40b45bf254ecc3374cb2e0 https://conda.anaconda.org/conda-forge/noarch/pip-24.0-pyhd8ed1ab_0.conda#f586ac1e56c8638b64f9c8122a7b8a67 -https://conda.anaconda.org/conda-forge/noarch/pyproject-metadata-0.7.1-pyhd8ed1ab_0.conda#dcb27826ffc94d5f04e241322239983b +https://conda.anaconda.org/conda-forge/noarch/pyproject-metadata-0.8.0-pyhd8ed1ab_0.conda#573fe09d7bd0cd4bcc210d8369b5ca47 https://conda.anaconda.org/conda-forge/noarch/pytest-7.4.4-pyhd8ed1ab_0.conda#a9d145de8c5f064b5fa68fb34725d9f4 https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0-pyhd8ed1ab_0.conda#2cf4264fffb9e6eff6031c5b6884d61c https://conda.anaconda.org/conda-forge/linux-64/scipy-1.12.0-py39h6dedee3_2.conda#6c5d74bac41838f4377dfd45085e1fec https://conda.anaconda.org/conda-forge/linux-64/blas-2.122-openblas.conda#5065468105542a8b23ea47bd8b6fa55f https://conda.anaconda.org/conda-forge/noarch/importlib-resources-6.4.0-pyhd8ed1ab_0.conda#dcbadab7a68738a028e195ab68ab2d2e -https://conda.anaconda.org/conda-forge/noarch/meson-python-0.15.0-pyh0c530f3_0.conda#3bc64565ca78ce3bb80248d09926d8f9 +https://conda.anaconda.org/conda-forge/noarch/meson-python-0.16.0-pyh0c530f3_0.conda#e16f0dbf502da873be9f9adb0dc52547 https://conda.anaconda.org/conda-forge/linux-64/pyamg-5.1.0-py39h5fd064f_0.conda#04676d2a49da3cb608af77e04b796ce1 https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.5.0-pyhd8ed1ab_0.conda#d5f595da2daead898ca958ac62f0307b https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.8.4-py39h4e7d633_0.conda#58272019e595dde98d0844ae3ebf0cfe diff --git a/build_tools/azure/ubuntu_atlas_lock.txt b/build_tools/azure/ubuntu_atlas_lock.txt index c8e94493ccf28..f2110d2173a3c 100644 --- a/build_tools/azure/ubuntu_atlas_lock.txt +++ b/build_tools/azure/ubuntu_atlas_lock.txt @@ -35,7 +35,7 @@ pytest==7.4.4 # pytest-xdist pytest-xdist==3.5.0 # via -r build_tools/azure/ubuntu_atlas_requirements.txt -threadpoolctl==2.0.0 +threadpoolctl==3.1.0 # via -r build_tools/azure/ubuntu_atlas_requirements.txt tomli==2.0.1 # via diff --git a/build_tools/azure/ubuntu_atlas_requirements.txt b/build_tools/azure/ubuntu_atlas_requirements.txt index aab362dda0bf2..805d84d4d0aac 100644 --- a/build_tools/azure/ubuntu_atlas_requirements.txt +++ b/build_tools/azure/ubuntu_atlas_requirements.txt @@ -3,7 +3,7 @@ # build_tools/update_environments_and_lock_files.py cython==3.0.10 # min joblib==1.2.0 # min -threadpoolctl==2.0.0 # min +threadpoolctl==3.1.0 # min pytest<8 pytest-xdist ninja diff --git a/build_tools/circle/doc_linux-64_conda.lock b/build_tools/circle/doc_linux-64_conda.lock index 3f05468863ae6..60bbe8298c34c 100644 --- a/build_tools/circle/doc_linux-64_conda.lock +++ b/build_tools/circle/doc_linux-64_conda.lock @@ -318,5 +318,5 @@ https://conda.anaconda.org/conda-forge/noarch/sphinxext-opengraph-0.9.1-pyhd8ed1 # pip nbclient @ https://files.pythonhosted.org/packages/66/e8/00517a23d3eeaed0513e718fbc94aab26eaa1758f5690fc8578839791c79/nbclient-0.10.0-py3-none-any.whl#sha256=f13e3529332a1f1f81d82a53210322476a168bb7090a0289c795fe9cc11c9d3f # pip nbconvert @ https://files.pythonhosted.org/packages/23/8a/8d67cbd984739247e4b205c1143e2f71b25b4f71e180fe70f7cb2cf02633/nbconvert-7.16.3-py3-none-any.whl#sha256=ddeff14beeeedf3dd0bc506623e41e4507e551736de59df69a91f86700292b3b # pip jupyter-server @ https://files.pythonhosted.org/packages/07/46/6bb926b3bf878bf687b952fb6a4c09d014b4575a25960f2cd1a61793763f/jupyter_server-2.14.0-py3-none-any.whl#sha256=fb6be52c713e80e004fac34b35a0990d6d36ba06fd0a2b2ed82b899143a64210 -# pip jupyterlab-server @ https://files.pythonhosted.org/packages/6a/c9/b270a875916b18f137bb30ecd05031a2f05c95d47a8e8fbd3f805a72f593/jupyterlab_server-2.26.0-py3-none-any.whl#sha256=54622cbd330526a385ee0c1fdccdff3a1e7219bf3e864a335284a1270a1973df +# pip jupyterlab-server @ https://files.pythonhosted.org/packages/2f/b9/ed4ecad7cf1863a64920dc4c19b0376628b5d6bd28d2ec1e00cbac4ba2fb/jupyterlab_server-2.27.1-py3-none-any.whl#sha256=f5e26156e5258b24d532c84e7c74cc212e203bff93eb856f81c24c16daeecc75 # pip jupyterlite-sphinx @ https://files.pythonhosted.org/packages/38/c9/5f1142c005cf8d75830b10029e53f074324bc85cfca1f1d0f22a207b771c/jupyterlite_sphinx-0.9.3-py3-none-any.whl#sha256=be6332d16490ea2fa90b78187a2c5e1c357195966a25741d60b1790346571041 diff --git a/build_tools/update_environments_and_lock_files.py b/build_tools/update_environments_and_lock_files.py index fd77cfd3c0721..8a4a30d235e0b 100644 --- a/build_tools/update_environments_and_lock_files.py +++ b/build_tools/update_environments_and_lock_files.py @@ -141,7 +141,10 @@ def remove_from(alist, to_remove): "folder": "build_tools/azure", "platform": "osx-64", "channel": "defaults", - "conda_dependencies": remove_from(common_dependencies, ["cython"]) + ["ccache"], + "conda_dependencies": remove_from( + common_dependencies, ["cython", "threadpoolctl"] + ) + + ["ccache"], "package_constraints": { "blas": "[build=mkl]", # scipy 1.12.x crashes on this platform (https://github.com/scipy/scipy/pull/20086) @@ -149,9 +152,9 @@ def remove_from(alist, to_remove): # channel. "scipy": "<1.12", }, - # TODO: put cython back to conda dependencies when required version is - # available on the main channel - "pip_dependencies": ["cython"], + # TODO: put cython and threadpoolctl back to conda dependencies when required + # version is available on the main channel + "pip_dependencies": ["cython", "threadpoolctl"], }, { "name": "pymin_conda_defaults_openblas", @@ -161,7 +164,8 @@ def remove_from(alist, to_remove): "platform": "linux-64", "channel": "defaults", "conda_dependencies": remove_from( - common_dependencies, ["pandas", "cython", "pip", "ninja", "meson-python"] + common_dependencies, + ["pandas", "cython", "threadpoolctl", "pip", "ninja", "meson-python"], ) + ["ccache"], "package_constraints": { @@ -170,12 +174,11 @@ def remove_from(alist, to_remove): "numpy": "1.21", # the min version is not available on the defaults channel "scipy": "1.7", # the min version has some low level crashes "matplotlib": "min", - "threadpoolctl": "2.2.0", "cython": "min", }, - # TODO: put cython back to conda dependencies when required version is - # available on the main channel - "pip_dependencies": ["cython"], + # TODO: put cython and threadpoolctl back to conda dependencies when required + # version is available on the main channel + "pip_dependencies": ["cython", "threadpoolctl"], }, { "name": "pymin_conda_forge_openblas_ubuntu_2204", @@ -381,7 +384,7 @@ def remove_from(alist, to_remove): ], "package_constraints": { "joblib": "min", - "threadpoolctl": "2.2.0", + "threadpoolctl": "3.1.0", "pytest": "min", "pytest-cov": "min", # no pytest-xdist because it causes issue on 32bit diff --git a/pyproject.toml b/pyproject.toml index 828569ecc71ee..69d9702716cb5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ dependencies = [ "numpy>=1.19.5", "scipy>=1.6.0", "joblib>=1.2.0", - "threadpoolctl>=2.0.0", + "threadpoolctl>=3.1.0", ] requires-python = ">=3.9" license = {text = "new BSD"} @@ -45,7 +45,7 @@ tracker = "https://github.com/scikit-learn/scikit-learn/issues" [project.optional-dependencies] build = ["numpy>=1.19.5", "scipy>=1.6.0", "cython>=3.0.10", "meson-python>=0.15.0"] -install = ["numpy>=1.19.5", "scipy>=1.6.0", "joblib>=1.2.0", "threadpoolctl>=2.0.0"] +install = ["numpy>=1.19.5", "scipy>=1.6.0", "joblib>=1.2.0", "threadpoolctl>=3.1.0"] benchmark = ["matplotlib>=3.3.4", "pandas>=1.1.5", "memory_profiler>=0.57.0"] docs = [ "matplotlib>=3.3.4", diff --git a/sklearn/__init__.py b/sklearn/__init__.py index 30a3bbcdcbf66..45a26334a25f5 100644 --- a/sklearn/__init__.py +++ b/sklearn/__init__.py @@ -73,6 +73,15 @@ # We are not importing the rest of scikit-learn during the build # process, as it may not be compiled yet else: + # Import numpy, scipy to make sure that the BLAS libs are loaded before + # creating the ThreadpoolController. They would be imported just after + # when importing utils anyway. This makes it explicit and robust to changes + # in utils. + # (OpenMP is loaded by importing show_versions right after this block) + import numpy # noqa + import scipy.linalg # noqa + from threadpoolctl import ThreadpoolController + # `_distributor_init` allows distributors to run custom init code. # For instance, for the Windows wheel, this is used to pre-load the # vcomp shared library runtime for OpenMP embedded in the sklearn/.libs @@ -141,6 +150,12 @@ except ModuleNotFoundError: pass + # Set a global controller that can be used to locally limit the number of + # threads without looping through all shared libraries every time. + # This instantitation should not happen earlier because it needs all BLAS and + # OpenMP libs to be loaded first. + _threadpool_controller = ThreadpoolController() + def setup_module(module): """Fixture for the tests to assure globally controllable seeding of RNGs""" diff --git a/sklearn/_min_dependencies.py b/sklearn/_min_dependencies.py index a487a048c53c1..00315f31d4c3f 100644 --- a/sklearn/_min_dependencies.py +++ b/sklearn/_min_dependencies.py @@ -7,7 +7,7 @@ NUMPY_MIN_VERSION = "1.19.5" SCIPY_MIN_VERSION = "1.6.0" JOBLIB_MIN_VERSION = "1.2.0" -THREADPOOLCTL_MIN_VERSION = "2.0.0" +THREADPOOLCTL_MIN_VERSION = "3.1.0" PYTEST_MIN_VERSION = "7.1.2" CYTHON_MIN_VERSION = "3.0.10" diff --git a/sklearn/cluster/_kmeans.py b/sklearn/cluster/_kmeans.py index 16df12838d91f..2ab6f1e95563b 100644 --- a/sklearn/cluster/_kmeans.py +++ b/sklearn/cluster/_kmeans.py @@ -18,6 +18,7 @@ import numpy as np import scipy.sparse as sp +from .. import _threadpool_controller from ..base import ( BaseEstimator, ClassNamePrefixFeaturesOutMixin, @@ -31,7 +32,6 @@ from ..utils._openmp_helpers import _openmp_effective_n_threads from ..utils._param_validation import Interval, StrOptions, validate_params from ..utils.extmath import row_norms, stable_cumsum -from ..utils.fixes import threadpool_info, threadpool_limits from ..utils.sparsefuncs import mean_variance_axis from ..utils.sparsefuncs_fast import assign_rows_csr from ..utils.validation import ( @@ -622,6 +622,9 @@ def _kmeans_single_elkan( return labels, inertia, centers, i + 1 +# Threadpoolctl context to limit the number of threads in second level of +# nested parallelism (i.e. BLAS) to avoid oversubscription. +@_threadpool_controller.wrap(limits=1, user_api="blas") def _kmeans_single_lloyd( X, sample_weight, @@ -697,59 +700,56 @@ def _kmeans_single_lloyd( strict_convergence = False - # Threadpoolctl context to limit the number of threads in second level of - # nested parallelism (i.e. BLAS) to avoid oversubscription. - with threadpool_limits(limits=1, user_api="blas"): - for i in range(max_iter): - lloyd_iter( - X, - sample_weight, - centers, - centers_new, - weight_in_clusters, - labels, - center_shift, - n_threads, - ) + for i in range(max_iter): + lloyd_iter( + X, + sample_weight, + centers, + centers_new, + weight_in_clusters, + labels, + center_shift, + n_threads, + ) - if verbose: - inertia = _inertia(X, sample_weight, centers, labels, n_threads) - print(f"Iteration {i}, inertia {inertia}.") + if verbose: + inertia = _inertia(X, sample_weight, centers, labels, n_threads) + print(f"Iteration {i}, inertia {inertia}.") - centers, centers_new = centers_new, centers + centers, centers_new = centers_new, centers - if np.array_equal(labels, labels_old): - # First check the labels for strict convergence. + if np.array_equal(labels, labels_old): + # First check the labels for strict convergence. + if verbose: + 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}: strict convergence.") - strict_convergence = True + print( + f"Converged at iteration {i}: center shift " + f"{center_shift_tot} within tolerance {tol}." + ) 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 + labels_old[:] = labels - if not strict_convergence: - # rerun E-step so that predicted labels match cluster centers - lloyd_iter( - X, - sample_weight, - centers, - centers, - weight_in_clusters, - labels, - center_shift, - n_threads, - update_centers=False, - ) + if not strict_convergence: + # rerun E-step so that predicted labels match cluster centers + lloyd_iter( + X, + sample_weight, + centers, + centers, + weight_in_clusters, + labels, + center_shift, + n_threads, + update_centers=False, + ) inertia = _inertia(X, sample_weight, centers, labels, n_threads) @@ -826,14 +826,10 @@ def _labels_inertia(X, sample_weight, centers, n_threads=1, return_inertia=True) return labels -def _labels_inertia_threadpool_limit( - X, sample_weight, centers, n_threads=1, return_inertia=True -): - """Same as _labels_inertia but in a threadpool_limits context.""" - with threadpool_limits(limits=1, user_api="blas"): - result = _labels_inertia(X, sample_weight, centers, n_threads, return_inertia) - - return result +# Same as _labels_inertia but in a threadpool_limits context. +_labels_inertia_threadpool_limit = _threadpool_controller.wrap( + limits=1, user_api="blas" +)(_labels_inertia) class _BaseKMeans( @@ -926,7 +922,7 @@ def _check_mkl_vcomp(self, X, n_samples): n_active_threads = int(np.ceil(n_samples / CHUNK_SIZE)) if n_active_threads < self._n_threads: - modules = threadpool_info() + modules = _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)) @@ -2148,7 +2144,7 @@ def fit(self, X, y=None, sample_weight=None): n_steps = (self.max_iter * n_samples) // self._batch_size - with threadpool_limits(limits=1, user_api="blas"): + with _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 @@ -2274,7 +2270,7 @@ def partial_fit(self, X, y=None, sample_weight=None): # Initialize number of samples seen since last reassignment self._n_since_last_reassign = 0 - with threadpool_limits(limits=1, user_api="blas"): + with _threadpool_controller.limit(limits=1, user_api="blas"): _mini_batch_step( X, sample_weight=sample_weight, diff --git a/sklearn/cluster/tests/test_k_means.py b/sklearn/cluster/tests/test_k_means.py index b6f718dce03e0..c3a41a65de632 100644 --- a/sklearn/cluster/tests/test_k_means.py +++ b/sklearn/cluster/tests/test_k_means.py @@ -8,6 +8,7 @@ import pytest from scipy import sparse as sp +from sklearn import _threadpool_controller from sklearn.base import clone from sklearn.cluster import KMeans, MiniBatchKMeans, k_means, kmeans_plusplus from sklearn.cluster._k_means_common import ( @@ -31,7 +32,7 @@ create_memmap_backed_data, ) from sklearn.utils.extmath import row_norms -from sklearn.utils.fixes import CSR_CONTAINERS, threadpool_limits +from sklearn.utils.fixes import CSR_CONTAINERS # non centered, sparse centers to check the centers = np.array( @@ -967,13 +968,13 @@ def test_result_equal_in_diff_n_threads(Estimator, global_random_seed): rnd = np.random.RandomState(global_random_seed) X = rnd.normal(size=(50, 10)) - with threadpool_limits(limits=1, user_api="openmp"): + with _threadpool_controller.limit(limits=1, user_api="openmp"): result_1 = ( Estimator(n_clusters=n_clusters, random_state=global_random_seed) .fit(X) .labels_ ) - with threadpool_limits(limits=2, user_api="openmp"): + with _threadpool_controller.limit(limits=2, user_api="openmp"): result_2 = ( Estimator(n_clusters=n_clusters, random_state=global_random_seed) .fit(X) diff --git a/sklearn/metrics/_pairwise_distances_reduction/_argkmin.pyx.tp b/sklearn/metrics/_pairwise_distances_reduction/_argkmin.pyx.tp index a686153c3ac9c..ef61158fedca8 100644 --- a/sklearn/metrics/_pairwise_distances_reduction/_argkmin.pyx.tp +++ b/sklearn/metrics/_pairwise_distances_reduction/_argkmin.pyx.tp @@ -14,7 +14,7 @@ from numbers import Integral from scipy.sparse import issparse from ...utils import check_array, check_scalar from ...utils.fixes import _in_unstable_openblas_configuration -from ...utils.fixes import threadpool_limits +from ... import _threadpool_controller {{for name_suffix in ['64', '32']}} @@ -58,7 +58,7 @@ cdef class ArgKmin{{name_suffix}}(BaseDistancesReduction{{name_suffix}}): """ # Limit the number of threads in second level of nested parallelism for BLAS # to avoid threads over-subscription (in DOT or GEMM for instance). - with threadpool_limits(limits=1, user_api='blas'): + with _threadpool_controller.limit(limits=1, user_api='blas'): if metric in ("euclidean", "sqeuclidean"): # Specialized implementation of ArgKmin for the Euclidean distance # for the dense-dense and sparse-sparse cases. diff --git a/sklearn/metrics/_pairwise_distances_reduction/_argkmin_classmode.pyx.tp b/sklearn/metrics/_pairwise_distances_reduction/_argkmin_classmode.pyx.tp index f9719f6959dfc..b875499f44ed4 100644 --- a/sklearn/metrics/_pairwise_distances_reduction/_argkmin_classmode.pyx.tp +++ b/sklearn/metrics/_pairwise_distances_reduction/_argkmin_classmode.pyx.tp @@ -4,10 +4,10 @@ from libcpp.map cimport map as cpp_map, pair as cpp_pair from libc.stdlib cimport free from ...utils._typedefs cimport intp_t, float64_t +from ... import _threadpool_controller import numpy as np from scipy.sparse import issparse -from sklearn.utils.fixes import threadpool_limits from ._classmode cimport WeightingStrategy {{for name_suffix in ["32", "64"]}} @@ -66,7 +66,7 @@ cdef class ArgKminClassMode{{name_suffix}}(ArgKmin{{name_suffix}}): # Limit the number of threads in second level of nested parallelism for BLAS # to avoid threads over-subscription (in GEMM for instance). - with threadpool_limits(limits=1, user_api="blas"): + with _threadpool_controller.limit(limits=1, user_api="blas"): if pda.execute_in_parallel_on_Y: pda._parallel_on_Y() else: diff --git a/sklearn/metrics/_pairwise_distances_reduction/_radius_neighbors.pyx.tp b/sklearn/metrics/_pairwise_distances_reduction/_radius_neighbors.pyx.tp index dcc97b4d32fd4..f4af378062bdc 100644 --- a/sklearn/metrics/_pairwise_distances_reduction/_radius_neighbors.pyx.tp +++ b/sklearn/metrics/_pairwise_distances_reduction/_radius_neighbors.pyx.tp @@ -17,7 +17,7 @@ from numbers import Real from scipy.sparse import issparse from ...utils import check_array, check_scalar from ...utils.fixes import _in_unstable_openblas_configuration -from ...utils.fixes import threadpool_limits +from ... import _threadpool_controller cnp.import_array() @@ -110,7 +110,7 @@ cdef class RadiusNeighbors{{name_suffix}}(BaseDistancesReduction{{name_suffix}}) # Limit the number of threads in second level of nested parallelism for BLAS # to avoid threads over-subscription (in GEMM for instance). - with threadpool_limits(limits=1, user_api="blas"): + with _threadpool_controller.limit(limits=1, user_api="blas"): if pda.execute_in_parallel_on_Y: pda._parallel_on_Y() else: diff --git a/sklearn/metrics/_pairwise_distances_reduction/_radius_neighbors_classmode.pyx.tp b/sklearn/metrics/_pairwise_distances_reduction/_radius_neighbors_classmode.pyx.tp index 25067b43cd20c..ab12d7904c7fd 100644 --- a/sklearn/metrics/_pairwise_distances_reduction/_radius_neighbors_classmode.pyx.tp +++ b/sklearn/metrics/_pairwise_distances_reduction/_radius_neighbors_classmode.pyx.tp @@ -8,7 +8,7 @@ from ...utils._typedefs cimport intp_t, float64_t import numpy as np from scipy.sparse import issparse -from ...utils.fixes import threadpool_limits +from ... import _threadpool_controller {{for name_suffix in ["32", "64"]}} @@ -60,7 +60,7 @@ cdef class RadiusNeighborsClassMode{{name_suffix}}(RadiusNeighbors{{name_suffix} # Limit the number of threads in second level of nested parallelism for BLAS # to avoid threads over-subscription (in GEMM for instance). - with threadpool_limits(limits=1, user_api="blas"): + with _threadpool_controller.limit(limits=1, user_api="blas"): if pda.execute_in_parallel_on_Y: pda._parallel_on_Y() else: diff --git a/sklearn/metrics/tests/test_pairwise_distances_reduction.py b/sklearn/metrics/tests/test_pairwise_distances_reduction.py index e5983f9273d94..95dfa98178ee7 100644 --- a/sklearn/metrics/tests/test_pairwise_distances_reduction.py +++ b/sklearn/metrics/tests/test_pairwise_distances_reduction.py @@ -5,9 +5,9 @@ import numpy as np import pytest -import threadpoolctl from scipy.spatial.distance import cdist +from sklearn import _threadpool_controller from sklearn.metrics import euclidean_distances, pairwise_distances from sklearn.metrics._pairwise_distances_reduction import ( ArgKmin, @@ -1200,7 +1200,7 @@ def test_n_threads_agnosticism( **compute_parameters, ) - with threadpoolctl.threadpool_limits(limits=1, user_api="openmp"): + with _threadpool_controller.limit(limits=1, user_api="openmp"): dist, indices = Dispatcher.compute( X, Y, diff --git a/sklearn/utils/_show_versions.py b/sklearn/utils/_show_versions.py index 1431108477263..cc17b71b23799 100644 --- a/sklearn/utils/_show_versions.py +++ b/sklearn/utils/_show_versions.py @@ -9,8 +9,9 @@ import platform import sys +from threadpoolctl import threadpool_info + from .. import __version__ -from ..utils.fixes import threadpool_info from ._openmp_helpers import _openmp_parallelism_enabled diff --git a/sklearn/utils/fixes.py b/sklearn/utils/fixes.py index e33519a3154d8..21e62150b0356 100644 --- a/sklearn/utils/fixes.py +++ b/sklearn/utils/fixes.py @@ -18,7 +18,6 @@ import scipy import scipy.sparse.linalg import scipy.stats -import threadpoolctl import sklearn @@ -97,42 +96,6 @@ def _percentile(a, q, *, method="linear", **kwargs): from numpy import percentile # type: ignore # noqa -# compatibility fix for threadpoolctl >= 3.0.0 -# since version 3 it's possible to setup a global threadpool controller to avoid -# looping through all loaded shared libraries each time. -# the global controller is created during the first call to threadpoolctl. -def _get_threadpool_controller(): - if not hasattr(threadpoolctl, "ThreadpoolController"): - return None - - if not hasattr(sklearn, "_sklearn_threadpool_controller"): - sklearn._sklearn_threadpool_controller = threadpoolctl.ThreadpoolController() - - return sklearn._sklearn_threadpool_controller - - -def threadpool_limits(limits=None, user_api=None): - controller = _get_threadpool_controller() - if controller is not None: - return controller.limit(limits=limits, user_api=user_api) - else: - return threadpoolctl.threadpool_limits(limits=limits, user_api=user_api) - - -threadpool_limits.__doc__ = threadpoolctl.threadpool_limits.__doc__ - - -def threadpool_info(): - controller = _get_threadpool_controller() - if controller is not None: - return controller.info() - else: - return threadpoolctl.threadpool_info() - - -threadpool_info.__doc__ = threadpoolctl.threadpool_info.__doc__ - - # TODO: Remove when SciPy 1.11 is the minimum supported version def _mode(a, axis=0): if sp_version >= parse_version("1.9.0"): @@ -428,7 +391,7 @@ def _in_unstable_openblas_configuration(): import numpy # noqa import scipy # noqa - modules_info = threadpool_info() + modules_info = sklearn._threadpool_controller.info() open_blas_used = any(info["internal_api"] == "openblas" for info in modules_info) if not open_blas_used: diff --git a/sklearn/utils/tests/test_show_versions.py b/sklearn/utils/tests/test_show_versions.py index bd166dfd8e522..aade231e46f56 100644 --- a/sklearn/utils/tests/test_show_versions.py +++ b/sklearn/utils/tests/test_show_versions.py @@ -1,6 +1,7 @@ +from threadpoolctl import threadpool_info + from sklearn.utils._show_versions import _get_deps_info, _get_sys_info, show_versions from sklearn.utils._testing import ignore_warnings -from sklearn.utils.fixes import threadpool_info def test_get_sys_info(): From 684e499477ae1f4d067b45fd227c870c8cd59009 Mon Sep 17 00:00:00 2001 From: Gyeongjae Choi Date: Wed, 24 Apr 2024 23:18:40 +0900 Subject: [PATCH 057/344] MAINT Replace PYODIDE_PACKAGE_ABI with PYODIDE (#28880) Co-authored-by: Olivier Grisel --- sklearn/_build_utils/openmp_helpers.py | 2 +- sklearn/_build_utils/pre_build_helpers.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sklearn/_build_utils/openmp_helpers.py b/sklearn/_build_utils/openmp_helpers.py index ed9bf0ea3eea0..66e6089e33fef 100644 --- a/sklearn/_build_utils/openmp_helpers.py +++ b/sklearn/_build_utils/openmp_helpers.py @@ -34,7 +34,7 @@ def get_openmp_flag(): def check_openmp_support(): """Check whether OpenMP test code can be compiled and run""" - if "PYODIDE_PACKAGE_ABI" in os.environ: + if "PYODIDE" in os.environ: # Pyodide doesn't support OpenMP return False diff --git a/sklearn/_build_utils/pre_build_helpers.py b/sklearn/_build_utils/pre_build_helpers.py index b73fa8658739f..8de9b562d916b 100644 --- a/sklearn/_build_utils/pre_build_helpers.py +++ b/sklearn/_build_utils/pre_build_helpers.py @@ -60,7 +60,7 @@ def compile_test_program(code, extra_preargs=None, extra_postargs=None): def basic_check_build(): """Check basic compilation and linking of C code""" - if "PYODIDE_PACKAGE_ABI" in os.environ: + if "PYODIDE" in os.environ: # The following check won't work in pyodide return From 2a2643f583812c9095f815564b1b346279c00a72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Dock=C3=A8s?= Date: Wed, 24 Apr 2024 20:07:47 +0200 Subject: [PATCH 058/344] ENH improve the remainder index dtype to be consistent with transformers (#27657) Co-authored-by: Guillaume Lemaitre Co-authored-by: jeremiedbb --- doc/whats_new/v1.5.rst | 5 + sklearn/compose/_column_transformer.py | 187 +++++++++++++++++- .../compose/tests/test_column_transformer.py | 163 ++++++++++++--- sklearn/utils/_indexing.py | 5 +- 4 files changed, 333 insertions(+), 27 deletions(-) diff --git a/doc/whats_new/v1.5.rst b/doc/whats_new/v1.5.rst index e60846cb33a7e..5364b6a914bd7 100644 --- a/doc/whats_new/v1.5.rst +++ b/doc/whats_new/v1.5.rst @@ -178,6 +178,11 @@ Changelog being explicitly set as well. :pr:`28483` by :user:`Stefanie Senger `. +- |Enhancement| :class:`compose.ColumnTransformer` can now expose the "remainder" + columns in the fitted `transformers_` attribute as column names or boolean + masks, rather than column indices. + :pr:`27657` by :user:`Jérôme Dockès `. + - |Fix| Fixed an bug in :class:`compose.ColumnTransformer` with `n_jobs > 1`, where the intermediate selected columns were passed to the transformers as read-only arrays. :pr:`28822` by :user:`Jérémie du Boisberranger `. diff --git a/sklearn/compose/_column_transformer.py b/sklearn/compose/_column_transformer.py index 364cdbe932197..e594df3da92e7 100644 --- a/sklearn/compose/_column_transformer.py +++ b/sklearn/compose/_column_transformer.py @@ -8,7 +8,7 @@ # Joris Van den Bossche # License: BSD import warnings -from collections import Counter +from collections import Counter, UserList from itertools import chain from numbers import Integral, Real @@ -20,7 +20,7 @@ from ..preprocessing import FunctionTransformer from ..utils import Bunch from ..utils._estimator_html_repr import _VisualBlock -from ..utils._indexing import _get_column_indices +from ..utils._indexing import _determine_key_type, _get_column_indices from ..utils._metadata_requests import METHODS from ..utils._param_validation import HasMethods, Hidden, Interval, StrOptions from ..utils._set_output import ( @@ -143,6 +143,23 @@ class ColumnTransformer(TransformerMixin, _BaseComposition): .. versionadded:: 1.0 + 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. + + .. versionadded:: 1.5 + + .. versionchanged:: 1.7 + The default value for `force_int_remainder_cols` will change from + `True` to `False` in version 1.7. + Attributes ---------- transformers_ : list @@ -157,6 +174,17 @@ class ColumnTransformer(TransformerMixin, _BaseComposition): ``len(transformers_)==len(transformers)+1``, otherwise ``len(transformers_)==len(transformers)``. + .. versionchanged:: 1.5 + 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 @@ -256,6 +284,7 @@ class ColumnTransformer(TransformerMixin, _BaseComposition): "transformer_weights": [dict, None], "verbose": ["verbose"], "verbose_feature_names_out": ["boolean"], + "force_int_remainder_cols": ["boolean"], } def __init__( @@ -268,6 +297,7 @@ def __init__( transformer_weights=None, verbose=False, verbose_feature_names_out=True, + force_int_remainder_cols=True, ): self.transformers = transformers self.remainder = remainder @@ -276,6 +306,7 @@ def __init__( self.transformer_weights = transformer_weights self.verbose = verbose self.verbose_feature_names_out = verbose_feature_names_out + self.force_int_remainder_cols = force_int_remainder_cols @property def _transformers(self): @@ -429,6 +460,14 @@ def _iter(self, fitted, column_as_labels, skip_drop, skip_empty_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) + get_weight = (self.transformer_weights or {}).get for name, trans, columns in transformers: @@ -506,8 +545,30 @@ def _validate_remainder(self, X): """ cols = set(chain(*self._transformer_to_input_indices.values())) remaining = sorted(set(range(self.n_features_in_)) - cols) - self._remainder = ("remainder", self.remainder, remaining) 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)) + except ValueError: + # _determine_key_type raises a ValueError if some transformer + # columns are Callables + return "int" + return "int" + + 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): @@ -662,7 +723,7 @@ def _update_fitted_transformers(self, transformers): # sanity check that transformers is exhausted assert not list(fitted_transformers) - self.transformers_ = transformers_ + self.transformers_ = _with_dtype_warning_enabled_set_to(True, transformers_) def _validate_output(self, result): """ @@ -1278,6 +1339,7 @@ def make_column_transformer( n_jobs=None, verbose=False, verbose_feature_names_out=True, + force_int_remainder_cols=True, ): """Construct a ColumnTransformer from the given transformers. @@ -1350,6 +1412,23 @@ def make_column_transformer( .. versionadded:: 1.0 + 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 + :attr:`ColumnTransformer.transformers_` attribute for details. + + .. note:: + If you do not access the list of columns for the remainder columns + in the :attr:`ColumnTransformer.transformers_` fitted attribute, + you do not need to set this parameter. + + .. versionadded:: 1.5 + + .. versionchanged:: 1.7 + The default value for `force_int_remainder_cols` will change from + `True` to `False` in version 1.7. + Returns ------- ct : ColumnTransformer @@ -1383,6 +1462,7 @@ def make_column_transformer( sparse_threshold=sparse_threshold, verbose=verbose, verbose_feature_names_out=verbose_feature_names_out, + force_int_remainder_cols=force_int_remainder_cols, ) @@ -1473,3 +1553,102 @@ def __call__(self, df): 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. + + Parameters + ---------- + 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. + """ + + def __init__( + 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 + + def __getitem__(self, index): + 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. + warnings.warn( + ( + "\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)) + return result diff --git a/sklearn/compose/tests/test_column_transformer.py b/sklearn/compose/tests/test_column_transformer.py index d2c7b920d0e1d..d0f2274272230 100644 --- a/sklearn/compose/tests/test_column_transformer.py +++ b/sklearn/compose/tests/test_column_transformer.py @@ -5,6 +5,7 @@ import pickle import re import warnings +from unittest.mock import Mock import joblib import numpy as np @@ -18,6 +19,7 @@ make_column_selector, make_column_transformer, ) +from sklearn.compose._column_transformer import _RemainderColsList from sklearn.exceptions import NotFittedError from sklearn.feature_selection import VarianceThreshold from sklearn.preprocessing import ( @@ -788,6 +790,7 @@ def test_column_transformer_get_set_params(): "transformer_weights": None, "verbose_feature_names_out": True, "verbose": False, + "force_int_remainder_cols": True, } assert ct.get_params() == exp @@ -809,6 +812,7 @@ def test_column_transformer_get_set_params(): "transformer_weights": None, "verbose_feature_names_out": True, "verbose": False, + "force_int_remainder_cols": True, } assert ct.get_params() == exp @@ -938,38 +942,135 @@ def test_column_transformer_remainder(): assert ct.remainder == "drop" +# TODO(1.7): check for deprecated force_int_remainder_cols +# TODO(1.9): remove force_int but keep the test @pytest.mark.parametrize( - "key", [[0], np.array([0]), slice(0, 1), np.array([True, False])] + "cols1, cols2", + [ + ([0], [False, True, False]), # mix types + ([0], [1]), # ints + (lambda x: [0], lambda x: [1]), # callables + ], +) +@pytest.mark.parametrize("force_int", [False, True]) +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)) + + ct = make_column_transformer( + (Trans(), cols1), + (Trans(), cols2), + remainder="passthrough", + force_int_remainder_cols=force_int, + ) + with warnings.catch_warnings(): + warnings.simplefilter("error") + ct.fit_transform(X) + assert ct.transformers_[-1][-1][0] == 2 + + +# TODO(1.7): check for deprecated force_int_remainder_cols +# TODO(1.9): remove force_int but keep the test +@pytest.mark.parametrize( + "force_int, cols1, cols2, expected_cols", + [ + (True, ["A"], ["B"], [2]), + (False, ["A"], ["B"], ["C"]), + (True, [True, False, False], [False, True, False], [2]), + (False, [True, False, False], [False, True, False], [False, False, True]), + ], +) +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`. + """ + X = np.ones((1, 3)) + + if isinstance(cols1[0], str): + pd = pytest.importorskip("pandas") + 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 + ct = make_column_transformer( + (Trans(), cols1), + (Trans(), cols2), + remainder="passthrough", + force_int_remainder_cols=force_int, + ) + with warnings.catch_warnings(): + warnings.simplefilter("error") + ct.fit_transform(X) + + 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: + with warnings.catch_warnings(): + warnings.simplefilter("error") + 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]") + + +@pytest.mark.parametrize( + "key, expected_cols", + [ + ([0], [1]), + (np.array([0]), [1]), + (slice(0, 1), [1]), + (np.array([True, False]), [False, True]), + ], ) -def test_column_transformer_remainder_numpy(key): +def test_column_transformer_remainder_numpy(key, expected_cols): # test different ways that columns are specified with passthrough X_array = np.array([[0, 1, 2], [2, 4, 6]]).T X_res_both = X_array - ct = ColumnTransformer([("trans1", Trans(), key)], remainder="passthrough") + ct = ColumnTransformer( + [("trans1", Trans(), key)], + remainder="passthrough", + force_int_remainder_cols=False, + ) 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 assert ct.transformers_[-1][0] == "remainder" assert isinstance(ct.transformers_[-1][1], FunctionTransformer) - assert_array_equal(ct.transformers_[-1][2], [1]) + assert ct.transformers_[-1][2] == expected_cols @pytest.mark.parametrize( - "key", + "key, expected_cols", [ - [0], - slice(0, 1), - np.array([True, False]), - ["first"], - "pd-index", - np.array(["first"]), - np.array(["first"], dtype=object), - slice(None, "first"), - slice("first", "first"), + ([0], [1]), + (slice(0, 1), [1]), + (np.array([True, False]), [False, True]), + (["first"], ["second"]), + ("pd-index", ["second"]), + (np.array(["first"]), ["second"]), + (np.array(["first"], dtype=object), ["second"]), + (slice(None, "first"), ["second"]), + (slice("first", "first"), ["second"]), ], ) -def test_column_transformer_remainder_pandas(key): +def test_column_transformer_remainder_pandas(key, expected_cols): # test different ways that columns are specified with passthrough pd = pytest.importorskip("pandas") if isinstance(key, str) and key == "pd-index": @@ -979,33 +1080,47 @@ def test_column_transformer_remainder_pandas(key): X_df = pd.DataFrame(X_array, columns=["first", "second"]) X_res_both = X_array - ct = ColumnTransformer([("trans1", Trans(), key)], remainder="passthrough") + ct = ColumnTransformer( + [("trans1", Trans(), key)], + remainder="passthrough", + force_int_remainder_cols=False, + ) 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 len(ct.transformers_) == 2 assert ct.transformers_[-1][0] == "remainder" assert isinstance(ct.transformers_[-1][1], FunctionTransformer) - assert_array_equal(ct.transformers_[-1][2], [1]) + assert ct.transformers_[-1][2] == expected_cols @pytest.mark.parametrize( - "key", [[0], np.array([0]), slice(0, 1), np.array([True, False, False])] + "key, expected_cols", + [ + ([0], [1, 2]), + (np.array([0]), [1, 2]), + (slice(0, 1), [1, 2]), + (np.array([True, False, False]), [False, True, True]), + ], ) -def test_column_transformer_remainder_transformer(key): +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 - ct = ColumnTransformer([("trans1", Trans(), key)], remainder=DoubleTrans()) + ct = ColumnTransformer( + [("trans1", Trans(), key)], + remainder=DoubleTrans(), + force_int_remainder_cols=False, + ) 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 assert ct.transformers_[-1][0] == "remainder" assert isinstance(ct.transformers_[-1][1], DoubleTrans) - assert_array_equal(ct.transformers_[-1][2], [1, 2]) + assert ct.transformers_[-1][2] == expected_cols def test_column_transformer_no_remaining_remainder_transformer(): @@ -1100,6 +1215,7 @@ def test_column_transformer_get_set_params_with_remainder(): "transformer_weights": None, "verbose_feature_names_out": True, "verbose": False, + "force_int_remainder_cols": True, } assert ct.get_params() == exp @@ -1120,6 +1236,7 @@ def test_column_transformer_get_set_params_with_remainder(): "transformer_weights": None, "verbose_feature_names_out": True, "verbose": False, + "force_int_remainder_cols": True, } assert ct.get_params() == exp @@ -1476,7 +1593,9 @@ def test_sk_visual_block_remainder_fitted_pandas(remainder): pd = pytest.importorskip("pandas") ohe = OneHotEncoder() ct = ColumnTransformer( - transformers=[("ohe", ohe, ["col1", "col2"])], remainder=remainder + transformers=[("ohe", ohe, ["col1", "col2"])], + remainder=remainder, + force_int_remainder_cols=False, ) df = pd.DataFrame( { diff --git a/sklearn/utils/_indexing.py b/sklearn/utils/_indexing.py index 49e04a451dcb0..ca2327f2bb109 100644 --- a/sklearn/utils/_indexing.py +++ b/sklearn/utils/_indexing.py @@ -1,6 +1,7 @@ import numbers import sys import warnings +from collections import UserList from itertools import compress, islice import numpy as np @@ -142,7 +143,9 @@ def _determine_key_type(key, accept_slice=True): if key_start_type is not None: return key_start_type return key_stop_type - if isinstance(key, (list, tuple)): + # 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: From 171e124656a5d1d4fdbb41979e2b9da266fde9f4 Mon Sep 17 00:00:00 2001 From: Franck Charras Date: Thu, 25 Apr 2024 11:52:04 +0200 Subject: [PATCH 059/344] FEA: Ridge support for Array API compliant inputs (#27800) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Eric Lindgren Co-authored-by: Olivier Grisel Co-authored-by: Tim Head Co-authored-by: Loïc Estève Co-authored-by: Oleksii Kachaiev Co-authored-by: Julien Jerphanion Co-authored-by: Guillaume Lemaitre --- doc/modules/array_api.rst | 1 + doc/whats_new/v1.5.rst | 5 + sklearn/linear_model/_base.py | 77 +++++++---- sklearn/linear_model/_ridge.py | 159 +++++++++++++++++------ sklearn/linear_model/tests/test_ridge.py | 141 +++++++++++++++++++- sklearn/metrics/_regression.py | 11 +- sklearn/utils/_array_api.py | 82 +++++++++--- sklearn/utils/tests/test_array_api.py | 22 ++++ sklearn/utils/validation.py | 2 +- 9 files changed, 413 insertions(+), 87 deletions(-) diff --git a/doc/modules/array_api.rst b/doc/modules/array_api.rst index 6037d644d3f7d..7a21274a7250f 100644 --- a/doc/modules/array_api.rst +++ b/doc/modules/array_api.rst @@ -95,6 +95,7 @@ Estimators - :class:`decomposition.PCA` (with `svd_solver="full"`, `svd_solver="randomized"` and `power_iteration_normalizer="QR"`) +- :class:`linear_model.Ridge` (with `solver="svd"`) - :class:`discriminant_analysis.LinearDiscriminantAnalysis` (with `solver="svd"`) - :class:`preprocessing.KernelCenterer` - :class:`preprocessing.MaxAbsScaler` diff --git a/doc/whats_new/v1.5.rst b/doc/whats_new/v1.5.rst index 5364b6a914bd7..0e84202f876e4 100644 --- a/doc/whats_new/v1.5.rst +++ b/doc/whats_new/v1.5.rst @@ -72,6 +72,11 @@ See :ref:`array_api` for more details. **Classes:** +- :class:`linear_model.Ridge` now supports the Array API for the `svd` solver. + See :ref:`array_api` for more details. + :pr:`27800` by :user:`Franck Charras `, :user:`Olivier Grisel ` + and :user:`Tim Head `. + Support for building with Meson ------------------------------- diff --git a/sklearn/linear_model/_base.py b/sklearn/linear_model/_base.py index be8c9097332eb..eac754f3f88b4 100644 --- a/sklearn/linear_model/_base.py +++ b/sklearn/linear_model/_base.py @@ -33,7 +33,14 @@ _fit_context, ) from ..utils import check_array, check_random_state -from ..utils._array_api import get_namespace, indexing_dtype +from ..utils._array_api import ( + _asarray_with_order, + _average, + get_namespace, + get_namespace_and_device, + indexing_dtype, + supported_float_dtypes, +) from ..utils._seq_dataset import ( ArrayDataset32, ArrayDataset64, @@ -43,7 +50,7 @@ from ..utils.extmath import safe_sparse_dot from ..utils.parallel import Parallel, delayed from ..utils.sparsefuncs import mean_variance_axis -from ..utils.validation import FLOAT_DTYPES, _check_sample_weight, check_is_fitted +from ..utils.validation import _check_sample_weight, check_is_fitted # TODO: bayesian_ridge_regression and bayesian_regression_ard # should be squashed into its respective objects. @@ -155,43 +162,51 @@ def _preprocess_data( 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) + n_samples, n_features = X.shape + X_is_sparse = sp.issparse(X) + if isinstance(sample_weight, numbers.Number): sample_weight = None if sample_weight is not None: - sample_weight = np.asarray(sample_weight) + sample_weight = xp.asarray(sample_weight) if check_input: - X = check_array(X, copy=copy, accept_sparse=["csr", "csc"], dtype=FLOAT_DTYPES) + X = check_array( + 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 = y.astype(X.dtype, copy=copy_y) + y = xp.astype(y, X.dtype, copy=copy_y) if copy: - if sp.issparse(X): + if X_is_sparse: X = X.copy() else: - X = X.copy(order="K") + X = _asarray_with_order(X, order="K", copy=True, xp=xp) + + dtype_ = X.dtype if fit_intercept: - if sp.issparse(X): + if X_is_sparse: X_offset, X_var = mean_variance_axis(X, axis=0, weights=sample_weight) else: - X_offset = np.average(X, axis=0, weights=sample_weight) + X_offset = _average(X, axis=0, weights=sample_weight, xp=xp) - X_offset = X_offset.astype(X.dtype, copy=False) + X_offset = xp.astype(X_offset, X.dtype, copy=False) X -= X_offset - y_offset = np.average(y, axis=0, weights=sample_weight) + y_offset = _average(y, axis=0, weights=sample_weight, xp=xp) y -= y_offset else: - X_offset = np.zeros(X.shape[1], dtype=X.dtype) + X_offset = xp.zeros(n_features, dtype=X.dtype, device=device_) if y.ndim == 1: - y_offset = X.dtype.type(0) + y_offset = xp.asarray(0.0, dtype=dtype_, device=device_) else: - y_offset = np.zeros(y.shape[1], dtype=X.dtype) + 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 = np.ones(X.shape[1], dtype=X.dtype) + X_scale = xp.ones(n_features, dtype=X.dtype, device=device_) return X, y, X_offset, y_offset, X_scale @@ -224,8 +239,9 @@ def _rescale_data(X, y, sample_weight, inplace=False): """ # Assume that _validate_data and _check_sample_weight have been called by # the caller. + xp, _ = get_namespace(X, y, sample_weight) n_samples = X.shape[0] - sample_weight_sqrt = np.sqrt(sample_weight) + sample_weight_sqrt = xp.sqrt(sample_weight) if sp.issparse(X) or sp.issparse(y): sw_matrix = sparse.dia_matrix( @@ -236,9 +252,9 @@ def _rescale_data(X, y, sample_weight, inplace=False): X = safe_sparse_dot(sw_matrix, X) else: if inplace: - X *= sample_weight_sqrt[:, np.newaxis] + X *= sample_weight_sqrt[:, None] else: - X = X * sample_weight_sqrt[:, np.newaxis] + X = X * sample_weight_sqrt[:, None] if sp.issparse(y): y = safe_sparse_dot(sw_matrix, y) @@ -247,12 +263,12 @@ def _rescale_data(X, y, sample_weight, inplace=False): if y.ndim == 1: y *= sample_weight_sqrt else: - y *= sample_weight_sqrt[:, np.newaxis] + y *= sample_weight_sqrt[:, None] else: if y.ndim == 1: y = y * sample_weight_sqrt else: - y = y * sample_weight_sqrt[:, np.newaxis] + y = y * sample_weight_sqrt[:, None] return X, y, sample_weight_sqrt @@ -267,7 +283,11 @@ def _decision_function(self, X): check_is_fitted(self) X = self._validate_data(X, accept_sparse=["csr", "csc", "coo"], reset=False) - return safe_sparse_dot(X, self.coef_.T, dense_output=True) + self.intercept_ + coef_ = self.coef_ + if coef_.ndim == 1: + return X @ coef_ + self.intercept_ + else: + return X @ coef_.T + self.intercept_ def predict(self, X): """ @@ -287,11 +307,22 @@ def predict(self, X): def _set_intercept(self, X_offset, y_offset, X_scale): """Set the intercept_""" + + xp, _ = get_namespace(X_offset, y_offset, X_scale) + if self.fit_intercept: # We always want coef_.dtype=X.dtype. For instance, X.dtype can differ from # coef_.dtype if warm_start=True. - self.coef_ = np.divide(self.coef_, X_scale, dtype=X_scale.dtype) - self.intercept_ = y_offset - np.dot(X_offset, self.coef_.T) + coef_ = xp.astype(self.coef_, X_scale.dtype, copy=False) + coef_ = self.coef_ = xp.divide(coef_, X_scale) + + if coef_.ndim == 1: + intercept_ = y_offset - X_offset @ coef_ + else: + intercept_ = y_offset - X_offset @ coef_.T + + self.intercept_ = intercept_ + else: self.intercept_ = 0.0 diff --git a/sklearn/linear_model/_ridge.py b/sklearn/linear_model/_ridge.py index 71760efd056fe..2babc21631f8a 100644 --- a/sklearn/linear_model/_ridge.py +++ b/sklearn/linear_model/_ridge.py @@ -32,6 +32,13 @@ column_or_1d, compute_sample_weight, ) +from ..utils._array_api import ( + _is_numpy_namespace, + _ravel, + device, + get_namespace, + get_namespace_and_device, +) from ..utils._param_validation import Interval, StrOptions, validate_params from ..utils.extmath import row_norms, safe_sparse_dot from ..utils.fixes import _sparse_linalg_cg @@ -277,15 +284,16 @@ def _solve_cholesky_kernel(K, y, alpha, sample_weight=None, copy=False): return dual_coefs.T -def _solve_svd(X, y, alpha): - U, s, Vt = linalg.svd(X, full_matrices=False) +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][:, np.newaxis] - UTy = np.dot(U.T, y) - d = np.zeros((s.size, alpha.size), dtype=X.dtype) + 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 np.dot(Vt.T, d_UT_y).T + return (Vt.T @ d_UT_y).T def _solve_lbfgs( @@ -600,28 +608,29 @@ def _ridge_regression( random_state=None, return_n_iter=False, return_intercept=False, + return_solver=False, X_scale=None, X_offset=None, check_input=True, 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 - if solver == "auto": - if positive: - solver = "lbfgs" - elif return_intercept: - # sag supports fitting intercept directly - solver = "sag" - elif not sparse.issparse(X): - solver = "cholesky" - else: - solver = "sparse_cg" + solver = resolve_solver(solver, positive, return_intercept, X_is_sparse, xp) + + if is_numpy_namespace and not X_is_sparse: + X = np.asarray(X) - if solver not in ("sparse_cg", "cholesky", "svd", "lsqr", "sag", "saga", "lbfgs"): + if not is_numpy_namespace and solver != "svd": raise ValueError( - "Known solvers are 'sparse_cg', 'cholesky', 'svd'" - " 'lsqr', 'sag', 'saga' or 'lbfgs'. Got %s." % solver + f"Array API dispatch to namespace {xp.__name__} only supports " + f"solver 'svd'. Got '{solver}'." ) if positive and solver != "lbfgs": @@ -645,8 +654,8 @@ def _ridge_regression( ) if check_input: - _dtype = [np.float64, np.float32] - _accept_sparse = _get_valid_accept_sparse(sparse.issparse(X), solver) + _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) check_consistent_length(X, y) @@ -658,7 +667,7 @@ def _ridge_regression( ravel = False if y.ndim == 1: - y = y.reshape(-1, 1) + y = xp.reshape(y, (-1, 1)) ravel = True n_samples_, n_targets = y.shape @@ -679,7 +688,7 @@ def _ridge_regression( # 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, np.ndarray): + if alpha is not None and not isinstance(alpha, type(xp.asarray([0.0]))): alpha = check_scalar( alpha, "alpha", @@ -689,15 +698,17 @@ def _ridge_regression( ) # There should be either 1 or n_targets penalties - alpha = np.asarray(alpha, dtype=X.dtype).ravel() - if alpha.size not in [1, n_targets]: + alpha = _ravel(xp.asarray(alpha, device=device_, dtype=X.dtype), xp=xp) + if alpha.shape[0] not in [1, n_targets]: raise ValueError( "Number of targets and number of penalties do not correspond: %d != %d" - % (alpha.size, n_targets) + % (alpha.shape[0], n_targets) ) - if alpha.size == 1 and n_targets > 1: - alpha = np.repeat(alpha, 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": @@ -779,7 +790,6 @@ def _ridge_regression( if intercept.shape[0] == 1: intercept = intercept[0] - coef = np.asarray(coef) elif solver == "lbfgs": coef = _solve_lbfgs( @@ -795,22 +805,71 @@ def _ridge_regression( ) if solver == "svd": - if sparse.issparse(X): + if X_is_sparse: raise TypeError("SVD solver does not support sparse inputs currently") - coef = _solve_svd(X, y, alpha) + coef = _solve_svd(X, y, alpha, xp) if ravel: - # When y was passed as a 1d-array, we flatten the coefficients. - coef = coef.ravel() + coef = _ravel(coef) + + coef = xp.asarray(coef) if return_n_iter and return_intercept: - return coef, n_iter, intercept + res = coef, n_iter, intercept elif return_intercept: - return coef, intercept + res = coef, intercept elif return_n_iter: - return coef, n_iter + res = coef, n_iter else: - return coef + 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 + + is_numpy_namespace = _is_numpy_namespace(xp) + + auto_solver_np = resolve_solver_for_numpy(positive, return_intercept, is_sparse) + if is_numpy_namespace: + return auto_solver_np + + if positive: + raise ValueError( + "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: + warnings.warn( + 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): + if positive: + 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): @@ -852,6 +911,8 @@ def __init__( self.random_state = random_state def fit(self, X, y, sample_weight=None): + xp, is_array_api_compliant = get_namespace(X, y, sample_weight) + if self.solver == "lbfgs" and not self.positive: raise ValueError( "'lbfgs' solver can be used only when positive=True. " @@ -903,7 +964,7 @@ def fit(self, X, y, sample_weight=None): ) if solver == "sag" and sparse.issparse(X) and self.fit_intercept: - self.coef_, self.n_iter_, self.intercept_ = _ridge_regression( + self.coef_, self.n_iter_, self.intercept_, self.solver_ = _ridge_regression( X, y, alpha=self.alpha, @@ -915,6 +976,7 @@ def fit(self, X, y, sample_weight=None): random_state=self.random_state, return_n_iter=True, return_intercept=True, + return_solver=True, check_input=False, ) # add the offset which was subtracted by _preprocess_data @@ -928,7 +990,7 @@ def fit(self, X, y, sample_weight=None): # for dense matrices or when intercept is set to 0 params = {} - self.coef_, self.n_iter_ = _ridge_regression( + self.coef_, self.n_iter_, self.solver_ = _ridge_regression( X, y, alpha=self.alpha, @@ -940,6 +1002,7 @@ def fit(self, X, y, sample_weight=None): random_state=self.random_state, return_n_iter=True, return_intercept=False, + return_solver=True, check_input=False, fit_intercept=self.fit_intercept, **params, @@ -1095,6 +1158,12 @@ class Ridge(MultiOutputMixin, RegressorMixin, _BaseRidge): .. versionadded:: 1.0 + solver_ : str + The solver that was used at fit time by the computational + routines. + + .. versionadded:: 1.5 + See Also -------- RidgeClassifier : Ridge classifier. @@ -1168,16 +1237,20 @@ def fit(self, X, y, sample_weight=None): Fitted estimator. """ _accept_sparse = _get_valid_accept_sparse(sparse.issparse(X), self.solver) + xp, _ = get_namespace(X, y, sample_weight) X, y = self._validate_data( X, y, accept_sparse=_accept_sparse, - dtype=[np.float64, np.float32], + dtype=[xp.float64, xp.float32], multi_output=True, y_numeric=True, ) return super().fit(X, y, sample_weight=sample_weight) + def _more_tags(self): + return {"array_api_support": True} + class _RidgeClassifierMixin(LinearClassifierMixin): def _prepare_data(self, X, y, sample_weight, solver): @@ -1403,6 +1476,12 @@ class RidgeClassifier(_RidgeClassifierMixin, _BaseRidge): .. versionadded:: 1.0 + solver_ : str + The solver that was used at fit time by the computational + routines. + + .. versionadded:: 1.5 + See Also -------- Ridge : Ridge regression. diff --git a/sklearn/linear_model/tests/test_ridge.py b/sklearn/linear_model/tests/test_ridge.py index 187ef3a324741..f850fa5dcfa99 100644 --- a/sklearn/linear_model/tests/test_ridge.py +++ b/sklearn/linear_model/tests/test_ridge.py @@ -5,7 +5,8 @@ import pytest from scipy import linalg -from sklearn import datasets +from sklearn import config_context, datasets +from sklearn.base import clone from sklearn.datasets import ( make_classification, make_low_rank_matrix, @@ -40,6 +41,13 @@ ) from sklearn.preprocessing import minmax_scale from sklearn.utils import check_random_state +from sklearn.utils._array_api import ( + _NUMPY_NAMESPACE_NAMES, + _atol_for_type, + _convert_to_numpy, + yield_namespace_device_dtype_combinations, + yield_namespaces, +) from sklearn.utils._testing import ( assert_allclose, assert_almost_equal, @@ -47,6 +55,11 @@ assert_array_equal, ignore_warnings, ) +from sklearn.utils.estimator_checks import ( + _array_api_for_tests, + _get_check_estimator_ids, + check_array_api_input_and_values, +) from sklearn.utils.fixes import ( _IS_32BIT, COO_CONTAINERS, @@ -194,6 +207,8 @@ def test_ridge_regression(solver, fit_intercept, ols_ridge_dataset, global_rando assert_allclose(model.coef_, coef) assert model.score(X, y) == pytest.approx(R2_Ridge) + assert model.solver_ == solver + @pytest.mark.parametrize("solver", SOLVERS) @pytest.mark.parametrize("fit_intercept", [True, False]) @@ -1201,6 +1216,130 @@ def _test_tolerance(sparse_container): assert score >= score2 +def check_array_api_attributes(name, estimator, array_namespace, device, dtype_name): + xp = _array_api_for_tests(array_namespace, device) + + 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_ + + with config_context(array_api_dispatch=True): + 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 + + assert_allclose( + _convert_to_numpy(coef_xp, xp=xp), + coef_np, + atol=_atol_for_type(dtype_name), + ) + intercept_xp = estimator_xp.intercept_ + assert intercept_xp.shape == () + assert intercept_xp.dtype == X_iris_xp.dtype + + assert_allclose( + _convert_to_numpy(intercept_xp, xp=xp), + intercept_np, + atol=_atol_for_type(dtype_name), + ) + + +@pytest.mark.parametrize( + "array_namespace, device, dtype_name", yield_namespace_device_dtype_combinations() +) +@pytest.mark.parametrize( + "check", + [check_array_api_input_and_values, check_array_api_attributes], + ids=_get_check_estimator_ids, +) +@pytest.mark.parametrize( + "estimator", + [Ridge(solver="svd")], + ids=_get_check_estimator_ids, +) +def test_ridge_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) + + +@pytest.mark.parametrize( + "array_namespace", yield_namespaces(include_numpy_namespaces=False) +) +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") + expected_msg = ( + f"Array API dispatch to namespace {xp.__name__} only supports " + f"solver 'svd'. Got '{solver}'." + ) + + with pytest.raises(ValueError, match=expected_msg): + with config_context(array_api_dispatch=True): + ridge.fit(X_iris_xp, y_iris_xp) + + ridge = Ridge(solver="auto", positive=True) + expected_msg = ( + "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`." + ) + + with pytest.raises(ValueError, match=expected_msg): + with config_context(array_api_dispatch=True): + ridge.fit(X_iris_xp, y_iris_xp) + + ridge = Ridge() + expected_msg = ( + 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." + ) + with pytest.warns(UserWarning, match=expected_msg): + with config_context(array_api_dispatch=True): + ridge.fit(X_iris_xp, y_iris_xp) + + +@pytest.mark.parametrize("array_namespace", sorted(_NUMPY_NAMESPACE_NAMES)) +def test_array_api_numpy_namespace_no_warning(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]) + + ridge = Ridge() + expected_msg = ( + "Results might be different than when Array API dispatch is " + "disabled, or when a numpy-like namespace is used" + ) + + with warnings.catch_warnings(): + warnings.filterwarnings("error", message=expected_msg, category=UserWarning) + with config_context(array_api_dispatch=True): + ridge.fit(X_iris_xp, y_iris_xp) + + # All numpy namespaces are compatible with all solver, in particular + # solvers that support `positive=True` (like 'lbfgs') should work. + with config_context(array_api_dispatch=True): + Ridge(solver="auto", positive=True).fit(X_iris_xp, y_iris_xp) + + @pytest.mark.parametrize( "test_func", ( diff --git a/sklearn/metrics/_regression.py b/sklearn/metrics/_regression.py index ad5c76810f36a..b5605f18803ab 100644 --- a/sklearn/metrics/_regression.py +++ b/sklearn/metrics/_regression.py @@ -37,8 +37,9 @@ from ..utils._array_api import ( _average, _find_matching_floating_dtype, - device, get_namespace, + get_namespace_and_device, + size, ) from ..utils._param_validation import Hidden, Interval, StrOptions, validate_params from ..utils.stats import _weighted_percentile @@ -907,7 +908,7 @@ def _assemble_r2_explained_variance( avg_weights = multioutput result = _average(output_scores, weights=avg_weights) - if result.size == 1: + if size(result) == 1: return float(result) return result @@ -1194,9 +1195,9 @@ def r2_score( >>> r2_score(y_true, y_pred, force_finite=False) -inf """ - input_arrays = [y_true, y_pred, sample_weight, multioutput] - xp, _ = get_namespace(*input_arrays) - device_ = device(*input_arrays) + xp, _, device_ = get_namespace_and_device( + y_true, y_pred, sample_weight, multioutput + ) dtype = _find_matching_floating_dtype(y_true, y_pred, sample_weight, xp=xp) diff --git a/sklearn/utils/_array_api.py b/sklearn/utils/_array_api.py index 70e210e7e913e..7c3fd12ad4dee 100644 --- a/sklearn/utils/_array_api.py +++ b/sklearn/utils/_array_api.py @@ -13,10 +13,10 @@ _NUMPY_NAMESPACE_NAMES = {"numpy", "array_api_compat.numpy"} -def yield_namespace_device_dtype_combinations(include_numpy_namespaces=True): - """Yield supported namespace, device, dtype tuples for testing. +def yield_namespaces(include_numpy_namespaces=True): + """Yield supported namespace. - Use this to test that an estimator works with all combinations. + This is meant to be used for testing purposes only. Parameters ---------- @@ -27,14 +27,6 @@ def yield_namespace_device_dtype_combinations(include_numpy_namespaces=True): ------- array_namespace : str The name of the Array API namespace. - - 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 [ # The following is used to test the array_api_compat wrapper when @@ -50,6 +42,35 @@ def yield_namespace_device_dtype_combinations(include_numpy_namespaces=True): ]: 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. + + Parameters + ---------- + include_numpy_namespaces : bool, default=True + If True, also yield numpy namespaces. + + Returns + ------- + array_namespace : str + The name of the Array API namespace. + + 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") @@ -531,6 +552,20 @@ def get_namespace(*arrays, remove_none=True, remove_types=(str,), xp=None): return namespace, is_array_api_compliant +def get_namespace_and_device(*array_list, remove_none=True, remove_types=(str,)): + """Combination into one single function of `get_namespace` and `device`.""" + array_list = _remove_non_arrays( + *array_list, remove_none=remove_none, remove_types=remove_types + ) + + skip_remove_kwargs = dict(remove_none=False, remove_types=[]) + + return ( + *get_namespace(*array_list, **skip_remove_kwargs), + device(*array_list, **skip_remove_kwargs), + ) + + def _expit(X, xp=None): xp, _ = get_namespace(X, xp=xp) if _is_numpy_namespace(xp): @@ -588,10 +623,7 @@ def _average(a, axis=None, weights=None, normalize=True, xp=None): https://numpy.org/doc/stable/reference/generated/numpy.average.html but only for the common cases needed in scikit-learn. """ - input_arrays = [a, weights] - xp, _ = get_namespace(*input_arrays, xp=xp) - - device_ = device(*input_arrays) + xp, _, device_ = get_namespace_and_device(a, weights) if _is_numpy_namespace(xp): if normalize: @@ -690,7 +722,9 @@ def _nanmax(X, axis=None, xp=None): return X -def _asarray_with_order(array, dtype=None, order=None, copy=None, *, xp=None): +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, @@ -715,7 +749,21 @@ def _asarray_with_order(array, dtype=None, order=None, copy=None, *, xp=None): # container that is consistent with the input's namespace. return xp.asarray(array) else: - return xp.asarray(array, dtype=dtype, copy=copy) + 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. + """ + xp, _ = get_namespace(array, xp=xp) + if _is_numpy_namespace(xp): + array = numpy.asarray(array) + return xp.asarray(numpy.ravel(array, order="C")) + + return xp.reshape(array, shape=(-1,)) def _convert_to_numpy(array, xp): diff --git a/sklearn/utils/tests/test_array_api.py b/sklearn/utils/tests/test_array_api.py index ec4a487f0b1df..d0b368cd7fe91 100644 --- a/sklearn/utils/tests/test_array_api.py +++ b/sklearn/utils/tests/test_array_api.py @@ -14,9 +14,11 @@ _average, _convert_to_numpy, _estimator_with_converted_arrays, + _is_numpy_namespace, _nanmax, _nanmin, _NumPyAPIWrapper, + _ravel, device, get_namespace, indexing_dtype, @@ -348,6 +350,26 @@ def test_nan_reductions(library, X, reduction, expected): assert_allclose(result, expected) +@pytest.mark.parametrize( + "namespace, _device, _dtype", yield_namespace_device_dtype_combinations() +) +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) + with config_context(array_api_dispatch=True): + result = _ravel(array_xp) + + result = _convert_to_numpy(result, xp) + expected = numpy.ravel(array, order="C") + + assert_allclose(expected, result) + + if _is_numpy_namespace(xp): + assert numpy.asarray(result).flags["C_CONTIGUOUS"] + + @skip_if_array_api_compat_not_configured @pytest.mark.parametrize("library", ["cupy", "torch", "cupy.array_api"]) def test_convert_to_numpy_gpu(library): # pragma: nocover diff --git a/sklearn/utils/validation.py b/sklearn/utils/validation.py index d0a2fb098931f..5fac2ae6ae6c2 100644 --- a/sklearn/utils/validation.py +++ b/sklearn/utils/validation.py @@ -1303,7 +1303,7 @@ def _check_y(y, multi_output=False, y_numeric=False, estimator=None): y = column_or_1d(y, warn=True) _assert_all_finite(y, input_name="y", estimator_name=estimator_name) _ensure_no_complex_data(y) - if y_numeric and y.dtype.kind == "O": + if y_numeric and hasattr(y.dtype, "kind") and y.dtype.kind == "O": y = y.astype(np.float64) return y From db58c6f70efdcd26649d94e43f7d23f9e108c2e0 Mon Sep 17 00:00:00 2001 From: Xiao Yuan Date: Thu, 25 Apr 2024 19:54:27 +0800 Subject: [PATCH 060/344] DOC Add links to AgglomerativeClustering examples in docs and the user guide (#28885) --- doc/modules/clustering.rst | 9 ++++++++- sklearn/cluster/_agglomerative.py | 6 ++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/doc/modules/clustering.rst b/doc/modules/clustering.rst index 1d9fa51b6c834..ed27b369171e5 100644 --- a/doc/modules/clustering.rst +++ b/doc/modules/clustering.rst @@ -702,6 +702,9 @@ Single linkage can also perform well on non-globular data. * :ref:`sphx_glr_auto_examples_cluster_plot_digits_linkage.py`: exploration of the different linkage strategies in a real dataset. + * :ref:`sphx_glr_auto_examples_cluster_plot_linkage_comparison.py`: exploration of + the different linkage strategies in toy datasets. + Visualization of cluster hierarchy ---------------------------------- @@ -714,6 +717,10 @@ of the data, though more so in the case of small sample sizes. :target: ../auto_examples/cluster/plot_agglomerative_dendrogram.html :scale: 42 +.. topic:: Examples: + + * :ref:`sphx_glr_auto_examples_cluster_plot_agglomerative_dendrogram.py` + Adding connectivity constraints ------------------------------- @@ -1042,7 +1049,7 @@ scales by building an alternative representation of the clustering problem. .. topic:: Examples: * :ref:`sphx_glr_auto_examples_cluster_plot_hdbscan.py` - + Mutual Reachability Graph ------------------------- diff --git a/sklearn/cluster/_agglomerative.py b/sklearn/cluster/_agglomerative.py index fcecacc9ca57c..e5ba5f6efed61 100644 --- a/sklearn/cluster/_agglomerative.py +++ b/sklearn/cluster/_agglomerative.py @@ -846,6 +846,9 @@ class AgglomerativeClustering(ClusterMixin, BaseEstimator): .. versionadded:: 0.20 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 @@ -860,6 +863,9 @@ class AgglomerativeClustering(ClusterMixin, BaseEstimator): .. versionadded:: 0.24 + For an example of dendrogram visualization, see + :ref:`sphx_glr_auto_examples_cluster_plot_agglomerative_dendrogram.py`. + Attributes ---------- n_clusters_ : int From a05662eba7f252dc168eefab568ff224499c3b28 Mon Sep 17 00:00:00 2001 From: Tuhin Sharma Date: Thu, 25 Apr 2024 20:40:00 +0530 Subject: [PATCH 061/344] DOC add links to examples/linear_model/plot_bayesian_ridge_curvefit.py (#28876) Co-authored-by: adrinjalali --- sklearn/linear_model/_bayes.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sklearn/linear_model/_bayes.py b/sklearn/linear_model/_bayes.py index bf11f36cec599..a572c82e6e158 100644 --- a/sklearn/linear_model/_bayes.py +++ b/sklearn/linear_model/_bayes.py @@ -31,6 +31,9 @@ class BayesianRidge(RegressorMixin, LinearModel): lambda (precision of the weights) and alpha (precision of the noise). Read more in the :ref:`User Guide `. + 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`. Parameters ---------- From 4f9777ab89f67fff77dcfa2a93591f7857176625 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20du=20Boisberranger?= Date: Thu, 25 Apr 2024 17:31:50 +0200 Subject: [PATCH 062/344] Fix __array__ requires a copy param (#28893) --- sklearn/utils/estimator_checks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sklearn/utils/estimator_checks.py b/sklearn/utils/estimator_checks.py index daacac9398f96..59d371bad57cd 100644 --- a/sklearn/utils/estimator_checks.py +++ b/sklearn/utils/estimator_checks.py @@ -808,7 +808,7 @@ class _NotAnArray: def __init__(self, data): self.data = np.asarray(data) - def __array__(self, dtype=None): + def __array__(self, dtype=None, copy=None): return self.data def __array_function__(self, func, types, args, kwargs): From e2f91c9b7e70c06ba9a235b9424206779f17ae8f Mon Sep 17 00:00:00 2001 From: Tialo <65392801+Tialo@users.noreply.github.com> Date: Fri, 26 Apr 2024 12:31:20 +0300 Subject: [PATCH 063/344] DOC Add links to plot_document_clustering example (#26951) --- sklearn/feature_extraction/text.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/sklearn/feature_extraction/text.py b/sklearn/feature_extraction/text.py index 756a5222c437d..826b3bc7a6706 100644 --- a/sklearn/feature_extraction/text.py +++ b/sklearn/feature_extraction/text.py @@ -603,6 +603,10 @@ class HashingVectorizer( For an efficiency comparison of the different feature extractors, see :ref:`sphx_glr_auto_examples_text_plot_hashing_vs_dict_vectorizer.py`. + 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 `. Parameters @@ -1728,6 +1732,10 @@ class TfidfVectorizer(CountVectorizer): For an efficiency comparison of the different feature extractors, see :ref:`sphx_glr_auto_examples_text_plot_hashing_vs_dict_vectorizer.py`. + For an example of document clustering and comparison with + :class:`~sklearn.feature_extraction.text.HashingVectorizer`, see + :ref:`sphx_glr_auto_examples_text_plot_document_clustering.py`. + Read more in the :ref:`User Guide `. Parameters From 62818c3fd8070a712c09c67b1c998bd3177706cf Mon Sep 17 00:00:00 2001 From: Lucy Liu Date: Fri, 26 Apr 2024 22:42:35 +1000 Subject: [PATCH 064/344] DOC Fix `cosine_similarity` docstring return (#28896) --- sklearn/metrics/pairwise.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sklearn/metrics/pairwise.py b/sklearn/metrics/pairwise.py index 6b2ea04002682..d30c1775823a5 100644 --- a/sklearn/metrics/pairwise.py +++ b/sklearn/metrics/pairwise.py @@ -1651,7 +1651,7 @@ def cosine_similarity(X, Y=None, dense_output=True): Returns ------- - similarities : ndarray of shape (n_samples_X, n_samples_Y) + 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 bd8ca4764388623b3e4de811ef351f6b4caf9603 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20du=20Boisberranger?= Date: Fri, 26 Apr 2024 17:21:13 +0200 Subject: [PATCH 065/344] TST Make test_neighbors_metric robust to rng (#28888) --- sklearn/neighbors/tests/test_neighbors.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/sklearn/neighbors/tests/test_neighbors.py b/sklearn/neighbors/tests/test_neighbors.py index d3fc71478e6f5..3aac121f6b06b 100644 --- a/sklearn/neighbors/tests/test_neighbors.py +++ b/sklearn/neighbors/tests/test_neighbors.py @@ -1644,8 +1644,16 @@ def test_nearest_neighbors_validate_params(): + DISTANCE_METRIC_OBJS, ) def test_neighbors_metrics( - global_dtype, metric, n_samples=20, n_features=3, n_query_pts=2, n_neighbors=5 + global_dtype, + global_random_seed, + metric, + n_samples=20, + n_features=3, + n_query_pts=2, + n_neighbors=5, ): + rng = np.random.RandomState(global_random_seed) + metric = _parse_metric(metric, global_dtype) # Test computing the neighbors for various metrics @@ -1697,15 +1705,19 @@ def test_neighbors_metrics( brute_dst, brute_idx = results["brute"] ball_tree_dst, ball_tree_idx = results["ball_tree"] - assert_allclose(brute_dst, ball_tree_dst) + # 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) + 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) + assert_allclose(ball_tree_dst, kd_tree_dst, rtol=rtol) assert_array_equal(ball_tree_idx, kd_tree_idx) From f4cc02963559e4b7a335e97024010a8721c3dc26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20du=20Boisberranger?= Date: Fri, 26 Apr 2024 17:22:28 +0200 Subject: [PATCH 066/344] CI Fix wheel builder on osx (#28886) --- .github/workflows/wheels.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 1c4255706972f..d30f85ff3d1e6 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -90,16 +90,16 @@ jobs: manylinux_image: manylinux2014 # MacOS x86_64 - - os: macos-latest + - os: macos-12 python: 39 platform_id: macosx_x86_64 - - os: macos-latest + - os: macos-12 python: 310 platform_id: macosx_x86_64 - - os: macos-latest + - os: macos-12 python: 311 platform_id: macosx_x86_64 - - os: macos-latest + - os: macos-12 python: 312 platform_id: macosx_x86_64 From c35a719fe88bbffb34b5e099faab0b87f2eb1039 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20du=20Boisberranger?= Date: Fri, 26 Apr 2024 17:51:47 +0200 Subject: [PATCH 067/344] MAINT Clean up deprecations for 1.5: in AdditiveChi2Sampler (#28814) --- sklearn/kernel_approximation.py | 81 ++++++---------------- sklearn/tests/test_kernel_approximation.py | 13 ---- 2 files changed, 23 insertions(+), 71 deletions(-) diff --git a/sklearn/kernel_approximation.py b/sklearn/kernel_approximation.py index 3df9d318fe2c8..44bfb0b898913 100644 --- a/sklearn/kernel_approximation.py +++ b/sklearn/kernel_approximation.py @@ -27,7 +27,7 @@ _fit_context, ) from .metrics.pairwise import KERNEL_PARAMS, PAIRWISE_KERNEL_FUNCTIONS, pairwise_kernels -from .utils import check_random_state, deprecated +from .utils import check_random_state from .utils._param_validation import Interval, StrOptions from .utils.extmath import safe_sparse_dot from .utils.validation import ( @@ -600,13 +600,6 @@ class AdditiveChi2Sampler(TransformerMixin, BaseEstimator): Attributes ---------- - sample_interval_ : float - Stored sampling interval. Specified as a parameter if `sample_steps` - not in {1,2,3}. - - .. deprecated:: 1.3 - `sample_interval_` serves internal purposes only and will be removed in 1.5. - n_features_in_ : int Number of features seen during :term:`fit`. @@ -693,37 +686,14 @@ def fit(self, X, y=None): X = self._validate_data(X, accept_sparse="csr") check_non_negative(X, "X in AdditiveChi2Sampler.fit") - # TODO(1.5): remove the setting of _sample_interval from fit - if self.sample_interval is None: - # See figure 2 c) of "Efficient additive kernels via explicit feature maps" - # - # A. Vedaldi and A. Zisserman, Pattern Analysis and Machine Intelligence, - # 2011 - if self.sample_steps == 1: - self._sample_interval = 0.8 - elif self.sample_steps == 2: - self._sample_interval = 0.5 - elif self.sample_steps == 3: - self._sample_interval = 0.4 - else: - raise ValueError( - "If sample_steps is not in [1, 2, 3]," - " you need to provide sample_interval" - ) - else: - self._sample_interval = self.sample_interval + if self.sample_interval is None and self.sample_steps not in (1, 2, 3): + raise ValueError( + "If sample_steps is not in [1, 2, 3]," + " you need to provide sample_interval" + ) return self - # TODO(1.5): remove - @deprecated( # type: ignore - "The ``sample_interval_`` attribute was deprecated in version 1.3 and " - "will be removed 1.5." - ) - @property - def sample_interval_(self): - return self._sample_interval - def transform(self, X): """Apply approximate feature map to X. @@ -744,29 +714,24 @@ def transform(self, X): check_non_negative(X, "X in AdditiveChi2Sampler.transform") sparse = sp.issparse(X) - if hasattr(self, "_sample_interval"): - # TODO(1.5): remove this branch - sample_interval = self._sample_interval - - else: - if self.sample_interval is None: - # See figure 2 c) of "Efficient additive kernels via explicit feature maps" # noqa - # - # A. Vedaldi and A. Zisserman, Pattern Analysis and Machine Intelligence, # noqa - # 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: - raise ValueError( - "If sample_steps is not in [1, 2, 3]," - " you need to provide sample_interval" - ) + if self.sample_interval is None: + # See figure 2 c) of "Efficient additive kernels via explicit feature maps" # noqa + # + # A. Vedaldi and A. Zisserman, Pattern Analysis and Machine Intelligence, # noqa + # 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: - sample_interval = self.sample_interval + raise ValueError( + "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 diff --git a/sklearn/tests/test_kernel_approximation.py b/sklearn/tests/test_kernel_approximation.py index 170113fd0d768..a25baa45823ae 100644 --- a/sklearn/tests/test_kernel_approximation.py +++ b/sklearn/tests/test_kernel_approximation.py @@ -144,19 +144,6 @@ def test_additive_chi2_sampler_sample_steps(method, sample_steps): assert transformer.sample_interval == sample_interval -# TODO(1.5): remove -def test_additive_chi2_sampler_future_warnings(): - """Check that we raise a FutureWarning when accessing to `sample_interval_`.""" - transformer = AdditiveChi2Sampler() - transformer.fit(X) - msg = re.escape( - "The ``sample_interval_`` attribute was deprecated in version 1.3 and " - "will be removed 1.5." - ) - with pytest.warns(FutureWarning, match=msg): - assert transformer.sample_interval_ is not None - - @pytest.mark.parametrize("method", ["fit", "fit_transform", "transform"]) def test_additive_chi2_sampler_wrong_sample_steps(method): """Check that we raise a ValueError on invalid sample_steps""" From fa6ddba41249185199eb463741499b4016fad670 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20du=20Boisberranger?= Date: Fri, 26 Apr 2024 17:52:50 +0200 Subject: [PATCH 068/344] TST make sure test_pca_sparse passes on all random seeds (#28861) --- sklearn/decomposition/tests/test_pca.py | 44 ++++++++++++++----------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/sklearn/decomposition/tests/test_pca.py b/sklearn/decomposition/tests/test_pca.py index d099bf9a91e00..59401fd8742da 100644 --- a/sklearn/decomposition/tests/test_pca.py +++ b/sklearn/decomposition/tests/test_pca.py @@ -34,15 +34,18 @@ SPARSE_MAX_COMPONENTS = min(SPARSE_M, SPARSE_N) -def _check_fitted_pca_close(pca1, pca2, rtol): - assert_allclose(pca1.components_, pca2.components_, rtol=rtol) - assert_allclose(pca1.explained_variance_, pca2.explained_variance_, rtol=rtol) - assert_allclose(pca1.singular_values_, pca2.singular_values_, rtol=rtol) - assert_allclose(pca1.mean_, pca2.mean_, rtol=rtol) - assert_allclose(pca1.n_components_, pca2.n_components_, rtol=rtol) - assert_allclose(pca1.n_samples_, pca2.n_samples_, rtol=rtol) - assert_allclose(pca1.noise_variance_, pca2.noise_variance_, rtol=rtol) - assert_allclose(pca1.n_features_in_, pca2.n_features_in_, rtol=rtol) +def _check_fitted_pca_close(pca1, pca2, rtol=1e-7, atol=1e-12): + assert_allclose(pca1.components_, pca2.components_, rtol=rtol, atol=atol) + assert_allclose( + 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_ @pytest.mark.parametrize("svd_solver", PCA_SOLVERS) @@ -75,9 +78,12 @@ def test_pca(svd_solver, n_components): def test_pca_sparse( global_random_seed, svd_solver, sparse_container, n_components, density, scale ): - # Make sure any tolerance changes pass with SKLEARN_TESTS_GLOBAL_RANDOM_SEED="all" - rtol = 5e-07 - transform_rtol = 3e-05 + """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( @@ -108,7 +114,7 @@ def test_pca_sparse( pcad.fit(Xd) # Fitted attributes equality - _check_fitted_pca_close(pca, pcad, rtol=rtol) + _check_fitted_pca_close(pca, pcad, atol=atol) # Test transform X2 = sparse_container( @@ -121,8 +127,8 @@ def test_pca_sparse( ) X2d = X2.toarray() - assert_allclose(pca.transform(X2), pca.transform(X2d), rtol=transform_rtol) - assert_allclose(pca.transform(X2), pcad.transform(X2d), rtol=transform_rtol) + assert_allclose(pca.transform(X2), pca.transform(X2d), atol=transform_atol) + assert_allclose(pca.transform(X2), pcad.transform(X2d), atol=transform_atol) @pytest.mark.parametrize("sparse_container", CSR_CONTAINERS + CSC_CONTAINERS) @@ -153,10 +159,10 @@ def test_pca_sparse_fit_transform(global_random_seed, sparse_container): pca_fit.fit(X) transformed_X = pca_fit_transform.fit_transform(X) - _check_fitted_pca_close(pca_fit, pca_fit_transform, rtol=1e-10) - assert_allclose(transformed_X, pca_fit_transform.transform(X), rtol=2e-9) - assert_allclose(transformed_X, pca_fit.transform(X), rtol=2e-9) - assert_allclose(pca_fit.transform(X2), pca_fit_transform.transform(X2), rtol=2e-9) + _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)) @pytest.mark.parametrize("svd_solver", ["randomized", "full"]) From be5372b7873f83435fd1917f71ae4a31d547179c Mon Sep 17 00:00:00 2001 From: scikit-learn-bot Date: Mon, 29 Apr 2024 09:13:00 +0200 Subject: [PATCH 069/344] :lock: :robot: CI Update lock files for main CI build(s) :lock: :robot: (#28907) Co-authored-by: Lock file bot --- build_tools/azure/debian_atlas_32bit_lock.txt | 2 +- .../pylatest_conda_forge_mkl_linux-64_conda.lock | 8 ++++---- .../pylatest_conda_forge_mkl_osx-64_conda.lock | 2 +- ...ylatest_conda_mkl_no_openmp_osx-64_conda.lock | 4 ++-- ...atest_pip_openblas_pandas_linux-64_conda.lock | 8 ++++---- ...n_conda_defaults_openblas_linux-64_conda.lock | 2 +- .../pymin_conda_forge_mkl_win-64_conda.lock | 2 +- ...orge_openblas_ubuntu_2204_linux-64_conda.lock | 6 +++--- build_tools/azure/ubuntu_atlas_lock.txt | 2 +- build_tools/circle/doc_linux-64_conda.lock | 16 ++++++++-------- .../doc_min_dependencies_linux-64_conda.lock | 6 +++--- 11 files changed, 29 insertions(+), 29 deletions(-) diff --git a/build_tools/azure/debian_atlas_32bit_lock.txt b/build_tools/azure/debian_atlas_32bit_lock.txt index 5bff365bb1eea..61ad07e857cb8 100644 --- a/build_tools/azure/debian_atlas_32bit_lock.txt +++ b/build_tools/azure/debian_atlas_32bit_lock.txt @@ -6,7 +6,7 @@ # attrs==23.2.0 # via pytest -coverage==7.4.4 +coverage==7.5.0 # via pytest-cov cython==3.0.10 # via -r build_tools/azure/debian_atlas_32bit_requirements.txt diff --git a/build_tools/azure/pylatest_conda_forge_mkl_linux-64_conda.lock b/build_tools/azure/pylatest_conda_forge_mkl_linux-64_conda.lock index aa525371df1ef..3194bf106d6c2 100644 --- a/build_tools/azure/pylatest_conda_forge_mkl_linux-64_conda.lock +++ b/build_tools/azure/pylatest_conda_forge_mkl_linux-64_conda.lock @@ -146,7 +146,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libclang13-18.1.3-default_h5d682 https://conda.anaconda.org/conda-forge/linux-64/libcups-2.3.3-h4637d8d_4.conda#d4529f4dff3057982a7617c7ac58fde3 https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.7.1-hca28451_0.conda#755c7f876815003337d2c61ff5d047e5 https://conda.anaconda.org/conda-forge/linux-64/libflac-1.4.3-h59595ed_0.conda#ee48bf17cc83a00f59ca1494d5646869 -https://conda.anaconda.org/conda-forge/linux-64/libgpg-error-1.48-h71f35ed_0.conda#4d18d86916705d352d5f4adfb7f0edd3 +https://conda.anaconda.org/conda-forge/linux-64/libgpg-error-1.49-h4f305b6_0.conda#dfcfd72c7a430d3616763ecfbefe4ca9 https://conda.anaconda.org/conda-forge/linux-64/libpq-16.2-h33b98f1_1.conda#9e49ec2a61d02623b379dc332eb6889d https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyh9f0ad1d_0.tar.bz2#2ba8498c1018c1e9c61eb99b973dfe19 https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.2-h488ebb8_0.conda#7f2e286780f072ed750df46dc2631138 @@ -172,7 +172,7 @@ https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.11-hd590300_ https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.7.3-h28f7589_1.conda#97503d3e565004697f1651753aa95b9e https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.9.3-hb447be9_1.conda#c520669eb0be9269a5f0d8ef62531882 https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.0-h3faef2a_0.conda#f907bb958910dc404647326ca80c263e -https://conda.anaconda.org/conda-forge/linux-64/coverage-7.4.4-py311h459d7ec_0.conda#1aa22cb84e68841ec206ee066457bdf0 +https://conda.anaconda.org/conda-forge/linux-64/coverage-7.5.0-py311h331c9d8_0.conda#5420e3594638adf670fca1a601d7efb9 https://conda.anaconda.org/conda-forge/linux-64/fonttools-4.51.0-py311h459d7ec_0.conda#17e1997cc17c571d5ad27bd0159f616c https://conda.anaconda.org/conda-forge/linux-64/glib-2.80.0-hf2295e7_6.conda#a1e026a82a562b443845db5614ca568a https://conda.anaconda.org/conda-forge/noarch/joblib-1.4.0-pyhd8ed1ab_0.conda#e0ed1bf13ce3a440e022157bf4764465 @@ -191,7 +191,7 @@ https://conda.anaconda.org/conda-forge/linux-64/sip-6.7.12-py311hb755f60_0.conda https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.3.14-hf3aad02_1.conda#a968ffa7e9fe0c257628033d393e512f https://conda.anaconda.org/conda-forge/linux-64/blas-1.0-mkl.tar.bz2#349aef876b1d8c9dccae01de20d5b385 https://conda.anaconda.org/conda-forge/linux-64/gstreamer-1.24.1-h98fc4e7_1.conda#b04b5cdf3ba01430db27979250bc5a1d -https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-8.3.0-h3d44ed6_0.conda#5a6f6c00ef982a9bc83558d9ac8f64a0 +https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-8.4.0-h3d44ed6_0.conda#27f46291a6aaa3c2a4f798ebd35a7ddb https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-16_linux64_mkl.tar.bz2#85f61af03fd291dae33150ffe89dc09a https://conda.anaconda.org/conda-forge/linux-64/libsystemd0-255-h3516f8a_1.conda#3366af27f0b593544a6cd453c7932ac5 https://conda.anaconda.org/conda-forge/noarch/meson-python-0.16.0-pyh0c530f3_0.conda#e16f0dbf502da873be9f9adb0dc52547 @@ -210,7 +210,7 @@ https://conda.anaconda.org/conda-forge/noarch/array-api-strict-1.1.1-pyhd8ed1ab_ https://conda.anaconda.org/conda-forge/linux-64/contourpy-1.2.1-py311h9547e67_0.conda#74ad0ae64f1ef565e27eda87fa749e84 https://conda.anaconda.org/conda-forge/linux-64/libarrow-12.0.1-hb87d912_8_cpu.conda#3f3b11398fe79b578e3c44dd00a44e4a https://conda.anaconda.org/conda-forge/linux-64/pandas-2.2.2-py311h320fe9a_0.conda#c79e96ece4110fdaf2657c9f8e16f749 -https://conda.anaconda.org/conda-forge/linux-64/polars-0.20.21-py311h78b473b_1.conda#f5731dac66c9ee9f861679523807f47d +https://conda.anaconda.org/conda-forge/linux-64/polars-0.20.23-py311h00856b1_0.conda#c000e1629d890ad00bb8c20963028d9f https://conda.anaconda.org/conda-forge/linux-64/pyqt-5.15.9-py311hf0fb5b6_5.conda#ec7e45bc76d9d0b69a74a2075932b8e8 https://conda.anaconda.org/conda-forge/linux-64/pytorch-1.13.1-cpu_py311h410fd25_1.conda#ddd2fadddf89e3dc3d541a2537fce010 https://conda.anaconda.org/conda-forge/linux-64/scipy-1.13.0-py311h64a7726_0.conda#d443c70b4a05f50236c70b9c79beff64 diff --git a/build_tools/azure/pylatest_conda_forge_mkl_osx-64_conda.lock b/build_tools/azure/pylatest_conda_forge_mkl_osx-64_conda.lock index 801ac1192a038..86443fd97ae20 100644 --- a/build_tools/azure/pylatest_conda_forge_mkl_osx-64_conda.lock +++ b/build_tools/azure/pylatest_conda_forge_mkl_osx-64_conda.lock @@ -83,7 +83,7 @@ https://conda.anaconda.org/conda-forge/noarch/wheel-0.43.0-pyhd8ed1ab_1.conda#0b https://conda.anaconda.org/conda-forge/osx-64/ccache-4.9.1-h41adc32_0.conda#45aaf96b67840bd98a928de8679098fa https://conda.anaconda.org/conda-forge/osx-64/cctools_osx-64-986-ha1c5b94_0.conda#a8951de2506df5649f5a3295fdfd9f2c https://conda.anaconda.org/conda-forge/osx-64/clang-16-16.0.6-default_h7151d67_6.conda#1c298568c30efe7d9369c7c15b748461 -https://conda.anaconda.org/conda-forge/osx-64/coverage-7.4.4-py312h41838bb_0.conda#b0e22bba5fbc3c8d02e25aeb33475fce +https://conda.anaconda.org/conda-forge/osx-64/coverage-7.5.0-py312h5fa3f64_0.conda#0ec479f31895645cfaabaa7ea318e6a5 https://conda.anaconda.org/conda-forge/osx-64/fonttools-4.51.0-py312h41838bb_0.conda#ebe40134b860cf704ddaf81f684f95a5 https://conda.anaconda.org/conda-forge/osx-64/gfortran_impl_osx-64-12.3.0-hc328e78_3.conda#b3d751dc7073bbfdfa9d863e39b9685d https://conda.anaconda.org/conda-forge/noarch/joblib-1.4.0-pyhd8ed1ab_0.conda#e0ed1bf13ce3a440e022157bf4764465 diff --git a/build_tools/azure/pylatest_conda_mkl_no_openmp_osx-64_conda.lock b/build_tools/azure/pylatest_conda_mkl_no_openmp_osx-64_conda.lock index 7da0e513ac143..dc2fea78e7b80 100644 --- a/build_tools/azure/pylatest_conda_mkl_no_openmp_osx-64_conda.lock +++ b/build_tools/azure/pylatest_conda_mkl_no_openmp_osx-64_conda.lock @@ -43,7 +43,7 @@ https://repo.anaconda.com/pkgs/main/osx-64/coverage-7.2.2-py312h6c40b1e_0.conda# https://repo.anaconda.com/pkgs/main/noarch/cycler-0.11.0-pyhd3eb1b0_0.conda#f5e365d2cdb66d547eb8c3ab93843aab https://repo.anaconda.com/pkgs/main/noarch/execnet-1.9.0-pyhd3eb1b0_0.conda#f895937671af67cebb8af617494b3513 https://repo.anaconda.com/pkgs/main/noarch/iniconfig-1.1.1-pyhd3eb1b0_0.tar.bz2#e40edff2c5708f342cef43c7f280c507 -https://repo.anaconda.com/pkgs/main/osx-64/joblib-1.2.0-py312hecd8cb5_0.conda#aeeb33f85c1e6776700b67a4762d2e6d +https://repo.anaconda.com/pkgs/main/osx-64/joblib-1.4.0-py312hecd8cb5_0.conda#0af12a3a87d9c8051ae6ba2ed2c3882a https://repo.anaconda.com/pkgs/main/osx-64/kiwisolver-1.4.4-py312hcec6c5f_0.conda#2ba6561ddd1d05936fe74f5d118ce7dd https://repo.anaconda.com/pkgs/main/osx-64/lcms2-2.12-hf1fd2bf_0.conda#697aba7a3308226df7a93ccfeae16ffa https://repo.anaconda.com/pkgs/main/osx-64/mkl-service-2.4.0-py312h6c40b1e_1.conda#b1ef860be9043b35c5e8d9388b858514 @@ -53,7 +53,7 @@ https://repo.anaconda.com/pkgs/main/osx-64/packaging-23.2-py312hecd8cb5_0.conda# https://repo.anaconda.com/pkgs/main/osx-64/pluggy-1.0.0-py312hecd8cb5_1.conda#647fada22f1697691fdee90b52c99bcb https://repo.anaconda.com/pkgs/main/osx-64/pyparsing-3.0.9-py312hecd8cb5_0.conda#d85cf2b81c6d9326a57a6418e14db258 https://repo.anaconda.com/pkgs/main/noarch/python-tzdata-2023.3-pyhd3eb1b0_0.conda#479c037de0186d114b9911158427624e -https://repo.anaconda.com/pkgs/main/osx-64/pytz-2023.3.post1-py312hecd8cb5_0.conda#2636382c9a424f69cbc36b1c5dc1f2fc +https://repo.anaconda.com/pkgs/main/osx-64/pytz-2024.1-py312hecd8cb5_0.conda#2b28ec0e0d07f5c0c701f75200b1e8b6 https://repo.anaconda.com/pkgs/main/osx-64/setuptools-68.2.2-py312hecd8cb5_0.conda#64235f0c451427d86808c70c1c31cb8b https://repo.anaconda.com/pkgs/main/noarch/six-1.16.0-pyhd3eb1b0_1.conda#34586824d411d36af2fa40e799c172d0 https://repo.anaconda.com/pkgs/main/noarch/toml-0.10.2-pyhd3eb1b0_0.conda#cda05f5f6d8509529d1a2743288d197a diff --git a/build_tools/azure/pylatest_pip_openblas_pandas_linux-64_conda.lock b/build_tools/azure/pylatest_pip_openblas_pandas_linux-64_conda.lock index 58bdbdad5044d..7534de9fbd5f6 100644 --- a/build_tools/azure/pylatest_pip_openblas_pandas_linux-64_conda.lock +++ b/build_tools/azure/pylatest_pip_openblas_pandas_linux-64_conda.lock @@ -29,7 +29,7 @@ https://repo.anaconda.com/pkgs/main/linux-64/pip-23.3.1-py39h06a4308_0.conda#685 # pip charset-normalizer @ https://files.pythonhosted.org/packages/98/69/5d8751b4b670d623aa7a47bef061d69c279e9f922f6705147983aa76c3ce/charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796 # pip cycler @ https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl#sha256=85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30 # pip cython @ https://files.pythonhosted.org/packages/a7/f5/3dde4d96076888ceaa981827b098274c2b45ddd4b20d75a8cfaa92b91eec/Cython-3.0.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=651a15a8534ebfb9b58cb0b87c269c70984b6f9c88bfe65e4f635f0e3f07dfcd -# pip docutils @ https://files.pythonhosted.org/packages/ea/6a/1c934b70bc12be40be886060709317c2ad5a5fe2180469410bd9344f5235/docutils-0.21.1-py3-none-any.whl#sha256=14c8d34a55b46c88f9f714adb29cefbdd69fb82f3fef825e59c5faab935390d8 +# pip docutils @ https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl#sha256=dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2 # pip exceptiongroup @ https://files.pythonhosted.org/packages/01/90/79fe92dd413a9cab314ef5c591b5aa9b9ba787ae4cadab75055b0ae00b33/exceptiongroup-1.2.1-py3-none-any.whl#sha256=5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad # pip execnet @ https://files.pythonhosted.org/packages/43/09/2aea36ff60d16dd8879bdb2f5b3ee0ba8d08cbbdcdfe870e695ce3784385/execnet-2.1.1-py3-none-any.whl#sha256=26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc # pip fonttools @ https://files.pythonhosted.org/packages/8b/c6/636f008104908a93b80419f756be755bb91df4b8a0c88d5158bb52c82c3a/fonttools-4.51.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=0d145976194a5242fdd22df18a1b451481a88071feadf251221af110ca8f00ce @@ -64,7 +64,7 @@ https://repo.anaconda.com/pkgs/main/linux-64/pip-23.3.1-py39h06a4308_0.conda#685 # pip urllib3 @ https://files.pythonhosted.org/packages/a2/73/a68704750a7679d0b6d3ad7aa8d4da8e14e151ae82e6fee774e6e0d05ec8/urllib3-2.2.1-py3-none-any.whl#sha256=450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d # pip zipp @ https://files.pythonhosted.org/packages/c2/0a/ba9d0ee9536d3ef73a3448e931776e658b36f128d344e175bc32b092a8bf/zipp-3.18.1-py3-none-any.whl#sha256=206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b # pip contourpy @ https://files.pythonhosted.org/packages/31/a2/2f12e3a6e45935ff694654b710961b03310b0e1ec997ee9f416d3c873f87/contourpy-1.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=e1d59258c3c67c865435d8fbeb35f8c59b8bef3d6f46c1f29f6123556af28445 -# pip coverage @ https://files.pythonhosted.org/packages/5b/ec/9bd500128995e9eec2ab50361ce8b853bab2b4839316ddcfd6a34f5bbfed/coverage-7.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4 +# pip coverage @ https://files.pythonhosted.org/packages/12/7f/9b787ffc31bc39aa9e98c7005b698e7c6539bd222043e4a9c83b83c782a2/coverage-7.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=db2de4e546f0ec4b2787d625e0b16b78e99c3e21bc1722b4977c0dddf11ca84e # pip imageio @ https://files.pythonhosted.org/packages/a3/b6/39c7dad203d9984225f47e0aa39ac3ba3a47c77a02d0ef2a7be691855a06/imageio-2.34.1-py3-none-any.whl#sha256=408c1d4d62f72c9e8347e7d1ca9bc11d8673328af3913868db3b828e28b40a4c # pip importlib-metadata @ https://files.pythonhosted.org/packages/2d/0a/679461c511447ffaf176567d5c496d1de27cbe34a87df6677d7171b2fbd4/importlib_metadata-7.1.0-py3-none-any.whl#sha256=30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570 # pip importlib-resources @ https://files.pythonhosted.org/packages/75/06/4df55e1b7b112d183f65db9503bff189e97179b256e1ea450a3c365241e0/importlib_resources-6.4.0-py3-none-any.whl#sha256=50d10f043df931902d4194ea07ec57960f66a80449ff867bfe782b4c486ba78c @@ -75,14 +75,14 @@ https://repo.anaconda.com/pkgs/main/linux-64/pip-23.3.1-py39h06a4308_0.conda#685 # pip python-dateutil @ https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl#sha256=a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 # pip requests @ https://files.pythonhosted.org/packages/70/8e/0e2d847013cb52cd35b38c009bb167a1a26b2ce6cd6965bf26b47bc0bf44/requests-2.31.0-py3-none-any.whl#sha256=58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f # pip scipy @ https://files.pythonhosted.org/packages/c6/ba/a778e6c0020d728c119b0379805a357135fe8c9bc87fdb7e0750ca11319f/scipy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=28e286bf9ac422d6beb559bc61312c348ca9b0f0dae0d7c5afde7f722d6ea13d -# pip tifffile @ https://files.pythonhosted.org/packages/50/d8/870c966ced9aae912d113f2ca6a651bdc9db69d1ddf0afb5a87840ce0011/tifffile-2024.4.18-py3-none-any.whl#sha256=72643b5c9ef886669a00a659c9fd60a81f220d2fb6572d184c3e147435ccec43 +# pip tifffile @ https://files.pythonhosted.org/packages/88/23/6398b7bca8967c853b90ba2f8da5e3ad1e9b2ca5b9f869a8c26ea41543e2/tifffile-2024.4.24-py3-none-any.whl#sha256=8d0b982f4b01ace358835ae6c2beb5a70cb7287f5d3a2e96c318bd5befa97b1f # pip lightgbm @ https://files.pythonhosted.org/packages/ba/11/cb8b67f3cbdca05b59a032bb57963d4fe8c8d18c3870f30bed005b7f174d/lightgbm-4.3.0-py3-none-manylinux_2_28_x86_64.whl#sha256=104496a3404cb2452d3412cbddcfbfadbef9c372ea91e3a9b8794bcc5183bf07 # pip matplotlib @ https://files.pythonhosted.org/packages/5e/2c/513395a63a9e1124a5648addbf73be23cc603f955af026b04416da98dc96/matplotlib-3.8.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=606e3b90897554c989b1e38a258c626d46c873523de432b1462f295db13de6f9 # pip meson-python @ https://files.pythonhosted.org/packages/91/c0/104cb6244c83fe6bc3886f144cc433db0c0c78efac5dc00e409a5a08c87d/meson_python-0.16.0-py3-none-any.whl#sha256=842dc9f5dc29e55fc769ff1b6fe328412fe6c870220fc321060a1d2d395e69e8 # pip pandas @ https://files.pythonhosted.org/packages/bb/30/f6f1f1ac36250f50c421b1b6af08c35e5a8b5a84385ef928625336b93e6f/pandas-2.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=66b479b0bd07204e37583c191535505410daa8df638fd8e75ae1b383851fe921 # pip pyamg @ https://files.pythonhosted.org/packages/68/a9/aed9f557e7eb779d2cb4fa090663f8540979e0c04dadd16e9a0bdc9632c5/pyamg-5.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=5817d4567fb240dab4779bb1630bbb3035b3827731fcdaeb9ecc9c8814319995 # pip pytest-cov @ https://files.pythonhosted.org/packages/78/3a/af5b4fa5961d9a1e6237b530eb87dd04aea6eb83da09d2a4073d81b54ccf/pytest_cov-5.0.0-py3-none-any.whl#sha256=4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652 -# pip pytest-xdist @ https://files.pythonhosted.org/packages/e0/df/585742c204227526baf641e45a95d3c0a94978b85a1414622c1017ebfcf8/pytest_xdist-3.6.0-py3-none-any.whl#sha256=958e08f38472e1b3a83450d8d3e682e90fdbffee39a97dd0f27185a3bd9074d1 +# pip pytest-xdist @ https://files.pythonhosted.org/packages/6d/82/1d96bf03ee4c0fdc3c0cbe61470070e659ca78dc0086fb88b66c185e2449/pytest_xdist-3.6.1-py3-none-any.whl#sha256=9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7 # pip scikit-image @ https://files.pythonhosted.org/packages/a3/7e/4cd853a855ac34b4ef3ef6a5c3d1c2e96eaca1154fc6be75db55ffa87393/scikit_image-0.22.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=3b7a6c89e8d6252332121b58f50e1625c35f7d6a85489c0b6b7ee4f5155d547a # pip sphinx @ https://files.pythonhosted.org/packages/b4/fa/130c32ed94cf270e3d0b9ded16fb7b2c8fea86fa7263c29a696a30c1dde7/sphinx-7.3.7-py3-none-any.whl#sha256=413f75440be4cacf328f580b4274ada4565fb2187d696a84970c23f77b64d8c3 # pip numpydoc @ https://files.pythonhosted.org/packages/f0/fa/dcfe0f65660661db757ee9ebd84e170ff98edd5d80235f62457d9088f85f/numpydoc-1.7.0-py3-none-any.whl#sha256=5a56419d931310d79a06cfc2a126d1558700feeb9b4f3d8dcae1a8134be829c9 diff --git a/build_tools/azure/pymin_conda_defaults_openblas_linux-64_conda.lock b/build_tools/azure/pymin_conda_defaults_openblas_linux-64_conda.lock index fef1871be2088..12684bc738fdb 100644 --- a/build_tools/azure/pymin_conda_defaults_openblas_linux-64_conda.lock +++ b/build_tools/azure/pymin_conda_defaults_openblas_linux-64_conda.lock @@ -62,7 +62,7 @@ https://repo.anaconda.com/pkgs/main/linux-64/exceptiongroup-1.2.0-py39h06a4308_0 https://repo.anaconda.com/pkgs/main/noarch/execnet-1.9.0-pyhd3eb1b0_0.conda#f895937671af67cebb8af617494b3513 https://repo.anaconda.com/pkgs/main/linux-64/glib-2.78.4-h6a678d5_0.conda#045ff487547f7b2b7ff01648681b8ebe https://repo.anaconda.com/pkgs/main/noarch/iniconfig-1.1.1-pyhd3eb1b0_0.tar.bz2#e40edff2c5708f342cef43c7f280c507 -https://repo.anaconda.com/pkgs/main/linux-64/joblib-1.2.0-py39h06a4308_0.conda#ac1f5687d70aa1128cbecb26bc9e559d +https://repo.anaconda.com/pkgs/main/linux-64/joblib-1.4.0-py39h06a4308_0.conda#95af0a1da233f22d37c3f5950068e9fc https://repo.anaconda.com/pkgs/main/linux-64/kiwisolver-1.4.4-py39h6a678d5_0.conda#3d57aedbfbd054ce57fb3c1e4448828c https://repo.anaconda.com/pkgs/main/linux-64/mysql-5.7.24-h721c034_2.conda#dfc19ca2466d275c4c1f73b62c57f37b https://repo.anaconda.com/pkgs/main/linux-64/numpy-base-1.21.6-py39h375b286_0.conda#4ceaa5d6e6307fe06961d555f78b266f diff --git a/build_tools/azure/pymin_conda_forge_mkl_win-64_conda.lock b/build_tools/azure/pymin_conda_forge_mkl_win-64_conda.lock index 52f7233deb707..b98735a4336bb 100644 --- a/build_tools/azure/pymin_conda_forge_mkl_win-64_conda.lock +++ b/build_tools/azure/pymin_conda_forge_mkl_win-64_conda.lock @@ -81,7 +81,7 @@ https://conda.anaconda.org/conda-forge/win-64/xorg-libxau-1.0.11-hcd874cb_0.cond https://conda.anaconda.org/conda-forge/win-64/xorg-libxdmcp-1.1.3-hcd874cb_0.tar.bz2#46878ebb6b9cbd8afcf8088d7ef00ece https://conda.anaconda.org/conda-forge/noarch/zipp-3.17.0-pyhd8ed1ab_0.conda#2e4d6bc0b14e10f895fc6791a7d9b26a https://conda.anaconda.org/conda-forge/win-64/brotli-1.1.0-hcfcfb64_1.conda#f47f6db2528e38321fb00ae31674c133 -https://conda.anaconda.org/conda-forge/win-64/coverage-7.4.4-py39ha55989b_0.conda#ca4fca57e0e713af82c73a9e6c5b9716 +https://conda.anaconda.org/conda-forge/win-64/coverage-7.5.0-py39ha55e580_0.conda#53799e32a839e6a86e5b104a768dcd9d https://conda.anaconda.org/conda-forge/win-64/glib-tools-2.80.0-h0a98069_6.conda#40d452e4012c00f644b1dd6319fcdbcf https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.4.0-pyhd8ed1ab_0.conda#c5d3907ad8bd7bf557521a1833cf7e6d https://conda.anaconda.org/conda-forge/noarch/joblib-1.4.0-pyhd8ed1ab_0.conda#e0ed1bf13ce3a440e022157bf4764465 diff --git a/build_tools/azure/pymin_conda_forge_openblas_ubuntu_2204_linux-64_conda.lock b/build_tools/azure/pymin_conda_forge_openblas_ubuntu_2204_linux-64_conda.lock index 1525847b0153f..c7a155bece187 100644 --- a/build_tools/azure/pymin_conda_forge_openblas_ubuntu_2204_linux-64_conda.lock +++ b/build_tools/azure/pymin_conda_forge_openblas_ubuntu_2204_linux-64_conda.lock @@ -108,7 +108,7 @@ https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhd8ed1ab_0.conda#5cd86562580f274031ede6aa6aa24441 https://conda.anaconda.org/conda-forge/linux-64/cython-3.0.10-py39h3d6467e_0.conda#76b5d215fb735a6dc43010ffbe78040e https://conda.anaconda.org/conda-forge/linux-64/dbus-1.13.6-h5008d03_3.tar.bz2#ecfff944ba3960ecb334b9a2663d708d -https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.1-pyhd8ed1ab_0.conda#04ffd8609a7483e8b262aa0f121e7360 +https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_0.conda#e8cd5d629f65bdf0f3bb312cde14659e https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.0-pyhd8ed1ab_2.conda#8d652ea2ee8eaee02ed8dc820bc794aa https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_0.conda#15dda3cdbf330abfe9f555d22f66db46 https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.14.2-h14ed4e7_0.conda#0f69b688f52ff6da70bccb7ff7001d1d @@ -123,7 +123,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libclang-cpp15-15.0.7-default_h1 https://conda.anaconda.org/conda-forge/linux-64/libclang13-18.1.3-default_h5d6823c_0.conda#5fff487759736b275dc3e4a263cac666 https://conda.anaconda.org/conda-forge/linux-64/libcups-2.3.3-h4637d8d_4.conda#d4529f4dff3057982a7617c7ac58fde3 https://conda.anaconda.org/conda-forge/linux-64/libflac-1.4.3-h59595ed_0.conda#ee48bf17cc83a00f59ca1494d5646869 -https://conda.anaconda.org/conda-forge/linux-64/libgpg-error-1.48-h71f35ed_0.conda#4d18d86916705d352d5f4adfb7f0edd3 +https://conda.anaconda.org/conda-forge/linux-64/libgpg-error-1.49-h4f305b6_0.conda#dfcfd72c7a430d3616763ecfbefe4ca9 https://conda.anaconda.org/conda-forge/linux-64/libpq-16.2-h33b98f1_1.conda#9e49ec2a61d02623b379dc332eb6889d https://conda.anaconda.org/conda-forge/linux-64/markupsafe-2.1.5-py39hd1e30aa_0.conda#9a9a22eb1f83c44953319ee3b027769f https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyh9f0ad1d_0.tar.bz2#2ba8498c1018c1e9c61eb99b973dfe19 @@ -175,7 +175,7 @@ https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0-pyhd8ed1ab_0 https://conda.anaconda.org/conda-forge/linux-64/sip-6.7.12-py39h3d6467e_0.conda#e667a3ab0df62c54e60e1843d2e6defb https://conda.anaconda.org/conda-forge/noarch/urllib3-2.2.1-pyhd8ed1ab_0.conda#08807a87fa7af10754d46f63b368e016 https://conda.anaconda.org/conda-forge/linux-64/gstreamer-1.24.1-h98fc4e7_1.conda#b04b5cdf3ba01430db27979250bc5a1d -https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-8.3.0-h3d44ed6_0.conda#5a6f6c00ef982a9bc83558d9ac8f64a0 +https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-8.4.0-h3d44ed6_0.conda#27f46291a6aaa3c2a4f798ebd35a7ddb https://conda.anaconda.org/conda-forge/noarch/importlib-resources-6.4.0-pyhd8ed1ab_0.conda#dcbadab7a68738a028e195ab68ab2d2e https://conda.anaconda.org/conda-forge/linux-64/liblapacke-3.9.0-22_linux64_openblas.conda#1fd156abd41a4992835952f6f4d951d0 https://conda.anaconda.org/conda-forge/linux-64/libsystemd0-255-h3516f8a_1.conda#3366af27f0b593544a6cd453c7932ac5 diff --git a/build_tools/azure/ubuntu_atlas_lock.txt b/build_tools/azure/ubuntu_atlas_lock.txt index f2110d2173a3c..d1674c678b254 100644 --- a/build_tools/azure/ubuntu_atlas_lock.txt +++ b/build_tools/azure/ubuntu_atlas_lock.txt @@ -33,7 +33,7 @@ pytest==7.4.4 # via # -r build_tools/azure/ubuntu_atlas_requirements.txt # pytest-xdist -pytest-xdist==3.5.0 +pytest-xdist==3.6.1 # via -r build_tools/azure/ubuntu_atlas_requirements.txt threadpoolctl==3.1.0 # via -r build_tools/azure/ubuntu_atlas_requirements.txt diff --git a/build_tools/circle/doc_linux-64_conda.lock b/build_tools/circle/doc_linux-64_conda.lock index 60bbe8298c34c..baccc168b059d 100644 --- a/build_tools/circle/doc_linux-64_conda.lock +++ b/build_tools/circle/doc_linux-64_conda.lock @@ -138,7 +138,7 @@ https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhd8ed1ab_0.conda#5cd86562580f274031ede6aa6aa24441 https://conda.anaconda.org/conda-forge/linux-64/cython-3.0.10-py39h3d6467e_0.conda#76b5d215fb735a6dc43010ffbe78040e https://conda.anaconda.org/conda-forge/linux-64/dbus-1.13.6-h5008d03_3.tar.bz2#ecfff944ba3960ecb334b9a2663d708d -https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.1-pyhd8ed1ab_0.conda#04ffd8609a7483e8b262aa0f121e7360 +https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_0.conda#e8cd5d629f65bdf0f3bb312cde14659e https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.0-pyhd8ed1ab_2.conda#8d652ea2ee8eaee02ed8dc820bc794aa https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_0.conda#15dda3cdbf330abfe9f555d22f66db46 https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.14.2-h14ed4e7_0.conda#0f69b688f52ff6da70bccb7ff7001d1d @@ -157,7 +157,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libclang-cpp15-15.0.7-default_h1 https://conda.anaconda.org/conda-forge/linux-64/libclang13-18.1.3-default_h5d6823c_0.conda#5fff487759736b275dc3e4a263cac666 https://conda.anaconda.org/conda-forge/linux-64/libcups-2.3.3-h4637d8d_4.conda#d4529f4dff3057982a7617c7ac58fde3 https://conda.anaconda.org/conda-forge/linux-64/libflac-1.4.3-h59595ed_0.conda#ee48bf17cc83a00f59ca1494d5646869 -https://conda.anaconda.org/conda-forge/linux-64/libgpg-error-1.48-h71f35ed_0.conda#4d18d86916705d352d5f4adfb7f0edd3 +https://conda.anaconda.org/conda-forge/linux-64/libgpg-error-1.49-h4f305b6_0.conda#dfcfd72c7a430d3616763ecfbefe4ca9 https://conda.anaconda.org/conda-forge/linux-64/libpq-16.2-h33b98f1_1.conda#9e49ec2a61d02623b379dc332eb6889d https://conda.anaconda.org/conda-forge/linux-64/markupsafe-2.1.5-py39hd1e30aa_0.conda#9a9a22eb1f83c44953319ee3b027769f https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyh9f0ad1d_0.tar.bz2#2ba8498c1018c1e9c61eb99b973dfe19 @@ -165,7 +165,7 @@ https://conda.anaconda.org/conda-forge/noarch/networkx-3.2.1-pyhd8ed1ab_0.conda# https://conda.anaconda.org/conda-forge/linux-64/openblas-0.3.27-pthreads_h7a3da1a_0.conda#4b422ebe8fc6a5320d0c1c22e5a46032 https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.2-h488ebb8_0.conda#7f2e286780f072ed750df46dc2631138 https://conda.anaconda.org/conda-forge/noarch/packaging-24.0-pyhd8ed1ab_0.conda#248f521b64ce055e7feae3105e7abeb8 -https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.2.0-pyhd8ed1ab_0.conda#a0bc3eec34b0fab84be6b2da94e98e20 +https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.2.1-pyhd8ed1ab_0.conda#d478a8a3044cdff1aa6e62f9269cefe0 https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_0.conda#d3483c8fc2dc2cc3f5cf43e26d60cabf https://conda.anaconda.org/conda-forge/noarch/ply-3.11-pyhd8ed1ab_2.conda#18c6deb6f9602e32446398203c8f0e91 https://conda.anaconda.org/conda-forge/linux-64/psutil-5.9.8-py39hd1e30aa_0.conda#ec86403fde8793ac1c36f8afa3d15902 @@ -220,7 +220,7 @@ https://conda.anaconda.org/conda-forge/linux-64/sip-6.7.12-py39h3d6467e_0.conda# https://conda.anaconda.org/conda-forge/noarch/urllib3-2.2.1-pyhd8ed1ab_0.conda#08807a87fa7af10754d46f63b368e016 https://conda.anaconda.org/conda-forge/linux-64/compilers-1.7.0-ha770c72_0.conda#81458b3aed8ab8711951ec3c0c04e097 https://conda.anaconda.org/conda-forge/linux-64/gstreamer-1.24.1-h98fc4e7_1.conda#b04b5cdf3ba01430db27979250bc5a1d -https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-8.3.0-h3d44ed6_0.conda#5a6f6c00ef982a9bc83558d9ac8f64a0 +https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-8.4.0-h3d44ed6_0.conda#27f46291a6aaa3c2a4f798ebd35a7ddb https://conda.anaconda.org/conda-forge/noarch/importlib-resources-6.4.0-pyhd8ed1ab_0.conda#dcbadab7a68738a028e195ab68ab2d2e https://conda.anaconda.org/conda-forge/noarch/lazy_loader-0.4-pyhd8ed1ab_0.conda#a284ff318fbdb0dd83928275b4b6087c https://conda.anaconda.org/conda-forge/linux-64/liblapacke-3.9.0-22_linux64_openblas.conda#1fd156abd41a4992835952f6f4d951d0 @@ -237,7 +237,7 @@ https://conda.anaconda.org/conda-forge/linux-64/imagecodecs-2024.1.1-py39ha98d97 https://conda.anaconda.org/conda-forge/noarch/imageio-2.34.1-pyh4b66e23_0.conda#bcf6a6f4c6889ca083e8d33afbafb8d5 https://conda.anaconda.org/conda-forge/linux-64/pandas-2.2.2-py39hddac248_0.conda#259c4e76e6bda8888aefc098ae1ba749 https://conda.anaconda.org/conda-forge/noarch/patsy-0.5.6-pyhd8ed1ab_0.conda#a5b55d1cb110cdcedc748b5c3e16e687 -https://conda.anaconda.org/conda-forge/linux-64/polars-0.20.21-py39h87fa3cb_1.conda#67b1ece36573ad3e1c195bcd580277ba +https://conda.anaconda.org/conda-forge/linux-64/polars-0.20.23-py39ha963410_0.conda#4871f09d653e979d598d2d4cd5fa868d https://conda.anaconda.org/conda-forge/noarch/pooch-1.8.1-pyhd8ed1ab_0.conda#d15917f33140f8d2ac9ca44db7ec8a25 https://conda.anaconda.org/conda-forge/linux-64/pulseaudio-client-17.0-hb77b528_0.conda#07f45f1be1c25345faddb8db0de8039b https://conda.anaconda.org/conda-forge/linux-64/pywavelets-1.4.1-py39h44dd56e_1.conda#d037c20e3da2e85f03ebd20ad480c359 @@ -291,7 +291,7 @@ https://conda.anaconda.org/conda-forge/noarch/sphinxext-opengraph-0.9.1-pyhd8ed1 # pip uri-template @ https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl#sha256=a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363 # pip webcolors @ https://files.pythonhosted.org/packages/d5/e1/3e9013159b4cbb71df9bd7611cbf90dc2c621c8aeeb677fc41dad72f2261/webcolors-1.13-py3-none-any.whl#sha256=29bc7e8752c0a1bd4a1f03c14d6e6a72e93d82193738fa860cbff59d0fcc11bf # pip webencodings @ https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl#sha256=a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 -# pip websocket-client @ https://files.pythonhosted.org/packages/1e/70/1e88138a9afbed1d37093b85f0bebc3011623c4f47c166431599fe9d6c93/websocket_client-1.7.0-py3-none-any.whl#sha256=f4c3d22fec12a2461427a29957ff07d35098ee2d976d3ba244e688b8b4057588 +# pip websocket-client @ https://files.pythonhosted.org/packages/5a/84/44687a29792a70e111c5c477230a72c4b957d88d16141199bf9acb7537a3/websocket_client-1.8.0-py3-none-any.whl#sha256=17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526 # pip anyio @ https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl#sha256=048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8 # pip arrow @ https://files.pythonhosted.org/packages/f8/ed/e97229a566617f2ae958a6b13e7cc0f585470eac730a73e9e82c32a3cdd2/arrow-1.3.0-py3-none-any.whl#sha256=c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80 # pip beautifulsoup4 @ https://files.pythonhosted.org/packages/b1/fe/e8c672695b37eecc5cbf43e1d0638d88d66ba3a44c4d321c796f4e59167f/beautifulsoup4-4.12.3-py3-none-any.whl#sha256=b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed @@ -299,10 +299,10 @@ https://conda.anaconda.org/conda-forge/noarch/sphinxext-opengraph-0.9.1-pyhd8ed1 # pip cffi @ https://files.pythonhosted.org/packages/ea/ac/e9e77bc385729035143e54cc8c4785bd480eaca9df17565963556b0b7a93/cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098 # pip doit @ https://files.pythonhosted.org/packages/44/83/a2960d2c975836daa629a73995134fd86520c101412578c57da3d2aa71ee/doit-0.36.0-py3-none-any.whl#sha256=ebc285f6666871b5300091c26eafdff3de968a6bd60ea35dd1e3fc6f2e32479a # pip jupyter-core @ https://files.pythonhosted.org/packages/c9/fb/108ecd1fe961941959ad0ee4e12ee7b8b1477247f30b1fdfd83ceaf017f0/jupyter_core-5.7.2-py3-none-any.whl#sha256=4f7315d2f6b4bcf2e3e7cb6e46772eba760ae459cd1f59d29eb57b0a01bd7409 -# pip referencing @ https://files.pythonhosted.org/packages/42/8e/ae1de7b12223986e949bdb886c004de7c304b6fa94de5b87c926c1099656/referencing-0.34.0-py3-none-any.whl#sha256=d53ae300ceddd3169f1ffa9caf2cb7b769e92657e4fafb23d34b93679116dfd4 +# pip referencing @ https://files.pythonhosted.org/packages/8f/ad/0a39c92d2d2769eb02adfdd50282e25341dccee3a14753c972d7327de664/referencing-0.35.0-py3-none-any.whl#sha256=8080727b30e364e5783152903672df9b6b091c926a146a759080b62ca3126cd6 # pip rfc3339-validator @ https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl#sha256=24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa # pip terminado @ https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl#sha256=a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0 -# pip tinycss2 @ https://files.pythonhosted.org/packages/da/99/fd23634d6962c2791fb8cb6ccae1f05dcbfc39bce36bba8b1c9a8d92eae8/tinycss2-1.2.1-py3-none-any.whl#sha256=2b80a96d41e7c3914b8cda8bc7f705a4d9c49275616e886103dd839dfc847847 +# pip tinycss2 @ https://files.pythonhosted.org/packages/2c/4d/0db5b8a613d2a59bbc29bc5bb44a2f8070eb9ceab11c50d477502a8a0092/tinycss2-1.3.0-py3-none-any.whl#sha256=54a8dbdffb334d536851be0226030e9505965bb2f30f21a4a82c55fb2a80fae7 # pip argon2-cffi-bindings @ https://files.pythonhosted.org/packages/ec/f7/378254e6dd7ae6f31fe40c8649eea7d4832a42243acaf0f1fff9083b2bed/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae # pip isoduration @ https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl#sha256=b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042 # pip jsonschema-specifications @ https://files.pythonhosted.org/packages/ee/07/44bd408781594c4d0a027666ef27fab1e441b109dc3b76b4f836f8fd04fe/jsonschema_specifications-2023.12.1-py3-none-any.whl#sha256=87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c diff --git a/build_tools/circle/doc_min_dependencies_linux-64_conda.lock b/build_tools/circle/doc_min_dependencies_linux-64_conda.lock index ac92112d663fc..69eca7785d55c 100644 --- a/build_tools/circle/doc_min_dependencies_linux-64_conda.lock +++ b/build_tools/circle/doc_min_dependencies_linux-64_conda.lock @@ -139,14 +139,14 @@ https://conda.anaconda.org/conda-forge/linux-64/libclang-cpp15-15.0.7-default_h1 https://conda.anaconda.org/conda-forge/linux-64/libclang13-18.1.3-default_h5d6823c_0.conda#5fff487759736b275dc3e4a263cac666 https://conda.anaconda.org/conda-forge/linux-64/libcups-2.3.3-h4637d8d_4.conda#d4529f4dff3057982a7617c7ac58fde3 https://conda.anaconda.org/conda-forge/linux-64/libflac-1.4.3-h59595ed_0.conda#ee48bf17cc83a00f59ca1494d5646869 -https://conda.anaconda.org/conda-forge/linux-64/libgpg-error-1.48-h71f35ed_0.conda#4d18d86916705d352d5f4adfb7f0edd3 +https://conda.anaconda.org/conda-forge/linux-64/libgpg-error-1.49-h4f305b6_0.conda#dfcfd72c7a430d3616763ecfbefe4ca9 https://conda.anaconda.org/conda-forge/linux-64/libpq-16.2-h33b98f1_1.conda#9e49ec2a61d02623b379dc332eb6889d https://conda.anaconda.org/conda-forge/noarch/locket-1.0.0-pyhd8ed1ab_0.tar.bz2#91e27ef3d05cc772ce627e51cff111c4 https://conda.anaconda.org/conda-forge/linux-64/markupsafe-2.1.5-py39hd1e30aa_0.conda#9a9a22eb1f83c44953319ee3b027769f https://conda.anaconda.org/conda-forge/noarch/networkx-3.2-pyhd8ed1ab_0.conda#cec8cc498664cc00a070676aa89e69a7 https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.2-h488ebb8_0.conda#7f2e286780f072ed750df46dc2631138 https://conda.anaconda.org/conda-forge/noarch/packaging-24.0-pyhd8ed1ab_0.conda#248f521b64ce055e7feae3105e7abeb8 -https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.2.0-pyhd8ed1ab_0.conda#a0bc3eec34b0fab84be6b2da94e98e20 +https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.2.1-pyhd8ed1ab_0.conda#d478a8a3044cdff1aa6e62f9269cefe0 https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_0.conda#d3483c8fc2dc2cc3f5cf43e26d60cabf https://conda.anaconda.org/conda-forge/noarch/ply-3.11-pyhd8ed1ab_2.conda#18c6deb6f9602e32446398203c8f0e91 https://conda.anaconda.org/conda-forge/linux-64/psutil-5.9.8-py39hd1e30aa_0.conda#ec86403fde8793ac1c36f8afa3d15902 @@ -199,7 +199,7 @@ https://conda.anaconda.org/conda-forge/linux-64/sip-6.7.12-py39h3d6467e_0.conda# https://conda.anaconda.org/conda-forge/noarch/urllib3-2.2.1-pyhd8ed1ab_0.conda#08807a87fa7af10754d46f63b368e016 https://conda.anaconda.org/conda-forge/linux-64/compilers-1.7.0-ha770c72_0.conda#81458b3aed8ab8711951ec3c0c04e097 https://conda.anaconda.org/conda-forge/linux-64/gstreamer-1.24.1-h98fc4e7_1.conda#b04b5cdf3ba01430db27979250bc5a1d -https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-8.3.0-h3d44ed6_0.conda#5a6f6c00ef982a9bc83558d9ac8f64a0 +https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-8.4.0-h3d44ed6_0.conda#27f46291a6aaa3c2a4f798ebd35a7ddb https://conda.anaconda.org/conda-forge/noarch/importlib_metadata-7.1.0-hd8ed1ab_0.conda#6ef2b72d291b39e479d7694efa2b2b98 https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-22_linux64_mkl.conda#eb6deb4ba6f92ea3f31c09cb8b764738 https://conda.anaconda.org/conda-forge/linux-64/libsystemd0-255-h3516f8a_1.conda#3366af27f0b593544a6cd453c7932ac5 From 910a6ba77d50f49f626700e61d2cffe22158c9ab Mon Sep 17 00:00:00 2001 From: scikit-learn-bot Date: Mon, 29 Apr 2024 09:16:03 +0200 Subject: [PATCH 070/344] :lock: :robot: CI Update lock files for scipy-dev CI build(s) :lock: :robot: (#28871) Co-authored-by: Lock file bot --- .../azure/pylatest_pip_scipy_dev_linux-64_conda.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/build_tools/azure/pylatest_pip_scipy_dev_linux-64_conda.lock b/build_tools/azure/pylatest_pip_scipy_dev_linux-64_conda.lock index a4d18dc37716a..dd70d9af4d30a 100644 --- a/build_tools/azure/pylatest_pip_scipy_dev_linux-64_conda.lock +++ b/build_tools/azure/pylatest_pip_scipy_dev_linux-64_conda.lock @@ -30,8 +30,8 @@ https://repo.anaconda.com/pkgs/main/linux-64/pip-23.3.1-py312h06a4308_0.conda#e1 # pip babel @ https://files.pythonhosted.org/packages/0d/35/4196b21041e29a42dc4f05866d0c94fa26c9da88ce12c38c2265e42c82fb/Babel-2.14.0-py3-none-any.whl#sha256=efb1a25b7118e67ce3a259bed20545c29cb68be8ad2c784c83689981b7a57287 # pip certifi @ https://files.pythonhosted.org/packages/ba/06/a07f096c664aeb9f01624f858c3add0a4e913d6c96257acb4fce61e7de14/certifi-2024.2.2-py3-none-any.whl#sha256=dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1 # pip charset-normalizer @ https://files.pythonhosted.org/packages/ee/fb/14d30eb4956408ee3ae09ad34299131fb383c47df355ddb428a7331cfa1e/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b -# pip coverage @ https://files.pythonhosted.org/packages/98/79/185cb42910b6a2b2851980407c8445ac0da0750dff65e420e86f973c8396/coverage-7.4.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51 -# pip docutils @ https://files.pythonhosted.org/packages/ea/6a/1c934b70bc12be40be886060709317c2ad5a5fe2180469410bd9344f5235/docutils-0.21.1-py3-none-any.whl#sha256=14c8d34a55b46c88f9f714adb29cefbdd69fb82f3fef825e59c5faab935390d8 +# pip coverage @ https://files.pythonhosted.org/packages/fa/d9/ec4ba0913195d240d026670d41b91f3e5b9a8a143a385f93a09e97c90f5c/coverage-7.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=adf032b6c105881f9d77fa17d9eebe0ad1f9bfb2ad25777811f97c5362aa07f2 +# pip docutils @ https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl#sha256=dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2 # pip execnet @ https://files.pythonhosted.org/packages/43/09/2aea36ff60d16dd8879bdb2f5b3ee0ba8d08cbbdcdfe870e695ce3784385/execnet-2.1.1-py3-none-any.whl#sha256=26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc # pip idna @ https://files.pythonhosted.org/packages/e5/3e/741d8c82801c347547f8a2a06aa57dbb1992be9e948df2ea0eda2c8b79e8/idna-3.7-py3-none-any.whl#sha256=82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0 # pip imagesize @ https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl#sha256=0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b @@ -40,7 +40,7 @@ https://repo.anaconda.com/pkgs/main/linux-64/pip-23.3.1-py312h06a4308_0.conda#e1 # pip meson @ https://files.pythonhosted.org/packages/33/75/b1a37fa7b2dbca8c0dbb04d5cdd7e2720c8ef6febe41b4a74866350e041c/meson-1.4.0-py3-none-any.whl#sha256=476a458d51fcfa322a6bdc64da5138997c542d08e6b2e49b9fa68c46fd7c4475 # pip ninja @ https://files.pythonhosted.org/packages/6d/92/8d7aebd4430ab5ff65df2bfee6d5745f95c004284db2d8ca76dcbfd9de47/ninja-1.11.1.1-py2.py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl#sha256=84502ec98f02a037a169c4b0d5d86075eaf6afc55e1879003d6cab51ced2ea4b # pip packaging @ https://files.pythonhosted.org/packages/49/df/1fceb2f8900f8639e278b056416d49134fb8d84c5942ffaa01ad34782422/packaging-24.0-py3-none-any.whl#sha256=2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5 -# pip platformdirs @ https://files.pythonhosted.org/packages/55/72/4898c44ee9ea6f43396fbc23d9bfaf3d06e01b83698bdf2e4c919deceb7c/platformdirs-4.2.0-py3-none-any.whl#sha256=0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068 +# pip platformdirs @ https://files.pythonhosted.org/packages/b0/15/1691fa5aaddc0c4ea4901c26f6137c29d5f6673596fe960a0340e8c308e1/platformdirs-4.2.1-py3-none-any.whl#sha256=17d5a1161b3fd67b390023cb2d3b026bbd40abde6fdb052dfbd3a29c3ba22ee1 # pip pluggy @ https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl#sha256=44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 # pip pygments @ https://files.pythonhosted.org/packages/97/9c/372fef8377a6e340b1704768d20daaded98bf13282b5327beb2e2fe2c7ef/pygments-2.17.2-py3-none-any.whl#sha256=b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c # pip six @ https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl#sha256=8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 @@ -62,6 +62,6 @@ https://repo.anaconda.com/pkgs/main/linux-64/pip-23.3.1-py312h06a4308_0.conda#e1 # pip meson-python @ https://files.pythonhosted.org/packages/91/c0/104cb6244c83fe6bc3886f144cc433db0c0c78efac5dc00e409a5a08c87d/meson_python-0.16.0-py3-none-any.whl#sha256=842dc9f5dc29e55fc769ff1b6fe328412fe6c870220fc321060a1d2d395e69e8 # pip pooch @ https://files.pythonhosted.org/packages/f4/72/8ae0f1ba4ce6a4f6d4d01a60a9fdf690fde188c45c1872b0b4ddb0607ace/pooch-1.8.1-py3-none-any.whl#sha256=6b56611ac320c239faece1ac51a60b25796792599ce5c0b1bb87bf01df55e0a9 # pip pytest-cov @ https://files.pythonhosted.org/packages/78/3a/af5b4fa5961d9a1e6237b530eb87dd04aea6eb83da09d2a4073d81b54ccf/pytest_cov-5.0.0-py3-none-any.whl#sha256=4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652 -# pip pytest-xdist @ https://files.pythonhosted.org/packages/e0/df/585742c204227526baf641e45a95d3c0a94978b85a1414622c1017ebfcf8/pytest_xdist-3.6.0-py3-none-any.whl#sha256=958e08f38472e1b3a83450d8d3e682e90fdbffee39a97dd0f27185a3bd9074d1 +# pip pytest-xdist @ https://files.pythonhosted.org/packages/6d/82/1d96bf03ee4c0fdc3c0cbe61470070e659ca78dc0086fb88b66c185e2449/pytest_xdist-3.6.1-py3-none-any.whl#sha256=9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7 # pip sphinx @ https://files.pythonhosted.org/packages/b4/fa/130c32ed94cf270e3d0b9ded16fb7b2c8fea86fa7263c29a696a30c1dde7/sphinx-7.3.7-py3-none-any.whl#sha256=413f75440be4cacf328f580b4274ada4565fb2187d696a84970c23f77b64d8c3 # pip numpydoc @ https://files.pythonhosted.org/packages/f0/fa/dcfe0f65660661db757ee9ebd84e170ff98edd5d80235f62457d9088f85f/numpydoc-1.7.0-py3-none-any.whl#sha256=5a56419d931310d79a06cfc2a126d1558700feeb9b4f3d8dcae1a8134be829c9 From f9cab767830487588329905dbdfac43a8def77b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20du=20Boisberranger?= Date: Mon, 29 Apr 2024 09:18:27 +0200 Subject: [PATCH 071/344] CI Revert PyPy lock file after threadpoolctl min version bump (#28894) --- build_tools/azure/pypy3_linux-64_conda.lock | 22 ++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/build_tools/azure/pypy3_linux-64_conda.lock b/build_tools/azure/pypy3_linux-64_conda.lock index 342bd1af08406..23710cfe35cb8 100644 --- a/build_tools/azure/pypy3_linux-64_conda.lock +++ b/build_tools/azure/pypy3_linux-64_conda.lock @@ -4,23 +4,23 @@ @EXPLICIT https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2024.2.2-hbcca054_0.conda#2f4327a1cbe7f022401b236e915a5fef -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-13.2.0-h95c4c6d_6.conda#3cfab3e709f77e9f1b3d380eb622494a +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-13.2.0-h7e041cc_5.conda#f6f6600d18a4047b54f803cf708b868a https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.9-4_pypy39_pp73.conda#c1b2f29111681a4036ed21eaa3f44620 https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda#161081fc7cec0bfda0d86d7cb595f8d8 https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_kmp_llvm.tar.bz2#562b26ba2e19059551a811e72ab7f793 -https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-hc881cc4_6.conda#df88796bd09a0d2ed292e59101478ad8 +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-h807b86a_5.conda#d4ff227c46917d3b4565302a2bbb276b https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hd590300_5.conda#69b8b6202a07720f448be700e300ccf4 https://conda.anaconda.org/conda-forge/linux-64/lerc-4.0.0-h27087fc_0.tar.bz2#76bbff344f0134279f225174e9064c8f https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.1.0-hd590300_1.conda#aec6c91c7371c26392a06708a73c70e5 https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.20-hd590300_0.conda#8e88f9389f1165d7c0936fe40d9a9a79 https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.6.2-h59595ed_0.conda#e7ba12deb7020dd080c6c70e7b6f6a3d https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.2-h7f98852_5.tar.bz2#d645c6d2ac96843a2bfaccd2d62b3ac3 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-13.2.0-h43f5ff8_6.conda#e54a5ddc67e673f9105cf2a2e9c070b0 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-13.2.0-ha4646dd_5.conda#7a6bd7a12a4bd359e2afe6c0fa1acace https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.0.0-hd590300_1.conda#ea25936bb4080d843790b586850f82b8 -https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.4.0-hd590300_0.conda#b26e8aa824079e1be0294e7152ca4559 +https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.3.2-hd590300_1.conda#049b7df8bae5e184d1de42cdf64855f8 https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.2.13-hd590300_5.conda#f36c115f1ee199da648e0597ec2047ad https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.4.20240210-h59595ed_0.conda#97da8860a0da5413c7c98a3b3838a645 -https://conda.anaconda.org/conda-forge/linux-64/ninja-1.12.0-h00ab1b0_0.conda#b048701d52e7cbb5f59ddd4d3b17bbf5 +https://conda.anaconda.org/conda-forge/linux-64/ninja-1.11.1-h924138e_0.conda#73a4953a2d9c115bdc10ff30a52f675f https://conda.anaconda.org/conda-forge/linux-64/openssl-3.2.1-hd590300_1.conda#9d731343cff6ee2e5a25c4a091bf8e2a https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-h36c2ea0_1001.tar.bz2#22dad4df6e8630e8dff2428f6f6a7036 https://conda.anaconda.org/conda-forge/linux-64/xorg-kbproto-1.0.7-h7f98852_1002.tar.bz2#4b230e8381279d76131116660f5a241a @@ -32,9 +32,9 @@ https://conda.anaconda.org/conda-forge/linux-64/xz-5.2.6-h166bdaf_0.tar.bz2#2161 https://conda.anaconda.org/conda-forge/linux-64/expat-2.6.2-h59595ed_0.conda#53fb86322bdb89496d7579fe3f02fd61 https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.1.0-hd590300_1.conda#f07002e225d7a60a694d42a7bf5ff53f https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.1.0-hd590300_1.conda#5fc11c6020d421960607d821310fcd4d -https://conda.anaconda.org/conda-forge/linux-64/libgfortran-ng-13.2.0-h69a702a_6.conda#3666a850342f8f3be88f9a93d948d027 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-ng-13.2.0-h69a702a_5.conda#e73e9cfd1191783392131e6238bdb3e9 https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.43-h2797004_0.conda#009981dd9cfcaa4dbfa25ffaed86bcae -https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.45.3-h2797004_0.conda#b3316cbe90249da4f8e84cd66e1cc55b +https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.45.2-h2797004_0.conda#866983a220e27a80cb75e85cb30466a1 https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.15-h0b41bf4_0.conda#33277193f5b92bad9fdd230eb700929c https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda#47d31b792659ce70f470b5c82fdfb7a4 https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda#d453b98d9c83e71da0741bb0ff4d76bc @@ -47,7 +47,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libhiredis-1.0.2-h2cc385e_0.tar. https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.27-pthreads_h413a1c8_0.conda#a356024784da6dfd4683dc5ecf45b155 https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.6.0-h1dd3fc0_3.conda#66f03896ffbe1a110ffda05c7a856504 https://conda.anaconda.org/conda-forge/linux-64/llvm-openmp-18.1.3-h4dfa4b3_0.conda#d39965123dffcad4d750989be65bcb7c -https://conda.anaconda.org/conda-forge/linux-64/sqlite-3.45.3-h2c6b66d_0.conda#be7d70f2db41b674733667bdd69bd000 +https://conda.anaconda.org/conda-forge/linux-64/sqlite-3.45.2-h2c6b66d_0.conda#1423efca06ed343c1da0fc429bae0779 https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.9-h8ee46fc_0.conda#077b6e8ad6a3ddb741fce2496dd01bec https://conda.anaconda.org/conda-forge/linux-64/brotli-1.1.0-hd590300_1.conda#f27a24d46e3ea7b70a1f98e50c62508f https://conda.anaconda.org/conda-forge/linux-64/ccache-4.9.1-h1fcd64f_0.conda#3620f564bcf28c3524951b6f64f5c5ac @@ -72,7 +72,7 @@ https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyh9f0ad1d_0.tar.bz2 https://conda.anaconda.org/conda-forge/linux-64/numpy-1.26.4-py39h6dedee3_0.conda#557d64563e84ff21b14f586c7f662b7f https://conda.anaconda.org/conda-forge/noarch/packaging-24.0-pyhd8ed1ab_0.conda#248f521b64ce055e7feae3105e7abeb8 https://conda.anaconda.org/conda-forge/linux-64/pillow-10.3.0-py39h90a76f3_0.conda#799e6519cfffe2784db27b1db2ef33f3 -https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_0.conda#d3483c8fc2dc2cc3f5cf43e26d60cabf +https://conda.anaconda.org/conda-forge/noarch/pluggy-1.4.0-pyhd8ed1ab_0.conda#139e9feb65187e916162917bb2484976 https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.1.2-pyhd8ed1ab_0.conda#b9a4dacf97241704529131a0dfc0494f https://conda.anaconda.org/conda-forge/noarch/pypy-7.3.15-1_pypy39.conda#a418a6c16bd6f7ed56b92194214791a0 https://conda.anaconda.org/conda-forge/noarch/setuptools-69.5.1-pyhd8ed1ab_0.conda#7462280d81f639363e6e63c81276bd9e @@ -90,13 +90,13 @@ https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.4.0-pyhd8ed1 https://conda.anaconda.org/conda-forge/noarch/joblib-1.4.0-pyhd8ed1ab_0.conda#e0ed1bf13ce3a440e022157bf4764465 https://conda.anaconda.org/conda-forge/noarch/meson-1.4.0-pyhd8ed1ab_0.conda#52a0660cfa40b45bf254ecc3374cb2e0 https://conda.anaconda.org/conda-forge/noarch/pip-24.0-pyhd8ed1ab_0.conda#f586ac1e56c8638b64f9c8122a7b8a67 -https://conda.anaconda.org/conda-forge/noarch/pyproject-metadata-0.8.0-pyhd8ed1ab_0.conda#573fe09d7bd0cd4bcc210d8369b5ca47 +https://conda.anaconda.org/conda-forge/noarch/pyproject-metadata-0.7.1-pyhd8ed1ab_0.conda#dcb27826ffc94d5f04e241322239983b https://conda.anaconda.org/conda-forge/noarch/pytest-7.4.4-pyhd8ed1ab_0.conda#a9d145de8c5f064b5fa68fb34725d9f4 https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0-pyhd8ed1ab_0.conda#2cf4264fffb9e6eff6031c5b6884d61c https://conda.anaconda.org/conda-forge/linux-64/scipy-1.12.0-py39h6dedee3_2.conda#6c5d74bac41838f4377dfd45085e1fec https://conda.anaconda.org/conda-forge/linux-64/blas-2.122-openblas.conda#5065468105542a8b23ea47bd8b6fa55f https://conda.anaconda.org/conda-forge/noarch/importlib-resources-6.4.0-pyhd8ed1ab_0.conda#dcbadab7a68738a028e195ab68ab2d2e -https://conda.anaconda.org/conda-forge/noarch/meson-python-0.16.0-pyh0c530f3_0.conda#e16f0dbf502da873be9f9adb0dc52547 +https://conda.anaconda.org/conda-forge/noarch/meson-python-0.15.0-pyh0c530f3_0.conda#3bc64565ca78ce3bb80248d09926d8f9 https://conda.anaconda.org/conda-forge/linux-64/pyamg-5.1.0-py39h5fd064f_0.conda#04676d2a49da3cb608af77e04b796ce1 https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.5.0-pyhd8ed1ab_0.conda#d5f595da2daead898ca958ac62f0307b https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.8.4-py39h4e7d633_0.conda#58272019e595dde98d0844ae3ebf0cfe From 19c068f64249f95f745962b42a4dd581c7738218 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20du=20Boisberranger?= Date: Mon, 29 Apr 2024 10:05:54 +0200 Subject: [PATCH 072/344] MAINT Clean up deprecations for 1.5: in log_loss (#28851) Co-authored-by: Guillaume Lemaitre --- sklearn/metrics/_classification.py | 59 +++-------- sklearn/metrics/tests/test_classification.py | 104 +++++++++---------- sklearn/metrics/tests/test_common.py | 15 ++- 3 files changed, 75 insertions(+), 103 deletions(-) diff --git a/sklearn/metrics/_classification.py b/sklearn/metrics/_classification.py index 62a6c87428e9a..caa4db5479a29 100644 --- a/sklearn/metrics/_classification.py +++ b/sklearn/metrics/_classification.py @@ -2816,16 +2816,13 @@ def hamming_loss(y_true, y_pred, *, sample_weight=None): { "y_true": ["array-like"], "y_pred": ["array-like"], - "eps": [StrOptions({"auto"}), Interval(Real, 0, 1, closed="both")], "normalize": ["boolean"], "sample_weight": ["array-like", None], "labels": ["array-like", None], }, prefer_skip_nested_validation=True, ) -def log_loss( - y_true, y_pred, *, eps="auto", normalize=True, sample_weight=None, labels=None -): +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 @@ -2855,19 +2852,8 @@ def log_loss( ordered alphabetically, as done by :class:`~sklearn.preprocessing.LabelBinarizer`. - eps : float or "auto", default="auto" - Log loss is undefined for p=0 or p=1, so probabilities are - clipped to `max(eps, min(1 - eps, p))`. The default will depend on the - data type of `y_pred` and is set to `np.finfo(y_pred.dtype).eps`. - - .. versionadded:: 1.2 - - .. versionchanged:: 1.2 - The default value changed from `1e-15` to `"auto"` that is - equivalent to `np.finfo(y_pred.dtype).eps`. - - .. deprecated:: 1.3 - `eps` is deprecated in 1.3 and will be removed in 1.5. + `y_pred` values are clipped to `[eps, 1-eps]` where `eps` is the machine + precision for `y_pred`'s dtype. normalize : bool, default=True If true, return the mean loss per sample. @@ -2907,18 +2893,6 @@ def log_loss( y_pred = check_array( y_pred, ensure_2d=False, dtype=[np.float64, np.float32, np.float16] ) - if eps == "auto": - eps = np.finfo(y_pred.dtype).eps - else: - # TODO: Remove user defined eps in 1.5 - warnings.warn( - ( - "Setting the eps parameter is deprecated and will " - "be removed in 1.5. Instead eps will always have" - "a default value of `np.finfo(y_pred.dtype).eps`." - ), - FutureWarning, - ) check_consistent_length(y_pred, y_true, sample_weight) lb = LabelBinarizer() @@ -2949,9 +2923,6 @@ def log_loss( 1 - transformed_labels, transformed_labels, axis=1 ) - # Clipping - y_pred = np.clip(y_pred, eps, 1 - eps) - # If y_pred is of single dimension, assume y_true to be binary # and then check. if y_pred.ndim == 1: @@ -2959,6 +2930,19 @@ def log_loss( if y_pred.shape[1] == 1: y_pred = np.append(1 - y_pred, y_pred, axis=1) + eps = np.finfo(y_pred.dtype).eps + + # Make sure y_pred is normalized + y_pred_sum = y_pred.sum(axis=1) + if not np.allclose(y_pred_sum, 1, rtol=np.sqrt(eps)): + warnings.warn( + "The y_pred values do not sum to one. Make sure to pass probabilities.", + UserWarning, + ) + + # Clipping + y_pred = np.clip(y_pred, eps, 1 - eps) + # Check if dimensions are consistent. transformed_labels = check_array(transformed_labels) if len(lb.classes_) != y_pred.shape[1]: @@ -2979,17 +2963,6 @@ def log_loss( "labels: {0}".format(lb.classes_) ) - # Renormalize - y_pred_sum = y_pred.sum(axis=1) - if not np.isclose(y_pred_sum, 1, rtol=1e-15, atol=5 * eps).all(): - warnings.warn( - ( - "The y_pred values do not sum to one. Starting from 1.5 this" - "will result in an error." - ), - UserWarning, - ) - y_pred = y_pred / y_pred_sum[:, np.newaxis] loss = -xlogy(transformed_labels, y_pred).sum(axis=1) return float(_average(loss, weights=sample_weight, normalize=normalize)) diff --git a/sklearn/metrics/tests/test_classification.py b/sklearn/metrics/tests/test_classification.py index bbebe2cba2197..144871c8d02ee 100644 --- a/sklearn/metrics/tests/test_classification.py +++ b/sklearn/metrics/tests/test_classification.py @@ -2624,62 +2624,37 @@ def test_log_loss(): ) loss = log_loss(y_true, y_pred) loss_true = -np.mean(bernoulli.logpmf(np.array(y_true) == "yes", y_pred[:, 1])) - assert_almost_equal(loss, loss_true) + 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_almost_equal(loss, 0.6904911) + 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_almost_equal(loss, 0.6904911 * 6, decimal=6) - - user_warning_msg = "y_pred values do not sum to one" - # check eps and handling of absolute zero and one probabilities - y_pred = np.asarray(y_pred) > 0.5 - with pytest.warns(FutureWarning): - loss = log_loss(y_true, y_pred, normalize=True, eps=0.1) - with pytest.warns(UserWarning, match=user_warning_msg): - assert_almost_equal(loss, log_loss(y_true, np.clip(y_pred, 0.1, 0.9))) - - # binary case: check correct boundary values for eps = 0 - with pytest.warns(FutureWarning): - assert log_loss([0, 1], [0, 1], eps=0) == 0 - with pytest.warns(FutureWarning): - assert log_loss([0, 1], [0, 0], eps=0) == np.inf - with pytest.warns(FutureWarning): - assert log_loss([0, 1], [1, 1], eps=0) == np.inf - - # multiclass case: check correct boundary values for eps = 0 - with pytest.warns(FutureWarning): - assert log_loss([0, 1, 2], [[1, 0, 0], [0, 1, 0], [0, 0, 1]], eps=0) == 0 - with pytest.warns(FutureWarning): - assert ( - log_loss([0, 1, 2], [[0, 0.5, 0.5], [0, 1, 0], [0, 0, 1]], eps=0) == np.inf - ) + assert_allclose(loss, 0.6904911 * 6) # raise error if number of classes are not equal. y_true = [1, 0, 2] - y_pred = [[0.2, 0.7], [0.6, 0.5], [0.4, 0.1]] + y_pred = [[0.3, 0.7], [0.6, 0.4], [0.4, 0.6]] with pytest.raises(ValueError): log_loss(y_true, y_pred) # case when y_true is a string array object y_true = ["ham", "spam", "spam", "ham"] - y_pred = [[0.2, 0.7], [0.6, 0.5], [0.4, 0.1], [0.7, 0.2]] - with pytest.warns(UserWarning, match=user_warning_msg): - loss = log_loss(y_true, y_pred) - assert_almost_equal(loss, 1.0383217, decimal=6) + y_pred = [[0.3, 0.7], [0.6, 0.4], [0.4, 0.6], [0.7, 0.3]] + loss = log_loss(y_true, y_pred) + assert_allclose(loss, 0.7469410) # test labels option y_true = [2, 2] - y_pred = [[0.2, 0.7], [0.6, 0.5]] + y_pred = [[0.2, 0.8], [0.6, 0.4]] y_score = np.array([[0.1, 0.9], [0.1, 0.9]]) error_str = ( r"y_true contains only one label \(2\). Please provide " @@ -2688,50 +2663,66 @@ def test_log_loss(): with pytest.raises(ValueError, match=error_str): log_loss(y_true, y_pred) - y_pred = [[0.2, 0.7], [0.6, 0.5], [0.2, 0.3]] - error_str = "Found input variables with inconsistent numbers of samples: [3, 2]" - (ValueError, error_str, log_loss, y_true, y_pred) + y_pred = [[0.2, 0.8], [0.6, 0.4], [0.7, 0.3]] + error_str = r"Found input variables with inconsistent numbers of samples: \[3, 2\]" + with pytest.raises(ValueError, match=error_str): + log_loss(y_true, y_pred) # 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_almost_equal(calculated_log_loss, true_log_loss) + 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.2, 0.7, 0.3], [0.6, 0.5, 0.3], [0.3, 0.9, 0.1]] - with pytest.warns(UserWarning, match=user_warning_msg): - loss = log_loss(y_true, y_score2, labels=[1, 2, 3]) - assert_almost_equal(loss, 1.0630345, decimal=6) + 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)) + +@pytest.mark.parametrize("dtype", [np.float64, np.float32, np.float16]) +def test_log_loss_eps(dtype): + """Check the behaviour internal eps that changes depending on the input dtype. -def test_log_loss_eps_auto(global_dtype): - """Check the behaviour of `eps="auto"` that changes depending on the input - array dtype. Non-regression test for: https://github.com/scikit-learn/scikit-learn/issues/24315 """ - y_true = np.array([0, 1], dtype=global_dtype) - y_pred = y_true.copy() + y_true = np.array([0, 1], dtype=dtype) + y_pred = np.array([1, 0], dtype=dtype) - loss = log_loss(y_true, y_pred, eps="auto") + loss = log_loss(y_true, y_pred) assert np.isfinite(loss) -def test_log_loss_eps_auto_float16(): - """Check the behaviour of `eps="auto"` for np.float16""" - y_true = np.array([0, 1], dtype=np.float16) - y_pred = y_true.copy() +@pytest.mark.parametrize("dtype", [np.float64, np.float32, np.float16]) +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) - loss = log_loss(y_true, y_pred, eps="auto") - assert np.isfinite(loss) + with pytest.warns(UserWarning, match="The y_pred values do not sum to one."): + log_loss(y_true, y_pred) + + +@pytest.mark.parametrize( + "y_true, y_pred", + [ + ([0, 1, 0], [0, 1, 0]), + ([0, 1, 0], [[1, 0], [0, 1], [1, 0]]), + ([0, 1, 2], [[1, 0, 0], [0, 1, 0], [0, 0, 1]]), + ], +) +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.2, 0.7], [0.6, 0.5], [0.4, 0.1], [0.7, 0.2]]) + 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 @@ -2742,9 +2733,8 @@ def test_log_loss_pandas_input(): for TrueInputType, PredInputType in types: # y_pred dataframe, y_true series y_true, y_pred = TrueInputType(y_tr), PredInputType(y_pr) - with pytest.warns(UserWarning, match="y_pred values do not sum to one"): - loss = log_loss(y_true, y_pred) - assert_almost_equal(loss, 1.0383217, decimal=6) + loss = log_loss(y_true, y_pred) + assert_allclose(loss, 0.7469410) def test_brier_score_loss(): diff --git a/sklearn/metrics/tests/test_common.py b/sklearn/metrics/tests/test_common.py index e84ef1e358473..886f870da6adf 100644 --- a/sklearn/metrics/tests/test_common.py +++ b/sklearn/metrics/tests/test_common.py @@ -637,7 +637,10 @@ 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.normal(size=y_true.shape) + 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 @@ -1566,7 +1569,10 @@ def test_multilabel_sample_weight_invariance(name): ) y_true = np.vstack([ya, yb]) y_pred = np.vstack([ya, ya]) - y_score = random_state.randint(1, 4, size=y_true.shape) + 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) metric = ALL_METRICS[name] if name in THRESHOLDED_METRICS: @@ -1629,7 +1635,10 @@ def test_thresholded_multilabel_multioutput_permutations_invariance(name): random_state = check_random_state(0) n_samples, n_classes = 20, 4 y_true = random_state.randint(0, 2, size=(n_samples, n_classes)) - y_score = random_state.normal(size=y_true.shape) + 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) # Makes sure all samples have at least one label. This works around errors # when running metrics where average="sample" From 0bdc754e5dcfb155ce1a042d2a123b515a05efcb Mon Sep 17 00:00:00 2001 From: Will Dean <57733339+wd60622@users.noreply.github.com> Date: Mon, 29 Apr 2024 17:08:25 +0200 Subject: [PATCH 073/344] API Standardize `X` as `inverse_transform` input parameter (#28756) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jérémie du Boisberranger --- doc/whats_new/v1.5.rst | 11 ++++++ sklearn/cluster/_feature_agglomeration.py | 37 ++++++------------- .../tests/test_feature_agglomeration.py | 16 ++++---- sklearn/decomposition/_nmf.py | 29 +++++---------- sklearn/decomposition/tests/test_nmf.py | 21 ++++++----- sklearn/model_selection/_search.py | 13 ++++++- sklearn/model_selection/tests/test_search.py | 23 ++++++++++++ sklearn/pipeline.py | 20 +++++++--- sklearn/preprocessing/_discretization.py | 15 ++++++-- .../tests/test_discretization.py | 20 ++++++++++ sklearn/tests/test_pipeline.py | 21 +++++++++++ sklearn/utils/deprecation.py | 19 ++++++++++ 12 files changed, 172 insertions(+), 73 deletions(-) diff --git a/doc/whats_new/v1.5.rst b/doc/whats_new/v1.5.rst index 0e84202f876e4..9f53afd433ffc 100644 --- a/doc/whats_new/v1.5.rst +++ b/doc/whats_new/v1.5.rst @@ -56,6 +56,17 @@ Changed models signs across all `PCA` solvers, including the new `svd_solver="covariance_eigh"` option introduced in this release. +Changes impacting many modules +------------------------------ + +- |API| The name of the input of the `inverse_transform` method of estimators has been + standardized to `X`. As a consequence, `Xt` is deprecated and will be removed in + version 1.7 in the following estimators: :class:`cluster.FeatureAgglomeration`, + :class:`decomposition.MiniBatchNMF`, :class:`decomposition.NMF`, + :class:`model_selection.GridSearchCV`, :class:`model_selection.RandomizedSearchCV`, + :class:`pipeline.Pipeline` and :class:`preprocessing.KBinsDiscretizer`. + :pr:`28756` by :user:`Will Dean `. + Support for Array API --------------------- diff --git a/sklearn/cluster/_feature_agglomeration.py b/sklearn/cluster/_feature_agglomeration.py index 218db48ad2331..c91952061a6f6 100644 --- a/sklearn/cluster/_feature_agglomeration.py +++ b/sklearn/cluster/_feature_agglomeration.py @@ -6,13 +6,13 @@ # Author: V. Michel, A. Gramfort # License: BSD 3 clause -import warnings import numpy as np from scipy.sparse import issparse from ..base import TransformerMixin from ..utils import metadata_routing +from ..utils.deprecation import _deprecate_Xt_in_inverse_transform from ..utils.validation import check_is_fitted ############################################################################### @@ -25,9 +25,9 @@ class AgglomerationTransform(TransformerMixin): """ # This prevents ``set_split_inverse_transform`` to be generated for the - # non-standard ``Xred`` arg on ``inverse_transform``. - # TODO(1.5): remove when Xred is removed for inverse_transform. - __metadata_request__inverse_transform = {"Xred": metadata_routing.UNUSED} + # non-standard ``Xt`` arg on ``inverse_transform``. + # TODO(1.7): remove when Xt is removed for inverse_transform. + __metadata_request__inverse_transform = {"Xt": metadata_routing.UNUSED} def transform(self, X): """ @@ -63,19 +63,20 @@ def transform(self, X): nX = np.array(nX).T return nX - def inverse_transform(self, Xt=None, Xred=None): + def inverse_transform(self, X=None, *, Xt=None): """ Inverse the transformation and return a vector of size `n_features`. Parameters ---------- - Xt : array-like of shape (n_samples, n_clusters) or (n_clusters,) + X : array-like of shape (n_samples, n_clusters) or (n_clusters,) The values to be assigned to each cluster of samples. - Xred : deprecated - Use `Xt` instead. + Xt : array-like of shape (n_samples, n_clusters) or (n_clusters,) + The values to be assigned to each cluster of samples. - .. deprecated:: 1.3 + .. deprecated:: 1.5 + `Xt` was deprecated in 1.5 and will be removed in 1.7. Use `X` instead. Returns ------- @@ -83,23 +84,9 @@ def inverse_transform(self, Xt=None, Xred=None): A vector of size `n_samples` with the values of `Xred` assigned to each of the cluster of samples. """ - if Xt is None and Xred is None: - raise TypeError("Missing required positional argument: Xt") - - if Xred is not None and Xt is not None: - raise ValueError("Please provide only `Xt`, and not `Xred`.") - - if Xred is not None: - warnings.warn( - ( - "Input argument `Xred` was renamed to `Xt` in v1.3 and will be" - " removed in v1.5." - ), - FutureWarning, - ) - Xt = Xred + X = _deprecate_Xt_in_inverse_transform(X, Xt) check_is_fitted(self) unil, inverse = np.unique(self.labels_, return_inverse=True) - return Xt[..., inverse] + return X[..., inverse] diff --git a/sklearn/cluster/tests/test_feature_agglomeration.py b/sklearn/cluster/tests/test_feature_agglomeration.py index abeb81dca50aa..488dd638ad125 100644 --- a/sklearn/cluster/tests/test_feature_agglomeration.py +++ b/sklearn/cluster/tests/test_feature_agglomeration.py @@ -59,23 +59,23 @@ def test_feature_agglomeration_feature_names_out(): ) -# TODO(1.5): remove this test -def test_inverse_transform_Xred_deprecation(): +# TODO(1.7): remove this test +def test_inverse_transform_Xt_deprecation(): X = np.array([0, 0, 1]).reshape(1, 3) # (n_samples, n_features) est = FeatureAgglomeration(n_clusters=1, pooling_func=np.mean) est.fit(X) - Xt = est.transform(X) + X = est.transform(X) with pytest.raises(TypeError, match="Missing required positional argument"): est.inverse_transform() - with pytest.raises(ValueError, match="Please provide only"): - est.inverse_transform(Xt=Xt, Xred=Xt) + with pytest.raises(TypeError, match="Cannot use both X and Xt. Use X only."): + est.inverse_transform(X=X, Xt=X) with warnings.catch_warnings(record=True): warnings.simplefilter("error") - est.inverse_transform(Xt) + est.inverse_transform(X) - with pytest.warns(FutureWarning, match="Input argument `Xred` was renamed to `Xt`"): - est.inverse_transform(Xred=Xt) + with pytest.warns(FutureWarning, match="Xt was renamed X in version 1.5"): + est.inverse_transform(Xt=X) diff --git a/sklearn/decomposition/_nmf.py b/sklearn/decomposition/_nmf.py index 75266c5f64b2b..30725c33f4df3 100644 --- a/sklearn/decomposition/_nmf.py +++ b/sklearn/decomposition/_nmf.py @@ -32,6 +32,7 @@ StrOptions, validate_params, ) +from ..utils.deprecation import _deprecate_Xt_in_inverse_transform from ..utils.extmath import randomized_svd, safe_sparse_dot, squared_norm from ..utils.validation import ( check_is_fitted, @@ -1310,44 +1311,32 @@ def fit(self, X, y=None, **params): self.fit_transform(X, **params) return self - def inverse_transform(self, Xt=None, W=None): + def inverse_transform(self, X=None, *, Xt=None): """Transform data back to its original space. .. versionadded:: 0.18 Parameters ---------- - Xt : {ndarray, sparse matrix} of shape (n_samples, n_components) + X : {ndarray, sparse matrix} of shape (n_samples, n_components) Transformed data matrix. - W : deprecated - Use `Xt` instead. + Xt : {ndarray, sparse matrix} of shape (n_samples, n_components) + Transformed data matrix. - .. deprecated:: 1.3 + .. deprecated:: 1.5 + `Xt` was deprecated in 1.5 and will be removed in 1.7. Use `X` instead. Returns ------- X : ndarray of shape (n_samples, n_features) Returns a data matrix of the original shape. """ - if Xt is None and W is None: - raise TypeError("Missing required positional argument: Xt") - if W is not None and Xt is not None: - raise ValueError("Please provide only `Xt`, and not `W`.") - - if W is not None: - warnings.warn( - ( - "Input argument `W` was renamed to `Xt` in v1.3 and will be removed" - " in v1.5." - ), - FutureWarning, - ) - Xt = W + X = _deprecate_Xt_in_inverse_transform(X, Xt) check_is_fitted(self) - return Xt @ self.components_ + return X @ self.components_ @property def _n_features_out(self): diff --git a/sklearn/decomposition/tests/test_nmf.py b/sklearn/decomposition/tests/test_nmf.py index 2112b59129e25..b6eb4f9b1becc 100644 --- a/sklearn/decomposition/tests/test_nmf.py +++ b/sklearn/decomposition/tests/test_nmf.py @@ -933,30 +933,31 @@ def test_minibatch_nmf_verbose(): sys.stdout = old_stdout -# TODO(1.5): remove this test -def test_NMF_inverse_transform_W_deprecation(): - rng = np.random.mtrand.RandomState(42) +# TODO(1.7): remove this test +@pytest.mark.parametrize("Estimator", [NMF, MiniBatchNMF]) +def test_NMF_inverse_transform_Xt_deprecation(Estimator): + rng = np.random.RandomState(42) A = np.abs(rng.randn(6, 5)) - est = NMF( + est = Estimator( n_components=3, init="random", random_state=0, tol=1e-6, ) - Xt = est.fit_transform(A) + X = est.fit_transform(A) with pytest.raises(TypeError, match="Missing required positional argument"): est.inverse_transform() - with pytest.raises(ValueError, match="Please provide only"): - est.inverse_transform(Xt=Xt, W=Xt) + with pytest.raises(TypeError, match="Cannot use both X and Xt. Use X only"): + est.inverse_transform(X=X, Xt=X) with warnings.catch_warnings(record=True): warnings.simplefilter("error") - est.inverse_transform(Xt) + est.inverse_transform(X) - with pytest.warns(FutureWarning, match="Input argument `W` was renamed to `Xt`"): - est.inverse_transform(W=Xt) + with pytest.warns(FutureWarning, match="Xt was renamed X in version 1.5"): + est.inverse_transform(Xt=X) @pytest.mark.parametrize("Estimator", [NMF, MiniBatchNMF]) diff --git a/sklearn/model_selection/_search.py b/sklearn/model_selection/_search.py index 42fde09c16bce..a26ec0786849d 100644 --- a/sklearn/model_selection/_search.py +++ b/sklearn/model_selection/_search.py @@ -36,6 +36,7 @@ from ..utils._estimator_html_repr import _VisualBlock from ..utils._param_validation import HasMethods, Interval, StrOptions from ..utils._tags import _safe_tags +from ..utils.deprecation import _deprecate_Xt_in_inverse_transform from ..utils.metadata_routing import ( MetadataRouter, MethodMapping, @@ -637,7 +638,7 @@ def transform(self, X): return self.best_estimator_.transform(X) @available_if(_estimator_has("inverse_transform")) - def inverse_transform(self, Xt): + def inverse_transform(self, X=None, Xt=None): """Call inverse_transform on the estimator with the best found params. Only available if the underlying estimator implements @@ -645,18 +646,26 @@ def inverse_transform(self, Xt): Parameters ---------- + X : indexable, length n_samples + Must fulfill the input assumptions of the + underlying estimator. + Xt : indexable, length n_samples Must fulfill the input assumptions of the underlying estimator. + .. deprecated:: 1.5 + `Xt` was deprecated in 1.5 and will be removed in 1.7. Use `X` instead. + Returns ------- X : {ndarray, sparse matrix} of shape (n_samples, n_features) Result of the `inverse_transform` function for `Xt` based on the estimator with the best found parameters. """ + X = _deprecate_Xt_in_inverse_transform(X, Xt) check_is_fitted(self) - return self.best_estimator_.inverse_transform(Xt) + return self.best_estimator_.inverse_transform(X) @property def n_features_in_(self): diff --git a/sklearn/model_selection/tests/test_search.py b/sklearn/model_selection/tests/test_search.py index 9eb647df887c0..b59ed7168ff10 100644 --- a/sklearn/model_selection/tests/test_search.py +++ b/sklearn/model_selection/tests/test_search.py @@ -3,6 +3,7 @@ import pickle import re import sys +import warnings from collections.abc import Iterable, Sized from functools import partial from io import StringIO @@ -2553,6 +2554,28 @@ def test_search_html_repr(): assert "
LogisticRegression()
" in repr_html +# TODO(1.7): remove this test +@pytest.mark.parametrize("SearchCV", [GridSearchCV, RandomizedSearchCV]) +def test_inverse_transform_Xt_deprecation(SearchCV): + clf = MockClassifier() + search = SearchCV(clf, {"foo_param": [1, 2, 3]}, cv=3, verbose=3) + + X2 = search.fit(X, y).transform(X) + + with pytest.raises(TypeError, match="Missing required positional argument"): + search.inverse_transform() + + with pytest.raises(TypeError, match="Cannot use both X and Xt. Use X only"): + search.inverse_transform(X=X2, Xt=X2) + + with warnings.catch_warnings(record=True): + warnings.simplefilter("error") + search.inverse_transform(X2) + + with pytest.warns(FutureWarning, match="Xt was renamed X in version 1.5"): + search.inverse_transform(Xt=X2) + + # Metadata Routing Tests # ====================== diff --git a/sklearn/pipeline.py b/sklearn/pipeline.py index 93f9ef09fc40a..b200177b8606f 100644 --- a/sklearn/pipeline.py +++ b/sklearn/pipeline.py @@ -29,6 +29,7 @@ ) from .utils._tags import _safe_tags from .utils._user_interface import _print_elapsed_time +from .utils.deprecation import _deprecate_Xt_in_inverse_transform from .utils.metadata_routing import ( MetadataRouter, MethodMapping, @@ -909,19 +910,28 @@ 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, Xt, **params): + def inverse_transform(self, X=None, *, Xt=None, **params): """Apply `inverse_transform` for each step in a reverse order. All estimators in the pipeline must support `inverse_transform`. Parameters ---------- + 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. + Xt : 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. + .. deprecated:: 1.5 + `Xt` was deprecated in 1.5 and will be removed in 1.7. Use `X` instead. + **params : dict of str -> object Parameters requested and accepted by steps. Each step must have requested certain metadata for these parameters to be forwarded to @@ -940,15 +950,15 @@ def inverse_transform(self, Xt, **params): """ _raise_for_params(params, self, "inverse_transform") + X = _deprecate_Xt_in_inverse_transform(X, Xt) + # 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: - Xt = transform.inverse_transform( - Xt, **routed_params[name].inverse_transform - ) - return Xt + 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): diff --git a/sklearn/preprocessing/_discretization.py b/sklearn/preprocessing/_discretization.py index 02d144b87f798..ee8a336a75453 100644 --- a/sklearn/preprocessing/_discretization.py +++ b/sklearn/preprocessing/_discretization.py @@ -12,6 +12,7 @@ from ..base import BaseEstimator, TransformerMixin, _fit_context from ..utils import resample from ..utils._param_validation import Interval, Options, StrOptions +from ..utils.deprecation import _deprecate_Xt_in_inverse_transform from ..utils.stats import _weighted_percentile from ..utils.validation import ( _check_feature_names_in, @@ -389,7 +390,7 @@ def transform(self, X): self._encoder.dtype = dtype_init return Xt_enc - def inverse_transform(self, Xt): + def inverse_transform(self, X=None, *, Xt=None): """ Transform discretized data back to original feature space. @@ -398,20 +399,28 @@ def inverse_transform(self, Xt): Parameters ---------- + X : array-like of shape (n_samples, n_features) + Transformed data in the binned space. + Xt : array-like of shape (n_samples, n_features) Transformed data in the binned space. + .. deprecated:: 1.5 + `Xt` was deprecated in 1.5 and will be removed in 1.7. Use `X` instead. + Returns ------- Xinv : ndarray, dtype={np.float32, np.float64} Data in the original feature space. """ + X = _deprecate_Xt_in_inverse_transform(X, Xt) + check_is_fitted(self) if "onehot" in self.encode: - Xt = self._encoder.inverse_transform(Xt) + X = self._encoder.inverse_transform(X) - Xinv = check_array(Xt, copy=True, dtype=(np.float64, np.float32)) + Xinv = check_array(X, copy=True, dtype=(np.float64, np.float32)) n_features = self.n_bins_.shape[0] if Xinv.shape[1] != n_features: raise ValueError( diff --git a/sklearn/preprocessing/tests/test_discretization.py b/sklearn/preprocessing/tests/test_discretization.py index 19aaa5bdba850..fd16a3db3efac 100644 --- a/sklearn/preprocessing/tests/test_discretization.py +++ b/sklearn/preprocessing/tests/test_discretization.py @@ -478,3 +478,23 @@ def test_kbinsdiscretizer_subsample(strategy, global_random_seed): assert_allclose( kbd_subsampling.bin_edges_[0], kbd_no_subsampling.bin_edges_[0], rtol=1e-2 ) + + +# TODO(1.7): remove this test +def test_KBD_inverse_transform_Xt_deprecation(): + X = np.arange(10)[:, None] + kbd = KBinsDiscretizer() + X = kbd.fit_transform(X) + + with pytest.raises(TypeError, match="Missing required positional argument"): + kbd.inverse_transform() + + with pytest.raises(TypeError, match="Cannot use both X and Xt. Use X only"): + kbd.inverse_transform(X=X, Xt=X) + + with warnings.catch_warnings(record=True): + warnings.simplefilter("error") + kbd.inverse_transform(X) + + with pytest.warns(FutureWarning, match="Xt was renamed X in version 1.5"): + kbd.inverse_transform(Xt=X) diff --git a/sklearn/tests/test_pipeline.py b/sklearn/tests/test_pipeline.py index 1d4cfb3dd6e2b..c7f0afe642a65 100644 --- a/sklearn/tests/test_pipeline.py +++ b/sklearn/tests/test_pipeline.py @@ -6,6 +6,7 @@ import re import shutil import time +import warnings from tempfile import mkdtemp import joblib @@ -1792,6 +1793,26 @@ def test_feature_union_feature_names_in_(): assert not hasattr(union, "feature_names_in_") +# TODO(1.7): remove this test +def test_pipeline_inverse_transform_Xt_deprecation(): + X = np.random.RandomState(0).normal(size=(10, 5)) + pipe = Pipeline([("pca", PCA(n_components=2))]) + X = pipe.fit_transform(X) + + with pytest.raises(TypeError, match="Missing required positional argument"): + pipe.inverse_transform() + + with pytest.raises(TypeError, match="Cannot use both X and Xt. Use X only"): + pipe.inverse_transform(X=X, Xt=X) + + with warnings.catch_warnings(record=True): + warnings.simplefilter("error") + pipe.inverse_transform(X) + + with pytest.warns(FutureWarning, match="Xt was renamed X in version 1.5"): + pipe.inverse_transform(Xt=X) + + # Test that metadata is routed correctly for pipelines and FeatureUnion # ===================================================================== diff --git a/sklearn/utils/deprecation.py b/sklearn/utils/deprecation.py index c46149d943431..a3225597701c7 100644 --- a/sklearn/utils/deprecation.py +++ b/sklearn/utils/deprecation.py @@ -114,3 +114,22 @@ def _is_deprecated(func): [c.cell_contents for c in closures if isinstance(c.cell_contents, str)] ) return is_deprecated + + +# TODO: remove in 1.7 +def _deprecate_Xt_in_inverse_transform(X, Xt): + """Helper to deprecate the `Xt` argument in favor of `X` in inverse_transform.""" + if X is not None and Xt is not None: + raise TypeError("Cannot use both X and Xt. Use X only.") + + if X is None and Xt is None: + raise TypeError("Missing required positional argument: X.") + + if Xt is not None: + warnings.warn( + "Xt was renamed X in version 1.5 and will be removed in 1.7.", + FutureWarning, + ) + return Xt + + return X From f61dd6cfc0263e4f6b8f9f223ce1bc3510328057 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20du=20Boisberranger?= Date: Tue, 30 Apr 2024 17:27:24 +0200 Subject: [PATCH 074/344] MAINT Clean-up utils.__init__: move tests into corresponding test files (#28842) --- sklearn/utils/tests/test_mask.py | 19 ++++ sklearn/utils/tests/test_missing.py | 27 +++++ sklearn/utils/tests/test_utils.py | 148 +------------------------ sklearn/utils/tests/test_validation.py | 55 +++++++++ 4 files changed, 102 insertions(+), 147 deletions(-) create mode 100644 sklearn/utils/tests/test_mask.py create mode 100644 sklearn/utils/tests/test_missing.py diff --git a/sklearn/utils/tests/test_mask.py b/sklearn/utils/tests/test_mask.py new file mode 100644 index 0000000000000..0eb88e71771f8 --- /dev/null +++ b/sklearn/utils/tests/test_mask.py @@ -0,0 +1,19 @@ +import pytest + +from sklearn.utils._mask import safe_mask +from sklearn.utils.fixes import CSR_CONTAINERS +from sklearn.utils.validation import check_random_state + + +@pytest.mark.parametrize("csr_container", CSR_CONTAINERS) +def test_safe_mask(csr_container): + random_state = check_random_state(0) + X = random_state.rand(5, 4) + X_csr = csr_container(X) + 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 diff --git a/sklearn/utils/tests/test_missing.py b/sklearn/utils/tests/test_missing.py new file mode 100644 index 0000000000000..830e327f06a11 --- /dev/null +++ b/sklearn/utils/tests/test_missing.py @@ -0,0 +1,27 @@ +import numpy as np +import pytest + +from sklearn.utils._missing import is_scalar_nan + + +@pytest.mark.parametrize( + "value, result", + [ + (float("nan"), True), + (np.nan, True), + (float(np.nan), True), + (np.float32(np.nan), True), + (np.float64(np.nan), True), + (0, False), + (0.0, False), + (None, False), + ("", False), + ("nan", False), + ([np.nan], False), + (9867966753463435747313673, False), # Python int that overflows with C type + ], +) +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) diff --git a/sklearn/utils/tests/test_utils.py b/sklearn/utils/tests/test_utils.py index 8d9af93d216ea..4d71bf8860c81 100644 --- a/sklearn/utils/tests/test_utils.py +++ b/sklearn/utils/tests/test_utils.py @@ -1,153 +1,7 @@ -import warnings - import joblib -import numpy as np import pytest -from sklearn.utils import ( - check_random_state, - column_or_1d, - deprecated, - parallel_backend, - register_parallel_backend, - safe_mask, - tosequence, -) -from sklearn.utils._missing import is_scalar_nan -from sklearn.utils._testing import assert_array_equal -from sklearn.utils.fixes import CSR_CONTAINERS -from sklearn.utils.validation import _is_polars_df - - -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) - - rng_42 = np.random.RandomState(42) - assert check_random_state(rng_42) is rng_42 - - rng_42 = np.random.RandomState(42) - assert check_random_state(43).randint(100) != rng_42.randint(100) - - with pytest.raises(ValueError): - check_random_state("some invalid seed") - - -def test_deprecated(): - # Test whether the deprecated decorator issues appropriate warnings - # Copied almost verbatim from https://docs.python.org/library/warnings.html - - # First a function... - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - - @deprecated() - def ham(): - return "spam" - - spam = ham() - - assert spam == "spam" # function must remain usable - - assert len(w) == 1 - assert issubclass(w[0].category, FutureWarning) - assert "deprecated" in str(w[0].message).lower() - - # ... then a class. - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - - @deprecated("don't use this") - class Ham: - SPAM = 1 - - ham = Ham() - - assert hasattr(ham, "SPAM") - - assert len(w) == 1 - assert issubclass(w[0].category, FutureWarning) - assert "deprecated" in str(w[0].message).lower() - - -@pytest.mark.parametrize("csr_container", CSR_CONTAINERS) -def test_safe_mask(csr_container): - random_state = check_random_state(0) - X = random_state.rand(5, 4) - X_csr = csr_container(X) - 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_column_or_1d(): - EXAMPLES = [ - ("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]]), - ("multiclass-multioutput", [[1, 2, 3]]), - ("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: - with pytest.raises(ValueError): - column_or_1d(y) - - -@pytest.mark.parametrize( - "value, result", - [ - (float("nan"), True), - (np.nan, True), - (float(np.nan), True), - (np.float32(np.nan), True), - (np.float64(np.nan), True), - (0, False), - (0.0, False), - (None, False), - ("", False), - ("nan", False), - ([np.nan], False), - (9867966753463435747313673, False), # Python int that overflows with C type - ], -) -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 dummy_func(): - pass - - -def test__is_polars_df(): - """Check that _is_polars_df return False for non-dataframe objects.""" - - class LooksLikePolars: - def __init__(self): - self.columns = ["a", "b"] - self.schema = ["a", "b"] - - assert not _is_polars_df(LooksLikePolars()) +from sklearn.utils import parallel_backend, register_parallel_backend, tosequence # TODO(1.7): remove diff --git a/sklearn/utils/tests/test_validation.py b/sklearn/utils/tests/test_validation.py index 5e54443a84165..4b4eed2522102 100644 --- a/sklearn/utils/tests/test_validation.py +++ b/sklearn/utils/tests/test_validation.py @@ -80,11 +80,31 @@ check_is_fitted, check_memory, check_non_negative, + check_random_state, check_scalar, + column_or_1d, has_fit_parameter, ) +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) + + rng_42 = np.random.RandomState(42) + assert check_random_state(rng_42) is rng_42 + + rng_42 = np.random.RandomState(42) + assert check_random_state(43).randint(100) != rng_42.randint(100) + + with pytest.raises(ValueError): + 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) @@ -2061,3 +2081,38 @@ def test_to_object_array(sequence): assert isinstance(out, np.ndarray) assert out.dtype.kind == "O" assert out.ndim == 1 + + +def test_column_or_1d(): + EXAMPLES = [ + ("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]]), + ("multiclass-multioutput", [[1, 2, 3]]), + ("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: + with pytest.raises(ValueError): + column_or_1d(y) + + +def test__is_polars_df(): + """Check that _is_polars_df return False for non-dataframe objects.""" + + class LooksLikePolars: + def __init__(self): + self.columns = ["a", "b"] + self.schema = ["a", "b"] + + assert not _is_polars_df(LooksLikePolars()) From 2bafd7b68f28bfa108aa83fd412587be702cfdfd Mon Sep 17 00:00:00 2001 From: Stefanie Senger <91849487+StefanieSenger@users.noreply.github.com> Date: Thu, 2 May 2024 10:13:21 +0200 Subject: [PATCH 075/344] MNT metadata routing: remove `MethodMapping.from_str()` and sort `caller`, `callee` in `MethodPair()` (#28422) Co-authored-by: Adrin Jalali Co-authored-by: Guillaume Lemaitre --- .../miscellaneous/plot_metadata_routing.py | 28 +++--- sklearn/calibration.py | 4 +- sklearn/feature_selection/_from_model.py | 4 +- sklearn/linear_model/_coordinate_descent.py | 2 +- sklearn/linear_model/_least_angle.py | 2 +- sklearn/linear_model/_logistic.py | 6 +- sklearn/linear_model/_omp.py | 2 +- sklearn/metrics/_scorer.py | 4 +- sklearn/metrics/tests/test_score_objects.py | 8 +- sklearn/multiclass.py | 10 +-- sklearn/multioutput.py | 8 +- sklearn/tests/metadata_routing_common.py | 21 ++++- sklearn/tests/test_metadata_routing.py | 56 ++++++------ sklearn/utils/_metadata_requests.py | 85 ++++++------------- 14 files changed, 112 insertions(+), 128 deletions(-) diff --git a/examples/miscellaneous/plot_metadata_routing.py b/examples/miscellaneous/plot_metadata_routing.py index ef7012fb61aab..e96b54436cf30 100644 --- a/examples/miscellaneous/plot_metadata_routing.py +++ b/examples/miscellaneous/plot_metadata_routing.py @@ -167,9 +167,9 @@ def get_metadata_routing(self): router = MetadataRouter(owner=self.__class__.__name__).add( estimator=self.estimator, method_mapping=MethodMapping() - .add(callee="fit", caller="fit") - .add(callee="predict", caller="predict") - .add(callee="score", caller="score"), + .add(caller="fit", callee="fit") + .add(caller="predict", callee="predict") + .add(caller="score", callee="score"), ) return router @@ -356,9 +356,9 @@ def get_metadata_routing(self): .add( estimator=self.estimator, method_mapping=MethodMapping() - .add(callee="fit", caller="fit") - .add(callee="predict", caller="predict") - .add(callee="score", caller="score"), + .add(caller="fit", callee="fit") + .add(caller="predict", callee="predict") + .add(caller="score", callee="score"), ) ) return router @@ -488,16 +488,16 @@ def get_metadata_routing(self): # The metadata is routed such that it retraces how # `SimplePipeline` internally calls the transformer's `fit` and # `transform` methods in its own methods (`fit` and `predict`). - .add(callee="fit", caller="fit") - .add(callee="transform", caller="fit") - .add(callee="transform", caller="predict"), + .add(caller="fit", callee="fit") + .add(caller="fit", callee="transform") + .add(caller="predict", callee="transform"), ) # We add the routing for the classifier. .add( classifier=self.classifier, method_mapping=MethodMapping() - .add(callee="fit", caller="fit") - .add(callee="predict", caller="predict"), + .add(caller="fit", callee="fit") + .add(caller="predict", callee="predict"), ) ) return router @@ -612,7 +612,7 @@ def fit(self, X, y, **fit_params): def get_metadata_routing(self): router = MetadataRouter(owner=self.__class__.__name__).add( estimator=self.estimator, - method_mapping=MethodMapping().add(callee="fit", caller="fit"), + method_mapping=MethodMapping().add(caller="fit", callee="fit"), ) return router @@ -651,7 +651,7 @@ def get_metadata_routing(self): .add_self_request(self) .add( estimator=self.estimator, - method_mapping=MethodMapping().add(callee="fit", caller="fit"), + method_mapping=MethodMapping().add(caller="fit", callee="fit"), ) ) return router @@ -692,7 +692,7 @@ def predict(self, X): print(w.message) # %% -# In the end, we disable the configuration flag for metadata routing: +# At the end we disable the configuration flag for metadata routing: set_config(enable_metadata_routing=False) diff --git a/sklearn/calibration.py b/sklearn/calibration.py index 40d3e5363a7e0..2e1a46e6889b8 100644 --- a/sklearn/calibration.py +++ b/sklearn/calibration.py @@ -523,11 +523,11 @@ def get_metadata_routing(self): .add_self_request(self) .add( estimator=self._get_estimator(), - method_mapping=MethodMapping().add(callee="fit", caller="fit"), + method_mapping=MethodMapping().add(caller="fit", callee="fit"), ) .add( splitter=self.cv, - method_mapping=MethodMapping().add(callee="split", caller="fit"), + method_mapping=MethodMapping().add(caller="fit", callee="split"), ) ) return router diff --git a/sklearn/feature_selection/_from_model.py b/sklearn/feature_selection/_from_model.py index 5610121f152bf..46c2b9ebbb163 100644 --- a/sklearn/feature_selection/_from_model.py +++ b/sklearn/feature_selection/_from_model.py @@ -513,8 +513,8 @@ def get_metadata_routing(self): router = MetadataRouter(owner=self.__class__.__name__).add( estimator=self.estimator, method_mapping=MethodMapping() - .add(callee="partial_fit", caller="partial_fit") - .add(callee="fit", caller="fit"), + .add(caller="partial_fit", callee="partial_fit") + .add(caller="fit", callee="fit"), ) return router diff --git a/sklearn/linear_model/_coordinate_descent.py b/sklearn/linear_model/_coordinate_descent.py index 05d7b93f3e090..45cdb8bdf2ebb 100644 --- a/sklearn/linear_model/_coordinate_descent.py +++ b/sklearn/linear_model/_coordinate_descent.py @@ -1860,7 +1860,7 @@ def get_metadata_routing(self): .add_self_request(self) .add( splitter=check_cv(self.cv), - method_mapping=MethodMapping().add(callee="split", caller="fit"), + method_mapping=MethodMapping().add(caller="fit", callee="split"), ) ) return router diff --git a/sklearn/linear_model/_least_angle.py b/sklearn/linear_model/_least_angle.py index f29bcb4c8915c..81e8abb8bc5d6 100644 --- a/sklearn/linear_model/_least_angle.py +++ b/sklearn/linear_model/_least_angle.py @@ -1821,7 +1821,7 @@ def get_metadata_routing(self): """ router = MetadataRouter(owner=self.__class__.__name__).add( splitter=check_cv(self.cv), - method_mapping=MethodMapping().add(callee="split", caller="fit"), + method_mapping=MethodMapping().add(caller="fit", callee="split"), ) return router diff --git a/sklearn/linear_model/_logistic.py b/sklearn/linear_model/_logistic.py index 129d3f6cc9494..481ccf6c7e5ed 100644 --- a/sklearn/linear_model/_logistic.py +++ b/sklearn/linear_model/_logistic.py @@ -2166,13 +2166,13 @@ def get_metadata_routing(self): .add_self_request(self) .add( splitter=self.cv, - method_mapping=MethodMapping().add(callee="split", caller="fit"), + method_mapping=MethodMapping().add(caller="fit", callee="split"), ) .add( scorer=self._get_scorer(), method_mapping=MethodMapping() - .add(callee="score", caller="score") - .add(callee="score", caller="fit"), + .add(caller="score", callee="score") + .add(caller="fit", callee="score"), ) ) return router diff --git a/sklearn/linear_model/_omp.py b/sklearn/linear_model/_omp.py index 2d6fe48869742..f52ef553eab4c 100644 --- a/sklearn/linear_model/_omp.py +++ b/sklearn/linear_model/_omp.py @@ -1116,6 +1116,6 @@ def get_metadata_routing(self): router = MetadataRouter(owner=self.__class__.__name__).add( splitter=self.cv, - method_mapping=MethodMapping().add(callee="split", caller="fit"), + method_mapping=MethodMapping().add(caller="fit", callee="split"), ) return router diff --git a/sklearn/metrics/_scorer.py b/sklearn/metrics/_scorer.py index 19ca055a81b95..adeec587994e2 100644 --- a/sklearn/metrics/_scorer.py +++ b/sklearn/metrics/_scorer.py @@ -32,6 +32,7 @@ from ..utils.metadata_routing import ( MetadataRequest, MetadataRouter, + MethodMapping, _MetadataRequester, _raise_for_params, _routing_enabled, @@ -188,7 +189,8 @@ def get_metadata_routing(self): routing information. """ return MetadataRouter(owner=self.__class__.__name__).add( - **self._scorers, method_mapping="score" + **self._scorers, + method_mapping=MethodMapping().add(caller="score", callee="score"), ) diff --git a/sklearn/metrics/tests/test_score_objects.py b/sklearn/metrics/tests/test_score_objects.py index ac10e0413af0c..e45e0d30767f7 100644 --- a/sklearn/metrics/tests/test_score_objects.py +++ b/sklearn/metrics/tests/test_score_objects.py @@ -61,7 +61,7 @@ assert_array_equal, ignore_warnings, ) -from sklearn.utils.metadata_routing import MetadataRouter +from sklearn.utils.metadata_routing import MetadataRouter, MethodMapping REGRESSION_SCORERS = [ "d2_absolute_error_score", @@ -1233,7 +1233,8 @@ def test_scorer_metadata_request(name): # make sure putting the scorer in a router doesn't request anything by # default router = MetadataRouter(owner="test").add( - method_mapping="score", scorer=get_scorer(name) + 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"): @@ -1244,7 +1245,8 @@ def test_scorer_metadata_request(name): # make sure putting weighted_scorer in a router requests sample_weight router = MetadataRouter(owner="test").add( - scorer=weighted_scorer, method_mapping="score" + scorer=weighted_scorer, + method_mapping=MethodMapping().add(caller="score", callee="score"), ) router.validate_metadata(params={"sample_weight": 1}, method="score") routed_params = router.route_params(params={"sample_weight": 1}, caller="score") diff --git a/sklearn/multiclass.py b/sklearn/multiclass.py index 075095ad4146e..d8c7904b81cdf 100644 --- a/sklearn/multiclass.py +++ b/sklearn/multiclass.py @@ -619,8 +619,8 @@ def get_metadata_routing(self): .add( estimator=self.estimator, method_mapping=MethodMapping() - .add(callee="fit", caller="fit") - .add(callee="partial_fit", caller="partial_fit"), + .add(caller="fit", callee="fit") + .add(caller="partial_fit", callee="partial_fit"), ) ) return router @@ -1018,8 +1018,8 @@ def get_metadata_routing(self): .add( estimator=self.estimator, method_mapping=MethodMapping() - .add(callee="fit", caller="fit") - .add(callee="partial_fit", caller="partial_fit"), + .add(caller="fit", callee="fit") + .add(caller="partial_fit", callee="partial_fit"), ) ) return router @@ -1264,6 +1264,6 @@ def get_metadata_routing(self): router = MetadataRouter(owner=self.__class__.__name__).add( estimator=self.estimator, - method_mapping=MethodMapping().add(callee="fit", caller="fit"), + method_mapping=MethodMapping().add(caller="fit", callee="fit"), ) return router diff --git a/sklearn/multioutput.py b/sklearn/multioutput.py index e0da38357e792..d3814974c63e4 100644 --- a/sklearn/multioutput.py +++ b/sklearn/multioutput.py @@ -334,8 +334,8 @@ def get_metadata_routing(self): router = MetadataRouter(owner=self.__class__.__name__).add( estimator=self.estimator, method_mapping=MethodMapping() - .add(callee="partial_fit", caller="partial_fit") - .add(callee="fit", caller="fit"), + .add(caller="partial_fit", callee="partial_fit") + .add(caller="fit", callee="fit"), ) return router @@ -1096,7 +1096,7 @@ def get_metadata_routing(self): """ router = MetadataRouter(owner=self.__class__.__name__).add( estimator=self.base_estimator, - method_mapping=MethodMapping().add(callee="fit", caller="fit"), + method_mapping=MethodMapping().add(caller="fit", callee="fit"), ) return router @@ -1245,7 +1245,7 @@ def get_metadata_routing(self): """ router = MetadataRouter(owner=self.__class__.__name__).add( estimator=self.base_estimator, - method_mapping=MethodMapping().add(callee="fit", caller="fit"), + method_mapping=MethodMapping().add(caller="fit", callee="fit"), ) return router diff --git a/sklearn/tests/metadata_routing_common.py b/sklearn/tests/metadata_routing_common.py index 3df47d3f8dd4e..889524bc05ddb 100644 --- a/sklearn/tests/metadata_routing_common.py +++ b/sklearn/tests/metadata_routing_common.py @@ -19,6 +19,7 @@ ) from sklearn.utils.metadata_routing import ( MetadataRouter, + MethodMapping, process_routing, ) from sklearn.utils.multiclass import _check_partial_fit_first_call @@ -418,7 +419,8 @@ def fit(self, X, y, **fit_params): def get_metadata_routing(self): router = MetadataRouter(owner=self.__class__.__name__).add( - estimator=self.estimator, method_mapping="one-to-one" + estimator=self.estimator, + method_mapping=MethodMapping().add(caller="fit", callee="fit"), ) return router @@ -447,7 +449,12 @@ def get_metadata_routing(self): router = ( MetadataRouter(owner=self.__class__.__name__) .add_self_request(self) - .add(estimator=self.estimator, method_mapping="one-to-one") + .add( + estimator=self.estimator, + method_mapping=MethodMapping() + .add(caller="fit", callee="fit") + .add(caller="predict", callee="predict"), + ) ) return router @@ -472,7 +479,10 @@ def get_metadata_routing(self): router = ( MetadataRouter(owner=self.__class__.__name__) .add_self_request(self) - .add(estimator=self.estimator, method_mapping="fit") + .add( + estimator=self.estimator, + method_mapping=MethodMapping().add(caller="fit", callee="fit"), + ) ) return router @@ -494,5 +504,8 @@ def transform(self, X, y=None, **transform_params): def get_metadata_routing(self): return MetadataRouter(owner=self.__class__.__name__).add( - transformer=self.transformer, method_mapping="one-to-one" + transformer=self.transformer, + method_mapping=MethodMapping() + .add(caller="fit", callee="fit") + .add(caller="transform", callee="transform"), ) diff --git a/sklearn/tests/test_metadata_routing.py b/sklearn/tests/test_metadata_routing.py index 110452870d682..109c730bf0718 100644 --- a/sklearn/tests/test_metadata_routing.py +++ b/sklearn/tests/test_metadata_routing.py @@ -114,11 +114,16 @@ def get_metadata_routing(self): router.add( **{f"step_{i}": step}, method_mapping=MethodMapping() - .add(callee="fit", caller="fit") - .add(callee="transform", caller="fit") - .add(callee="transform", caller="predict"), + .add(caller="fit", callee="fit") + .add(caller="fit", callee="transform") + .add(caller="predict", callee="transform"), ) - router.add(predictor=self.steps[-1], method_mapping="one-to-one") + router.add( + predictor=self.steps[-1], + method_mapping=MethodMapping() + .add(caller="fit", callee="fit") + .add(caller="predict", callee="predict"), + ) return router @@ -150,7 +155,10 @@ def test_assert_request_is_empty(): assert_request_is_empty( MetadataRouter(owner="test") .add_self_request(WeightedMetaRegressor(estimator=None)) - .add(method_mapping="fit", estimator=ConsumingRegressor()) + .add( + estimator=ConsumingRegressor(), + method_mapping=MethodMapping().add(caller="fit", callee="fit"), + ) ) @@ -677,13 +685,13 @@ class ConsumingRegressorWarn(ConsumingRegressor): MetadataRequest(owner="test"), "{}", ), - (MethodMapping.from_str("score"), "[{'callee': 'score', 'caller': 'score'}]"), ( MetadataRouter(owner="test").add( - method_mapping="predict", estimator=ConsumingRegressor() + estimator=ConsumingRegressor(), + method_mapping=MethodMapping().add(caller="predict", callee="predict"), ), ( - "{'estimator': {'mapping': [{'callee': 'predict', 'caller':" + "{'estimator': {'mapping': [{'caller': 'predict', 'callee':" " 'predict'}], 'router': {'fit': {'sample_weight': None, 'metadata':" " None}, 'partial_fit': {'sample_weight': None, 'metadata': None}," " 'predict': {'sample_weight': None, 'metadata': None}, 'score':" @@ -702,24 +710,17 @@ def test_string_representations(obj, string): ( MethodMapping(), "add", - {"callee": "invalid", "caller": "fit"}, + {"caller": "fit", "callee": "invalid"}, ValueError, "Given callee", ), ( MethodMapping(), "add", - {"callee": "fit", "caller": "invalid"}, + {"caller": "invalid", "callee": "fit"}, ValueError, "Given caller", ), - ( - MethodMapping, - "from_str", - {"route": "invalid"}, - ValueError, - "route should be 'one-to-one' or a single method!", - ), ( MetadataRouter(owner="test"), "add_self_request", @@ -749,16 +750,17 @@ def test_methodmapping(): ) mm_list = list(mm) - assert mm_list[0] == ("transform", "fit") + assert mm_list[0] == ("fit", "transform") assert mm_list[1] == ("fit", "fit") - mm = MethodMapping.from_str("one-to-one") + 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.from_str("score") - assert repr(mm) == "[{'callee': 'score', 'caller': 'score'}]" + mm = MethodMapping().add(caller="score", callee="score") + assert repr(mm) == "[{'caller': 'score', 'callee': 'score'}]" def test_metadatarouter_add_self_request(): @@ -793,12 +795,12 @@ def test_metadatarouter_add_self_request(): def test_metadata_routing_add(): # adding one with a string `method_mapping` router = MetadataRouter(owner="test").add( - method_mapping="fit", est=ConsumingRegressor().set_fit_request(sample_weight="weights"), + method_mapping=MethodMapping().add(caller="fit", callee="fit"), ) assert ( str(router) - == "{'est': {'mapping': [{'callee': 'fit', 'caller': 'fit'}], 'router': {'fit':" + == "{'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':" @@ -807,12 +809,12 @@ def test_metadata_routing_add(): # adding one with an instance of MethodMapping router = MetadataRouter(owner="test").add( - method_mapping=MethodMapping().add(callee="score", caller="fit"), + method_mapping=MethodMapping().add(caller="fit", callee="score"), est=ConsumingRegressor().set_score_request(sample_weight=True), ) assert ( str(router) - == "{'est': {'mapping': [{'callee': 'score', 'caller': 'fit'}], 'router':" + == "{'est': {'mapping': [{'caller': 'fit', 'callee': 'score'}], 'router':" " {'fit': {'sample_weight': None, 'metadata': None}, 'partial_fit':" " {'sample_weight': None, 'metadata': None}, 'predict': {'sample_weight':" " None, 'metadata': None}, 'score': {'sample_weight': True, 'metadata':" @@ -829,17 +831,17 @@ def test_metadata_routing_get_param_names(): ) ) .add( - method_mapping="fit", trs=ConsumingTransformer().set_fit_request( sample_weight="transform_weights" ), + method_mapping=MethodMapping().add(caller="fit", callee="fit"), ) ) assert ( str(router) == "{'$self_request': {'fit': {'sample_weight': 'self_weights'}, 'score':" - " {'sample_weight': None}}, 'trs': {'mapping': [{'callee': 'fit', 'caller':" + " {'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}}}}" diff --git a/sklearn/utils/_metadata_requests.py b/sklearn/utils/_metadata_requests.py index 4acac0f9fd22f..f730539621177 100644 --- a/sklearn/utils/_metadata_requests.py +++ b/sklearn/utils/_metadata_requests.py @@ -693,19 +693,18 @@ def __str__(self): # A namedtuple storing a single method route. A collection of these namedtuples # is stored in a MetadataRouter. -MethodPair = namedtuple("MethodPair", ["callee", "caller"]) +MethodPair = namedtuple("MethodPair", ["caller", "callee"]) class MethodMapping: - """Stores the mapping between callee and caller methods for a router. + """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 a sub-object (a sub-estimator or a - scorer) to the router's methods. It stores a collection of ``Route`` - namedtuples. + scorer) to the router's methods. It stores a collection of namedtuples. Iterating through an instance of this class will yield named - ``MethodPair(callee, caller)`` tuples. + ``MethodPair(caller, callee)`` tuples. .. versionadded:: 1.3 """ @@ -716,33 +715,34 @@ def __init__(self): def __iter__(self): return iter(self._routes) - def add(self, *, callee, caller): + def add(self, *, caller, callee): """Add a method mapping. Parameters ---------- - callee : str - Child object's method name. This method is called in ``caller``. caller : str 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 Returns self. """ - if callee not in METHODS: + if caller not in METHODS: raise ValueError( - f"Given callee:{callee} is not a valid method. Valid methods are:" + f"Given caller:{caller} is not a valid method. Valid methods are:" f" {METHODS}" ) - if caller not in METHODS: + if callee not in METHODS: raise ValueError( - f"Given caller:{caller} is not a valid method. Valid methods are:" + f"Given callee:{callee} is not a valid method. Valid methods are:" f" {METHODS}" ) - self._routes.append(MethodPair(callee=callee, caller=caller)) + self._routes.append(MethodPair(caller=caller, callee=callee)) return self def _serialize(self): @@ -755,38 +755,9 @@ def _serialize(self): """ result = list() for route in self._routes: - result.append({"callee": route.callee, "caller": route.caller}) + result.append({"caller": route.caller, "callee": route.callee}) return result - @classmethod - def from_str(cls, route): - """Construct an instance from a string. - - Parameters - ---------- - route : str - A string representing the mapping, it can be: - - - `"one-to-one"`: a one to one mapping for all methods. - - `"method"`: the name of a single method, such as ``fit``, - ``transform``, ``score``, etc. - - Returns - ------- - obj : MethodMapping - A :class:`~sklearn.utils.metadata_routing.MethodMapping` instance - constructed from the given string. - """ - routing = cls() - if route == "one-to-one": - for method in METHODS: - routing.add(callee=method, caller=method) - elif route in METHODS: - routing.add(callee=route, caller=route) - else: - raise ValueError("route should be 'one-to-one' or a single method!") - return routing - def __repr__(self): return str(self._serialize()) @@ -868,10 +839,8 @@ def add(self, *, method_mapping, **objs): Parameters ---------- - method_mapping : MethodMapping or str - The mapping between the child and the parent's methods. If str, the - output of :func:`~sklearn.utils.metadata_routing.MethodMapping.from_str` - is used. + 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 @@ -882,10 +851,7 @@ def add(self, *, method_mapping, **objs): self : MetadataRouter Returns `self`. """ - if isinstance(method_mapping, str): - method_mapping = MethodMapping.from_str(method_mapping) - else: - method_mapping = deepcopy(method_mapping) + method_mapping = deepcopy(method_mapping) for name, obj in objs.items(): self._route_mappings[name] = RouterMappingPair( @@ -916,7 +882,7 @@ def consumes(self, method, params): res = res | self._self_request.consumes(method=method, params=params) for _, route_mapping in self._route_mappings.items(): - for callee, caller in route_mapping.mapping: + for caller, callee in route_mapping.mapping: if caller == method: res = res | route_mapping.router.consumes( method=callee, params=params @@ -959,7 +925,7 @@ def _get_param_names(self, *, method, return_alias, ignore_self_request): ) for name, route_mapping in self._route_mappings.items(): - for callee, caller in route_mapping.mapping: + for caller, callee in route_mapping.mapping: if caller == method: res = res.union( route_mapping.router._get_param_names( @@ -1065,7 +1031,7 @@ def route_params(self, *, caller, params): router, mapping = route_mapping.router, route_mapping.mapping res[name] = Bunch() - for _callee, _caller in mapping: + for _caller, _callee in mapping: if _caller == caller: res[name][_callee] = router._route_params( params=params, @@ -1127,12 +1093,11 @@ def _serialize(self): def __iter__(self): if self._self_request: - yield ( - "$self_request", - RouterMappingPair( - mapping=MethodMapping.from_str("one-to-one"), - router=self._self_request, - ), + method_mapping = MethodMapping() + for method in METHODS: + method_mapping.add(caller=method, callee=method) + yield "$self_request", RouterMappingPair( + mapping=method_mapping, router=self._self_request ) for name, route_mapping in self._route_mappings.items(): yield (name, route_mapping) From e19d8c27e563ef7b8facfe5322f2c8a9eb274439 Mon Sep 17 00:00:00 2001 From: Guillaume Lemaitre Date: Thu, 2 May 2024 13:22:27 +0200 Subject: [PATCH 076/344] FIX PLSRegression.coef_ takes X and Y variances into account when scale=True (#28612) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jérémie du Boisberranger --- doc/whats_new/v1.5.rst | 7 +++++- sklearn/cross_decomposition/_pls.py | 5 ++-- sklearn/cross_decomposition/tests/test_pls.py | 24 +++++++++++++++++-- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/doc/whats_new/v1.5.rst b/doc/whats_new/v1.5.rst index 9f53afd433ffc..1fe0df6f97a61 100644 --- a/doc/whats_new/v1.5.rst +++ b/doc/whats_new/v1.5.rst @@ -212,7 +212,12 @@ Changelog :class:`cross_decomposition.CCA`, and :class:`cross_decomposition.PLSSVD`. `Y` will be removed in version 1.7. - :pr:`28604` by :user:`David Leon ` + :pr:`28604` by :user:`David Leon `. + +- |Fix| The `coef_` fitted attribute of :class:`cross_decomposition.PLSRegression` + now takes into account both the scale of `X` and `Y` when `scale=True`. Note that + the previous predicted values were not affected by this bug. + :pr:`28612` by :user:`Guillaume Lemaitre `. :mod:`sklearn.datasets` ....................... diff --git a/sklearn/cross_decomposition/_pls.py b/sklearn/cross_decomposition/_pls.py index 858f123427fab..b6f7dd663724e 100644 --- a/sklearn/cross_decomposition/_pls.py +++ b/sklearn/cross_decomposition/_pls.py @@ -388,7 +388,7 @@ def fit(self, X, y=None, Y=None): pinv2(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.coef_ = (self.coef_ * self._y_std).T / self._x_std self.intercept_ = self._y_mean self._n_features_out = self.x_rotations_.shape[1] return self @@ -517,9 +517,8 @@ def predict(self, X, copy=True): """ check_is_fitted(self) X = self._validate_data(X, copy=copy, dtype=FLOAT_DTYPES, reset=False) - # Normalize + # Only center X but do not scale it since the coefficients are already scaled X -= self._x_mean - X /= self._x_std Ypred = X @ self.coef_.T + self.intercept_ return Ypred.ravel() if self._predict_1d else Ypred diff --git a/sklearn/cross_decomposition/tests/test_pls.py b/sklearn/cross_decomposition/tests/test_pls.py index 12d60cfef3194..c8de4ad8a78de 100644 --- a/sklearn/cross_decomposition/tests/test_pls.py +++ b/sklearn/cross_decomposition/tests/test_pls.py @@ -589,8 +589,6 @@ def test_pls_prediction(PLSEstimator, scale): y_mean = Y.mean(axis=0) X_trans = X - X.mean(axis=0) - if scale: - X_trans /= X.std(axis=0, ddof=1) assert_allclose(pls.intercept_, y_mean) assert_allclose(Y_pred, X_trans @ pls.coef_.T + pls.intercept_) @@ -646,6 +644,28 @@ def test_pls_regression_fit_1d_y(): 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`. + + Non-regression test for: + https://github.com/scikit-learn/scikit-learn/issues/27964 + """ + # handcrafted data where we can predict Y from X with an additional scaling factor + rng = np.random.RandomState(0) + 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) + + # TODO(1.7): Remove @pytest.mark.parametrize("Klass", [PLSRegression, CCA, PLSSVD, PLSCanonical]) def test_pls_fit_warning_on_deprecated_Y_argument(Klass): From 4c89b3b4f69fc8abf4715b96137aa022674c80e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20du=20Boisberranger?= Date: Thu, 2 May 2024 13:25:14 +0200 Subject: [PATCH 077/344] MAINT Clean up deprecations for 1.5: in NearestCentroid (#28813) Co-authored-by: Guillaume Lemaitre --- sklearn/neighbors/_nearest_centroid.py | 71 ++++--------------- .../neighbors/tests/test_nearest_centroid.py | 22 +----- 2 files changed, 15 insertions(+), 78 deletions(-) diff --git a/sklearn/neighbors/_nearest_centroid.py b/sklearn/neighbors/_nearest_centroid.py index 75086ee25448e..c9c99aeeaadb2 100644 --- a/sklearn/neighbors/_nearest_centroid.py +++ b/sklearn/neighbors/_nearest_centroid.py @@ -7,14 +7,11 @@ # # License: BSD 3 clause -import warnings from numbers import Real import numpy as np from scipy import sparse as sp -from sklearn.metrics.pairwise import _VALID_METRICS - from ..base import BaseEstimator, ClassifierMixin, _fit_context from ..metrics.pairwise import pairwise_distances_argmin from ..preprocessing import LabelEncoder @@ -34,25 +31,17 @@ class NearestCentroid(ClassifierMixin, BaseEstimator): Parameters ---------- - metric : str or callable, default="euclidean" - Metric to use for distance computation. See the documentation of - `scipy.spatial.distance - `_ and - the metrics listed in - :class:`~sklearn.metrics.pairwise.distance_metrics` for valid metric - values. Note that "wminkowski", "seuclidean" and "mahalanobis" are not - supported. - - The centroids for the samples corresponding to each class is - the point from which the sum of the distances (according to the metric) - of all samples that belong to that particular class are minimized. - If the `"manhattan"` metric is provided, this centroid is the median - and for all other metrics, the centroid is now set to be the mean. - - .. deprecated:: 1.3 - Support for metrics other than `euclidean` and `manhattan` and for - callables was deprecated in version 1.3 and will be removed in - version 1.5. + 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. .. versionchanged:: 0.19 `metric='precomputed'` was deprecated and now raises an error @@ -108,15 +97,8 @@ class NearestCentroid(ClassifierMixin, BaseEstimator): [1] """ - _valid_metrics = set(_VALID_METRICS) - {"mahalanobis", "seuclidean", "wminkowski"} - _parameter_constraints: dict = { - "metric": [ - StrOptions( - _valid_metrics, deprecated=_valid_metrics - {"manhattan", "euclidean"} - ), - callable, - ], + "metric": [StrOptions({"manhattan", "euclidean"})], "shrink_threshold": [Interval(Real, 0, None, closed="neither"), None], } @@ -143,19 +125,6 @@ def fit(self, X, y): self : object Fitted estimator. """ - if isinstance(self.metric, str) and self.metric not in ( - "manhattan", - "euclidean", - ): - warnings.warn( - ( - "Support for distance metrics other than euclidean and " - "manhattan and for callables was deprecated in version " - "1.3 and will be removed in version 1.5." - ), - FutureWarning, - ) - # 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": @@ -195,14 +164,7 @@ def fit(self, X, y): self.centroids_[cur_class] = np.median(X[center_mask], axis=0) else: self.centroids_[cur_class] = csc_median_axis_0(X[center_mask]) - else: - # TODO(1.5) remove warning when metric is only manhattan or euclidean - if self.metric != "euclidean": - warnings.warn( - "Averaging for metrics other than " - "euclidean and manhattan not supported. " - "The average is set to be the mean." - ) + else: # metric == "euclidean" self.centroids_[cur_class] = X[center_mask].mean(axis=0) if self.shrink_threshold: @@ -231,7 +193,6 @@ def fit(self, X, y): self.centroids_ = dataset_centroid_[np.newaxis, :] + msd return self - # TODO(1.5) remove note about precomputed metric def predict(self, X): """Perform classification on an array of test vectors `X`. @@ -246,12 +207,6 @@ def predict(self, X): ------- C : ndarray of shape (n_samples,) The predicted classes. - - Notes - ----- - If the metric constructor parameter is `"precomputed"`, `X` is assumed - to be the distance matrix between the data to be predicted and - `self.centroids_`. """ check_is_fitted(self) diff --git a/sklearn/neighbors/tests/test_nearest_centroid.py b/sklearn/neighbors/tests/test_nearest_centroid.py index 09c2501818fd3..5ce792ac29d56 100644 --- a/sklearn/neighbors/tests/test_nearest_centroid.py +++ b/sklearn/neighbors/tests/test_nearest_centroid.py @@ -56,21 +56,17 @@ def test_classification_toy(csr_container): assert_array_equal(clf.predict(T_csr.tolil()), true_result) -# TODO(1.5): Remove filterwarnings when support for some metrics is removed -@pytest.mark.filterwarnings("ignore:Support for distance metrics:FutureWarning:sklearn") def test_iris(): # Check consistency on dataset iris. - for metric in ("euclidean", "cosine"): + 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) -# TODO(1.5): Remove filterwarnings when support for some metrics is removed -@pytest.mark.filterwarnings("ignore:Support for distance metrics:FutureWarning:sklearn") def test_iris_shrinkage(): # Check consistency on dataset iris, when using shrinkage. - for metric in ("euclidean", "cosine"): + for metric in ("euclidean", "manhattan"): for shrink_threshold in [None, 0.1, 0.5]: clf = NearestCentroid(metric=metric, shrink_threshold=shrink_threshold) clf = clf.fit(iris.data, iris.target) @@ -151,20 +147,6 @@ def test_manhattan_metric(csr_container): assert_array_equal(dense_centroid, [[-1, -1], [1, 1]]) -# TODO(1.5): remove this test -@pytest.mark.parametrize( - "metric", sorted(list(NearestCentroid._valid_metrics - {"manhattan", "euclidean"})) -) -def test_deprecated_distance_metric_supports(metric): - # Check that a warning is raised for all deprecated distance metric supports - clf = NearestCentroid(metric=metric) - with pytest.warns( - FutureWarning, - match="Support for distance metrics other than euclidean and manhattan", - ): - clf.fit(X, y) - - def test_features_zero_var(): # Test that features with 0 variance throw error From 8f96794b635985374bbd3cc99a9bc509104a5769 Mon Sep 17 00:00:00 2001 From: Gael Varoquaux Date: Thu, 2 May 2024 13:34:15 +0200 Subject: [PATCH 078/344] DOC: rephrase the role of core devs (#28912) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jérémie du Boisberranger Co-authored-by: Adrin Jalali --- build_tools/generate_authors_table.py | 6 +- doc/about.rst | 32 +++++-- doc/governance.rst | 91 +++++++++---------- doc/{authors.rst => maintainers.rst} | 0 ..._emeritus.rst => maintainers_emeritus.rst} | 0 doc/templates/index.html | 3 +- 6 files changed, 70 insertions(+), 62 deletions(-) rename doc/{authors.rst => maintainers.rst} (100%) rename doc/{authors_emeritus.rst => maintainers_emeritus.rst} (100%) diff --git a/build_tools/generate_authors_table.py b/build_tools/generate_authors_table.py index 28bb267b6f721..483dc3739506e 100644 --- a/build_tools/generate_authors_table.py +++ b/build_tools/generate_authors_table.py @@ -214,11 +214,13 @@ def generate_list(contributors): documentation_team, ) = get_contributors() - with open(REPO_FOLDER / "doc" / "authors.rst", "w+", encoding="utf-8") as rst_file: + with open( + REPO_FOLDER / "doc" / "maintainers.rst", "w+", encoding="utf-8" + ) as rst_file: rst_file.write(generate_table(core_devs)) with open( - REPO_FOLDER / "doc" / "authors_emeritus.rst", "w+", encoding="utf-8" + REPO_FOLDER / "doc" / "maintainers_emeritus.rst", "w+", encoding="utf-8" ) as rst_file: rst_file.write(generate_list(emeritus)) diff --git a/doc/about.rst b/doc/about.rst index 2a689646c1abb..e7083569fd128 100644 --- a/doc/about.rst +++ b/doc/about.rst @@ -22,13 +22,27 @@ Governance The decision making process and governance structure of scikit-learn is laid out in the :ref:`governance document `. -Authors -------- +.. The "author" anchors below is there to ensure that old html links (in + the form of "about.html#author" still work) + +.. _authors: + +The people behind scikit-learn +------------------------------- -The following people are currently core contributors to scikit-learn's development -and maintenance: +Scikit-learn is a community project, developed by a large group of +people, all across the world. A few teams, listed below, have central +roles, however a more complete list of contributors can be found `on +github +`__. -.. include:: authors.rst +Maintainers Team +................ + +The following people are currently maintainers, in charge of +consolidating scikit-learn's development and maintenance: + +.. include:: maintainers.rst Please do not email the authors directly to ask for assistance or report issues. Instead, please see `What's the best way to ask questions about scikit-learn @@ -40,14 +54,14 @@ in the FAQ. :ref:`How you can contribute to the project ` Documentation Team ------------------- +.................. The following people help with documenting the project: .. include:: documentation_team.rst Contributor Experience Team ---------------------------- +........................... The following people are active contributors who also help with :ref:`triaging issues `, PRs, and general @@ -56,7 +70,7 @@ maintenance: .. include:: contributor_experience_team.rst Communication Team ------------------- +.................. The following people help with :ref:`communication around scikit-learn `. @@ -70,7 +84,7 @@ Emeritus Core Developers The following people have been active contributors in the past, but are no longer active in the project: -.. include:: authors_emeritus.rst +.. include:: maintainers_emeritus.rst Emeritus Communication Team --------------------------- diff --git a/doc/governance.rst b/doc/governance.rst index 33afd7dde8ddb..d6b07afe4eeb4 100644 --- a/doc/governance.rst +++ b/doc/governance.rst @@ -58,56 +58,47 @@ members and recant their rights until they become active again. The list of members, active and emeritus (with dates at which they became active) is public on the scikit-learn website. -The following teams form the core contributors group. - - -Contributor Experience Team -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The contributor experience team improves the experience of contributors by -helping with the triage of issues and pull requests, as well as noticing any -repeating patterns where people might struggle, and to help with improving -those aspects of the project. - -To this end, they have the required permissions on github to label and close -issues. :ref:`Their work ` is crucial to improve the -communication in the project and limit the crowding of the issue tracker. - -.. _communication_team: - -Communication team -~~~~~~~~~~~~~~~~~~ - -Members of the communication team help with outreach and communication -for scikit-learn. The goal of the team is to develop public awareness of -scikit-learn, of its features and usage, as well as branding. - -For this, they can operate the scikit-learn accounts on various social networks -and produce materials. They also have the required rights to our blog -repository and other relevant accounts and platforms. - -Documentation team -~~~~~~~~~~~~~~~~~~ - -Members of the documentation team engage with the documentation of the project -among other things. They might also be involved in other aspects of the -project, but their reviews on documentation contributions are considered -authoritative, and can merge such contributions. - -To this end, they have permissions to merge pull requests in scikit-learn's -repository. - -Maintainers -~~~~~~~~~~~ - -Maintainers are community members who have shown that they are dedicated to the -continued development of the project through ongoing engagement with the -community. They have shown they can be trusted to maintain scikit-learn with -care. Being a maintainer allows contributors to more easily carry on with their -project related activities by giving them direct access to the project's -repository. Maintainers are expected to review code contributions, merge -approved pull requests, cast votes for and against merging a pull-request, -and to be involved in deciding major changes to the API. +The following teams form the core contributors group: + +* **Contributor Experience Team** + The contributor experience team improves the experience of contributors by + helping with the triage of issues and pull requests, as well as noticing any + repeating patterns where people might struggle, and to help with improving + those aspects of the project. + + To this end, they have the required permissions on github to label and close + issues. :ref:`Their work ` is crucial to improve the + communication in the project and limit the crowding of the issue tracker. + + .. _communication_team: + +* **Communication Team** + Members of the communication team help with outreach and communication + for scikit-learn. The goal of the team is to develop public awareness of + scikit-learn, of its features and usage, as well as branding. + + For this, they can operate the scikit-learn accounts on various social networks + and produce materials. They also have the required rights to our blog + repository and other relevant accounts and platforms. + +* **Documentation Team** + Members of the documentation team engage with the documentation of the project + among other things. They might also be involved in other aspects of the + project, but their reviews on documentation contributions are considered + authoritative, and can merge such contributions. + + To this end, they have permissions to merge pull requests in scikit-learn's + repository. + +* **Maintainers Team** + Maintainers are community members who have shown that they are dedicated to the + continued development of the project through ongoing engagement with the + community. They have shown they can be trusted to maintain scikit-learn with + care. Being a maintainer allows contributors to more easily carry on with their + project related activities by giving them direct access to the project's + repository. Maintainers are expected to review code contributions, merge + approved pull requests, cast votes for and against merging a pull-request, + and to be involved in deciding major changes to the API. Technical Committee ------------------- diff --git a/doc/authors.rst b/doc/maintainers.rst similarity index 100% rename from doc/authors.rst rename to doc/maintainers.rst diff --git a/doc/authors_emeritus.rst b/doc/maintainers_emeritus.rst similarity index 100% rename from doc/authors_emeritus.rst rename to doc/maintainers_emeritus.rst diff --git a/doc/templates/index.html b/doc/templates/index.html index a3c91c30ca765..5b3a61a5b98bb 100644 --- a/doc/templates/index.html +++ b/doc/templates/index.html @@ -189,7 +189,8 @@

News

Community

    -
  • About us: See authors and contributing
  • +
  • About us: See people and contributing
  • More Machine Learning: Find related projects
  • Questions? See FAQ, support, and stackoverflow
  • Subscribe to the mailing list
  • From 8d89e8d96ae558ecbb935591c6c6dc12a0c6cca2 Mon Sep 17 00:00:00 2001 From: Omar Salman Date: Thu, 2 May 2024 19:08:06 +0500 Subject: [PATCH 079/344] FEA Add d2_log_loss_score (#28351) --- doc/modules/model_evaluation.rst | 45 +++++ doc/whats_new/v1.5.rst | 6 +- sklearn/metrics/__init__.py | 2 + sklearn/metrics/_classification.py | 99 ++++++++- sklearn/metrics/tests/test_classification.py | 200 ++++++++++++++++++- 5 files changed, 349 insertions(+), 3 deletions(-) diff --git a/doc/modules/model_evaluation.rst b/doc/modules/model_evaluation.rst index 59f014b732e35..7caacd697ea1c 100644 --- a/doc/modules/model_evaluation.rst +++ b/doc/modules/model_evaluation.rst @@ -2826,6 +2826,51 @@ Here are some usage examples of the :func:`d2_absolute_error_score` function:: |details-end| +|details-start| +**D² log loss score** +|details-split| + +The :func:`d2_log_loss_score` function implements the special case +of D² with the log loss, see :ref:`log_loss`, i.e.: + +.. math:: + + \text{dev}(y, \hat{y}) = \text{log_loss}(y, \hat{y}). + +The :math:`y_{\text{null}}` for the :func:`log_loss` is the per-class +proportion. + +Here are some usage examples of the :func:`d2_log_loss_score` function:: + + >>> from sklearn.metrics import d2_log_loss_score + >>> y_true = [1, 1, 2, 3] + >>> y_pred = [ + ... [0.5, 0.25, 0.25], + ... [0.5, 0.25, 0.25], + ... [0.5, 0.25, 0.25], + ... [0.5, 0.25, 0.25], + ... ] + >>> d2_log_loss_score(y_true, y_pred) + 0.0 + >>> y_true = [1, 2, 3] + >>> y_pred = [ + ... [0.98, 0.01, 0.01], + ... [0.01, 0.98, 0.01], + ... [0.01, 0.01, 0.98], + ... ] + >>> d2_log_loss_score(y_true, y_pred) + 0.981... + >>> y_true = [1, 2, 3] + >>> y_pred = [ + ... [0.1, 0.6, 0.3], + ... [0.1, 0.6, 0.3], + ... [0.4, 0.5, 0.1], + ... ] + >>> d2_log_loss_score(y_true, y_pred) + -0.552... + +|details-end| + .. _visualization_regression_evaluation: Visual evaluation of regression models diff --git a/doc/whats_new/v1.5.rst b/doc/whats_new/v1.5.rst index 1fe0df6f97a61..d3064851e7f87 100644 --- a/doc/whats_new/v1.5.rst +++ b/doc/whats_new/v1.5.rst @@ -169,7 +169,7 @@ Changelog .......................... - |Fix| Fixed a regression in :class:`calibration.CalibratedClassifierCV` where - an error was wrongly raised with string targets. + an error was wrongly raised with string targets. :pr:`28843` by :user:`Jérémie du Boisberranger `. :mod:`sklearn.cluster` @@ -406,6 +406,10 @@ Changelog is deprecated and will raise an error in v1.7. :pr:`18555` by :user:`Kaushik Amar Das `. +- |Feature| :func:`metrics.d2_log_loss_score` has been added which + calculates the D^2 score for the log loss. + :pr:`28351` by :user:`Omar Salman `. + :mod:`sklearn.mixture` ...................... diff --git a/sklearn/metrics/__init__.py b/sklearn/metrics/__init__.py index 8a818c885043c..af25a219c79f1 100644 --- a/sklearn/metrics/__init__.py +++ b/sklearn/metrics/__init__.py @@ -12,6 +12,7 @@ classification_report, cohen_kappa_score, confusion_matrix, + d2_log_loss_score, f1_score, fbeta_score, hamming_loss, @@ -113,6 +114,7 @@ "coverage_error", "d2_tweedie_score", "d2_absolute_error_score", + "d2_log_loss_score", "d2_pinball_score", "dcg_score", "davies_bouldin_score", diff --git a/sklearn/metrics/_classification.py b/sklearn/metrics/_classification.py index caa4db5479a29..04894a4d7a7e7 100644 --- a/sklearn/metrics/_classification.py +++ b/sklearn/metrics/_classification.py @@ -53,7 +53,11 @@ from ..utils.extmath import _nanaverage from ..utils.multiclass import type_of_target, unique_labels from ..utils.sparsefuncs import count_nonzero -from ..utils.validation import _check_pos_label_consistency, _num_samples +from ..utils.validation import ( + _check_pos_label_consistency, + _check_sample_weight, + _num_samples, +) def _check_zero_division(zero_division): @@ -3257,3 +3261,96 @@ def brier_score_loss( raise y_true = np.array(y_true == pos_label, int) return np.average((y_true - y_proba) ** 2, weights=sample_weight) + + +@validate_params( + { + "y_true": ["array-like"], + "y_pred": ["array-like"], + "sample_weight": ["array-like", None], + "labels": ["array-like", None], + }, + prefer_skip_nested_validation=True, +) +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 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 `. + + .. versionadded:: 1.5 + + Parameters + ---------- + y_true : array-like or label indicator matrix + The actuals labels for the n_samples samples. + + y_pred : array-like of shape (n_samples, n_classes) or (n_samples,) + Predicted probabilities, as returned by a classifier's + predict_proba method. If ``y_pred.shape = (n_samples,)`` + the probabilities provided are assumed to be that of the + positive class. The labels in ``y_pred`` are assumed to be + ordered alphabetically, as done by + :class:`~sklearn.preprocessing.LabelBinarizer`. + + sample_weight : array-like of shape (n_samples,), default=None + Sample weights. + + labels : array-like, default=None + 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 + ------- + 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_true=y_true, + y_pred=y_pred, + normalize=False, + sample_weight=sample_weight, + labels=labels, + ) + + # Proportion of labels in the dataset + weights = _check_sample_weight(sample_weight, y_true) + + _, 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_true=y_true, + y_pred=y_pred_null, + normalize=False, + sample_weight=sample_weight, + labels=labels, + ) + + return 1 - (numerator / denominator) diff --git a/sklearn/metrics/tests/test_classification.py b/sklearn/metrics/tests/test_classification.py index 144871c8d02ee..40b762bfa7308 100644 --- a/sklearn/metrics/tests/test_classification.py +++ b/sklearn/metrics/tests/test_classification.py @@ -35,7 +35,7 @@ recall_score, zero_one_loss, ) -from sklearn.metrics._classification import _check_targets +from sklearn.metrics._classification import _check_targets, d2_log_loss_score from sklearn.model_selection import cross_val_score from sklearn.preprocessing import LabelBinarizer, label_binarize from sklearn.tree import DecisionTreeClassifier @@ -2895,3 +2895,201 @@ def test_brier_score_loss_deprecation_warning(): y_prob=y_pred, y_proba=y_pred, ) + + +def test_d2_log_loss_score(): + y_true = [0, 0, 0, 1, 1, 1] + y_true_string = ["no", "no", "no", "yes", "yes", "yes"] + y_pred = np.array( + [ + [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( + [ + [0.5, 0.5], + [0.5, 0.5], + [0.5, 0.5], + [0.5, 0.5], + [0.5, 0.5], + [0.5, 0.5], + ] + ) + 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( + y_true=y_true, + y_pred=y_pred, + sample_weight=sample_weight, + normalize=False, + ) + log_likelihood_null = log_loss( + y_true=y_true, + y_pred=y_pred_null, + sample_weight=sample_weight, + normalize=False, + ) + d2_score_true = 1 - log_likelihood / log_likelihood_null + assert d2_score == pytest.approx(d2_score_true) + + # check if good predictions give a relatively higher value for the d2 score + y_pred = np.array( + [ + [0.9, 0.1], + [0.8, 0.2], + [0.9, 0.1], + [0.1, 0.9], + [0.2, 0.8], + [0.1, 0.9], + ] + ) + 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 + y_pred = np.array( + [ + [0.5, 0.5], + [0.1, 0.9], + [0.1, 0.9], + [0.9, 0.1], + [0.75, 0.25], + [0.1, 0.9], + ] + ) + d2_score = d2_log_loss_score(y_true, y_pred) + assert d2_score < 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 simply using the average of the classes as the predictions + # gives a d2 score of 0 + y_true = [0, 0, 0, 1, 1, 1] + y_pred = np.array( + [ + [0.5, 0.5], + [0.5, 0.5], + [0.5, 0.5], + [0.5, 0.5], + [0.5, 0.5], + [0.5, 0.5], + ] + ) + d2_score = d2_log_loss_score(y_true, y_pred) + assert d2_score == 0 + d2_score_string = d2_log_loss_score(y_true_string, y_pred) + assert d2_score_string == 0 + + # check if simply using the average of the classes as the predictions + # 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]]) + d2_score = d2_log_loss_score(y_true, y_pred) + assert d2_score == 0 + d2_score_string = d2_log_loss_score(y_true_string, y_pred) + assert d2_score_string == 0 + 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] + + y_pred = np.array( + [ + [0.8, 0.1, 0.1], + [0.8, 0.1, 0.1], + [0.1, 0.8, 0.1], + [0.1, 0.1, 0.8], + ] + ) + d2_score = d2_log_loss_score(y_true, y_pred) + assert 0.5 < d2_score < 1.0 + d2_score = d2_log_loss_score(y_true, y_pred, sample_weight=sample_weight) + assert 0.5 < d2_score < 1.0 + + y_pred = np.array( + [ + [0.2, 0.5, 0.3], + [0.1, 0.7, 0.2], + [0.1, 0.1, 0.8], + [0.2, 0.7, 0.1], + ] + ) + d2_score = d2_log_loss_score(y_true, y_pred) + assert d2_score < 0 + d2_score = d2_log_loss_score(y_true, y_pred, sample_weight=sample_weight) + assert d2_score < 0 + + +def test_d2_log_loss_score_raises(): + """Test that d2_log_loss raises error on invalid input.""" + y_true = [0, 1, 2] + 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_true = ["a", "b", "c"] + y_pred = [[0.5, 0.5], [0.5, 0.5], [0.5, 0.5]] + labels = [0, 1, 2] + err = "number of classes in labels is different" + with pytest.raises(ValueError, match=err): + d2_log_loss_score(y_true, y_pred, labels=labels) + + # check error if y_true and y_pred do not have equal lengths + y_true = [0, 1, 2] + y_pred = [[0.5, 0.5, 0.5], [0.6, 0.3, 0.1]] + err = "inconsistent numbers of samples" + with pytest.raises(ValueError, match=err): + d2_log_loss_score(y_true, y_pred) + + # 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): + d2_log_loss_score(y_true, y_pred) + + # check error when y_true only has 1 label + y_true = [1, 1, 1] + y_pred = [[0.5, 0.5], [0.5, 0.5], [0.5, 5]] + err = "y_true contains only one label" + with pytest.raises(ValueError, match=err): + d2_log_loss_score(y_true, y_pred) + + # check error when y_true only has 1 label and labels also has + # only 1 label + y_true = [1, 1, 1] + labels = [1] + y_pred = [[0.5, 0.5], [0.5, 0.5], [0.5, 5]] + err = "The labels array needs to contain at least two" + with pytest.raises(ValueError, match=err): + d2_log_loss_score(y_true, y_pred, labels=labels) From 03bea9a110dbd15958983d384ad8ca08062e8870 Mon Sep 17 00:00:00 2001 From: Guillaume Lemaitre Date: Thu, 2 May 2024 16:20:06 +0200 Subject: [PATCH 080/344] DOC mention ot put mixin on the left in developer guide (#28924) --- doc/developers/develop.rst | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/doc/developers/develop.rst b/doc/developers/develop.rst index f22c217203ac2..63e5be41b249a 100644 --- a/doc/developers/develop.rst +++ b/doc/developers/develop.rst @@ -54,7 +54,7 @@ multiple interfaces): :Transformer: - For modifying the data in a supervised or unsupervised way (e.g. by adding, changing, + For modifying the data in a supervised or unsupervised way (e.g. by adding, changing, or removing columns, but not by adding or removing rows). Implements:: new_data = transformer.transform(data) @@ -282,12 +282,16 @@ the correct interface more easily. in the scikit-learn-contrib `project template `__. + It is particularly important to notice that mixins should be "on the left" while + the ``BaseEstimator`` should be "on the right" in the inheritance list for proper + MRO. + >>> import numpy as np >>> from sklearn.base import BaseEstimator, ClassifierMixin >>> from sklearn.utils.validation import check_X_y, check_array, check_is_fitted >>> from sklearn.utils.multiclass import unique_labels >>> from sklearn.metrics import euclidean_distances - >>> class TemplateClassifier(BaseEstimator, ClassifierMixin): + >>> class TemplateClassifier(ClassifierMixin, BaseEstimator): ... ... def __init__(self, demo_param='demo'): ... self.demo_param = demo_param From dbebec7b228a2216032d8ceefe6f5f75deb00561 Mon Sep 17 00:00:00 2001 From: Olivier Grisel Date: Thu, 2 May 2024 16:23:01 +0200 Subject: [PATCH 081/344] MAINT pymin_conda_defaults_openblas (#28908) --- .../pymin_conda_defaults_openblas_environment.yml | 6 +++--- .../pymin_conda_defaults_openblas_linux-64_conda.lock | 8 ++++---- build_tools/update_environments_and_lock_files.py | 10 ++++++---- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/build_tools/azure/pymin_conda_defaults_openblas_environment.yml b/build_tools/azure/pymin_conda_defaults_openblas_environment.yml index 17824c9b97074..3a8379e28068e 100644 --- a/build_tools/azure/pymin_conda_defaults_openblas_environment.yml +++ b/build_tools/azure/pymin_conda_defaults_openblas_environment.yml @@ -8,7 +8,8 @@ dependencies: - numpy=1.21 - blas[build=openblas] - scipy=1.7 - - joblib + - cython=3.0.10 # min + - joblib=1.2.0 # min - matplotlib=3.3.4 # min - pyamg - pytest<8 @@ -19,5 +20,4 @@ dependencies: - ccache - pip - pip: - - cython==3.0.10 # min - - threadpoolctl + - threadpoolctl==3.1.0 # min diff --git a/build_tools/azure/pymin_conda_defaults_openblas_linux-64_conda.lock b/build_tools/azure/pymin_conda_defaults_openblas_linux-64_conda.lock index 12684bc738fdb..a1a9a668e9d2e 100644 --- a/build_tools/azure/pymin_conda_defaults_openblas_linux-64_conda.lock +++ b/build_tools/azure/pymin_conda_defaults_openblas_linux-64_conda.lock @@ -1,6 +1,6 @@ # Generated by conda-lock. # platform: linux-64 -# input_hash: 4f71ddcce93c9279161f5b016be417469aa5df726d06e4a1447c9270f60179e4 +# input_hash: 7d61cf4d650f87956531ca703b2ac2eabd6d427b07664416d5420eb73b39bdf1 @EXPLICIT https://repo.anaconda.com/pkgs/main/linux-64/_libgcc_mutex-0.1-main.conda#c3473ff8bdb3d124ed5ff11ec380d6f9 https://repo.anaconda.com/pkgs/main/linux-64/blas-1.0-openblas.conda#9ddfcaef10d79366c90128f5dc444be8 @@ -58,11 +58,12 @@ https://repo.anaconda.com/pkgs/main/linux-64/openjpeg-2.4.0-h3ad879b_0.conda#86b https://repo.anaconda.com/pkgs/main/linux-64/python-3.9.19-h955ad1f_0.conda#33cb019c40e3409df392c99e3c34f352 https://repo.anaconda.com/pkgs/main/linux-64/certifi-2024.2.2-py39h06a4308_0.conda#2bc1db9166ecbb968f61252e6f08c2ce https://repo.anaconda.com/pkgs/main/noarch/cycler-0.11.0-pyhd3eb1b0_0.conda#f5e365d2cdb66d547eb8c3ab93843aab +https://repo.anaconda.com/pkgs/main/linux-64/cython-3.0.10-py39h5eee18b_0.conda#1419a658ed2b4d5c3ac1964f33143b64 https://repo.anaconda.com/pkgs/main/linux-64/exceptiongroup-1.2.0-py39h06a4308_0.conda#960e2cb83ac5134df8e593a130aa11af https://repo.anaconda.com/pkgs/main/noarch/execnet-1.9.0-pyhd3eb1b0_0.conda#f895937671af67cebb8af617494b3513 https://repo.anaconda.com/pkgs/main/linux-64/glib-2.78.4-h6a678d5_0.conda#045ff487547f7b2b7ff01648681b8ebe https://repo.anaconda.com/pkgs/main/noarch/iniconfig-1.1.1-pyhd3eb1b0_0.tar.bz2#e40edff2c5708f342cef43c7f280c507 -https://repo.anaconda.com/pkgs/main/linux-64/joblib-1.4.0-py39h06a4308_0.conda#95af0a1da233f22d37c3f5950068e9fc +https://repo.anaconda.com/pkgs/main/linux-64/joblib-1.2.0-py39h06a4308_0.conda#ac1f5687d70aa1128cbecb26bc9e559d https://repo.anaconda.com/pkgs/main/linux-64/kiwisolver-1.4.4-py39h6a678d5_0.conda#3d57aedbfbd054ce57fb3c1e4448828c https://repo.anaconda.com/pkgs/main/linux-64/mysql-5.7.24-h721c034_2.conda#dfc19ca2466d275c4c1f73b62c57f37b https://repo.anaconda.com/pkgs/main/linux-64/numpy-base-1.21.6-py39h375b286_0.conda#4ceaa5d6e6307fe06961d555f78b266f @@ -95,5 +96,4 @@ https://repo.anaconda.com/pkgs/main/linux-64/pyamg-4.2.3-py39h79cecc1_0.conda#af https://repo.anaconda.com/pkgs/main/linux-64/qt-main-5.15.2-h53bd1ea_10.conda#bd0c79e82df6323f638bdcb871891b61 https://repo.anaconda.com/pkgs/main/linux-64/pyqt-5.15.10-py39h6a678d5_0.conda#52da5ff9b1144b078d2f41bab0b213f2 https://repo.anaconda.com/pkgs/main/linux-64/matplotlib-3.3.4-py39h06a4308_0.conda#384fc5e01ebfcf30e7161119d3029b5a -# pip cython @ https://files.pythonhosted.org/packages/a7/f5/3dde4d96076888ceaa981827b098274c2b45ddd4b20d75a8cfaa92b91eec/Cython-3.0.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=651a15a8534ebfb9b58cb0b87c269c70984b6f9c88bfe65e4f635f0e3f07dfcd -# pip threadpoolctl @ https://files.pythonhosted.org/packages/1e/84/ccd9b08653022b7785b6e3ee070ffb2825841e0dc119be22f0840b2b35cb/threadpoolctl-3.4.0-py3-none-any.whl#sha256=8f4c689a65b23e5ed825c8436a92b818aac005e0f3715f6a1664d7c7ee29d262 +# pip threadpoolctl @ https://files.pythonhosted.org/packages/61/cf/6e354304bcb9c6413c4e02a747b600061c21d38ba51e7e544ac7bc66aecc/threadpoolctl-3.1.0-py3-none-any.whl#sha256=8b99adda265feb6773280df41eece7b2e6561b772d21ffd52e372f999024907b diff --git a/build_tools/update_environments_and_lock_files.py b/build_tools/update_environments_and_lock_files.py index 8a4a30d235e0b..86da119ec4547 100644 --- a/build_tools/update_environments_and_lock_files.py +++ b/build_tools/update_environments_and_lock_files.py @@ -165,7 +165,7 @@ def remove_from(alist, to_remove): "channel": "defaults", "conda_dependencies": remove_from( common_dependencies, - ["pandas", "cython", "threadpoolctl", "pip", "ninja", "meson-python"], + ["pandas", "threadpoolctl", "pip", "ninja", "meson-python"], ) + ["ccache"], "package_constraints": { @@ -175,10 +175,12 @@ def remove_from(alist, to_remove): "scipy": "1.7", # the min version has some low level crashes "matplotlib": "min", "cython": "min", + "joblib": "min", + "threadpoolctl": "min", }, - # TODO: put cython and threadpoolctl back to conda dependencies when required - # version is available on the main channel - "pip_dependencies": ["cython", "threadpoolctl"], + # TODO: put pip dependencies back to conda dependencies when required + # version is available on the defaults channel. + "pip_dependencies": ["threadpoolctl"], }, { "name": "pymin_conda_forge_openblas_ubuntu_2204", From 4e20b01b244950a4b9e229bd55f0045b9d7123eb Mon Sep 17 00:00:00 2001 From: Lucy Liu Date: Fri, 3 May 2024 00:54:42 +1000 Subject: [PATCH 082/344] API Deprecate `cv_values_` in favour of `cv_results_` in `RidgeCV` estimators (#28915) Co-authored-by: jeremiedbb --- doc/whats_new/v1.5.rst | 4 + sklearn/linear_model/_ridge.py | 126 ++++++++++++++++++----- sklearn/linear_model/tests/test_ridge.py | 78 +++++++++----- 3 files changed, 154 insertions(+), 54 deletions(-) diff --git a/doc/whats_new/v1.5.rst b/doc/whats_new/v1.5.rst index d3064851e7f87..b7d93de9ac2b0 100644 --- a/doc/whats_new/v1.5.rst +++ b/doc/whats_new/v1.5.rst @@ -357,6 +357,10 @@ Changelog :class:`linear_model.SGDOneClassSVM`. Pass `average=False` instead. :pr:`28582` by :user:`Jérémie du Boisberranger `. +- |API| `store_cv_values` and `cv_values_` are deprecated in favor of + `store_cv_results` and `cv_results_` in `RidgeCV` and `RidgeClassifierCV`. + :pr:`28915` by :user:`Lucy Liu `. + :mod:`sklearn.manifold` ....................... diff --git a/sklearn/linear_model/_ridge.py b/sklearn/linear_model/_ridge.py index 2babc21631f8a..b336565cff1f6 100644 --- a/sklearn/linear_model/_ridge.py +++ b/sklearn/linear_model/_ridge.py @@ -31,6 +31,7 @@ check_scalar, column_or_1d, compute_sample_weight, + deprecated, ) from ..utils._array_api import ( _is_numpy_namespace, @@ -39,7 +40,7 @@ get_namespace, get_namespace_and_device, ) -from ..utils._param_validation import Interval, StrOptions, validate_params +from ..utils._param_validation import Hidden, Interval, StrOptions, validate_params from ..utils.extmath import row_norms, safe_sparse_dot from ..utils.fixes import _sparse_linalg_cg from ..utils.metadata_routing import ( @@ -1731,7 +1732,7 @@ def __init__( scoring=None, copy_X=True, gcv_mode=None, - store_cv_values=False, + store_cv_results=False, is_clf=False, alpha_per_target=False, ): @@ -1740,7 +1741,7 @@ def __init__( self.scoring = scoring self.copy_X = copy_X self.gcv_mode = gcv_mode - self.store_cv_values = store_cv_values + self.store_cv_results = store_cv_results self.is_clf = is_clf self.alpha_per_target = alpha_per_target @@ -2135,8 +2136,8 @@ def fit(self, X, y, sample_weight=None, score_params=None): 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_values: - self.cv_values_ = np.empty((n_samples * n_y, n_alphas), dtype=X.dtype) + 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 @@ -2145,12 +2146,12 @@ def fit(self, X, y, sample_weight=None, score_params=None): if scorer is None: squared_errors = (c / G_inverse_diag) ** 2 alpha_score = self._score_without_scorer(squared_errors=squared_errors) - if self.store_cv_values: - self.cv_values_[:, i] = squared_errors.ravel() + if self.store_cv_results: + self.cv_results_[:, i] = squared_errors.ravel() else: predictions = y - (c / G_inverse_diag) - if self.store_cv_values: - self.cv_values_[:, i] = predictions.ravel() + if self.store_cv_results: + self.cv_results_[:, i] = predictions.ravel() score_params = score_params or {} alpha_score = self._score( @@ -2193,12 +2194,12 @@ def fit(self, X, y, sample_weight=None, score_params=None): X_offset += X_mean * X_scale self._set_intercept(X_offset, y_offset, X_scale) - if self.store_cv_values: + if self.store_cv_results: if len(y.shape) == 1: - cv_values_shape = n_samples, n_alphas + cv_results_shape = n_samples, n_alphas else: - cv_values_shape = n_samples, n_y, n_alphas - self.cv_values_ = self.cv_values_.reshape(cv_values_shape) + cv_results_shape = n_samples, n_y, n_alphas + self.cv_results_ = self.cv_results_.reshape(cv_results_shape) return self @@ -2258,8 +2259,9 @@ class _BaseRidgeCV(LinearModel): "scoring": [StrOptions(set(get_scorer_names())), callable, None], "cv": ["cv_object"], "gcv_mode": [StrOptions({"auto", "svd", "eigen"}), None], - "store_cv_values": ["boolean"], + "store_cv_results": ["boolean", Hidden(None)], "alpha_per_target": ["boolean"], + "store_cv_values": ["boolean", Hidden(StrOptions({"deprecated"}))], } def __init__( @@ -2270,16 +2272,18 @@ def __init__( scoring=None, cv=None, gcv_mode=None, - store_cv_values=False, + store_cv_results=None, alpha_per_target=False, + store_cv_values="deprecated", ): self.alphas = alphas self.fit_intercept = fit_intercept self.scoring = scoring self.cv = cv self.gcv_mode = gcv_mode - self.store_cv_values = store_cv_values + self.store_cv_results = store_cv_results self.alpha_per_target = alpha_per_target + self.store_cv_values = store_cv_values def fit(self, X, y, sample_weight=None, **params): """Fit Ridge regression model with cv. @@ -2323,6 +2327,28 @@ def fit(self, X, y, sample_weight=None, **params): _raise_for_params(params, self, "fit") cv = self.cv + # TODO(1.7): Remove in 1.7 + # Also change `store_cv_results` default back to False + if self.store_cv_values != "deprecated": + if self.store_cv_results is not None: + raise ValueError( + "Both 'store_cv_values' and 'store_cv_results' were set. " + "'store_cv_values' is deprecated in version 1.5 and will be " + "removed in 1.7. To avoid this error, only set 'store_cv_results'." + ) + warnings.warn( + ( + "'store_cv_values' is deprecated in version 1.5 and will be " + "removed in 1.7. Use 'store_cv_results' instead." + ), + FutureWarning, + ) + self._store_cv_results = self.store_cv_values + elif self.store_cv_results is None: + self._store_cv_results = False + else: + self._store_cv_results = self.store_cv_results + # `_RidgeGCV` does not work for alpha = 0 if cv is None: check_scalar_alpha = partial( @@ -2368,7 +2394,7 @@ def fit(self, X, y, sample_weight=None, **params): fit_intercept=self.fit_intercept, scoring=self.scoring, gcv_mode=self.gcv_mode, - store_cv_values=self.store_cv_values, + store_cv_results=self._store_cv_results, is_clf=is_classifier(self), alpha_per_target=self.alpha_per_target, ) @@ -2380,11 +2406,11 @@ def fit(self, X, y, sample_weight=None, **params): ) self.alpha_ = estimator.alpha_ self.best_score_ = estimator.best_score_ - if self.store_cv_values: - self.cv_values_ = estimator.cv_values_ + if self._store_cv_results: + self.cv_results_ = estimator.cv_results_ else: - if self.store_cv_values: - raise ValueError("cv!=None and store_cv_values=True are incompatible") + if self._store_cv_results: + raise ValueError("cv!=None and store_cv_results=True are incompatible") if self.alpha_per_target: raise ValueError("cv!=None and alpha_per_target=True are incompatible") @@ -2445,6 +2471,16 @@ def get_metadata_routing(self): def _get_scorer(self): return check_scoring(self, scoring=self.scoring, allow_none=True) + # TODO(1.7): Remove + # mypy error: Decorated property not supported + @deprecated( # type: ignore + "Attribute `cv_values_` is deprecated in version 1.5 and will be removed " + "in 1.7. Use `cv_results_` instead." + ) + @property + def cv_values_(self): + return self.cv_results_ + class RidgeCV(MultiOutputMixin, RegressorMixin, _BaseRidgeCV): """Ridge regression with built-in cross-validation. @@ -2506,12 +2542,15 @@ class RidgeCV(MultiOutputMixin, RegressorMixin, _BaseRidgeCV): 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_values : bool, default=False + store_cv_results : bool, default=False Flag indicating if the cross-validation values corresponding to each alpha should be stored in the ``cv_values_`` attribute (see below). This flag is only compatible with ``cv=None`` (i.e. using Leave-One-Out Cross-Validation). + .. versionchanged:: 1.5 + 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 @@ -2521,16 +2560,29 @@ class RidgeCV(MultiOutputMixin, RegressorMixin, _BaseRidgeCV): .. versionadded:: 0.24 + store_cv_values : bool + Flag indicating if the cross-validation values corresponding to + each alpha should be stored in the ``cv_values_`` attribute (see + below). This flag is only compatible with ``cv=None`` (i.e. using + Leave-One-Out Cross-Validation). + + .. deprecated:: 1.5 + `store_cv_values` is deprecated in version 1.5 in favor of + `store_cv_results` and will be removed in version 1.7. + Attributes ---------- - cv_values_ : ndarray of shape (n_samples, n_alphas) or \ + 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_values=True`` and ``cv=None``). After ``fit()`` has been + ``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. + .. versionchanged:: 1.5 + `cv_values_` changed to `cv_results_`. + coef_ : ndarray of shape (n_features) or (n_targets, n_features) Weight vector(s). @@ -2670,20 +2722,36 @@ class RidgeClassifierCV(_RidgeClassifierMixin, _BaseRidgeCV): weights inversely proportional to class frequencies in the input data as ``n_samples / (n_classes * np.bincount(y))``. - store_cv_values : bool, default=False + store_cv_results : bool, default=False + Flag indicating if the cross-validation results 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). + + .. versionchanged:: 1.5 + Parameter name changed from `store_cv_values` to `store_cv_results`. + + store_cv_values : bool Flag indicating if the cross-validation values corresponding to each alpha should be stored in the ``cv_values_`` attribute (see below). This flag is only compatible with ``cv=None`` (i.e. using Leave-One-Out Cross-Validation). + .. deprecated:: 1.5 + `store_cv_values` is deprecated in version 1.5 in favor of + `store_cv_results` and will be removed in version 1.7. + Attributes ---------- - cv_values_ : ndarray of shape (n_samples, n_targets, n_alphas), optional - Cross-validation values for each alpha (only if ``store_cv_values=True`` and + 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. + .. versionchanged:: 1.5 + `cv_values_` changed to `cv_results_`. + coef_ : ndarray of shape (1, n_features) or (n_targets, n_features) Coefficient of the features in the decision function. @@ -2752,13 +2820,15 @@ def __init__( scoring=None, cv=None, class_weight=None, - store_cv_values=False, + store_cv_results=None, + store_cv_values="deprecated", ): super().__init__( alphas=alphas, fit_intercept=fit_intercept, scoring=scoring, cv=cv, + store_cv_results=store_cv_results, store_cv_values=store_cv_values, ) self.class_weight = class_weight diff --git a/sklearn/linear_model/tests/test_ridge.py b/sklearn/linear_model/tests/test_ridge.py index f850fa5dcfa99..167ce0bac4cba 100644 --- a/sklearn/linear_model/tests/test_ridge.py +++ b/sklearn/linear_model/tests/test_ridge.py @@ -920,15 +920,15 @@ def test_ridge_gcv_sample_weights( X_gcv = X_container(X) gcv_ridge = RidgeCV( alphas=alphas, - store_cv_values=True, + store_cv_results=True, gcv_mode=gcv_mode, fit_intercept=fit_intercept, ) gcv_ridge.fit(X_gcv, y, sample_weight=sample_weight) if len(y_shape) == 2: - gcv_errors = gcv_ridge.cv_values_[:, :, alphas.index(kfold.alpha_)] + gcv_errors = gcv_ridge.cv_results_[:, :, alphas.index(kfold.alpha_)] else: - gcv_errors = gcv_ridge.cv_values_[:, alphas.index(kfold.alpha_)] + 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) @@ -1034,15 +1034,15 @@ def _test_ridge_cv(sparse_container): @pytest.mark.parametrize( "ridge, make_dataset", [ - (RidgeCV(store_cv_values=False), make_regression), - (RidgeClassifierCV(store_cv_values=False), make_classification), + (RidgeCV(store_cv_results=False), make_regression), + (RidgeClassifierCV(store_cv_results=False), make_classification), ], ) -def test_ridge_gcv_cv_values_not_stored(ridge, make_dataset): - # Check that `cv_values_` is not stored when store_cv_values is False +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) ridge.fit(X, y) - assert not hasattr(ridge, "cv_values_") + assert not hasattr(ridge, "cv_results_") @pytest.mark.parametrize( @@ -1053,7 +1053,7 @@ def test_ridge_gcv_cv_values_not_stored(ridge, make_dataset): def test_ridge_best_score(ridge, make_dataset, cv): # check that the best_score_ is store X, y = make_dataset(n_samples=6, random_state=42) - ridge.set_params(store_cv_values=False, cv=cv) + ridge.set_params(store_cv_results=False, cv=cv) ridge.fit(X, y) assert hasattr(ridge, "best_score_") assert isinstance(ridge.best_score_, float) @@ -1090,27 +1090,27 @@ def test_ridge_cv_individual_penalties(): Ridge(alpha=ridge_cv.alpha_).fit(X, y).coef_, ridge_cv.coef_ ) - # Test shape of alpha_ and cv_values_ - ridge_cv = RidgeCV(alphas=alphas, alpha_per_target=True, store_cv_values=True).fit( + # 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_values_.shape == (n_samples, len(alphas), 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_values=True).fit(X, y) + ridge_cv = RidgeCV(alphas=1, 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_values_.shape == (n_samples, n_targets, 1) + assert ridge_cv.cv_results_.shape == (n_samples, n_targets, 1) # Test edge case of there being only one target - ridge_cv = RidgeCV(alphas=alphas, alpha_per_target=True, store_cv_values=True).fit( + ridge_cv = RidgeCV(alphas=alphas, alpha_per_target=True, store_cv_results=True).fit( X, y[:, 0] ) assert np.isscalar(ridge_cv.alpha_) assert np.isscalar(ridge_cv.best_score_) - assert ridge_cv.cv_values_.shape == (n_samples, len(alphas)) + 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) @@ -1444,7 +1444,7 @@ def test_class_weights_cv(): @pytest.mark.parametrize( "scoring", [None, "neg_mean_squared_error", _mean_squared_error_callable] ) -def test_ridgecv_store_cv_values(scoring): +def test_ridgecv_store_cv_results(scoring): rng = np.random.RandomState(42) n_samples = 8 @@ -1455,26 +1455,26 @@ def test_ridgecv_store_cv_values(scoring): scoring_ = make_scorer(scoring) if callable(scoring) else scoring - r = RidgeCV(alphas=alphas, cv=None, store_cv_values=True, scoring=scoring_) + r = RidgeCV(alphas=alphas, cv=None, store_cv_results=True, scoring=scoring_) # with len(y.shape) == 1 y = rng.randn(n_samples) r.fit(x, y) - assert r.cv_values_.shape == (n_samples, n_alphas) + assert r.cv_results_.shape == (n_samples, n_alphas) # with len(y.shape) == 2 n_targets = 3 y = rng.randn(n_samples, n_targets) r.fit(x, y) - assert r.cv_values_.shape == (n_samples, n_targets, n_alphas) + assert r.cv_results_.shape == (n_samples, n_targets, n_alphas) - r = RidgeCV(cv=3, store_cv_values=True, scoring=scoring) - with pytest.raises(ValueError, match="cv!=None and store_cv_values"): + 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) @pytest.mark.parametrize("scoring", [None, "accuracy", _accuracy_callable]) -def test_ridge_classifier_cv_store_cv_values(scoring): +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]) @@ -1485,13 +1485,13 @@ def test_ridge_classifier_cv_store_cv_values(scoring): scoring_ = make_scorer(scoring) if callable(scoring) else scoring r = RidgeClassifierCV( - alphas=alphas, cv=None, store_cv_values=True, scoring=scoring_ + alphas=alphas, cv=None, store_cv_results=True, scoring=scoring_ ) # with len(y.shape) == 1 n_targets = 1 r.fit(x, y) - assert r.cv_values_.shape == (n_samples, n_targets, n_alphas) + assert r.cv_results_.shape == (n_samples, n_targets, n_alphas) # with len(y.shape) == 2 y = np.array( @@ -1499,7 +1499,7 @@ def test_ridge_classifier_cv_store_cv_values(scoring): ).transpose() n_targets = y.shape[1] r.fit(x, y) - assert r.cv_values_.shape == (n_samples, n_targets, n_alphas) + assert r.cv_results_.shape == (n_samples, n_targets, n_alphas) @pytest.mark.parametrize("Estimator", [RidgeCV, RidgeClassifierCV]) @@ -2226,6 +2226,32 @@ def test_ridge_sample_weight_consistency( assert_allclose(reg1.intercept_, reg2.intercept_) +# TODO(1.7): Remove +def test_ridge_store_cv_values_deprecated(): + """Check `store_cv_values` parameter deprecated.""" + X, y = make_regression(n_samples=6, random_state=42) + ridge = RidgeCV(store_cv_values=True) + msg = "'store_cv_values' is deprecated" + with pytest.warns(FutureWarning, match=msg): + ridge.fit(X, y) + + # Error when both set + ridge = RidgeCV(store_cv_results=True, store_cv_values=True) + msg = "Both 'store_cv_values' and 'store_cv_results' were" + with pytest.raises(ValueError, match=msg): + ridge.fit(X, y) + + +def test_ridge_cv_values_deprecated(): + """Check `cv_values_` deprecated.""" + X, y = make_regression(n_samples=6, random_state=42) + ridge = RidgeCV(store_cv_results=True) + msg = "Attribute `cv_values_` is deprecated" + with pytest.warns(FutureWarning, match=msg): + ridge.fit(X, y) + ridge.cv_values_ + + # Metadata Routing Tests # ====================== From 20c8e69d0ff594a6067348f1ba0ad5cd70bc7657 Mon Sep 17 00:00:00 2001 From: Christian Lorentzen Date: Fri, 3 May 2024 14:48:25 +0200 Subject: [PATCH 083/344] API deprecate multi_class in LogisticRegression (#28703) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jérémie du Boisberranger --- asv_benchmarks/benchmarks/linear_model.py | 1 - benchmarks/bench_saga.py | 11 +- doc/developers/develop.rst | 2 +- doc/modules/linear_model.rst | 14 +- doc/modules/multiclass.rst | 8 +- doc/whats_new/v1.5.rst | 8 + .../plot_classification_probability.py | 15 +- .../linear_model/plot_logistic_multinomial.py | 16 +- ...sparse_logistic_regression_20newsgroups.py | 16 +- sklearn/ensemble/_voting.py | 2 +- sklearn/linear_model/_logistic.py | 191 +++++++++++---- sklearn/linear_model/_sag.py | 5 +- sklearn/linear_model/tests/test_logistic.py | 219 ++++++++++-------- sklearn/linear_model/tests/test_sag.py | 113 ++------- sklearn/metrics/tests/test_score_objects.py | 4 +- sklearn/multioutput.py | 2 +- 16 files changed, 339 insertions(+), 288 deletions(-) diff --git a/asv_benchmarks/benchmarks/linear_model.py b/asv_benchmarks/benchmarks/linear_model.py index 7e7b9d33540c6..24153895611df 100644 --- a/asv_benchmarks/benchmarks/linear_model.py +++ b/asv_benchmarks/benchmarks/linear_model.py @@ -52,7 +52,6 @@ def make_estimator(self, params): estimator = LogisticRegression( solver=solver, penalty=penalty, - multi_class="multinomial", tol=0.01, n_jobs=n_jobs, random_state=0, diff --git a/benchmarks/bench_saga.py b/benchmarks/bench_saga.py index c5b3e7728e2ec..97d4ba7b4b75b 100644 --- a/benchmarks/bench_saga.py +++ b/benchmarks/bench_saga.py @@ -20,6 +20,7 @@ from sklearn.linear_model import LogisticRegression from sklearn.metrics import log_loss from sklearn.model_selection import train_test_split +from sklearn.multiclass import OneVsRestClassifier from sklearn.preprocessing import LabelBinarizer, LabelEncoder from sklearn.utils.extmath import safe_sparse_dot, softmax from sklearn.utils.parallel import Parallel, delayed @@ -95,7 +96,6 @@ def fit_single( else: lr = LogisticRegression( solver=solver, - multi_class=multi_class, C=C, penalty=penalty, fit_intercept=False, @@ -103,6 +103,8 @@ def fit_single( max_iter=this_max_iter, random_state=42, ) + if multi_class == "ovr": + lr = OneVsRestClassifier(lr) # Makes cpu cache even for all fit calls X_train.max() @@ -118,8 +120,12 @@ def fit_single( except NotImplementedError: # Lightning predict_proba is not implemented for n_classes > 2 y_pred = _predict_proba(lr, X) + if isinstance(lr, OneVsRestClassifier): + coef = np.concatenate([est.coef_ for est in lr.estimators_]) + else: + coef = lr.coef_ score = log_loss(y, y_pred, normalize=False) / n_samples - score += 0.5 * alpha * np.sum(lr.coef_**2) + beta * np.sum(np.abs(lr.coef_)) + score += 0.5 * alpha * np.sum(coef**2) + beta * np.sum(np.abs(coef)) scores.append(score) train_score, test_score = tuple(scores) @@ -133,6 +139,7 @@ def fit_single( def _predict_proba(lr, X): + """Predict proba for lightning for n_classes >=3.""" pred = safe_sparse_dot(X, lr.coef_.T) if hasattr(lr, "intercept_"): pred += lr.intercept_ diff --git a/doc/developers/develop.rst b/doc/developers/develop.rst index 63e5be41b249a..97cb156da5812 100644 --- a/doc/developers/develop.rst +++ b/doc/developers/develop.rst @@ -353,7 +353,7 @@ The parameter `deep` will control whether or not the parameters of the subestimator__intercept_scaling -> 1 subestimator__l1_ratio -> None subestimator__max_iter -> 100 - subestimator__multi_class -> auto + subestimator__multi_class -> deprecated subestimator__n_jobs -> None subestimator__penalty -> l2 subestimator__random_state -> None diff --git a/doc/modules/linear_model.rst b/doc/modules/linear_model.rst index b92a8c2a01019..dd975c4d6e417 100644 --- a/doc/modules/linear_model.rst +++ b/doc/modules/linear_model.rst @@ -1047,24 +1047,24 @@ Solvers The solvers implemented in the class :class:`LogisticRegression` are "lbfgs", "liblinear", "newton-cg", "newton-cholesky", "sag" and "saga": -The following table summarizes the penalties supported by each solver: +The following table summarizes the penalties and multinomial multiclass supported by each solver: +------------------------------+-----------------+-------------+-----------------+-----------------------+-----------+------------+ | | **Solvers** | +------------------------------+-------------+-----------------+-----------------+-----------------------+-----------+------------+ | **Penalties** | **'lbfgs'** | **'liblinear'** | **'newton-cg'** | **'newton-cholesky'** | **'sag'** | **'saga'** | +------------------------------+-------------+-----------------+-----------------+-----------------------+-----------+------------+ -| Multinomial + L2 penalty | yes | no | yes | no | yes | yes | +| L2 penalty | yes | no | yes | no | yes | yes | +------------------------------+-------------+-----------------+-----------------+-----------------------+-----------+------------+ -| OVR + L2 penalty | yes | yes | yes | yes | yes | yes | +| L1 penalty | no | yes | no | no | no | yes | +------------------------------+-------------+-----------------+-----------------+-----------------------+-----------+------------+ -| Multinomial + L1 penalty | no | no | no | no | no | yes | +| Elastic-Net (L1 + L2) | no | no | no | no | no | yes | +------------------------------+-------------+-----------------+-----------------+-----------------------+-----------+------------+ -| OVR + L1 penalty | no | yes | no | no | no | yes | +| No penalty ('none') | yes | no | yes | yes | yes | yes | +------------------------------+-------------+-----------------+-----------------+-----------------------+-----------+------------+ -| Elastic-Net | no | no | no | no | no | yes | +| **Multiclass support** | | +------------------------------+-------------+-----------------+-----------------+-----------------------+-----------+------------+ -| No penalty ('none') | yes | no | yes | yes | yes | yes | +| multinomial multiclass | yes | no | yes | no | yes | yes | +------------------------------+-------------+-----------------+-----------------+-----------------------+-----------+------------+ | **Behaviors** | | +------------------------------+-------------+-----------------+-----------------+-----------------------+-----------+------------+ diff --git a/doc/modules/multiclass.rst b/doc/modules/multiclass.rst index 88f822dad091e..42762690ce8f7 100644 --- a/doc/modules/multiclass.rst +++ b/doc/modules/multiclass.rst @@ -63,8 +63,8 @@ can provide additional strategies beyond what is built-in: - :class:`semi_supervised.LabelSpreading` - :class:`discriminant_analysis.LinearDiscriminantAnalysis` - :class:`svm.LinearSVC` (setting multi_class="crammer_singer") - - :class:`linear_model.LogisticRegression` (setting multi_class="multinomial") - - :class:`linear_model.LogisticRegressionCV` (setting multi_class="multinomial") + - :class:`linear_model.LogisticRegression` (with most solvers) + - :class:`linear_model.LogisticRegressionCV` (with most solvers) - :class:`neural_network.MLPClassifier` - :class:`neighbors.NearestCentroid` - :class:`discriminant_analysis.QuadraticDiscriminantAnalysis` @@ -86,8 +86,8 @@ can provide additional strategies beyond what is built-in: - :class:`ensemble.GradientBoostingClassifier` - :class:`gaussian_process.GaussianProcessClassifier` (setting multi_class = "one_vs_rest") - :class:`svm.LinearSVC` (setting multi_class="ovr") - - :class:`linear_model.LogisticRegression` (setting multi_class="ovr") - - :class:`linear_model.LogisticRegressionCV` (setting multi_class="ovr") + - :class:`linear_model.LogisticRegression` (most solvers) + - :class:`linear_model.LogisticRegressionCV` (most solvers) - :class:`linear_model.SGDClassifier` - :class:`linear_model.Perceptron` - :class:`linear_model.PassiveAggressiveClassifier` diff --git a/doc/whats_new/v1.5.rst b/doc/whats_new/v1.5.rst index b7d93de9ac2b0..c813c7b3c06fd 100644 --- a/doc/whats_new/v1.5.rst +++ b/doc/whats_new/v1.5.rst @@ -357,6 +357,14 @@ Changelog :class:`linear_model.SGDOneClassSVM`. Pass `average=False` instead. :pr:`28582` by :user:`Jérémie du Boisberranger `. +- |API| Parameter `multi_class` was deprecated in + :class:`linear_model.LogisticRegression` and + :class:`linear_model.LogisticRegressionCV`. `multi_class` will be removed in 1.7, + and internally, for 3 and more classes, it will always use multinomial. + If you still want to use the one-vs-rest scheme, you can use + `OneVsRestClassifier(LogisticRegression(..))`. + :pr:`28703` by :user:`Christian Lorentzen `. + - |API| `store_cv_values` and `cv_values_` are deprecated in favor of `store_cv_results` and `cv_results_` in `RidgeCV` and `RidgeClassifierCV`. :pr:`28915` by :user:`Lucy Liu `. diff --git a/examples/classification/plot_classification_probability.py b/examples/classification/plot_classification_probability.py index 4e8f0763d3b47..42c8643b9107a 100644 --- a/examples/classification/plot_classification_probability.py +++ b/examples/classification/plot_classification_probability.py @@ -5,8 +5,8 @@ Plot the classification probability for different classifiers. We use a 3 class dataset, and we classify it with a Support Vector classifier, L1 and L2 -penalized logistic regression with either a One-Vs-Rest or multinomial setting, -and Gaussian process classification. +penalized logistic regression (multinomial multiclass), a One-Vs-Rest version with +logistic regression, and Gaussian process classification. Linear SVC is not a probabilistic classifier by default but it has a built-in calibration option enabled in this example (`probability=True`). @@ -30,6 +30,7 @@ from sklearn.inspection import DecisionBoundaryDisplay from sklearn.linear_model import LogisticRegression from sklearn.metrics import accuracy_score +from sklearn.multiclass import OneVsRestClassifier from sklearn.svm import SVC iris = datasets.load_iris() @@ -43,14 +44,12 @@ # Create different classifiers. classifiers = { - "L1 logistic": LogisticRegression( - C=C, penalty="l1", solver="saga", multi_class="multinomial", max_iter=10000 - ), + "L1 logistic": LogisticRegression(C=C, penalty="l1", solver="saga", max_iter=10000), "L2 logistic (Multinomial)": LogisticRegression( - C=C, penalty="l2", solver="saga", multi_class="multinomial", max_iter=10000 + C=C, penalty="l2", solver="saga", max_iter=10000 ), - "L2 logistic (OvR)": LogisticRegression( - C=C, penalty="l2", solver="saga", multi_class="ovr", max_iter=10000 + "L2 logistic (OvR)": OneVsRestClassifier( + LogisticRegression(C=C, penalty="l2", solver="saga", max_iter=10000) ), "Linear SVC": SVC(kernel="linear", C=C, probability=True, random_state=0), "GPC": GaussianProcessClassifier(kernel), diff --git a/examples/linear_model/plot_logistic_multinomial.py b/examples/linear_model/plot_logistic_multinomial.py index 791a788b2238b..c332aecea2ce7 100644 --- a/examples/linear_model/plot_logistic_multinomial.py +++ b/examples/linear_model/plot_logistic_multinomial.py @@ -18,6 +18,7 @@ from sklearn.datasets import make_blobs from sklearn.inspection import DecisionBoundaryDisplay from sklearn.linear_model import LogisticRegression +from sklearn.multiclass import OneVsRestClassifier # make 3-class dataset for classification centers = [[-5, 0], [0, 1.5], [5, -1]] @@ -26,9 +27,10 @@ X = np.dot(X, transformation) for multi_class in ("multinomial", "ovr"): - clf = LogisticRegression( - solver="sag", max_iter=100, random_state=42, multi_class=multi_class - ).fit(X, y) + clf = LogisticRegression(solver="sag", max_iter=100, random_state=42) + if multi_class == "ovr": + clf = OneVsRestClassifier(clf) + clf.fit(X, y) # print the training scores print("training score : %.3f (%s)" % (clf.score(X, y), multi_class)) @@ -51,8 +53,12 @@ # Plot the three one-against-all classifiers xmin, xmax = plt.xlim() ymin, ymax = plt.ylim() - coef = clf.coef_ - intercept = clf.intercept_ + if multi_class == "ovr": + coef = np.concatenate([est.coef_ for est in clf.estimators_]) + intercept = np.concatenate([est.intercept_ for est in clf.estimators_]) + else: + coef = clf.coef_ + intercept = clf.intercept_ def plot_hyperplane(c, color): def line(x0): diff --git a/examples/linear_model/plot_sparse_logistic_regression_20newsgroups.py b/examples/linear_model/plot_sparse_logistic_regression_20newsgroups.py index f62208aab154a..404250a855e0a 100644 --- a/examples/linear_model/plot_sparse_logistic_regression_20newsgroups.py +++ b/examples/linear_model/plot_sparse_logistic_regression_20newsgroups.py @@ -32,6 +32,7 @@ from sklearn.exceptions import ConvergenceWarning from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split +from sklearn.multiclass import OneVsRestClassifier warnings.filterwarnings("ignore", category=ConvergenceWarning, module="sklearn") t0 = timeit.default_timer() @@ -76,20 +77,25 @@ "[model=%s, solver=%s] Number of epochs: %s" % (model_params["name"], solver, this_max_iter) ) - lr = LogisticRegression( + clf = LogisticRegression( solver=solver, - multi_class=model, penalty="l1", max_iter=this_max_iter, random_state=42, ) + if model == "ovr": + clf = OneVsRestClassifier(clf) t1 = timeit.default_timer() - lr.fit(X_train, y_train) + clf.fit(X_train, y_train) train_time = timeit.default_timer() - t1 - y_pred = lr.predict(X_test) + y_pred = clf.predict(X_test) accuracy = np.sum(y_pred == y_test) / y_test.shape[0] - density = np.mean(lr.coef_ != 0, axis=1) * 100 + if model == "ovr": + coef = np.concatenate([est.coef_ for est in clf.estimators_]) + else: + coef = clf.coef_ + density = np.mean(coef != 0, axis=1) * 100 accuracies.append(accuracy) densities.append(density) times.append(train_time) diff --git a/sklearn/ensemble/_voting.py b/sklearn/ensemble/_voting.py index 4e7c7af369ab0..7c54be40dc013 100644 --- a/sklearn/ensemble/_voting.py +++ b/sklearn/ensemble/_voting.py @@ -287,7 +287,7 @@ class VotingClassifier(ClassifierMixin, _BaseVoting): >>> from sklearn.linear_model import LogisticRegression >>> from sklearn.naive_bayes import GaussianNB >>> from sklearn.ensemble import RandomForestClassifier, VotingClassifier - >>> clf1 = LogisticRegression(multi_class='multinomial', random_state=1) + >>> clf1 = LogisticRegression(random_state=1) >>> clf2 = RandomForestClassifier(n_estimators=50, random_state=1) >>> clf3 = GaussianNB() >>> X = np.array([[-1, -1], [-2, -1], [-3, -2], [1, 1], [2, 1], [3, 2]]) diff --git a/sklearn/linear_model/_logistic.py b/sklearn/linear_model/_logistic.py index 481ccf6c7e5ed..055ccc1c6a202 100644 --- a/sklearn/linear_model/_logistic.py +++ b/sklearn/linear_model/_logistic.py @@ -33,7 +33,7 @@ check_random_state, compute_class_weight, ) -from ..utils._param_validation import Interval, StrOptions +from ..utils._param_validation import Hidden, Interval, StrOptions from ..utils.extmath import row_norms, softmax from ..utils.metadata_routing import ( MetadataRouter, @@ -905,28 +905,33 @@ class LogisticRegression(LinearClassifierMixin, SparseCoefMixin, BaseEstimator): 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 multiclass problems, only 'newton-cg', 'sag', 'saga' and - 'lbfgs' handle multinomial loss; - - 'liblinear' is limited to one-versus-rest schemes. - - 'newton-cholesky' is a good choice for `n_samples` >> `n_features`, - especially with one-hot encoded categorical features with rare - categories. Note that it is limited to binary classification and the - one-versus-rest reduction for multiclass classification. Be aware that - the memory usage of this solver has a quadratic dependency on - `n_features` because it explicitly computes the Hessian matrix. + - For small datasets, 'liblinear' is a good choice, whereas 'sag' + and 'saga' are faster for large ones; + - For multiclass problems, only 'newton-cg', 'sag', 'saga' and + 'lbfgs' handle multinomial loss; + - 'liblinear' and 'newton-cholesky' can only handle binary classification + by default. To apply a one-versus-rest scheme for the multiclass setting + one can wrapt it with the `OneVsRestClassifier`. + - 'newton-cholesky' 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. .. warning:: - The choice of the algorithm depends on the penalty chosen. - Supported penalties by solver: - - - 'lbfgs' - ['l2', None] - - 'liblinear' - ['l1', 'l2'] - - 'newton-cg' - ['l2', None] - - 'newton-cholesky' - ['l2', None] - - 'sag' - ['l2', None] - - 'saga' - ['elasticnet', 'l1', 'l2', None] + 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 @@ -963,6 +968,13 @@ class LogisticRegression(LinearClassifierMixin, SparseCoefMixin, BaseEstimator): Stochastic Average Gradient descent solver for 'multinomial' case. .. versionchanged:: 0.22 Default changed from 'ovr' to 'auto' in 0.22. + .. 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. verbose : int, default=0 For the liblinear and lbfgs solvers set verbose to any positive @@ -1104,11 +1116,14 @@ class LogisticRegression(LinearClassifierMixin, SparseCoefMixin, BaseEstimator): ) ], "max_iter": [Interval(Integral, 0, None, closed="left")], - "multi_class": [StrOptions({"auto", "ovr", "multinomial"})], "verbose": ["verbose"], "warm_start": ["boolean"], "n_jobs": [None, Integral], "l1_ratio": [Interval(Real, 0, 1, closed="both"), None], + "multi_class": [ + StrOptions({"auto", "ovr", "multinomial"}), + Hidden(StrOptions({"deprecated"})), + ], } def __init__( @@ -1124,7 +1139,7 @@ def __init__( random_state=None, solver="lbfgs", max_iter=100, - multi_class="auto", + multi_class="deprecated", verbose=0, warm_start=False, n_jobs=None, @@ -1216,7 +1231,40 @@ def fit(self, X, y, sample_weight=None): check_classification_targets(y) self.classes_ = np.unique(y) - multi_class = _check_multi_class(self.multi_class, solver, len(self.classes_)) + # TODO(1.7) remove multi_class + multi_class = self.multi_class + if self.multi_class == "multinomial" and len(self.classes_) == 2: + warnings.warn( + ( + "'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." + ), + FutureWarning, + ) + elif self.multi_class in ("multinomial", "auto"): + warnings.warn( + ( + "'multi_class' was deprecated in version 1.5 and will be removed in" + " 1.7. From then on, it will always use 'multinomial'." + " Leave it to its default value to avoid this warning." + ), + FutureWarning, + ) + elif self.multi_class == "ovr": + warnings.warn( + ( + "'multi_class' was deprecated in version 1.5 and will be removed in" + " 1.7. Use OneVsRestClassifier(LogisticRegression(..)) instead." + " Leave it to its default value to avoid this warning." + ), + FutureWarning, + ) + else: + # Set to old default value. + multi_class = "auto" + multi_class = _check_multi_class(multi_class, solver, len(self.classes_)) if solver == "liblinear": if effective_n_jobs(self.n_jobs) != 1: @@ -1373,7 +1421,7 @@ def predict_proba(self, X): check_is_fitted(self) ovr = self.multi_class in ["ovr", "warn"] or ( - self.multi_class == "auto" + self.multi_class in ["auto", "deprecated"] and ( self.classes_.size <= 2 or self.solver in ("liblinear", "newton-cholesky") @@ -1485,30 +1533,35 @@ class LogisticRegressionCV(LogisticRegression, LinearClassifierMixin, BaseEstima 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 multiclass problems, only 'newton-cg', 'sag', 'saga' and - 'lbfgs' handle multinomial loss; - - 'liblinear' might be slower in :class:`LogisticRegressionCV` - because it does not handle warm-starting. 'liblinear' is - limited to one-versus-rest schemes. - - 'newton-cholesky' is a good choice for `n_samples` >> `n_features`, - especially with one-hot encoded categorical features with rare - categories. Note that it is limited to binary classification and the - one-versus-rest reduction for multiclass classification. Be aware that - the memory usage of this solver has a quadratic dependency on - `n_features` because it explicitly computes the Hessian matrix. + - For small datasets, 'liblinear' is a good choice, whereas 'sag' + and 'saga' are faster for large ones; + - For multiclass problems, only 'newton-cg', 'sag', 'saga' and + 'lbfgs' handle multinomial loss; + - 'liblinear' might be slower in :class:`LogisticRegressionCV` + because it does not handle warm-starting. + - 'liblinear' and 'newton-cholesky' can only handle binary classification + by default. To apply a one-versus-rest scheme for the multiclass setting + one can wrapt it with the `OneVsRestClassifier`. + - 'newton-cholesky' 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. .. warning:: - The choice of the algorithm depends on the penalty chosen. - Supported penalties by solver: - - - 'lbfgs' - ['l2'] - - 'liblinear' - ['l1', 'l2'] - - 'newton-cg' - ['l2'] - - 'newton-cholesky' - ['l2'] - - 'sag' - ['l2'] - - 'saga' - ['elasticnet', 'l1', 'l2'] + The choice of the algorithm depends on the penalty chosen and on + (multinomial) multiclass support: + + ================= ============================== ====================== + solver penalty multinomial multiclass + ================= ============================== ====================== + 'lbfgs' 'l2' yes + 'liblinear' 'l1', 'l2' no + 'newton-cg' 'l2' yes + 'newton-cholesky' 'l2', no + 'sag' 'l2', yes + 'saga' 'elasticnet', 'l1', 'l2' yes + ================= ============================== ====================== .. note:: 'sag' and 'saga' fast convergence is only guaranteed on features @@ -1584,6 +1637,13 @@ class LogisticRegressionCV(LogisticRegression, LinearClassifierMixin, BaseEstima Stochastic Average Gradient descent solver for 'multinomial' case. .. versionchanged:: 0.22 Default changed from 'ovr' to 'auto' in 0.22. + .. 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(LogisticRegressionCV())` if you + still want to use OvR. random_state : int, RandomState instance, default=None Used when `solver='sag'`, 'saga' or 'liblinear' to shuffle the data. @@ -1725,7 +1785,7 @@ def __init__( verbose=0, refit=True, intercept_scaling=1.0, - multi_class="auto", + multi_class="deprecated", random_state=None, l1_ratios=None, ): @@ -1829,7 +1889,40 @@ def fit(self, X, y, sample_weight=None, **params): classes = self.classes_ = label_encoder.classes_ encoded_labels = label_encoder.transform(label_encoder.classes_) - multi_class = _check_multi_class(self.multi_class, solver, len(classes)) + # TODO(1.7) remove multi_class + multi_class = self.multi_class + if self.multi_class == "multinomial" and len(self.classes_) == 2: + warnings.warn( + ( + "'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." + ), + FutureWarning, + ) + elif self.multi_class in ("multinomial", "auto"): + warnings.warn( + ( + "'multi_class' was deprecated in version 1.5 and will be removed in" + " 1.7. From then on, it will always use 'multinomial'." + " Leave it to its default value to avoid this warning." + ), + FutureWarning, + ) + elif self.multi_class == "ovr": + warnings.warn( + ( + "'multi_class' was deprecated in version 1.5 and will be removed in" + " 1.7. Use OneVsRestClassifier(LogisticRegressionCV(..)) instead." + " Leave it to its default value to avoid this warning." + ), + FutureWarning, + ) + else: + # Set to old default value. + multi_class = "auto" + multi_class = _check_multi_class(multi_class, solver, len(classes)) if solver in ["sag", "saga"]: max_squared_sum = row_norms(X, squared=True).max() diff --git a/sklearn/linear_model/_sag.py b/sklearn/linear_model/_sag.py index 2626955ec2a7f..758e361fc1ad9 100644 --- a/sklearn/linear_model/_sag.py +++ b/sklearn/linear_model/_sag.py @@ -220,10 +220,9 @@ def sag_solver( >>> X = np.array([[-1, -1], [-2, -1], [1, 1], [2, 1]]) >>> y = np.array([1, 1, 2, 2]) - >>> clf = linear_model.LogisticRegression( - ... solver='sag', multi_class='multinomial') + >>> clf = linear_model.LogisticRegression(solver='sag') >>> clf.fit(X, y) - LogisticRegression(multi_class='multinomial', solver='sag') + LogisticRegression(solver='sag') References ---------- diff --git a/sklearn/linear_model/tests/test_logistic.py b/sklearn/linear_model/tests/test_logistic.py index deda8a9984048..daa6f5114ebcc 100644 --- a/sklearn/linear_model/tests/test_logistic.py +++ b/sklearn/linear_model/tests/test_logistic.py @@ -35,6 +35,7 @@ cross_val_score, train_test_split, ) +from sklearn.multiclass import OneVsRestClassifier from sklearn.preprocessing import LabelEncoder, StandardScaler, scale from sklearn.svm import l1_min_c from sklearn.utils import compute_class_weight, shuffle @@ -144,14 +145,14 @@ def test_predict_3_classes(csr_container): check_predictions(LogisticRegression(C=10), csr_container(X), Y2) +# TODO(1.7): remove filterwarnings after the deprecation of multi_class +@pytest.mark.filterwarnings("ignore:.*'multi_class' was deprecated.*:FutureWarning") @pytest.mark.parametrize( "clf", [ LogisticRegression(C=len(iris.data), solver="liblinear", multi_class="ovr"), - LogisticRegression(C=len(iris.data), solver="lbfgs", multi_class="multinomial"), - LogisticRegression( - C=len(iris.data), solver="newton-cg", multi_class="multinomial" - ), + LogisticRegression(C=len(iris.data), solver="lbfgs"), + LogisticRegression(C=len(iris.data), solver="newton-cg"), LogisticRegression( C=len(iris.data), solver="sag", tol=1e-2, multi_class="ovr", random_state=42 ), @@ -195,6 +196,8 @@ def test_predict_iris(clf): assert np.mean(pred == target) > 0.95 +# TODO(1.7): remove filterwarnings after the deprecation of multi_class +@pytest.mark.filterwarnings("ignore:.*'multi_class' was deprecated.*:FutureWarning") @pytest.mark.parametrize("LR", [LogisticRegression, LogisticRegressionCV]) def test_check_solver_option(LR): X, y = iris.data, iris.target @@ -245,6 +248,8 @@ def test_elasticnet_l1_ratio_err_helpful(LR): model.fit(np.array([[1, 2], [3, 4]]), np.array([0, 1])) +# TODO(1.7): remove whole test with deprecation of multi_class +@pytest.mark.filterwarnings("ignore:.*'multi_class' was deprecated.*:FutureWarning") @pytest.mark.parametrize("solver", ["lbfgs", "newton-cg", "sag", "saga"]) def test_multinomial_binary(solver): # Test multinomial LR on a binary problem. @@ -268,6 +273,10 @@ def test_multinomial_binary(solver): assert np.mean(pred == target) > 0.9 +# TODO(1.7): remove filterwarnings after the deprecation of multi_class +# Maybe even remove this whole test as correctness of multinomial loss is tested +# elsewhere. +@pytest.mark.filterwarnings("ignore:.*'multi_class' was deprecated.*:FutureWarning") def test_multinomial_binary_probabilities(global_random_seed): # Test multinomial LR gives expected probabilities based on the # decision function, for a binary problem. @@ -373,7 +382,6 @@ def test_consistency_path(): tol=1e-5, solver=solver, max_iter=1000, - multi_class="ovr", random_state=0, ) for i, C in enumerate(Cs): @@ -382,7 +390,6 @@ def test_consistency_path(): fit_intercept=False, tol=1e-5, solver=solver, - multi_class="ovr", random_state=0, max_iter=1000, ) @@ -403,14 +410,12 @@ def test_consistency_path(): solver=solver, intercept_scaling=10000.0, random_state=0, - multi_class="ovr", ) lr = LogisticRegression( C=Cs[0], tol=1e-6, intercept_scaling=10000.0, random_state=0, - multi_class="ovr", solver=solver, ) lr.fit(X, y) @@ -450,7 +455,6 @@ def test_liblinear_dual_random_state(): dual=True, tol=1e-3, solver="liblinear", - multi_class="ovr", ) lr1.fit(X, y) lr2 = LogisticRegression( @@ -458,7 +462,6 @@ def test_liblinear_dual_random_state(): dual=True, tol=1e-3, solver="liblinear", - multi_class="ovr", ) lr2.fit(X, y) lr3 = LogisticRegression( @@ -466,7 +469,6 @@ def test_liblinear_dual_random_state(): dual=True, tol=1e-3, solver="liblinear", - multi_class="ovr", ) lr3.fit(X, y) @@ -487,12 +489,10 @@ def test_logistic_cv(): X_ref -= X_ref.mean() X_ref /= X_ref.std() lr_cv = LogisticRegressionCV( - Cs=[1.0], fit_intercept=False, solver="liblinear", multi_class="ovr", cv=3 + 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", multi_class="ovr" - ) + lr = LogisticRegression(C=1.0, fit_intercept=False, solver="liblinear") lr.fit(X_ref, y) assert_array_almost_equal(lr.coef_, lr_cv.coef_) @@ -530,7 +530,7 @@ def test_logistic_cv_multinomial_score(scoring, multiclass_agg_list): 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, multi_class="multinomial") + 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 @@ -551,7 +551,7 @@ def test_logistic_cv_multinomial_score(scoring, multiclass_agg_list): max_squared_sum=None, sample_weight=None, score_params=None, - **params, + **(params | {"multi_class": "multinomial"}), )[2][0], scorer(lr, X[test], y[test]), ) @@ -571,10 +571,10 @@ def test_multinomial_logistic_regression_string_inputs(): # For numerical labels, let y values be taken from set (-1, 0, 1) y = np.array(y) - 1 # Test for string labels - lr = LogisticRegression(multi_class="multinomial") - lr_cv = LogisticRegressionCV(multi_class="multinomial", Cs=3) - lr_str = LogisticRegression(multi_class="multinomial") - lr_cv_str = LogisticRegressionCV(multi_class="multinomial", Cs=3) + lr = LogisticRegression() + lr_cv = LogisticRegressionCV(Cs=3) + lr_str = LogisticRegression() + lr_cv_str = LogisticRegressionCV(Cs=3) lr.fit(X_ref, y) lr_cv.fit(X_ref, y) @@ -592,9 +592,9 @@ def test_multinomial_logistic_regression_string_inputs(): 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}, multi_class="multinomial" - ).fit(X_ref, y_str) + 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"] @@ -613,6 +613,9 @@ def test_logistic_cv_sparse(csr_container): assert clfs.C_ == clf.C_ +# TODO(1.7): remove filterwarnings after the deprecation of multi_class +# Best remove this whole test. +@pytest.mark.filterwarnings("ignore:.*'multi_class' was deprecated.*:FutureWarning") def test_ovr_multinomial_iris(): # Test that OvR and multinomial are correct using the iris dataset. train, target = iris.data, iris.target @@ -655,7 +658,6 @@ def test_ovr_multinomial_iris(): max_iter = 500 if solver in ["sag", "saga"] else 30 clf_multi = LogisticRegressionCV( solver=solver, - multi_class="multinomial", max_iter=max_iter, random_state=42, tol=1e-3 if solver in ["sag", "saga"] else 1e-2, @@ -684,7 +686,7 @@ 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, multi_class="ovr") + params = dict(fit_intercept=False, random_state=42) regressors = { solver: LogisticRegression(solver=solver, **params).fit(X, y) @@ -702,18 +704,18 @@ def test_logistic_regression_solvers_multiclass(): X, y = make_classification( n_samples=20, n_features=20, n_informative=10, n_classes=3, random_state=0 ) - tol = 1e-7 - params = dict(fit_intercept=False, tol=tol, random_state=42, multi_class="ovr") + tol = 1e-8 + params = dict(fit_intercept=False, tol=tol, random_state=42) # Override max iteration count for specific solvers to allow for # proper convergence. - solver_max_iter = {"sag": 1000, "saga": 10000} + solver_max_iter = {"sag": 10_000, "saga": 10_000} regressors = { solver: LogisticRegression( solver=solver, max_iter=solver_max_iter.get(solver, 100), **params ).fit(X, y) - for solver in SOLVERS + for solver in set(SOLVERS) - set(["liblinear", "newton-cholesky"]) } for solver_1, solver_2 in itertools.combinations(regressors, r=2): @@ -745,7 +747,6 @@ def test_logistic_regressioncv_class_weights(weight, class_weight, global_random params = dict( Cs=1, fit_intercept=False, - multi_class="ovr", class_weight=class_weight, tol=1e-8, ) @@ -761,7 +762,7 @@ def test_logistic_regressioncv_class_weights(weight, class_weight, global_random with ignore_warnings(category=ConvergenceWarning): clf_lbfgs.fit(X, y) - for solver in set(SOLVERS) - set(["lbfgs"]): + for solver in set(SOLVERS) - set(["lbfgs", "liblinear", "newton-cholesky"]): clf = LogisticRegressionCV(solver=solver, **params) if solver in ("sag", "saga"): clf.set_params( @@ -781,7 +782,7 @@ def test_logistic_regression_sample_weights(): sample_weight = y + 1 for LR in [LogisticRegression, LogisticRegressionCV]: - kw = {"random_state": 42, "fit_intercept": False, "multi_class": "ovr"} + kw = {"random_state": 42, "fit_intercept": False} if LR is LogisticRegressionCV: kw.update({"Cs": 3, "cv": 3}) @@ -798,7 +799,7 @@ def test_logistic_regression_sample_weights(): # newton-cg, newton-cholesky and 'sag' solvers clf_sw_lbfgs = LR(**kw, tol=1e-5) clf_sw_lbfgs.fit(X, y, sample_weight=sample_weight) - for solver in set(SOLVERS) - set(("lbfgs", "saga")): + for solver in set(SOLVERS) - set(["lbfgs"]): clf_sw = LR(solver=solver, tol=1e-10 if solver == "sag" else 1e-5, **kw) # ignore convergence warning due to small dataset with sag with ignore_warnings(): @@ -824,7 +825,6 @@ def test_logistic_regression_sample_weights(): penalty="l1", tol=1e-5, random_state=42, - multi_class="ovr", ) clf_cw.fit(X, y) clf_sw = LogisticRegression( @@ -833,7 +833,6 @@ def test_logistic_regression_sample_weights(): penalty="l1", tol=1e-5, random_state=42, - multi_class="ovr", ) clf_sw.fit(X, y, sample_weight) assert_array_almost_equal(clf_cw.coef_, clf_sw.coef_, decimal=4) @@ -845,7 +844,6 @@ def test_logistic_regression_sample_weights(): penalty="l2", dual=True, random_state=42, - multi_class="ovr", ) clf_cw.fit(X, y) clf_sw = LogisticRegression( @@ -854,7 +852,6 @@ def test_logistic_regression_sample_weights(): penalty="l2", dual=True, random_state=42, - multi_class="ovr", ) clf_sw.fit(X, y, sample_weight) assert_array_almost_equal(clf_cw.coef_, clf_sw.coef_, decimal=4) @@ -868,38 +865,40 @@ def _compute_class_weight_dictionary(y): return class_weight_dict -def test_logistic_regression_class_weights(): +@pytest.mark.parametrize("csr_container", [lambda x: x] + CSR_CONTAINERS) +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:, :] + X = csr_container(X) y = iris.target[45:] - solvers = ("lbfgs", "newton-cg") class_weight_dict = _compute_class_weight_dictionary(y) - for solver in solvers: - clf1 = LogisticRegression( - solver=solver, multi_class="multinomial", class_weight="balanced" - ) - clf2 = LogisticRegression( - solver=solver, multi_class="multinomial", class_weight=class_weight_dict - ) + 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_array_almost_equal(clf1.coef_, clf2.coef_, decimal=4) + 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] class_weight_dict = _compute_class_weight_dictionary(y) - for solver in set(SOLVERS) - set(("sag", "saga")): - clf1 = LogisticRegression( - solver=solver, multi_class="ovr", class_weight="balanced" - ) - clf2 = LogisticRegression( - solver=solver, multi_class="ovr", class_weight=class_weight_dict - ) + for solver in SOLVERS: + 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_array_almost_equal(clf1.coef_, clf2.coef_, decimal=6) @@ -922,10 +921,8 @@ def test_logistic_regression_multinomial(): # 'lbfgs' is used as a referenced solver = "lbfgs" - ref_i = LogisticRegression(solver=solver, multi_class="multinomial", tol=1e-6) - ref_w = LogisticRegression( - solver=solver, multi_class="multinomial", fit_intercept=False, tol=1e-6 - ) + 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) @@ -933,14 +930,12 @@ def test_logistic_regression_multinomial(): for solver in ["sag", "saga", "newton-cg"]: clf_i = LogisticRegression( solver=solver, - multi_class="multinomial", random_state=42, max_iter=2000, tol=1e-7, ) clf_w = LogisticRegression( solver=solver, - multi_class="multinomial", random_state=42, max_iter=2000, tol=1e-7, @@ -961,7 +956,7 @@ def test_logistic_regression_multinomial(): # folds, it need not be exactly the same. for solver in ["lbfgs", "newton-cg", "sag", "saga"]: clf_path = LogisticRegressionCV( - solver=solver, max_iter=2000, tol=1e-6, multi_class="multinomial", Cs=[1.0] + 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) @@ -975,7 +970,7 @@ def test_liblinear_decision_function_zero(): # See Issue: https://github.com/scikit-learn/scikit-learn/issues/3600 # and the PR https://github.com/scikit-learn/scikit-learn/pull/3623 X, y = make_classification(n_samples=5, n_features=5, random_state=0) - clf = LogisticRegression(fit_intercept=False, solver="liblinear", multi_class="ovr") + clf = LogisticRegression(fit_intercept=False, solver="liblinear") clf.fit(X, y) # Dummy data such that the decision function becomes zero. @@ -988,7 +983,7 @@ 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", multi_class="ovr") + clf = LogisticRegressionCV(solver="liblinear") clf.fit(csr_container(X), y) @@ -1024,7 +1019,6 @@ def test_logreg_l1(): C=1.0, solver="liblinear", fit_intercept=False, - multi_class="ovr", tol=1e-10, ) lr_liblinear.fit(X, y) @@ -1034,7 +1028,6 @@ def test_logreg_l1(): C=1.0, solver="saga", fit_intercept=False, - multi_class="ovr", max_iter=1000, tol=1e-10, ) @@ -1066,7 +1059,6 @@ def test_logreg_l1_sparse_data(csr_container): C=1.0, solver="liblinear", fit_intercept=False, - multi_class="ovr", tol=1e-10, ) lr_liblinear.fit(X, y) @@ -1076,7 +1068,6 @@ def test_logreg_l1_sparse_data(csr_container): C=1.0, solver="saga", fit_intercept=False, - multi_class="ovr", max_iter=1000, tol=1e-10, ) @@ -1093,7 +1084,6 @@ def test_logreg_l1_sparse_data(csr_container): C=1.0, solver="saga", fit_intercept=False, - multi_class="ovr", max_iter=1000, tol=1e-10, ) @@ -1134,10 +1124,10 @@ def test_logreg_predict_proba_multinomial(): # Predicted probabilities using the true-entropy loss should give a # smaller loss than those using the ovr method. - clf_multi = LogisticRegression(multi_class="multinomial", solver="lbfgs") + clf_multi = LogisticRegression(solver="lbfgs") clf_multi.fit(X, y) clf_multi_loss = log_loss(y, clf_multi.predict_proba(X)) - clf_ovr = LogisticRegression(multi_class="ovr", solver="lbfgs") + 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 @@ -1191,6 +1181,8 @@ def test_max_iter(max_iter, multi_class, solver, message): assert lr.n_iter_[0] == max_iter +# TODO(1.7): remove filterwarnings after the deprecation of multi_class +@pytest.mark.filterwarnings("ignore:.*'multi_class' was deprecated.*:FutureWarning") @pytest.mark.parametrize("solver", SOLVERS) def test_n_iter(solver): # Test that self.n_iter_ has the correct format. @@ -1241,23 +1233,19 @@ def test_n_iter(solver): assert clf_cv.n_iter_.shape == (1, n_cv_fold, n_Cs) -@pytest.mark.parametrize("solver", sorted(set(SOLVERS) - set(["liblinear"]))) +@pytest.mark.parametrize( + "solver", sorted(set(SOLVERS) - set(["liblinear", "newton-cholesky"])) +) @pytest.mark.parametrize("warm_start", (True, False)) @pytest.mark.parametrize("fit_intercept", (True, False)) -@pytest.mark.parametrize("multi_class", ["ovr", "multinomial"]) -def test_warm_start(solver, warm_start, fit_intercept, multi_class): +def test_warm_start(solver, warm_start, fit_intercept): # A 1-iteration second fit on same data should give almost same result # with warm starting, and quite different result without warm starting. # Warm starting does not work with liblinear solver. X, y = iris.data, iris.target - if solver == "newton-cholesky" and multi_class == "multinomial": - # solver does only support OvR - return - clf = LogisticRegression( tol=1e-4, - multi_class=multi_class, warm_start=warm_start, solver=solver, random_state=42, @@ -1271,9 +1259,8 @@ def test_warm_start(solver, warm_start, fit_intercept, multi_class): clf.fit(X, y) cum_diff = np.sum(np.abs(coef_1 - clf.coef_)) msg = ( - "Warm starting issue with %s solver in %s mode " - "with fit_intercept=%s and warm_start=%s" - % (solver, multi_class, str(fit_intercept), str(warm_start)) + f"Warm starting issue with solver {solver}" + f"with {fit_intercept=} and {warm_start=}" ) if warm_start: assert 2.0 > cum_diff, msg @@ -1304,7 +1291,6 @@ def test_saga_vs_liblinear(csr_container): saga = LogisticRegression( C=1.0 / (n_samples * alpha), solver="saga", - multi_class="ovr", max_iter=200, fit_intercept=False, penalty=penalty, @@ -1315,7 +1301,6 @@ def test_saga_vs_liblinear(csr_container): liblinear = LogisticRegression( C=1.0 / (n_samples * alpha), solver="liblinear", - multi_class="ovr", max_iter=200, fit_intercept=False, penalty=penalty, @@ -1329,6 +1314,8 @@ def test_saga_vs_liblinear(csr_container): assert_array_almost_equal(saga.coef_, liblinear.coef_, 3) +# TODO(1.7): remove filterwarnings after the deprecation of multi_class +@pytest.mark.filterwarnings("ignore:.*'multi_class' was deprecated.*:FutureWarning") @pytest.mark.parametrize("multi_class", ["ovr", "multinomial"]) @pytest.mark.parametrize( "solver", ["liblinear", "newton-cg", "newton-cholesky", "saga"] @@ -1414,12 +1401,8 @@ def test_warm_start_converge_LR(): rng = np.random.RandomState(0) X = np.concatenate((rng.randn(100, 2) + [1, 1], rng.randn(100, 2))) y = np.array([1] * 100 + [-1] * 100) - lr_no_ws = LogisticRegression( - multi_class="multinomial", solver="sag", warm_start=False, random_state=0 - ) - lr_ws = LogisticRegression( - multi_class="multinomial", solver="sag", warm_start=True, random_state=0 - ) + 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)) for i in range(5): @@ -1552,19 +1535,14 @@ def enet_objective(lr): assert enet_objective(lr_enet) < enet_objective(lr_l2) -@pytest.mark.parametrize("multi_class", ("ovr", "multinomial")) -def test_LogisticRegressionCV_GridSearchCV_elastic_net(multi_class): +@pytest.mark.parametrize("n_classes", (2, 3)) +def test_LogisticRegressionCV_GridSearchCV_elastic_net(n_classes): # make sure LogisticRegressionCV gives same best params (l1 and C) as # GridSearchCV when penalty is elasticnet - if multi_class == "ovr": - # This is actually binary classification, ovr multiclass is treated in - # test_LogisticRegressionCV_GridSearchCV_elastic_net_ovr - X, y = make_classification(random_state=0) - else: - X, y = make_classification( - n_samples=100, n_classes=3, n_informative=3, random_state=0 - ) + X, y = make_classification( + n_samples=100, n_classes=n_classes, n_informative=3, random_state=0 + ) cv = StratifiedKFold(5) @@ -1578,7 +1556,6 @@ def test_LogisticRegressionCV_GridSearchCV_elastic_net(multi_class): cv=cv, l1_ratios=l1_ratios, random_state=0, - multi_class=multi_class, tol=1e-2, ) lrcv.fit(X, y) @@ -1588,7 +1565,6 @@ def test_LogisticRegressionCV_GridSearchCV_elastic_net(multi_class): penalty="elasticnet", solver="saga", random_state=0, - multi_class=multi_class, tol=1e-2, ) gs = GridSearchCV(lr, param_grid, cv=cv) @@ -1598,6 +1574,9 @@ def test_LogisticRegressionCV_GridSearchCV_elastic_net(multi_class): assert gs.best_params_["C"] == lrcv.C_[0] +# TODO(1.7): remove filterwarnings after the deprecation of multi_class +# Maybe remove whole test after removal of the deprecated multi_class. +@pytest.mark.filterwarnings("ignore:.*'multi_class' was deprecated.*:FutureWarning") def test_LogisticRegressionCV_GridSearchCV_elastic_net_ovr(): # make sure LogisticRegressionCV gives same best params (l1 and C) as # GridSearchCV when penalty is elasticnet and multiclass is ovr. We can't @@ -1643,6 +1622,8 @@ def test_LogisticRegressionCV_GridSearchCV_elastic_net_ovr(): assert (lrcv.predict(X_test) == gs.predict(X_test)).mean() >= 0.8 +# TODO(1.7): remove filterwarnings after the deprecation of multi_class +@pytest.mark.filterwarnings("ignore:.*'multi_class' was deprecated.*:FutureWarning") @pytest.mark.parametrize("penalty", ("l2", "elasticnet")) @pytest.mark.parametrize("multi_class", ("ovr", "multinomial", "auto")) def test_LogisticRegressionCV_no_refit(penalty, multi_class): @@ -1680,6 +1661,10 @@ def test_LogisticRegressionCV_no_refit(penalty, multi_class): assert lrcv.coef_.shape == (n_classes, n_features) +# TODO(1.7): remove filterwarnings after the deprecation of multi_class +# Remove multi_class an change first element of the expected n_iter_.shape from +# n_classes to 1 (according to the docstring). +@pytest.mark.filterwarnings("ignore:.*'multi_class' was deprecated.*:FutureWarning") 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) @@ -1806,6 +1791,8 @@ def test_logistic_regression_path_coefs_multinomial(): assert_array_almost_equal(coefs[1], coefs[2], decimal=1) +# TODO(1.7): remove filterwarnings after the deprecation of multi_class +@pytest.mark.filterwarnings("ignore:.*'multi_class' was deprecated.*:FutureWarning") @pytest.mark.parametrize( "est", [ @@ -2005,7 +1992,6 @@ def test_multinomial_identifiability_on_iris(fit_intercept): clf = LogisticRegression( C=len(iris.data), solver="lbfgs", - multi_class="multinomial", fit_intercept=fit_intercept, ) # Scaling X to ease convergence. @@ -2018,6 +2004,8 @@ def test_multinomial_identifiability_on_iris(fit_intercept): clf.intercept_.sum(axis=0) == pytest.approx(0, abs=1e-15) +# TODO(1.7): remove filterwarnings after the deprecation of multi_class +@pytest.mark.filterwarnings("ignore:.*'multi_class' was deprecated.*:FutureWarning") @pytest.mark.parametrize("multi_class", ["ovr", "multinomial", "auto"]) @pytest.mark.parametrize("class_weight", [{0: 1.0, 1: 10.0, 2: 1.0}, "balanced"]) def test_sample_weight_not_modified(multi_class, class_weight): @@ -2192,3 +2180,28 @@ def test_passing_params_without_enabling_metadata_routing(): with pytest.raises(ValueError, match=msg): lr_cv.score(X, y, **params) + + +# TODO(1.7): remove +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" + with pytest.warns(FutureWarning, match=msg): + lr.fit(X, y) + + lrCV = LogisticRegressionCV(multi_class="ovr") + with pytest.warns(FutureWarning, match=msg): + 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" + with pytest.warns(FutureWarning, match=msg): + lr.fit(X, y) + + lrCV = LogisticRegressionCV(multi_class="multinomial") + with pytest.warns(FutureWarning, match=msg): + lrCV.fit(X, y) diff --git a/sklearn/linear_model/tests/test_sag.py b/sklearn/linear_model/tests/test_sag.py index 96f8a79726833..a51d1406559ff 100644 --- a/sklearn/linear_model/tests/test_sag.py +++ b/sklearn/linear_model/tests/test_sag.py @@ -18,6 +18,7 @@ from sklearn.linear_model._linear_loss import LinearModelLoss from sklearn.linear_model._sag import get_auto_step_size from sklearn.linear_model._sag_fast import _multinomial_grad_loss_all_samples +from sklearn.multiclass import OneVsRestClassifier from sklearn.preprocessing import LabelBinarizer, LabelEncoder from sklearn.utils import check_random_state, compute_class_weight from sklearn.utils._testing import ( @@ -272,7 +273,6 @@ def test_classifier_matching(): C=1.0 / alpha / n_samples, max_iter=n_iter, random_state=10, - multi_class="ovr", ) clf.fit(X, y) @@ -371,7 +371,6 @@ def test_sag_pobj_matches_logistic_regression(csr_container): C=1.0 / alpha / n_samples, max_iter=max_iter, random_state=10, - multi_class="ovr", ) clf2 = clone(clf1) clf3 = LogisticRegression( @@ -380,7 +379,6 @@ def test_sag_pobj_matches_logistic_regression(csr_container): C=1.0 / alpha / n_samples, max_iter=max_iter, random_state=10, - multi_class="ovr", ) clf1.fit(X, y) @@ -619,7 +617,6 @@ def test_sag_classifier_computed_correctly(csr_container): tol=tol, random_state=77, fit_intercept=fit_intercept, - multi_class="ovr", ) clf2 = clone(clf1) @@ -659,21 +656,22 @@ def test_sag_multiclass_computed_correctly(csr_container): """tests if the multiclass classifier is computed correctly""" alpha = 0.1 n_samples = 20 - tol = 0.00001 - max_iter = 40 + tol = 1e-5 + max_iter = 70 fit_intercept = True X, y = make_blobs(n_samples=n_samples, centers=3, random_state=0, cluster_std=0.1) step_size = get_step_size(X, alpha, fit_intercept, classification=True) classes = np.unique(y) - clf1 = LogisticRegression( - solver="sag", - C=1.0 / alpha / n_samples, - max_iter=max_iter, - tol=tol, - random_state=77, - fit_intercept=fit_intercept, - multi_class="ovr", + clf1 = OneVsRestClassifier( + LogisticRegression( + solver="sag", + C=1.0 / alpha / n_samples, + max_iter=max_iter, + tol=tol, + random_state=77, + fit_intercept=fit_intercept, + ) ) clf2 = clone(clf1) @@ -719,11 +717,12 @@ def test_sag_multiclass_computed_correctly(csr_container): intercept2 = np.array(intercept2) for i, cl in enumerate(classes): - assert_array_almost_equal(clf1.coef_[i].ravel(), coef1[i].ravel(), decimal=2) - assert_almost_equal(clf1.intercept_[i], intercept1[i], decimal=1) + assert_allclose(clf1.estimators_[i].coef_.ravel(), coef1[i], rtol=1e-2) + assert_allclose(clf1.estimators_[i].intercept_, intercept1[i], rtol=1e-1) - assert_array_almost_equal(clf2.coef_[i].ravel(), coef2[i].ravel(), decimal=2) - assert_almost_equal(clf2.intercept_[i], intercept2[i], decimal=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) @pytest.mark.parametrize("csr_container", CSR_CONTAINERS) @@ -780,7 +779,6 @@ def test_binary_classifier_class_weight(csr_container): tol=tol, random_state=77, fit_intercept=fit_intercept, - multi_class="ovr", class_weight=class_weight, ) clf2 = clone(clf1) @@ -820,83 +818,6 @@ def test_binary_classifier_class_weight(csr_container): assert_almost_equal(clf2.intercept_, spintercept2, decimal=1) -@pytest.mark.filterwarnings("ignore:The max_iter was reached") -@pytest.mark.parametrize("csr_container", CSR_CONTAINERS) -def test_multiclass_classifier_class_weight(csr_container): - """tests multiclass with classweights for each class""" - alpha = 0.1 - n_samples = 20 - tol = 0.00001 - max_iter = 50 - class_weight = {0: 0.45, 1: 0.55, 2: 0.75} - fit_intercept = True - X, y = make_blobs(n_samples=n_samples, centers=3, random_state=0, cluster_std=0.1) - step_size = get_step_size(X, alpha, fit_intercept, classification=True) - classes = np.unique(y) - - clf1 = LogisticRegression( - solver="sag", - C=1.0 / alpha / n_samples, - max_iter=max_iter, - tol=tol, - random_state=77, - fit_intercept=fit_intercept, - multi_class="ovr", - class_weight=class_weight, - ) - clf2 = clone(clf1) - clf1.fit(X, y) - clf2.fit(csr_container(X), y) - - le = LabelEncoder() - class_weight_ = compute_class_weight(class_weight, classes=np.unique(y), y=y) - sample_weight = class_weight_[le.fit_transform(y)] - - 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, - step_size, - alpha, - n_iter=max_iter, - dloss=log_dloss, - sample_weight=sample_weight, - ) - spweights2, spintercept2 = sag_sparse( - X, - y_encoded, - step_size, - alpha, - n_iter=max_iter, - dloss=log_dloss, - sample_weight=sample_weight, - 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) - - for i, cl in enumerate(classes): - assert_array_almost_equal(clf1.coef_[i].ravel(), coef1[i].ravel(), decimal=2) - assert_almost_equal(clf1.intercept_[i], intercept1[i], decimal=1) - - assert_array_almost_equal(clf2.coef_[i].ravel(), coef2[i].ravel(), decimal=2) - assert_almost_equal(clf2.intercept_[i], intercept2[i], decimal=1) - - def test_classifier_single_class(): """tests if ValueError is thrown with only one class""" X = [[1, 2], [3, 4]] diff --git a/sklearn/metrics/tests/test_score_objects.py b/sklearn/metrics/tests/test_score_objects.py index e45e0d30767f7..9960c32fc3938 100644 --- a/sklearn/metrics/tests/test_score_objects.py +++ b/sklearn/metrics/tests/test_score_objects.py @@ -926,7 +926,7 @@ def test_multiclass_roc_proba_scorer(scorer_name, metric): X, y = make_classification( n_classes=3, n_informative=3, n_samples=20, random_state=0 ) - lr = LogisticRegression(multi_class="multinomial").fit(X, y) + lr = LogisticRegression().fit(X, y) y_proba = lr.predict_proba(X) expected_score = metric(y, y_proba) @@ -943,7 +943,7 @@ def test_multiclass_roc_proba_scorer_label(): X, y = make_classification( n_classes=3, n_informative=3, n_samples=20, random_state=0 ) - lr = LogisticRegression(multi_class="multinomial").fit(X, y) + lr = LogisticRegression().fit(X, y) y_proba = lr.predict_proba(X) y_binary = y == 0 diff --git a/sklearn/multioutput.py b/sklearn/multioutput.py index d3814974c63e4..d1f45f91d2db6 100644 --- a/sklearn/multioutput.py +++ b/sklearn/multioutput.py @@ -1191,7 +1191,7 @@ class RegressorChain(MetaEstimatorMixin, RegressorMixin, _BaseChain): -------- >>> from sklearn.multioutput import RegressorChain >>> from sklearn.linear_model import LogisticRegression - >>> logreg = LogisticRegression(solver='lbfgs',multi_class='multinomial') + >>> logreg = LogisticRegression(solver='lbfgs') >>> X, Y = [[1, 0], [0, 1], [1, 1]], [[0, 2], [1, 1], [2, 0]] >>> chain = RegressorChain(base_estimator=logreg, order=[0, 1]).fit(X, Y) >>> chain.predict(X) From c5aa12b68c59f01eba50ef64329081f8163342ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20du=20Boisberranger?= Date: Fri, 3 May 2024 14:48:50 +0200 Subject: [PATCH 084/344] MAINT Follow-up inverse_transform standardization and deprecation clean-up (#28932) --- sklearn/decomposition/_nmf.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sklearn/decomposition/_nmf.py b/sklearn/decomposition/_nmf.py index 30725c33f4df3..0970c93deb1ec 100644 --- a/sklearn/decomposition/_nmf.py +++ b/sklearn/decomposition/_nmf.py @@ -1140,9 +1140,9 @@ class _BaseNMF(ClassNamePrefixFeaturesOutMixin, TransformerMixin, BaseEstimator, """Base class for NMF and MiniBatchNMF.""" # This prevents ``set_split_inverse_transform`` to be generated for the - # non-standard ``W`` arg on ``inverse_transform``. - # TODO: remove when W is removed in v1.5 for inverse_transform - __metadata_request__inverse_transform = {"W": metadata_routing.UNUSED} + # non-standard ``Xt`` arg on ``inverse_transform``. + # TODO(1.7): remove when Xt is removed in v1.7 for inverse_transform + __metadata_request__inverse_transform = {"Xt": metadata_routing.UNUSED} _parameter_constraints: dict = { "n_components": [ From 7bc1dbb67cf2a7839b5a7156c22b3294ba18614b Mon Sep 17 00:00:00 2001 From: Xiao Yuan Date: Fri, 3 May 2024 23:34:02 +0800 Subject: [PATCH 085/344] FIX HistGradientBoosting raising ValueError with monotonic_cst and categorical_feature (#28925) Co-authored-by: Olivier Grisel --- doc/whats_new/v1.5.rst | 5 +++ .../gradient_boosting.py | 13 ++++++- .../tests/test_monotonic_contraints.py | 39 ++++++++++++------- 3 files changed, 42 insertions(+), 15 deletions(-) diff --git a/doc/whats_new/v1.5.rst b/doc/whats_new/v1.5.rst index c813c7b3c06fd..16b147be9135f 100644 --- a/doc/whats_new/v1.5.rst +++ b/doc/whats_new/v1.5.rst @@ -285,6 +285,11 @@ Changelog pre-sorting the data before finding the thresholds for binning. :pr:`28102` by :user:`Christian Lorentzen `. +- |Fix| Fixes a bug in :class:`ensemble.HistGradientBoostingClassifier` and + :class:`ensemble.HistGradientBoostingRegressor` when `monotonic_cst` is specified + for non-categorical features. + :pr:`28925` by :user:`Xiao Yuan `. + :mod:`sklearn.feature_extraction` ................................. diff --git a/sklearn/ensemble/_hist_gradient_boosting/gradient_boosting.py b/sklearn/ensemble/_hist_gradient_boosting/gradient_boosting.py index d3929480552f9..78f8456e969de 100644 --- a/sklearn/ensemble/_hist_gradient_boosting/gradient_boosting.py +++ b/sklearn/ensemble/_hist_gradient_boosting/gradient_boosting.py @@ -583,6 +583,17 @@ def fit(self, X, y, sample_weight=None): 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 @@ -895,7 +906,7 @@ def fit(self, X, y, sample_weight=None): 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, + monotonic_cst=monotonic_cst_remapped, interaction_cst=interaction_cst, max_leaf_nodes=self.max_leaf_nodes, max_depth=self.max_depth, diff --git a/sklearn/ensemble/_hist_gradient_boosting/tests/test_monotonic_contraints.py b/sklearn/ensemble/_hist_gradient_boosting/tests/test_monotonic_contraints.py index 7782b5b32eb68..56b6068d794e8 100644 --- a/sklearn/ensemble/_hist_gradient_boosting/tests/test_monotonic_contraints.py +++ b/sklearn/ensemble/_hist_gradient_boosting/tests/test_monotonic_contraints.py @@ -206,9 +206,9 @@ def test_nodes_values(monotonic_cst, seed): @pytest.mark.parametrize("use_feature_names", (True, False)) def test_predictions(global_random_seed, use_feature_names): - # Train a model with a POS constraint on the first feature and a NEG - # constraint on the second feature, and make sure the constraints are - # respected by checking the predictions. + # 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 @@ -216,9 +216,16 @@ def test_predictions(global_random_seed, use_feature_names): n_samples = 1000 f_0 = rng.rand(n_samples) # positive correlation with y - f_1 = rng.rand(n_samples) # negative correslation with y - X = np.c_[f_0, f_1] - columns_name = ["f_0", "f_1"] + 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) @@ -227,10 +234,14 @@ def test_predictions(global_random_seed, use_feature_names): if use_feature_names: monotonic_cst = {"f_0": +1, "f_1": -1} + categorical_features = ["f_a", "f_b", "f_c"] else: - monotonic_cst = [+1, -1] + monotonic_cst = [0, +1, 0, -1, 0] + categorical_features = [0, 2, 4] - gbdt = HistGradientBoostingRegressor(monotonic_cst=monotonic_cst) + gbdt = HistGradientBoostingRegressor( + monotonic_cst=monotonic_cst, categorical_features=categorical_features + ) gbdt.fit(X, y) linspace = np.linspace(0, 1, 100) @@ -247,26 +258,26 @@ def test_predictions(global_random_seed, use_feature_names): # The constraint does not guanrantee that # x0 < x0' => f(x0, x1) < f(x0', x1') - # First feature (POS) + # First non-categorical feature (POS) # assert pred is all increasing when f_0 is all increasing - X = np.c_[linspace, constant] + X = np.c_[constant, linspace, constant, constant, constant] X = _convert_container(X, constructor_name, columns_name=columns_name) pred = gbdt.predict(X) assert is_increasing(pred) # assert pred actually follows the variations of f_0 - X = np.c_[sin, constant] + X = np.c_[constant, sin, constant, constant, constant] X = _convert_container(X, constructor_name, columns_name=columns_name) pred = gbdt.predict(X) assert np.all((np.diff(pred) >= 0) == (np.diff(sin) >= 0)) - # Second feature (NEG) + # Second non-categorical feature (NEG) # assert pred is all decreasing when f_1 is all increasing - X = np.c_[constant, linspace] + X = np.c_[constant, constant, constant, linspace, constant] X = _convert_container(X, constructor_name, columns_name=columns_name) pred = gbdt.predict(X) assert is_decreasing(pred) # assert pred actually follows the inverse variations of f_1 - X = np.c_[constant, sin] + X = np.c_[constant, constant, constant, sin, constant] X = _convert_container(X, constructor_name, columns_name=columns_name) pred = gbdt.predict(X) assert ((np.diff(pred) <= 0) == (np.diff(sin) >= 0)).all() From 1e49c34c7ef5786c39700c2437cb31566dbf7fb0 Mon Sep 17 00:00:00 2001 From: Guillaume Lemaitre Date: Fri, 3 May 2024 18:15:25 +0200 Subject: [PATCH 086/344] FEA add TunedThresholdClassifier meta-estimator to post-tune the cut-off threshold (#26120) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jérémie du Boisberranger <34657725+jeremiedbb@users.noreply.github.com> Co-authored-by: Christian Lorentzen Co-authored-by: Arturo Amor <86408019+ArturoAmorQ@users.noreply.github.com> Co-authored-by: Olivier Grisel Co-authored-by: Omar Salman Co-authored-by: Adrin Jalali Co-authored-by: Omar Salman Co-authored-by: Soledad Galli Co-authored-by: Andreas Mueller Co-authored-by: Thomas J. Fan Co-authored-by: Joel Nothman Co-authored-by: Jérémie du Boisberranger --- doc/model_selection.rst | 1 + doc/modules/classes.rst | 11 + doc/modules/classification_threshold.rst | 156 +++ doc/whats_new/v1.5.rst | 7 + .../plot_cost_sensitive_learning.py | 702 ++++++++++++ .../plot_tuned_decision_threshold.py | 184 +++ sklearn/metrics/_scorer.py | 19 + sklearn/model_selection/__init__.py | 6 + .../_classification_threshold.py | 1000 +++++++++++++++++ .../tests/test_classification_threshold.py | 684 +++++++++++ sklearn/utils/_mocking.py | 26 +- sklearn/utils/_response.py | 17 +- sklearn/utils/tests/test_mocking.py | 13 +- sklearn/utils/tests/test_response.py | 60 +- 14 files changed, 2850 insertions(+), 36 deletions(-) create mode 100644 doc/modules/classification_threshold.rst create mode 100644 examples/model_selection/plot_cost_sensitive_learning.py create mode 100644 examples/model_selection/plot_tuned_decision_threshold.py create mode 100644 sklearn/model_selection/_classification_threshold.py create mode 100644 sklearn/model_selection/tests/test_classification_threshold.py diff --git a/doc/model_selection.rst b/doc/model_selection.rst index 25cd2b655ccc5..522544aefc820 100644 --- a/doc/model_selection.rst +++ b/doc/model_selection.rst @@ -14,5 +14,6 @@ Model selection and evaluation modules/cross_validation modules/grid_search + modules/classification_threshold modules/model_evaluation modules/learning_curve diff --git a/doc/modules/classes.rst b/doc/modules/classes.rst index 55336389f93d5..804546eababef 100644 --- a/doc/modules/classes.rst +++ b/doc/modules/classes.rst @@ -1248,6 +1248,17 @@ Hyper-parameter optimizers model_selection.RandomizedSearchCV model_selection.HalvingRandomSearchCV +Post-fit model tuning +--------------------- + +.. currentmodule:: sklearn + +.. autosummary:: + :toctree: generated/ + :template: class.rst + + model_selection.FixedThresholdClassifier + model_selection.TunedThresholdClassifierCV Model validation ---------------- diff --git a/doc/modules/classification_threshold.rst b/doc/modules/classification_threshold.rst new file mode 100644 index 0000000000000..712a094a43246 --- /dev/null +++ b/doc/modules/classification_threshold.rst @@ -0,0 +1,156 @@ +.. currentmodule:: sklearn.model_selection + +.. _TunedThresholdClassifierCV: + +================================================== +Tuning the decision threshold for class prediction +================================================== + +Classification is best divided into two parts: + +* the statistical problem of learning a model to predict, ideally, class probabilities; +* the decision problem to take concrete action based on those probability predictions. + +Let's take a straightforward example related to weather forecasting: the first point is +related to answering "what is the chance that it will rain tomorrow?" while the second +point is related to answering "should I take an umbrella tomorrow?". + +When it comes to the scikit-learn API, the first point is addressed providing scores +using :term:`predict_proba` or :term:`decision_function`. The former returns conditional +probability estimates :math:`P(y|X)` for each class, while the latter returns a decision +score for each class. + +The decision corresponding to the labels are obtained with :term:`predict`. In binary +classification, a decision rule or action is then defined by thresholding the scores, +leading to the prediction of a single class label for each sample. For binary +classification in scikit-learn, class labels predictions are obtained by hard-coded +cut-off rules: a positive class is predicted when the conditional probability +:math:`P(y|X)` is greater than 0.5 (obtained with :term:`predict_proba`) or if the +decision score is greater than 0 (obtained with :term:`decision_function`). + +Here, we show an example that illustrates the relation between conditional +probability estimates :math:`P(y|X)` and class labels:: + + >>> from sklearn.datasets import make_classification + >>> from sklearn.tree import DecisionTreeClassifier + >>> X, y = make_classification(random_state=0) + >>> classifier = DecisionTreeClassifier(max_depth=2, random_state=0).fit(X, y) + >>> classifier.predict_proba(X[:4]) + array([[0.94 , 0.06 ], + [0.94 , 0.06 ], + [0.0416..., 0.9583...], + [0.0416..., 0.9583...]]) + >>> classifier.predict(X[:4]) + array([0, 0, 1, 1]) + +While these hard-coded rules might at first seem reasonable as default behavior, they +are most certainly not ideal for most use cases. Let's illustrate with an example. + +Consider a scenario where a predictive model is being deployed to assist +physicians in detecting tumors. In this setting, physicians will most likely be +interested in identifying all patients with cancer and not missing anyone with cancer so +that they can provide them with the right treatment. In other words, physicians +prioritize achieving a high recall rate. This emphasis on recall comes, of course, with +the trade-off of potentially more false-positive predictions, reducing the precision of +the model. That is a risk physicians are willing to take because the cost of a missed +cancer is much higher than the cost of further diagnostic tests. Consequently, when it +comes to deciding whether to classify a patient as having cancer or not, it may be more +beneficial to classify them as positive for cancer when the conditional probability +estimate is much lower than 0.5. + +Post-tuning the decision threshold +================================== + +One solution to address the problem stated in the introduction is to tune the decision +threshold of the classifier once the model has been trained. The +:class:`~sklearn.model_selection.TunedThresholdClassifierCV` tunes this threshold using +an internal cross-validation. The optimum threshold is chosen to maximize a given +metric. + +The following image illustrates the tuning of the decision threshold for a gradient +boosting classifier. While the vanilla and tuned classifiers provide the same +:term:`predict_proba` outputs and thus the same Receiver Operating Characteristic (ROC) +and Precision-Recall curves, the class label predictions differ because of the tuned +decision threshold. The vanilla classifier predicts the class of interest for a +conditional probability greater than 0.5 while the tuned classifier predicts the class +of interest for a very low probability (around 0.02). This decision threshold optimizes +a utility metric defined by the business (in this case an insurance company). + +.. figure:: ../auto_examples/model_selection/images/sphx_glr_plot_cost_sensitive_learning_002.png + :target: ../auto_examples/model_selection/plot_cost_sensitive_learning.html + :align: center + +Options to tune the decision threshold +-------------------------------------- + +The decision threshold can be tuned through different strategies controlled by the +parameter `scoring`. + +One way to tune the threshold is by maximizing a pre-defined scikit-learn metric. These +metrics can be found by calling the function :func:`~sklearn.metrics.get_scorer_names`. +By default, the balanced accuracy is the metric used but be aware that one should choose +a meaningful metric for their use case. + +.. note:: + + It is important to notice that these metrics come with default parameters, notably + the label of the class of interest (i.e. `pos_label`). Thus, if this label is not + the right one for your application, you need to define a scorer and pass the right + `pos_label` (and additional parameters) using the + :func:`~sklearn.metrics.make_scorer`. Refer to :ref:`scoring` to get + information to define your own scoring function. For instance, we show how to pass + the information to the scorer that the label of interest is `0` when maximizing the + :func:`~sklearn.metrics.f1_score`:: + + >>> from sklearn.linear_model import LogisticRegression + >>> from sklearn.model_selection import TunedThresholdClassifierCV + >>> from sklearn.metrics import make_scorer, f1_score + >>> X, y = make_classification( + ... n_samples=1_000, weights=[0.1, 0.9], random_state=0) + >>> pos_label = 0 + >>> scorer = make_scorer(f1_score, pos_label=pos_label) + >>> base_model = LogisticRegression() + >>> model = TunedThresholdClassifierCV(base_model, scoring=scorer) + >>> scorer(model.fit(X, y), X, y) + 0.88... + >>> # compare it with the internal score found by cross-validation + >>> model.best_score_ + 0.86... + +Important notes regarding the internal cross-validation +------------------------------------------------------- + +By default :class:`~sklearn.model_selection.TunedThresholdClassifierCV` uses a 5-fold +stratified cross-validation to tune the decision threshold. The parameter `cv` allows to +control the cross-validation strategy. It is possible to bypass cross-validation by +setting `cv="prefit"` and providing a fitted classifier. In this case, the decision +threshold is tuned on the data provided to the `fit` method. + +However, you should be extremely careful when using this option. You should never use +the same data for training the classifier and tuning the decision threshold due to the +risk of overfitting. Refer to the following example section for more details (cf. +:ref:`TunedThresholdClassifierCV_no_cv`). If you have limited resources, consider using +a float number for `cv` to limit to an internal single train-test split. + +The option `cv="prefit"` should only be used when the provided classifier was already +trained, and you just want to find the best decision threshold using a new validation +set. + +.. _FixedThresholdClassifier: + +Manually setting the decision threshold +--------------------------------------- + +The previous sections discussed strategies to find an optimal decision threshold. It is +also possible to manually set the decision threshold using the class +:class:`~sklearn.model_selection.FixedThresholdClassifier`. + +Examples +-------- + +- See the example entitled + :ref:`sphx_glr_auto_examples_model_selection_plot_tuned_decision_threshold.py`, + to get insights on the post-tuning of the decision threshold. +- See the example entitled + :ref:`sphx_glr_auto_examples_model_selection_plot_cost_sensitive_learning.py`, + to learn about cost-sensitive learning and decision threshold tuning. diff --git a/doc/whats_new/v1.5.rst b/doc/whats_new/v1.5.rst index 16b147be9135f..ede5d5dcbf1ec 100644 --- a/doc/whats_new/v1.5.rst +++ b/doc/whats_new/v1.5.rst @@ -438,6 +438,13 @@ Changelog :mod:`sklearn.model_selection` .............................. +- |MajorFeature| :class:`model_selection.TunedThresholdClassifierCV` finds + the decision threshold of a binary classifier that maximizes a + classification metric through cross-validation. + :class:`model_selection.FixedThresholdClassifier` is an alternative when one wants + to use a fixed decision threshold without any tuning scheme. + :pr:`26120` by :user:`Guillaume Lemaitre `. + - |Enhancement| :term:`CV splitters ` that ignores the group parameter now raises a warning when groups are passed in to :term:`split`. :pr:`28210` by `Thomas Fan`_. diff --git a/examples/model_selection/plot_cost_sensitive_learning.py b/examples/model_selection/plot_cost_sensitive_learning.py new file mode 100644 index 0000000000000..7b64af48139f2 --- /dev/null +++ b/examples/model_selection/plot_cost_sensitive_learning.py @@ -0,0 +1,702 @@ +""" +============================================================== +Post-tuning the decision threshold for cost-sensitive learning +============================================================== + +Once a classifier is trained, the output of the :term:`predict` method outputs class +label predictions corresponding to a thresholding of either the :term:`decision +function` or the :term:`predict_proba` output. For a binary classifier, the default +threshold is defined as a posterior probability estimate of 0.5 or a decision score of +0.0. + +However, this default strategy is most likely not optimal for the task at hand. +Here, we use the "Statlog" German credit dataset [1]_ to illustrate a use case. +In this dataset, the task is to predict whether a person has a "good" or "bad" credit. +In addition, a cost-matrix is provided that specifies the cost of +misclassification. Specifically, misclassifying a "bad" credit as "good" is five +times more costly on average than misclassifying a "good" credit as "bad". + +We use the :class:`~sklearn.model_selection.TunedThresholdClassifierCV` to select the +cut-off point of the decision function that minimizes the provided business +cost. + +In the second part of the example, we further extend this approach by +considering the problem of fraud detection in credit card transactions: in this +case, the business metric depends on the amount of each individual transaction. +.. topic:: References + + .. [1] "Statlog (German Credit Data) Data Set", UCI Machine Learning Repository, + `Link + `_. + + .. [2] `Charles Elkan, "The Foundations of Cost-Sensitive Learning", + International joint conference on artificial intelligence. + Vol. 17. No. 1. Lawrence Erlbaum Associates Ltd, 2001. + `_ +""" + +# %% +# Cost-sensitive learning with constant gains and costs +# ----------------------------------------------------- +# +# In this first section, we illustrate the use of the +# :class:`~sklearn.model_selection.TunedThresholdClassifierCV` in a setting of +# cost-sensitive learning when the gains and costs associated to each entry of the +# confusion matrix are constant. We use the problematic presented in [2]_ using the +# "Statlog" German credit dataset [1]_. +# +# "Statlog" German credit dataset +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +# +# We fetch the German credit dataset from OpenML. +import sklearn +from sklearn.datasets import fetch_openml + +sklearn.set_config(transform_output="pandas") + +german_credit = fetch_openml(data_id=31, as_frame=True, parser="pandas") +X, y = german_credit.data, german_credit.target + +# %% +# We check the feature types available in `X`. +X.info() + +# %% +# Many features are categorical and usually string-encoded. We need to encode +# these categories when we develop our predictive model. Let's check the targets. +y.value_counts() + +# %% +# Another observation is that the dataset is imbalanced. We would need to be careful +# when evaluating our predictive model and use a family of metrics that are adapted +# to this setting. +# +# In addition, we observe that the target is string-encoded. Some metrics +# (e.g. precision and recall) require to provide the label of interest also called +# the "positive label". Here, we define that our goal is to predict whether or not +# a sample is a "bad" credit. +pos_label, neg_label = "bad", "good" + +# %% +# To carry our analysis, we split our dataset using a single stratified split. +from sklearn.model_selection import train_test_split + +X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y, random_state=0) + +# %% +# We are ready to design our predictive model and the associated evaluation strategy. +# +# Evaluation metrics +# ^^^^^^^^^^^^^^^^^^ +# +# In this section, we define a set of metrics that we use later. To see +# the effect of tuning the cut-off point, we evaluate the predictive model using +# the Receiver Operating Characteristic (ROC) curve and the Precision-Recall curve. +# The values reported on these plots are therefore the true positive rate (TPR), +# also known as the recall or the sensitivity, and the false positive rate (FPR), +# also known as the specificity, for the ROC curve and the precision and recall for +# the Precision-Recall curve. +# +# From these four metrics, scikit-learn does not provide a scorer for the FPR. We +# therefore need to define a small custom function to compute it. +from sklearn.metrics import confusion_matrix + + +def fpr_score(y, y_pred, neg_label, pos_label): + cm = confusion_matrix(y, y_pred, labels=[neg_label, pos_label]) + tn, fp, _, _ = cm.ravel() + tnr = tn / (tn + fp) + return 1 - tnr + + +# %% +# As previously stated, the "positive label" is not defined as the value "1" and calling +# some of the metrics with this non-standard value raise an error. We need to +# provide the indication of the "positive label" to the metrics. +# +# We therefore need to define a scikit-learn scorer using +# :func:`~sklearn.metrics.make_scorer` where the information is passed. We store all +# the custom scorers in a dictionary. To use them, we need to pass the fitted model, +# the data and the target on which we want to evaluate the predictive model. +from sklearn.metrics import make_scorer, precision_score, recall_score + +tpr_score = recall_score # TPR and recall are the same metric +scoring = { + "precision": make_scorer(precision_score, pos_label=pos_label), + "recall": make_scorer(recall_score, pos_label=pos_label), + "fpr": make_scorer(fpr_score, neg_label=neg_label, pos_label=pos_label), + "tpr": make_scorer(tpr_score, pos_label=pos_label), +} + +# %% +# In addition, the original research [1]_ defines a custom business metric. We +# call a "business metric" any metric function that aims at quantifying how the +# predictions (correct or wrong) might impact the business value of deploying a +# given machine learning model in a specific application context. For our +# credit prediction task, the authors provide a custom cost-matrix which +# encodes that classifying a a "bad" credit as "good" is 5 times more costly on +# average than the opposite: it is less costly for the financing institution to +# not grant a credit to a potential customer that will not default (and +# therefore miss a good customer that would have otherwise both reimbursed the +# credit and payed interests) than to grant a credit to a customer that will +# default. +# +# We define a python function that weight the confusion matrix and return the +# overall cost. +import numpy as np + + +def credit_gain_score(y, y_pred, neg_label, pos_label): + cm = confusion_matrix(y, y_pred, labels=[neg_label, pos_label]) + # The rows of the confusion matrix hold the counts of observed classes + # while the columns hold counts of predicted classes. Recall that here we + # consider "bad" as the positive class (second row and column). + # Scikit-learn model selection tools expect that we follow a convention + # that "higher" means "better", hence the following gain matrix assigns + # negative gains (costs) to the two kinds of prediction errors: + # - a gain of -1 for each false positive ("good" credit labeled as "bad"), + # - a gain of -5 for each false negative ("bad" credit labeled as "good"), + # The true positives and true negatives are assigned null gains in this + # metric. + # + # Note that theoretically, given that our model is calibrated and our data + # set representative and large enough, we do not need to tune the + # threshold, but can safely set it to the cost ration 1/5, as stated by Eq. + # (2) in Elkan paper [2]_. + gain_matrix = np.array( + [ + [0, -1], # -1 gain for false positives + [-5, 0], # -5 gain for false negatives + ] + ) + return np.sum(cm * gain_matrix) + + +scoring["cost_gain"] = make_scorer( + credit_gain_score, neg_label=neg_label, pos_label=pos_label +) +# %% +# Vanilla predictive model +# ^^^^^^^^^^^^^^^^^^^^^^^^ +# +# We use :class:`~sklearn.ensemble.HistGradientBoostingClassifier` as a predictive model +# that natively handles categorical features and missing values. +from sklearn.ensemble import HistGradientBoostingClassifier + +model = HistGradientBoostingClassifier( + categorical_features="from_dtype", random_state=0 +).fit(X_train, y_train) +model + +# %% +# We evaluate the performance of our predictive model using the ROC and Precision-Recall +# curves. +import matplotlib.pyplot as plt + +from sklearn.metrics import PrecisionRecallDisplay, RocCurveDisplay + +fig, axs = plt.subplots(nrows=1, ncols=2, figsize=(14, 6)) + +PrecisionRecallDisplay.from_estimator( + model, X_test, y_test, pos_label=pos_label, ax=axs[0], name="GBDT" +) +axs[0].plot( + scoring["recall"](model, X_test, y_test), + scoring["precision"](model, X_test, y_test), + marker="o", + markersize=10, + color="tab:blue", + label="Default cut-off point at a probability of 0.5", +) +axs[0].set_title("Precision-Recall curve") +axs[0].legend() + +RocCurveDisplay.from_estimator( + model, + X_test, + y_test, + pos_label=pos_label, + ax=axs[1], + name="GBDT", + plot_chance_level=True, +) +axs[1].plot( + scoring["fpr"](model, X_test, y_test), + scoring["tpr"](model, X_test, y_test), + marker="o", + markersize=10, + color="tab:blue", + label="Default cut-off point at a probability of 0.5", +) +axs[1].set_title("ROC curve") +axs[1].legend() +_ = fig.suptitle("Evaluation of the vanilla GBDT model") + +# %% +# We recall that these curves give insights on the statistical performance of the +# predictive model for different cut-off points. For the Precision-Recall curve, the +# reported metrics are the precision and recall and for the ROC curve, the reported +# metrics are the TPR (same as recall) and FPR. +# +# Here, the different cut-off points correspond to different levels of posterior +# probability estimates ranging between 0 and 1. By default, `model.predict` uses a +# cut-off point at a probability estimate of 0.5. The metrics for such a cut-off point +# are reported with the blue dot on the curves: it corresponds to the statistical +# performance of the model when using `model.predict`. +# +# However, we recall that the original aim was to minimize the cost (or maximize the +# gain) as defined by the business metric. We can compute the value of the business +# metric: +print(f"Business defined metric: {scoring['cost_gain'](model, X_test, y_test)}") + +# %% +# At this stage we don't know if any other cut-off can lead to a greater gain. To find +# the optimal one, we need to compute the cost-gain using the business metric for all +# possible cut-off points and choose the best. This strategy can be quite tedious to +# implement by hand, but the +# :class:`~sklearn.model_selection.TunedThresholdClassifierCV` class is here to help us. +# It automatically computes the cost-gain for all possible cut-off points and optimizes +# for the `scoring`. +# +# .. _cost_sensitive_learning_example: +# +# Tuning the cut-off point +# ^^^^^^^^^^^^^^^^^^^^^^^^ +# +# We use :class:`~sklearn.model_selection.TunedThresholdClassifierCV` to tune the +# cut-off point. We need to provide the business metric to optimize as well as the +# positive label. Internally, the optimum cut-off point is chosen such that it maximizes +# the business metric via cross-validation. By default a 5-fold stratified +# cross-validation is used. +from sklearn.model_selection import TunedThresholdClassifierCV + +tuned_model = TunedThresholdClassifierCV( + estimator=model, + scoring=scoring["cost_gain"], + store_cv_results=True, # necessary to inspect all results +) +tuned_model.fit(X_train, y_train) +print(f"{tuned_model.best_threshold_=:0.2f}") + +# %% +# We plot the ROC and Precision-Recall curves for the vanilla model and the tuned model. +# Also we plot the cut-off points that would be used by each model. Because, we are +# reusing the same code later, we define a function that generates the plots. + + +def plot_roc_pr_curves(vanilla_model, tuned_model, *, title): + fig, axs = plt.subplots(nrows=1, ncols=3, figsize=(21, 6)) + + linestyles = ("dashed", "dotted") + markerstyles = ("o", ">") + colors = ("tab:blue", "tab:orange") + names = ("Vanilla GBDT", "Tuned GBDT") + for idx, (est, linestyle, marker, color, name) in enumerate( + zip((vanilla_model, tuned_model), linestyles, markerstyles, colors, names) + ): + decision_threshold = getattr(est, "best_threshold_", 0.5) + PrecisionRecallDisplay.from_estimator( + est, + X_test, + y_test, + pos_label=pos_label, + linestyle=linestyle, + color=color, + ax=axs[0], + name=name, + ) + axs[0].plot( + scoring["recall"](est, X_test, y_test), + scoring["precision"](est, X_test, y_test), + marker, + markersize=10, + color=color, + label=f"Cut-off point at probability of {decision_threshold:.2f}", + ) + RocCurveDisplay.from_estimator( + est, + X_test, + y_test, + pos_label=pos_label, + linestyle=linestyle, + color=color, + ax=axs[1], + name=name, + plot_chance_level=idx == 1, + ) + axs[1].plot( + scoring["fpr"](est, X_test, y_test), + scoring["tpr"](est, X_test, y_test), + marker, + markersize=10, + color=color, + label=f"Cut-off point at probability of {decision_threshold:.2f}", + ) + + axs[0].set_title("Precision-Recall curve") + axs[0].legend() + axs[1].set_title("ROC curve") + axs[1].legend() + + axs[2].plot( + tuned_model.cv_results_["thresholds"], + tuned_model.cv_results_["scores"], + color="tab:orange", + ) + axs[2].plot( + tuned_model.best_threshold_, + tuned_model.best_score_, + "o", + markersize=10, + color="tab:orange", + label="Optimal cut-off point for the business metric", + ) + axs[2].legend() + axs[2].set_xlabel("Decision threshold (probability)") + axs[2].set_ylabel("Objective score (using cost-matrix)") + axs[2].set_title("Objective score as a function of the decision threshold") + fig.suptitle(title) + + +# %% +title = "Comparison of the cut-off point for the vanilla and tuned GBDT model" +plot_roc_pr_curves(model, tuned_model, title=title) + +# %% +# The first remark is that both classifiers have exactly the same ROC and +# Precision-Recall curves. It is expected because by default, the classifier is fitted +# on the same training data. In a later section, we discuss more in detail the +# available options regarding model refitting and cross-validation. +# +# The second remark is that the cut-off points of the vanilla and tuned model are +# different. To understand why the tuned model has chosen this cut-off point, we can +# look at the right-hand side plot that plots the objective score that is our exactly +# the same as our business metric. We see that the optimum threshold corresponds to the +# maximum of the objective score. This maximum is reached for a decision threshold +# much lower than 0.5: the tuned model enjoys a much higher recall at the cost of +# of significantly lower precision: the tuned model is much more eager to +# predict the "bad" class label to larger fraction of individuals. +# +# We can now check if choosing this cut-off point leads to a better score on the testing +# set: +print(f"Business defined metric: {scoring['cost_gain'](tuned_model, X_test, y_test)}") + +# %% +# We observe that tuning the decision threshold almost improves our business gains +# by factor of 2. +# +# .. _TunedThresholdClassifierCV_no_cv: +# +# Consideration regarding model refitting and cross-validation +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +# +# In the above experiment, we used the default setting of the +# :class:`~sklearn.model_selection.TunedThresholdClassifierCV`. In particular, the +# cut-off point is tuned using a 5-fold stratified cross-validation. Also, the +# underlying predictive model is refitted on the entire training data once the cut-off +# point is chosen. +# +# These two strategies can be changed by providing the `refit` and `cv` parameters. +# For instance, one could provide a fitted `estimator` and set `cv="prefit"`, in which +# case the cut-off point is found on the entire dataset provided at fitting time. +# Also, the underlying classifier is not be refitted by setting `refit=False`. Here, we +# can try to do such experiment. +model.fit(X_train, y_train) +tuned_model.set_params(cv="prefit", refit=False).fit(X_train, y_train) +print(f"{tuned_model.best_threshold_=:0.2f}") + + +# %% +# Then, we evaluate our model with the same approach as before: +title = "Tuned GBDT model without refitting and using the entire dataset" +plot_roc_pr_curves(model, tuned_model, title=title) + +# %% +# We observe the that the optimum cut-off point is different from the one found +# in the previous experiment. If we look at the right-hand side plot, we +# observe that the business gain has large plateau of near-optimal 0 gain for a +# large span of decision thresholds. This behavior is symptomatic of an +# overfitting. Because we disable cross-validation, we tuned the cut-off point +# on the same set as the model was trained on, and this is the reason for the +# observed overfitting. +# +# This option should therefore be used with caution. One needs to make sure that the +# data provided at fitting time to the +# :class:`~sklearn.model_selection.TunedThresholdClassifierCV` is not the same as the +# data used to train the underlying classifier. This could happen sometimes when the +# idea is just to tune the predictive model on a completely new validation set without a +# costly complete refit. +# +# When cross-validation is too costly, a potential alternative is to use a +# single train-test split by providing a floating number in range `[0, 1]` to the `cv` +# parameter. It splits the data into a training and testing set. Let's explore this +# option: +tuned_model.set_params(cv=0.75).fit(X_train, y_train) + +# %% +title = "Tuned GBDT model without refitting and using the entire dataset" +plot_roc_pr_curves(model, tuned_model, title=title) + +# %% +# Regarding the cut-off point, we observe that the optimum is similar to the multiple +# repeated cross-validation case. However, be aware that a single split does not account +# for the variability of the fit/predict process and thus we are unable to know if there +# is any variance in the cut-off point. The repeated cross-validation averages out +# this effect. +# +# Another observation concerns the ROC and Precision-Recall curves of the tuned model. +# As expected, these curves differ from those of the vanilla model, given that we +# trained the underlying classifier on a subset of the data provided during fitting and +# reserved a validation set for tuning the cut-off point. +# +# Cost-sensitive learning when gains and costs are not constant +# ------------------------------------------------------------- +# +# As stated in [2]_, gains and costs are generally not constant in real-world problems. +# In this section, we use a similar example as in [2]_ for the problem of +# detecting fraud in credit card transaction records. +# +# The credit card dataset +# ^^^^^^^^^^^^^^^^^^^^^^^ +credit_card = fetch_openml(data_id=1597, as_frame=True, parser="pandas") +credit_card.frame.info() + +# %% +# The dataset contains information about credit card records from which some are +# fraudulent and others are legitimate. The goal is therefore to predict whether or +# not a credit card record is fraudulent. +columns_to_drop = ["Class"] +data = credit_card.frame.drop(columns=columns_to_drop) +target = credit_card.frame["Class"].astype(int) + +# %% +# First, we check the class distribution of the datasets. +target.value_counts(normalize=True) + +# %% +# The dataset is highly imbalanced with fraudulent transaction representing only 0.17% +# of the data. Since we are interested in training a machine learning model, we should +# also make sure that we have enough samples in the minority class to train the model. +target.value_counts() + +# %% +# We observe that we have around 500 samples that is on the low end of the number of +# samples required to train a machine learning model. In addition of the target +# distribution, we check the distribution of the amount of the +# fraudulent transactions. +fraud = target == 1 +amount_fraud = data["Amount"][fraud] +_, ax = plt.subplots() +ax.hist(amount_fraud, bins=100) +ax.set_title("Amount of fraud transaction") +_ = ax.set_xlabel("Amount ($)") + +# %% +# Addressing the problem with a business metric +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +# +# Now, we create the business metric that depends on the amount of each transaction. We +# define the cost matrix similarly to [2]_. Accepting a legitimate transaction provides +# a gain of 2% of the amount of the transaction. However, accepting a fraudulent +# transaction result in a loss of the amount of the transaction. As stated in [2]_, the +# gain and loss related to refusals (of fraudulent and legitimate transactions) are not +# trivial to define. Here, we define that a refusal of a legitimate transaction is +# estimated to a loss of $5 while the refusal of a fraudulent transaction is estimated +# to a gain of $50 dollars and the amount of the transaction. Therefore, we define the +# following function to compute the total benefit of a given decision: + + +def business_metric(y_true, y_pred, amount): + mask_true_positive = (y_true == 1) & (y_pred == 1) + mask_true_negative = (y_true == 0) & (y_pred == 0) + mask_false_positive = (y_true == 0) & (y_pred == 1) + mask_false_negative = (y_true == 1) & (y_pred == 0) + fraudulent_refuse = (mask_true_positive.sum() * 50) + amount[ + mask_true_positive + ].sum() + fraudulent_accept = -amount[mask_false_negative].sum() + legitimate_refuse = mask_false_positive.sum() * -5 + legitimate_accept = (amount[mask_true_negative] * 0.02).sum() + return fraudulent_refuse + fraudulent_accept + legitimate_refuse + legitimate_accept + + +# %% +# From this business metric, we create a scikit-learn scorer that given a fitted +# classifier and a test set compute the business metric. In this regard, we use +# the :func:`~sklearn.metrics.make_scorer` factory. The variable `amount` is an +# additional metadata to be passed to the scorer and we need to use +# :ref:`metadata routing ` to take into account this information. +sklearn.set_config(enable_metadata_routing=True) +business_scorer = make_scorer(business_metric).set_score_request(amount=True) + +# %% +# So at this stage, we observe that the amount of the transaction is used twice: once +# as a feature to train our predictive model and once as a metadata to compute the +# the business metric and thus the statistical performance of our model. When used as a +# feature, we are only required to have a column in `data` that contains the amount of +# each transaction. To use this information as metadata, we need to have an external +# variable that we can pass to the scorer or the model that internally routes this +# metadata to the scorer. So let's create this variable. +amount = credit_card.frame["Amount"].to_numpy() + +# %% +# We first start to train a dummy classifier to have some baseline results. +from sklearn.model_selection import train_test_split + +data_train, data_test, target_train, target_test, amount_train, amount_test = ( + train_test_split( + data, target, amount, stratify=target, test_size=0.5, random_state=42 + ) +) + +# %% +from sklearn.dummy import DummyClassifier + +easy_going_classifier = DummyClassifier(strategy="constant", constant=0) +easy_going_classifier.fit(data_train, target_train) +benefit_cost = business_scorer( + easy_going_classifier, data_test, target_test, amount=amount_test +) +print(f"Benefit/cost of our easy-going classifier: ${benefit_cost:,.2f}") + +# %% +# A classifier that predict all transactions as legitimate would create a profit of +# around $220,000. We make the same evaluation for a classifier that predicts all +# transactions as fraudulent. +intolerant_classifier = DummyClassifier(strategy="constant", constant=1) +intolerant_classifier.fit(data_train, target_train) +benefit_cost = business_scorer( + intolerant_classifier, data_test, target_test, amount=amount_test +) +print(f"Benefit/cost of our intolerant classifier: ${benefit_cost:,.2f}") + +# %% +# Such a classifier create a loss of around $670,000. A predictive model should allow +# us to make a profit larger than $220,000. It is interesting to compare this business +# metric with another "standard" statistical metric such as the balanced accuracy. +from sklearn.metrics import get_scorer + +balanced_accuracy_scorer = get_scorer("balanced_accuracy") +print( + "Balanced accuracy of our easy-going classifier: " + f"{balanced_accuracy_scorer(easy_going_classifier, data_test, target_test):.3f}" +) +print( + "Balanced accuracy of our intolerant classifier: " + f"{balanced_accuracy_scorer(intolerant_classifier, data_test, target_test):.3f}" +) + +# %% +# This is not a surprise that the balanced accuracy is at 0.5 for both classifiers. +# However, we need to be careful in the rest of the evaluation: we potentially can +# obtain a model with a decent balanced accuracy that does not make any profit. +# In this case, the model would be harmful for our business. +# +# Let's now create a predictive model using a logistic regression without tuning the +# decision threshold. +from sklearn.linear_model import LogisticRegression +from sklearn.model_selection import GridSearchCV +from sklearn.pipeline import make_pipeline +from sklearn.preprocessing import StandardScaler + +logistic_regression = make_pipeline(StandardScaler(), LogisticRegression()) +param_grid = {"logisticregression__C": np.logspace(-6, 6, 13)} +model = GridSearchCV(logistic_regression, param_grid, scoring="neg_log_loss").fit( + data_train, target_train +) + +print( + "Benefit/cost of our logistic regression: " + f"${business_scorer(model, data_test, target_test, amount=amount_test):,.2f}" +) +print( + "Balanced accuracy of our logistic regression: " + f"{balanced_accuracy_scorer(model, data_test, target_test):.3f}" +) + +# %% +# By observing the balanced accuracy, we see that our predictive model is learning +# some associations between the features and the target. The business metric also shows +# that our model is beating the baseline in terms of profit and it would be already +# beneficial to use it instead of ignoring the fraud detection problem. +# +# Tuning the decision threshold +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +# +# Now the question is: is our model optimum for the type of decision that we want to do? +# Up to now, we did not optimize the decision threshold. We use the +# :class:`~sklearn.model_selection.TunedThresholdClassifierCV` to optimize the decision +# given our business scorer. To avoid a nested cross-validation, we will use the +# best estimator found during the previous grid-search. +tuned_model = TunedThresholdClassifierCV( + estimator=model.best_estimator_, + scoring=business_scorer, + thresholds=100, + n_jobs=2, +) + +# %% +# Since our business scorer requires the amount of each transaction, we need to pass +# this information in the `fit` method. The +# :class:`~sklearn.model_selection.TunedThresholdClassifierCV` is in charge of +# automatically dispatching this metadata to the underlying scorer. +tuned_model.fit(data_train, target_train, amount=amount_train) + +# %% +print( + "Benefit/cost of our logistic regression: " + f"${business_scorer(tuned_model, data_test, target_test, amount=amount_test):,.2f}" +) +print( + "Balanced accuracy of our logistic regression: " + f"{balanced_accuracy_scorer(tuned_model, data_test, target_test):.3f}" +) + +# %% +# We observe that tuning the decision threshold increases the expected profit of +# deploying our model as estimated by the business metric. +# Eventually, the balanced accuracy also increased. Note that it might not always be +# the case because the statistical metric is not necessarily a surrogate of the +# business metric. It is therefore important, whenever possible, optimize the decision +# threshold with respect to the business metric. +# +# Finally, the estimate of the business metric itself can be unreliable, in +# particular when the number of data points in the minority class is so small. +# Any business impact estimated by cross-validation of a business metric on +# historical data (offline evaluation) should ideally be confirmed by A/B testing +# on live data (online evaluation). Note however that A/B testing models is +# beyond the scope of the scikit-learn library itself. +# +# Manually setting the decision threshold instead of tuning it +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +# +# In the previous example, we used the +# :class:`~sklearn.model_selection.TunedThresholdClassifierCV` to find the optimal +# decision threshold. However, in some cases, we might have some prior knowledge about +# the problem at hand and we might be happy to set the decision threshold manually. +# +# The class :class:`~sklearn.model_selection.FixedThresholdClassifier` allows us to +# manually set the decision threshold. At prediction time, it behave as the previous +# tuned model but no search is performed during the fitting process. +# +# Here, we will reuse the decision threshold found in the previous section to create a +# new model and check that it gives the same results. +from sklearn.model_selection import FixedThresholdClassifier + +model_fixed_threshold = FixedThresholdClassifier( + estimator=model, threshold=tuned_model.best_threshold_ +).fit(data_train, target_train) + +# %% +business_score = business_scorer( + model_fixed_threshold, data_test, target_test, amount=amount_test +) +print(f"Benefit/cost of our logistic regression: ${business_score:,.2f}") +print( + "Balanced accuracy of our logistic regression: " + f"{balanced_accuracy_scorer(model_fixed_threshold, data_test, target_test):.3f}" +) + +# %% +# We observe that we obtained the exact same results but the fitting process was much +# faster since we did not perform any search. diff --git a/examples/model_selection/plot_tuned_decision_threshold.py b/examples/model_selection/plot_tuned_decision_threshold.py new file mode 100644 index 0000000000000..7e997ee255e4d --- /dev/null +++ b/examples/model_selection/plot_tuned_decision_threshold.py @@ -0,0 +1,184 @@ +""" +====================================================== +Post-hoc tuning the cut-off point of decision function +====================================================== + +Once a binary classifier is trained, the :term:`predict` method outputs class label +predictions corresponding to a thresholding of either the :term:`decision_function` or +the :term:`predict_proba` output. The default threshold is defined as a posterior +probability estimate of 0.5 or a decision score of 0.0. However, this default strategy +may not be optimal for the task at hand. + +This example shows how to use the +:class:`~sklearn.model_selection.TunedThresholdClassifierCV` to tune the decision +threshold, depending on a metric of interest. +""" + +# %% +# The diabetes dataset +# -------------------- +# +# To illustrate the tuning of the decision threshold, we will use the diabetes dataset. +# This dataset is available on OpenML: https://www.openml.org/d/37. We use the +# :func:`~sklearn.datasets.fetch_openml` function to fetch this dataset. +from sklearn.datasets import fetch_openml + +diabetes = fetch_openml(data_id=37, as_frame=True, parser="pandas") +data, target = diabetes.data, diabetes.target + +# %% +# We look at the target to understand the type of problem we are dealing with. +target.value_counts() + +# %% +# We can see that we are dealing with a binary classification problem. Since the +# labels are not encoded as 0 and 1, we make it explicit that we consider the class +# labeled "tested_negative" as the negative class (which is also the most frequent) +# and the class labeled "tested_positive" the positive as the positive class: +neg_label, pos_label = target.value_counts().index + +# %% +# We can also observe that this binary problem is slightly imbalanced where we have +# around twice more samples from the negative class than from the positive class. When +# it comes to evaluation, we should consider this aspect to interpret the results. +# +# Our vanilla classifier +# ---------------------- +# +# We define a basic predictive model composed of a scaler followed by a logistic +# regression classifier. +from sklearn.linear_model import LogisticRegression +from sklearn.pipeline import make_pipeline +from sklearn.preprocessing import StandardScaler + +model = make_pipeline(StandardScaler(), LogisticRegression()) +model + +# %% +# We evaluate our model using cross-validation. We use the accuracy and the balanced +# accuracy to report the performance of our model. The balanced accuracy is a metric +# that is less sensitive to class imbalance and will allow us to put the accuracy +# score in perspective. +# +# Cross-validation allows us to study the variance of the decision threshold across +# different splits of the data. However, the dataset is rather small and it would be +# detrimental to use more than 5 folds to evaluate the dispersion. Therefore, we use +# a :class:`~sklearn.model_selection.RepeatedStratifiedKFold` where we apply several +# repetitions of 5-fold cross-validation. +import pandas as pd + +from sklearn.model_selection import RepeatedStratifiedKFold, cross_validate + +scoring = ["accuracy", "balanced_accuracy"] +cv_scores = [ + "train_accuracy", + "test_accuracy", + "train_balanced_accuracy", + "test_balanced_accuracy", +] +cv = RepeatedStratifiedKFold(n_splits=5, n_repeats=10, random_state=42) +cv_results_vanilla_model = pd.DataFrame( + cross_validate( + model, + data, + target, + scoring=scoring, + cv=cv, + return_train_score=True, + return_estimator=True, + ) +) +cv_results_vanilla_model[cv_scores].aggregate(["mean", "std"]).T + +# %% +# Our predictive model succeeds to grasp the relationship between the data and the +# target. The training and testing scores are close to each other, meaning that our +# predictive model is not overfitting. We can also observe that the balanced accuracy is +# lower than the accuracy, due to the class imbalance previously mentioned. +# +# For this classifier, we let the decision threshold, used convert the probability of +# the positive class into a class prediction, to its default value: 0.5. However, this +# threshold might not be optimal. If our interest is to maximize the balanced accuracy, +# we should select another threshold that would maximize this metric. +# +# The :class:`~sklearn.model_selection.TunedThresholdClassifierCV` meta-estimator allows +# to tune the decision threshold of a classifier given a metric of interest. +# +# Tuning the decision threshold +# ----------------------------- +# +# We create a :class:`~sklearn.model_selection.TunedThresholdClassifierCV` and +# configure it to maximize the balanced accuracy. We evaluate the model using the same +# cross-validation strategy as previously. +from sklearn.model_selection import TunedThresholdClassifierCV + +tuned_model = TunedThresholdClassifierCV(estimator=model, scoring="balanced_accuracy") +cv_results_tuned_model = pd.DataFrame( + cross_validate( + tuned_model, + data, + target, + scoring=scoring, + cv=cv, + return_train_score=True, + return_estimator=True, + ) +) +cv_results_tuned_model[cv_scores].aggregate(["mean", "std"]).T + +# %% +# In comparison with the vanilla model, we observe that the balanced accuracy score +# increased. Of course, it comes at the cost of a lower accuracy score. It means that +# our model is now more sensitive to the positive class but makes more mistakes on the +# negative class. +# +# However, it is important to note that this tuned predictive model is internally the +# same model as the vanilla model: they have the same fitted coefficients. +import matplotlib.pyplot as plt + +vanilla_model_coef = pd.DataFrame( + [est[-1].coef_.ravel() for est in cv_results_vanilla_model["estimator"]], + columns=diabetes.feature_names, +) +tuned_model_coef = pd.DataFrame( + [est.estimator_[-1].coef_.ravel() for est in cv_results_tuned_model["estimator"]], + columns=diabetes.feature_names, +) + +fig, ax = plt.subplots(ncols=2, figsize=(12, 4), sharex=True, sharey=True) +vanilla_model_coef.boxplot(ax=ax[0]) +ax[0].set_ylabel("Coefficient value") +ax[0].set_title("Vanilla model") +tuned_model_coef.boxplot(ax=ax[1]) +ax[1].set_title("Tuned model") +_ = fig.suptitle("Coefficients of the predictive models") + +# %% +# Only the decision threshold of each model was changed during the cross-validation. +decision_threshold = pd.Series( + [est.best_threshold_ for est in cv_results_tuned_model["estimator"]], +) +ax = decision_threshold.plot.kde() +ax.axvline( + decision_threshold.mean(), + color="k", + linestyle="--", + label=f"Mean decision threshold: {decision_threshold.mean():.2f}", +) +ax.set_xlabel("Decision threshold") +ax.legend(loc="upper right") +_ = ax.set_title( + "Distribution of the decision threshold \nacross different cross-validation folds" +) + +# %% +# In average, a decision threshold around 0.32 maximizes the balanced accuracy, which is +# different from the default decision threshold of 0.5. Thus tuning the decision +# threshold is particularly important when the output of the predictive model +# is used to make decisions. Besides, the metric used to tune the decision threshold +# should be chosen carefully. Here, we used the balanced accuracy but it might not be +# the most appropriate metric for the problem at hand. The choice of the "right" metric +# is usually problem-dependent and might require some domain knowledge. Refer to the +# example entitled, +# :ref:`sphx_glr_auto_examples_model_selection_plot_cost_sensitive_learning.py`, +# for more details. diff --git a/sklearn/metrics/_scorer.py b/sklearn/metrics/_scorer.py index adeec587994e2..bc9d8ab3d651a 100644 --- a/sklearn/metrics/_scorer.py +++ b/sklearn/metrics/_scorer.py @@ -195,6 +195,25 @@ def get_metadata_routing(self): class _BaseScorer(_MetadataRequester): + """Base scorer that is used as `scorer(estimator, X, y_true)`. + + Parameters + ---------- + 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 diff --git a/sklearn/model_selection/__init__.py b/sklearn/model_selection/__init__.py index d7d316d95ada4..c97d48f4b20b7 100644 --- a/sklearn/model_selection/__init__.py +++ b/sklearn/model_selection/__init__.py @@ -1,5 +1,9 @@ import typing +from ._classification_threshold import ( + FixedThresholdClassifier, + TunedThresholdClassifierCV, +) from ._plot import LearningCurveDisplay, ValidationCurveDisplay from ._search import GridSearchCV, ParameterGrid, ParameterSampler, RandomizedSearchCV from ._split import ( @@ -63,6 +67,8 @@ "StratifiedKFold", "StratifiedGroupKFold", "StratifiedShuffleSplit", + "FixedThresholdClassifier", + "TunedThresholdClassifierCV", "check_cv", "cross_val_predict", "cross_val_score", diff --git a/sklearn/model_selection/_classification_threshold.py b/sklearn/model_selection/_classification_threshold.py new file mode 100644 index 0000000000000..d5a864da10653 --- /dev/null +++ b/sklearn/model_selection/_classification_threshold.py @@ -0,0 +1,1000 @@ +from collections.abc import MutableMapping +from numbers import Integral, Real + +import numpy as np + +from ..base import ( + BaseEstimator, + ClassifierMixin, + MetaEstimatorMixin, + _fit_context, + clone, +) +from ..exceptions import NotFittedError +from ..metrics import ( + check_scoring, + get_scorer_names, +) +from ..metrics._scorer import _BaseScorer +from ..utils import _safe_indexing +from ..utils._param_validation import HasMethods, Interval, RealNotInt, StrOptions +from ..utils._response import _get_response_values_binary +from ..utils.metadata_routing import ( + MetadataRouter, + MethodMapping, + _raise_for_params, + process_routing, +) +from ..utils.metaestimators import available_if +from ..utils.multiclass import type_of_target +from ..utils.parallel import Parallel, delayed +from ..utils.validation import ( + _check_method_params, + _num_samples, + check_is_fitted, + indexable, +) +from ._split import StratifiedShuffleSplit, check_cv + + +def _estimator_has(attr): + """Check if we can delegate a method to the underlying estimator. + + First, we check the fitted estimator if available, otherwise we + check the unfitted estimator. + """ + + def check(self): + if hasattr(self, "estimator_"): + getattr(self.estimator_, attr) + else: + getattr(self.estimator, attr) + return True + + return check + + +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 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. + + .. versionadded:: 1.5 + + Parameters + ---------- + estimator : estimator instance + 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. + """ + + _required_parameters = ["estimator"] + _parameter_constraints: dict = { + "estimator": [ + HasMethods(["fit", "predict_proba"]), + HasMethods(["fit", "decision_function"]), + ], + "response_method": [StrOptions({"auto", "predict_proba", "decision_function"})], + } + + def __init__(self, estimator, *, response_method="auto"): + self.estimator = estimator + self.response_method = response_method + + @_fit_context( + # *ThresholdClassifier*.estimator is not validated yet + prefer_skip_nested_validation=False + ) + def fit(self, X, y, **params): + """Fit the classifier. + + Parameters + ---------- + X : {array-like, sparse matrix} of shape (n_samples, n_features) + Training data. + + y : array-like of shape (n_samples,) + Target values. + + **params : dict + Parameters to pass to the `fit` method of the underlying + classifier. + + Returns + ------- + self : object + Returns an instance of self. + """ + _raise_for_params(params, self, None) + + X, y = indexable(X, y) + + y_type = type_of_target(y, input_name="y") + if y_type != "binary": + raise ValueError( + f"Only binary classification is supported. Unknown label type: {y_type}" + ) + + if self.response_method == "auto": + self._response_method = ["predict_proba", "decision_function"] + else: + self._response_method = self.response_method + + self._fit(X, y, **params) + + if hasattr(self.estimator_, "n_features_in_"): + self.n_features_in_ = self.estimator_.n_features_in_ + if hasattr(self.estimator_, "feature_names_in_"): + self.feature_names_in_ = self.estimator_.feature_names_in_ + + return self + + @property + def classes_(self): + """Classes labels.""" + return self.estimator_.classes_ + + @available_if(_estimator_has("predict_proba")) + def predict_proba(self, X): + """Predict class probabilities for `X` using the fitted estimator. + + Parameters + ---------- + X : {array-like, sparse matrix} of shape (n_samples, n_features) + Training vectors, where `n_samples` is the number of samples and + `n_features` is the number of features. + + Returns + ------- + probabilities : ndarray of shape (n_samples, n_classes) + The class probabilities of the input samples. + """ + check_is_fitted(self, "estimator_") + return self.estimator_.predict_proba(X) + + @available_if(_estimator_has("predict_log_proba")) + def predict_log_proba(self, X): + """Predict logarithm class probabilities for `X` using the fitted estimator. + + Parameters + ---------- + X : {array-like, sparse matrix} of shape (n_samples, n_features) + Training vectors, where `n_samples` is the number of samples and + `n_features` is the number of features. + + Returns + ------- + log_probabilities : ndarray of shape (n_samples, n_classes) + The logarithm class probabilities of the input samples. + """ + check_is_fitted(self, "estimator_") + return self.estimator_.predict_log_proba(X) + + @available_if(_estimator_has("decision_function")) + def decision_function(self, X): + """Decision function for samples in `X` using the fitted estimator. + + Parameters + ---------- + X : {array-like, sparse matrix} of shape (n_samples, n_features) + Training vectors, where `n_samples` is the number of samples and + `n_features` is the number of features. + + Returns + ------- + decisions : ndarray of shape (n_samples,) + The decision function computed the fitted estimator. + """ + check_is_fitted(self, "estimator_") + return self.estimator_.decision_function(X) + + def _more_tags(self): + return { + "binary_only": True, + "_xfail_checks": { + "check_classifiers_train": "Threshold at probability 0.5 does not hold", + "check_sample_weights_invariance": ( + "Due to the cross-validation and sample ordering, removing a sample" + " is not strictly equal to putting is weight to zero. Specific unit" + " tests are added for TunedThresholdClassifierCV specifically." + ), + }, + } + + +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 `. + + .. versionadded:: 1.5 + + Parameters + ---------- + estimator : estimator instance + The binary classifier, fitted or not, for which we want to optimize + the decision threshold used during `predict`. + + 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`). + + pos_label : int, float, bool or str, default=None + 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. + + 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 `"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. + + Attributes + ---------- + estimator_ : estimator instance + The fitted classifier used when predicting. + + classes_ : ndarray of shape (n_classes,) + The class labels. + + n_features_in_ : int + Number of features seen during :term:`fit`. Only defined if the + underlying estimator exposes such an attribute when fit. + + feature_names_in_ : ndarray of shape (`n_features_in_`,) + Names of features seen during :term:`fit`. Only defined if the + underlying estimator exposes such an attribute when fit. + + 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.datasets import make_classification + >>> from sklearn.linear_model import LogisticRegression + >>> from sklearn.metrics import confusion_matrix + >>> from sklearn.model_selection import FixedThresholdClassifier, train_test_split + >>> X, y = make_classification( + ... n_samples=1_000, weights=[0.9, 0.1], class_sep=0.8, random_state=42 + ... ) + >>> X_train, X_test, y_train, y_test = train_test_split( + ... X, y, stratify=y, 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]] + """ + + _parameter_constraints: dict = { + **BaseThresholdClassifier._parameter_constraints, + "threshold": [StrOptions({"auto"}), Real], + "pos_label": [Real, str, "boolean", None], + } + + def __init__( + self, + estimator, + *, + threshold="auto", + pos_label=None, + response_method="auto", + ): + super().__init__(estimator=estimator, response_method=response_method) + self.pos_label = pos_label + self.threshold = threshold + + def _fit(self, X, y, **params): + """Fit the classifier. + + Parameters + ---------- + X : {array-like, sparse matrix} of shape (n_samples, n_features) + Training data. + + y : array-like of shape (n_samples,) + Target values. + + **params : dict + Parameters to pass to the `fit` method of the underlying + classifier. + + Returns + ------- + self : object + Returns an instance of self. + """ + routed_params = process_routing(self, "fit", **params) + self.estimator_ = clone(self.estimator).fit(X, y, **routed_params.estimator.fit) + return self + + def predict(self, X): + """Predict the target of new samples. + + Parameters + ---------- + X : {array-like, sparse matrix} of shape (n_samples, n_features) + The samples, as accepted by `estimator.predict`. + + Returns + ------- + class_labels : ndarray of shape (n_samples,) + The predicted class. + """ + check_is_fitted(self, "estimator_") + y_score, _, response_method_used = _get_response_values_binary( + self.estimator_, + X, + self._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 + ) + + def get_metadata_routing(self): + """Get metadata routing of this object. + + Please check :ref:`User Guide ` 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__).add( + estimator=self.estimator, + method_mapping=MethodMapping().add(callee="fit", caller="fit"), + ) + return router + + +class _CurveScorer(_BaseScorer): + """Scorer taking a continuous response and output a score for each threshold. + + Parameters + ---------- + 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. + + 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. + + response_method : str + The method to call on the estimator to get the response values. + """ + + def __init__(self, score_func, sign, kwargs, thresholds, response_method): + super().__init__( + score_func=score_func, + sign=sign, + kwargs=kwargs, + response_method=response_method, + ) + self._thresholds = thresholds + + @classmethod + 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, + response_method=response_method, + thresholds=thresholds, + kwargs=scorer._kwargs, + ) + # transfer the metadata request + instance._metadata_request = scorer._get_metadata_request() + return instance + + def _score(self, method_caller, estimator, X, y_true, **kwargs): + """Evaluate predicted target values for X relative to y_true. + + Parameters + ---------- + method_caller : callable + Returns predictions given an estimator, method name, and other + arguments, potentially caching results. + + estimator : object + Trained estimator to use for scoring. + + X : {array-like, sparse matrix} of shape (n_samples, n_features) + Test data that will be fed to estimator.predict. + + y_true : array-like of shape (n_samples,) + Gold standard target values for X. + + **kwargs : dict + Other parameters passed to the scorer. Refer to + :func:`set_score_request` for more details. + + 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 + ) + + scoring_kwargs = {**self._kwargs, **kwargs} + 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 + + +def _fit_and_score_over_thresholds( + classifier, + X, + y, + *, + fit_params, + train_idx, + val_idx, + curve_scorer, + score_params, +): + """Fit a classifier and compute the scores for different decision thresholds. + + Parameters + ---------- + classifier : estimator instance + The classifier to fit and use for scoring. If `classifier` is already fitted, + it will be used as is. + + X : {array-like, sparse matrix} of shape (n_samples, n_features) + The entire dataset. + + y : array-like of shape (n_samples,) + 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. + + score_params : dict + Parameters to pass to the `score` method of the underlying scorer. + + 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. + + Parameters + ---------- + 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 `. + + .. versionadded:: 1.5 + + Parameters + ---------- + estimator : estimator instance + The classifier, fitted or not, for which we want to optimize + the decision threshold used during `predict`. + + scoring : str or callable, default="balanced_accuracy" + The objective metric to be optimized. Can be one of: + + * a string associated to a scoring function for binary classification + (see model evaluation documentation); + * a scorer callable object created with :func:`~sklearn.metrics.make_scorer`; + + 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. + + 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. + Possible inputs for cv are: + + * `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. + + Refer :ref:`User Guide ` for the various + cross-validation strategies that can be used here. + + .. warning:: + 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`). + + refit : bool, default=True + 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. + + n_jobs : int, default=None + 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 + :obj:`joblib.parallel_backend` context. ``-1`` means using all + processors. See :term:`Glossary ` for more details. + + random_state : int, RandomState instance or None, default=None + Controls the randomness of cross-validation when `cv` is a float. + See :term:`Glossary `. + + store_cv_results : bool, default=False + Whether to store all scores and thresholds computed during the cross-validation + process. + + Attributes + ---------- + estimator_ : estimator instance + The fitted classifier used when predicting. + + 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"`. + + classes_ : ndarray of shape (n_classes,) + The class labels. + + n_features_in_ : int + Number of features seen during :term:`fit`. Only defined if the + underlying estimator exposes such an attribute when fit. + + feature_names_in_ : ndarray of shape (`n_features_in_`,) + Names of features seen during :term:`fit`. Only defined if the + underlying estimator exposes such an attribute when fit. + + See Also + -------- + sklearn.model_selection.FixedThresholdClassifier : Classifier that uses a + constant threshold. + sklearn.calibration.CalibratedClassifierCV : Estimator that calibrates + probabilities. + + Examples + -------- + >>> from sklearn.datasets import make_classification + >>> from sklearn.ensemble import RandomForestClassifier + >>> from sklearn.metrics import classification_report + >>> from sklearn.model_selection import TunedThresholdClassifierCV, train_test_split + >>> X, y = make_classification( + ... n_samples=1_000, weights=[0.9, 0.1], class_sep=0.8, random_state=42 + ... ) + >>> X_train, X_test, y_train, y_test = train_test_split( + ... X, y, stratify=y, random_state=42 + ... ) + >>> classifier = RandomForestClassifier(random_state=0).fit(X_train, y_train) + >>> print(classification_report(y_test, classifier.predict(X_test))) + precision recall f1-score support + + 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" + ... ).fit(X_train, y_train) + >>> 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))) + precision recall f1-score support + + 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 + + """ + + _parameter_constraints: dict = { + **BaseThresholdClassifier._parameter_constraints, + "scoring": [ + StrOptions(set(get_scorer_names())), + callable, + MutableMapping, + ], + "thresholds": [Interval(Integral, 1, None, closed="left"), "array-like"], + "cv": [ + "cv_object", + StrOptions({"prefit"}), + Interval(RealNotInt, 0.0, 1.0, closed="neither"), + ], + "refit": ["boolean"], + "n_jobs": [Integral, None], + "random_state": ["random_state"], + "store_cv_results": ["boolean"], + } + + def __init__( + self, + estimator, + *, + scoring="balanced_accuracy", + response_method="auto", + thresholds=100, + cv=None, + refit=True, + n_jobs=None, + random_state=None, + store_cv_results=False, + ): + super().__init__(estimator=estimator, response_method=response_method) + self.scoring = scoring + self.thresholds = thresholds + self.cv = cv + self.refit = refit + self.n_jobs = n_jobs + self.random_state = random_state + self.store_cv_results = store_cv_results + + def _fit(self, X, y, **params): + """Fit the classifier and post-tune the decision threshold. + + Parameters + ---------- + X : {array-like, sparse matrix} of shape (n_samples, n_features) + Training data. + + y : array-like of shape (n_samples,) + Target values. + + **params : dict + Parameters to pass to the `fit` method of the underlying + classifier and to the `scoring` scorer. + + Returns + ------- + self : object + Returns an instance of self. + """ + 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_") + except NotFittedError as exc: + raise NotFittedError( + """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.") + + routed_params = process_routing(self, "fit", **params) + 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": + self.estimator_ = self.estimator + classifier = self.estimator_ + splits = [(None, range(_num_samples(X)))] + else: + self.estimator_ = clone(self.estimator) + classifier = clone(self.estimator) + splits = cv.split(X, y, **routed_params.splitter.split) + + if self.refit: + # 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): + raise ValueError( + "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] + if self.store_cv_results: + self.cv_results_ = { + "thresholds": decision_thresholds, + "scores": objective_scores, + } + + return self + + def predict(self, X): + """Predict the target of new samples. + + Parameters + ---------- + X : {array-like, sparse matrix} of shape (n_samples, n_features) + The samples, as accepted by `estimator.predict`. + + Returns + ------- + class_labels : ndarray of shape (n_samples,) + The predicted class. + """ + check_is_fitted(self, "estimator_") + pos_label = self._curve_scorer._get_pos_label() + y_score, _ = _get_response_values_binary( + self.estimator_, + X, + self._response_method, + pos_label=pos_label, + ) + + return _threshold_scores_to_class_labels( + y_score, self.best_threshold_, self.classes_, pos_label + ) + + def get_metadata_routing(self): + """Get metadata routing of this object. + + Please check :ref:`User Guide ` 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__) + .add( + estimator=self.estimator, + method_mapping=MethodMapping().add(callee="fit", caller="fit"), + ) + .add( + splitter=self.cv, + method_mapping=MethodMapping().add(callee="split", caller="fit"), + ) + .add( + scorer=self._get_curve_scorer(), + method_mapping=MethodMapping().add(callee="score", caller="fit"), + ) + ) + return router + + 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._response_method, self.thresholds + ) + return curve_scorer diff --git a/sklearn/model_selection/tests/test_classification_threshold.py b/sklearn/model_selection/tests/test_classification_threshold.py new file mode 100644 index 0000000000000..f64edb2563c76 --- /dev/null +++ b/sklearn/model_selection/tests/test_classification_threshold.py @@ -0,0 +1,684 @@ +import numpy as np +import pytest + +from sklearn.base import clone +from sklearn.datasets import ( + load_breast_cancer, + load_iris, + make_classification, + make_multilabel_classification, +) +from sklearn.dummy import DummyClassifier +from sklearn.ensemble import GradientBoostingClassifier +from sklearn.exceptions import NotFittedError +from sklearn.linear_model import LogisticRegression +from sklearn.metrics import ( + balanced_accuracy_score, + f1_score, + fbeta_score, + make_scorer, + recall_score, +) +from sklearn.model_selection import ( + FixedThresholdClassifier, + StratifiedShuffleSplit, + TunedThresholdClassifierCV, +) +from sklearn.model_selection._classification_threshold import ( + _CurveScorer, + _fit_and_score_over_thresholds, +) +from sklearn.pipeline import make_pipeline +from sklearn.preprocessing import StandardScaler +from sklearn.svm import SVC +from sklearn.tree import DecisionTreeClassifier +from sklearn.utils._mocking import CheckingClassifier +from sklearn.utils._testing import ( + _convert_container, + assert_allclose, + assert_array_equal, +) + + +def test_curve_scorer(): + """Check the behaviour of the `_CurveScorer` class.""" + X, y = make_classification(random_state=0) + estimator = LogisticRegression().fit(X, y) + curve_scorer = _CurveScorer( + balanced_accuracy_score, + sign=1, + response_method="predict_proba", + 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 + curve_scorer = _CurveScorer( + balanced_accuracy_score, + sign=1, + response_method="predict_proba", + thresholds=10, + kwargs={"adjusted": True}, + ) + scores, thresholds = curve_scorer(estimator, X, y) + + # balanced accuracy should be between 0.5 and 1 when it is not adjusted + assert 0 <= scores.min() <= 0.5 + + # check that we can inverse the sign of the score when dealing with `neg_*` scorer + curve_scorer = _CurveScorer( + balanced_accuracy_score, + sign=-1, + response_method="predict_proba", + thresholds=10, + kwargs={"adjusted": True}, + ) + scores, thresholds = curve_scorer(estimator, X, y) + + 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 + X, y = make_classification( + n_samples=n_samples, weights=[0.9, 0.1], random_state=global_random_seed + ) + estimator = LogisticRegression().fit(X, y) + + curve_scorer = _CurveScorer( + recall_score, + sign=1, + response_method="predict_proba", + thresholds=10, + kwargs={"pos_label": 1}, + ) + scores_pos_label_1, thresholds_pos_label_1 = curve_scorer(estimator, X, y) + + curve_scorer = _CurveScorer( + recall_score, + sign=1, + response_method="predict_proba", + thresholds=10, + 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_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.""" + X, y = make_classification(n_samples=100, random_state=0) + train_idx, val_idx = np.arange(50), np.arange(50, 100) + classifier = LogisticRegression() + + curve_scorer = _CurveScorer( + score_func=balanced_accuracy_score, + sign=1, + response_method="predict_proba", + thresholds=10, + kwargs={}, + ) + scores, thresholds = _fit_and_score_over_thresholds( + classifier, + 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.""" + X, y = make_classification(n_samples=100, random_state=0) + + # `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) + + curve_scorer = _CurveScorer( + score_func=balanced_accuracy_score, + sign=1, + response_method="predict_proba", + thresholds=2, + kwargs={}, + ) + scores, thresholds = _fit_and_score_over_thresholds( + classifier, + 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_allclose(scores, [0.5, 1.0]) + + +@pytest.mark.usefixtures("enable_slep006") +def test_fit_and_score_over_thresholds_sample_weight(): + """Check that we dispatch the sample-weight to fit and score the classifier.""" + X, y = load_iris(return_X_y=True) + 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 = np.ones_like(y) + sample_weight[:50] *= 2 + + classifier = LogisticRegression() + train_repeated_idx = np.arange(X_repeated.shape[0]) + val_repeated_idx = np.arange(X_repeated.shape[0]) + curve_scorer = _CurveScorer( + score_func=balanced_accuracy_score, + sign=1, + response_method="predict_proba", + thresholds=10, + kwargs={}, + ) + scores_repeated, thresholds_repeated = _fit_and_score_over_thresholds( + classifier, + X_repeated, + y_repeated, + fit_params={}, + train_idx=train_repeated_idx, + val_idx=val_repeated_idx, + curve_scorer=curve_scorer, + score_params={}, + ) + + train_idx, val_idx = np.arange(X.shape[0]), np.arange(X.shape[0]) + scores, thresholds = _fit_and_score_over_thresholds( + classifier.set_fit_request(sample_weight=True), + X, + y, + fit_params={"sample_weight": sample_weight}, + train_idx=train_idx, + val_idx=val_idx, + 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) + + +@pytest.mark.usefixtures("enable_slep006") +@pytest.mark.parametrize("fit_params_type", ["list", "array"]) +def test_fit_and_score_over_thresholds_fit_params(fit_params_type): + """Check that we pass `fit_params` to the classifier when calling `fit`.""" + X, y = make_classification(n_samples=100, random_state=0) + fit_params = { + "a": _convert_container(y, fit_params_type), + "b": _convert_container(y, fit_params_type), + } + + classifier = CheckingClassifier(expected_fit_params=["a", "b"], random_state=0) + classifier.set_fit_request(a=True, b=True) + train_idx, val_idx = np.arange(50), np.arange(50, 100) + + curve_scorer = _CurveScorer( + score_func=balanced_accuracy_score, + sign=1, + response_method="predict_proba", + thresholds=10, + kwargs={}, + ) + _fit_and_score_over_thresholds( + classifier, + X, + y, + fit_params=fit_params, + train_idx=train_idx, + val_idx=val_idx, + curve_scorer=curve_scorer, + score_params={}, + ) + + +@pytest.mark.parametrize( + "data", + [ + make_classification(n_classes=3, n_clusters_per_class=1, random_state=0), + make_multilabel_classification(random_state=0), + ], +) +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." + with pytest.raises(ValueError, match=err_msg): + TunedThresholdClassifierCV(LogisticRegression()).fit(*data) + + +@pytest.mark.parametrize( + "params, err_type, err_msg", + [ + ( + {"cv": "prefit", "refit": True}, + ValueError, + "When cv='prefit', refit cannot be True.", + ), + ( + {"cv": 10, "refit": False}, + ValueError, + "When cv has several folds, refit cannot be False.", + ), + ( + {"cv": "prefit", "refit": False}, + NotFittedError, + "`estimator` must be fitted.", + ), + ], +) +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. + """ + X, y = make_classification(n_samples=100, random_state=0) + with pytest.raises(err_type, match=err_msg): + TunedThresholdClassifierCV(LogisticRegression(), **params).fit(X, y) + + +@pytest.mark.parametrize( + "estimator", + [LogisticRegression(), SVC(), GradientBoostingClassifier(n_estimators=4)], +) +@pytest.mark.parametrize( + "response_method", ["predict_proba", "predict_log_proba", "decision_function"] +) +@pytest.mark.parametrize( + "ThresholdClassifier", [FixedThresholdClassifier, TunedThresholdClassifierCV] +) +def test_threshold_classifier_estimator_response_methods( + ThresholdClassifier, estimator, response_method +): + """Check that `TunedThresholdClassifierCV` exposes the same response methods as the + underlying estimator. + """ + X, y = make_classification(n_samples=100, random_state=0) + + model = ThresholdClassifier(estimator=estimator) + assert hasattr(model, response_method) == hasattr(estimator, response_method) + + model.fit(X, y) + 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) + + +@pytest.mark.parametrize( + "response_method", ["auto", "decision_function", "predict_proba"] +) +def test_tuned_threshold_classifier_without_constraint_value(response_method): + """Check that `TunedThresholdClassifierCV` is optimizing a given objective + metric.""" + X, y = load_breast_cancer(return_X_y=True) + # 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, + scoring="balanced_accuracy", + response_method=response_method, + thresholds=thresholds, + store_cv_results=True, + ) + 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`. + """ + X, y = load_breast_cancer(return_X_y=True) + lr = make_pipeline(StandardScaler(), LogisticRegression()).fit(X, y) + model_fbeta_1 = TunedThresholdClassifierCV( + estimator=lr, scoring=make_scorer(fbeta_score, beta=1) + ).fit(X, y) + model_fbeta_2 = TunedThresholdClassifierCV( + estimator=lr, scoring=make_scorer(fbeta_score, beta=2) + ).fit(X, y) + model_f1 = TunedThresholdClassifierCV( + estimator=lr, scoring=make_scorer(f1_score) + ).fit(X, y) + + 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_) + + +@pytest.mark.parametrize( + "response_method", ["auto", "decision_function", "predict_proba"] +) +@pytest.mark.parametrize( + "metric", + [ + make_scorer(balanced_accuracy_score), + make_scorer(f1_score, pos_label="cancer"), + ], +) +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. + """ + X, y = load_breast_cancer(return_X_y=True) + # 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] + model = TunedThresholdClassifierCV( + estimator=make_pipeline(StandardScaler(), LogisticRegression()), + scoring=metric, + response_method=response_method, + thresholds=100, + ).fit(X, y) + assert_array_equal(model.classes_, np.sort(classes)) + y_pred = model.predict(X) + assert_array_equal(np.unique(y_pred), np.sort(classes)) + + +@pytest.mark.usefixtures("enable_slep006") +@pytest.mark.parametrize("with_sample_weight", [True, False]) +def test_tuned_threshold_classifier_refit(with_sample_weight, global_random_seed): + """Check the behaviour of the `refit` parameter.""" + rng = np.random.RandomState(global_random_seed) + X, y = make_classification(n_samples=100, random_state=0) + if with_sample_weight: + sample_weight = rng.randn(X.shape[0]) + sample_weight = np.abs(sample_weight, out=sample_weight) + else: + sample_weight = None + + # 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 + estimator.fit(X, y, sample_weight=sample_weight) + 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"` + estimator = LogisticRegression().set_fit_request(sample_weight=True) + estimator.fit(X, y, sample_weight=sample_weight) + coef = estimator.coef_.copy() + model = TunedThresholdClassifierCV(estimator, cv="prefit", refit=False).fit( + X, y, sample_weight=sample_weight + ) + + 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 + estimator = LogisticRegression().set_fit_request(sample_weight=True) + cv = [ + (np.arange(50), np.arange(50, 100)), + ] # single split + model = TunedThresholdClassifierCV(estimator, cv=cv, refit=False).fit( + X, y, sample_weight=sample_weight + ) + + assert model.estimator_ is not estimator + if with_sample_weight: + sw_train = sample_weight[cv[0][0]] + else: + sw_train = None + estimator.fit(X[cv[0][0]], y[cv[0][0]], sample_weight=sw_train) + assert_allclose(model.estimator_.coef_, estimator.coef_) + + +@pytest.mark.usefixtures("enable_slep006") +@pytest.mark.parametrize("fit_params_type", ["list", "array"]) +def test_tuned_threshold_classifier_fit_params(fit_params_type): + """Check that we pass `fit_params` to the classifier when calling `fit`.""" + X, y = make_classification(n_samples=100, random_state=0) + fit_params = { + "a": _convert_container(y, fit_params_type), + "b": _convert_container(y, fit_params_type), + } + + classifier = CheckingClassifier(expected_fit_params=["a", "b"], random_state=0) + classifier.set_fit_request(a=True, b=True) + model = TunedThresholdClassifierCV(classifier) + model.fit(X, y, **fit_params) + + +@pytest.mark.usefixtures("enable_slep006") +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.""" + X, y = load_iris(return_X_y=True) + # Scale the data to avoid any convergence issue + X = StandardScaler().fit_transform(X) + # 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 = np.zeros_like(y) + sample_weight[::2] = 1 + + estimator = LogisticRegression().set_fit_request(sample_weight=True) + 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]) + + assert_allclose( + 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) + assert_allclose(y_pred_with_weights, y_pred_without_weights) + + +def test_tuned_threshold_classifier_thresholds_array(): + """Check that we can pass an array to `thresholds` and it is used as candidate + threshold internally.""" + X, y = make_classification(random_state=0) + estimator = LogisticRegression() + thresholds = np.linspace(0, 1, 11) + tuned_model = TunedThresholdClassifierCV( + estimator, + thresholds=thresholds, + response_method="predict_proba", + store_cv_results=True, + ).fit(X, y) + assert_allclose(tuned_model.cv_results_["thresholds"], thresholds) + + +@pytest.mark.parametrize("store_cv_results", [True, False]) +def test_tuned_threshold_classifier_store_cv_results(store_cv_results): + """Check that if `cv_results_` exists depending on `store_cv_results`.""" + X, y = make_classification(random_state=0) + estimator = LogisticRegression() + tuned_model = TunedThresholdClassifierCV( + estimator, store_cv_results=store_cv_results + ).fit(X, y) + 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.""" + X, y = make_classification(random_state=0) + + # 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 = LogisticRegression() + tuned_model = TunedThresholdClassifierCV( + estimator, cv=test_size, refit=False, random_state=0 + ).fit(X, y) + 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) + + assert_allclose(tuned_model.estimator_.coef_, cloned_estimator.coef_) + + +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. + """ + X, y = make_classification(random_state=0) + estimator = DummyClassifier(strategy="constant", constant=1) + tuned_model = TunedThresholdClassifierCV(estimator, response_method="predict_proba") + err_msg = "The provided estimator makes constant predictions" + with pytest.raises(ValueError, match=err_msg): + tuned_model.fit(X, y) + + +@pytest.mark.parametrize( + "response_method", ["auto", "predict_proba", "decision_function"] +) +def test_fixed_threshold_classifier_equivalence_default(response_method): + """Check that `FixedThresholdClassifier` has the same behaviour as the vanilla + classifier. + """ + X, y = make_classification(random_state=0) + classifier = LogisticRegression().fit(X, y) + 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) + + +@pytest.mark.parametrize( + "response_method, threshold", [("predict_proba", 0.7), ("decision_function", 2.0)] +) +@pytest.mark.parametrize("pos_label", [0, 1]) +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. + """ + X, y = make_classification(n_samples=50, random_state=0) + logistic_regression = LogisticRegression().fit(X, y) + model = FixedThresholdClassifier( + estimator=clone(logistic_regression), + threshold=threshold, + response_method=response_method, + pos_label=pos_label, + ).fit(X, y) + + # check that the underlying estimator is the same + assert_allclose(model.estimator_.coef_, logistic_regression.coef_) + + # emulate the response method that should take into account the `pos_label` + if response_method == "predict_proba": + y_score = model.predict_proba(X)[:, pos_label] + else: # response_method == "decision_function" + 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"): + assert_allclose( + getattr(model, method)(X), getattr(logistic_regression, method)(X) + ) + assert_allclose( + getattr(model.estimator_, method)(X), + getattr(logistic_regression, method)(X), + ) + + +@pytest.mark.usefixtures("enable_slep006") +def test_fixed_threshold_classifier_metadata_routing(): + """Check that everything works with metadata routing.""" + X, y = make_classification(random_state=0) + sample_weight = np.ones_like(y) + 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_) diff --git a/sklearn/utils/_mocking.py b/sklearn/utils/_mocking.py index 16acabf03755b..0afed8c08cfaa 100644 --- a/sklearn/utils/_mocking.py +++ b/sklearn/utils/_mocking.py @@ -3,7 +3,13 @@ from ..base import BaseEstimator, ClassifierMixin from ..utils._metadata_requests import RequestMethod from .metaestimators import available_if -from .validation import _check_sample_weight, _num_samples, check_array, check_is_fitted +from .validation import ( + _check_sample_weight, + _num_samples, + check_array, + check_is_fitted, + check_random_state, +) class ArraySlicingWrapper: @@ -133,6 +139,7 @@ def __init__( foo_param=0, expected_sample_weight=None, expected_fit_params=None, + random_state=None, ): self.check_y = check_y self.check_y_params = check_y_params @@ -142,6 +149,7 @@ def __init__( self.foo_param = foo_param self.expected_sample_weight = expected_sample_weight self.expected_fit_params = expected_fit_params + self.random_state = random_state def _check_X_y(self, X, y=None, should_be_fitted=True): """Validate X and y and make extra check. @@ -243,7 +251,8 @@ def predict(self, X): """ if self.methods_to_check == "all" or "predict" in self.methods_to_check: X, y = self._check_X_y(X) - return self.classes_[np.zeros(_num_samples(X), dtype=int)] + rng = check_random_state(self.random_state) + return rng.choice(self.classes_, size=_num_samples(X)) def predict_proba(self, X): """Predict probabilities for each class. @@ -263,8 +272,10 @@ def predict_proba(self, X): """ if self.methods_to_check == "all" or "predict_proba" in self.methods_to_check: X, y = self._check_X_y(X) - proba = np.zeros((_num_samples(X), len(self.classes_))) - proba[:, 0] = 1 + rng = check_random_state(self.random_state) + proba = rng.randn(_num_samples(X), len(self.classes_)) + proba = np.abs(proba, out=proba) + proba /= np.sum(proba, axis=1)[:, np.newaxis] return proba def decision_function(self, X): @@ -286,14 +297,13 @@ def decision_function(self, X): or "decision_function" in self.methods_to_check ): X, y = self._check_X_y(X) + rng = check_random_state(self.random_state) if len(self.classes_) == 2: # for binary classifier, the confidence score is related to # classes_[1] and therefore should be null. - return np.zeros(_num_samples(X)) + return rng.randn(_num_samples(X)) else: - decision = np.zeros((_num_samples(X), len(self.classes_))) - decision[:, 0] = 1 - return decision + return rng.randn(_num_samples(X), len(self.classes_)) def score(self, X=None, Y=None): """Fake score. diff --git a/sklearn/utils/_response.py b/sklearn/utils/_response.py index 0207cc1205120..0381c872a94b0 100644 --- a/sklearn/utils/_response.py +++ b/sklearn/utils/_response.py @@ -243,7 +243,9 @@ def _get_response_values( return y_pred, pos_label -def _get_response_values_binary(estimator, X, response_method, pos_label=None): +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. Parameters @@ -266,6 +268,12 @@ def _get_response_values_binary(estimator, X, response_method, pos_label=None): the metrics. By default, `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. + + .. versionadded:: 1.5 + Returns ------- y_pred : ndarray of shape (n_samples,) @@ -275,6 +283,12 @@ def _get_response_values_binary(estimator, X, response_method, pos_label=None): pos_label : int, float, bool or str The class considered as the positive class when computing the metrics. + + response_method_used : str + The response method used to compute the response values. Only returned + if `return_response_method_used` is `True`. + + .. versionadded:: 1.5 """ classification_error = "Expected 'estimator' to be a binary classifier." @@ -296,4 +310,5 @@ def _get_response_values_binary(estimator, X, response_method, pos_label=None): X, response_method, pos_label=pos_label, + return_response_method_used=return_response_method_used, ) diff --git a/sklearn/utils/tests/test_mocking.py b/sklearn/utils/tests/test_mocking.py index 9c66d1345bb6d..bd143855e6dcd 100644 --- a/sklearn/utils/tests/test_mocking.py +++ b/sklearn/utils/tests/test_mocking.py @@ -1,6 +1,6 @@ import numpy as np import pytest -from numpy.testing import assert_allclose, assert_array_equal +from numpy.testing import assert_array_equal from scipy import sparse from sklearn.datasets import load_iris @@ -90,7 +90,7 @@ def test_checking_classifier(iris, input_type): assert clf.n_features_in_ == 4 y_pred = clf.predict(X) - assert_array_equal(y_pred, np.zeros(y_pred.size, dtype=int)) + assert all(pred in clf.classes_ for pred in y_pred) assert clf.score(X) == pytest.approx(0) clf.set_params(foo_param=10) @@ -98,13 +98,10 @@ def test_checking_classifier(iris, input_type): y_proba = clf.predict_proba(X) assert y_proba.shape == (150, 3) - assert_allclose(y_proba[:, 0], 1) - assert_allclose(y_proba[:, 1:], 0) + assert np.logical_and(y_proba >= 0, y_proba <= 1).all() y_decision = clf.decision_function(X) assert y_decision.shape == (150, 3) - assert_allclose(y_decision[:, 0], 1) - assert_allclose(y_decision[:, 1:], 0) # check the shape in case of binary classification first_2_classes = np.logical_or(y == 0, y == 1) @@ -114,12 +111,10 @@ def test_checking_classifier(iris, input_type): y_proba = clf.predict_proba(X) assert y_proba.shape == (100, 2) - assert_allclose(y_proba[:, 0], 1) - assert_allclose(y_proba[:, 1], 0) + assert np.logical_and(y_proba >= 0, y_proba <= 1).all() y_decision = clf.decision_function(X) assert y_decision.shape == (100,) - assert_allclose(y_decision, 0) @pytest.mark.parametrize("csr_container", CSR_CONTAINERS) diff --git a/sklearn/utils/tests/test_response.py b/sklearn/utils/tests/test_response.py index c84bf6030336a..858c16cca4df1 100644 --- a/sklearn/utils/tests/test_response.py +++ b/sklearn/utils/tests/test_response.py @@ -240,36 +240,60 @@ def test_get_response_error(estimator, X, y, err_msg, params): _get_response_values_binary(estimator, X, **params) -def test_get_response_predict_proba(): +@pytest.mark.parametrize("return_response_method_used", [True, False]) +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) - y_proba, pos_label = _get_response_values_binary( - classifier, X_binary, response_method="predict_proba" + results = _get_response_values_binary( + classifier, + X_binary, + response_method="predict_proba", + return_response_method_used=return_response_method_used, ) - assert_allclose(y_proba, classifier.predict_proba(X_binary)[:, 1]) - assert pos_label == 1 + assert_allclose(results[0], classifier.predict_proba(X_binary)[:, 1]) + assert results[1] == 1 + if return_response_method_used: + assert results[2] == "predict_proba" - y_proba, pos_label = _get_response_values_binary( - classifier, X_binary, response_method="predict_proba", pos_label=0 + results = _get_response_values_binary( + classifier, + X_binary, + response_method="predict_proba", + pos_label=0, + return_response_method_used=return_response_method_used, ) - assert_allclose(y_proba, classifier.predict_proba(X_binary)[:, 0]) - assert pos_label == 0 + assert_allclose(results[0], classifier.predict_proba(X_binary)[:, 0]) + assert results[1] == 0 + if return_response_method_used: + assert results[2] == "predict_proba" -def test_get_response_decision_function(): +@pytest.mark.parametrize("return_response_method_used", [True, False]) +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) - y_score, pos_label = _get_response_values_binary( - classifier, X_binary, response_method="decision_function" + results = _get_response_values_binary( + classifier, + X_binary, + response_method="decision_function", + return_response_method_used=return_response_method_used, ) - assert_allclose(y_score, classifier.decision_function(X_binary)) - assert pos_label == 1 + assert_allclose(results[0], classifier.decision_function(X_binary)) + assert results[1] == 1 + if return_response_method_used: + assert results[2] == "decision_function" - y_score, pos_label = _get_response_values_binary( - classifier, X_binary, response_method="decision_function", pos_label=0 + results = _get_response_values_binary( + classifier, + X_binary, + response_method="decision_function", + pos_label=0, + return_response_method_used=return_response_method_used, ) - assert_allclose(y_score, classifier.decision_function(X_binary) * -1) - assert pos_label == 0 + assert_allclose(results[0], classifier.decision_function(X_binary) * -1) + assert results[1] == 0 + if return_response_method_used: + assert results[2] == "decision_function" @pytest.mark.parametrize( From a5cf4cea0f753dcf649520f68c0c0f27d73dc1d4 Mon Sep 17 00:00:00 2001 From: Emad Izadifar <81484391+e-izdfr@users.noreply.github.com> Date: Sat, 4 May 2024 19:12:48 +0330 Subject: [PATCH 087/344] Fixed two typos. (#28948) --- sklearn/metrics/_ranking.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sklearn/metrics/_ranking.py b/sklearn/metrics/_ranking.py index e31b06a926dd1..6a53fb542fd32 100644 --- a/sklearn/metrics/_ranking.py +++ b/sklearn/metrics/_ranking.py @@ -1672,7 +1672,7 @@ def dcg_score( -------- >>> import numpy as np >>> from sklearn.metrics import dcg_score - >>> # we have groud-truth relevance of some answers to a query: + >>> # 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]]) @@ -1832,7 +1832,7 @@ def ndcg_score(y_true, y_score, *, k=None, sample_weight=None, ignore_ties=False -------- >>> import numpy as np >>> from sklearn.metrics import ndcg_score - >>> # we have groud-truth relevance of some answers to a query: + >>> # we have ground-truth relevance of some answers to a query: >>> true_relevance = np.asarray([[10, 0, 0, 1, 5]]) >>> # we predict some scores (relevance) for the answers >>> scores = np.asarray([[.1, .2, .3, 4, 70]]) From 17a7575532518e0124f567f7e4af9fde6b6b0c8f Mon Sep 17 00:00:00 2001 From: scikit-learn-bot Date: Mon, 6 May 2024 10:59:57 +0200 Subject: [PATCH 088/344] :lock: :robot: CI Update lock files for cirrus-arm CI build(s) :lock: :robot: (#28954) --- ...pymin_conda_forge_linux-aarch64_conda.lock | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/build_tools/cirrus/pymin_conda_forge_linux-aarch64_conda.lock b/build_tools/cirrus/pymin_conda_forge_linux-aarch64_conda.lock index d9fa69b319d28..585a75c078d8c 100644 --- a/build_tools/cirrus/pymin_conda_forge_linux-aarch64_conda.lock +++ b/build_tools/cirrus/pymin_conda_forge_linux-aarch64_conda.lock @@ -4,17 +4,17 @@ @EXPLICIT https://conda.anaconda.org/conda-forge/linux-aarch64/ca-certificates-2024.2.2-hcefe29a_0.conda#57c226edb90c4e973b9b7503537dd339 https://conda.anaconda.org/conda-forge/linux-aarch64/ld_impl_linux-aarch64-2.40-hba4e955_0.conda#b55c1cb33c63d23b542fa53f24541e56 -https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-ng-13.2.0-h9a76618_5.conda#1b79d37dce0fad96bdf3de03925f43b4 +https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-ng-13.2.0-h3f4de04_6.conda#dfe2ae16945dc08f163307a6bb3e70e0 https://conda.anaconda.org/conda-forge/linux-aarch64/python_abi-3.9-4_cp39.conda#c191905a08694e4a5cb1238e90233878 https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda#161081fc7cec0bfda0d86d7cb595f8d8 https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-2_kmp_llvm.tar.bz2#98a1185182fec3c434069fa74e6473d6 -https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-ng-13.2.0-hf8544c7_5.conda#dee934e640275d9e74e7bbd455f25162 +https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-ng-13.2.0-he277a41_6.conda#5ca8651e635390d41004c847f03c2d3c https://conda.anaconda.org/conda-forge/linux-aarch64/bzip2-1.0.8-h31becfc_5.conda#a64e35f01e0b7a2a152eca87d33b9c87 https://conda.anaconda.org/conda-forge/linux-aarch64/lerc-4.0.0-h4de3ea5_0.tar.bz2#1a0ffc65e03ce81559dbcb0695ad1476 https://conda.anaconda.org/conda-forge/linux-aarch64/libbrotlicommon-1.1.0-h31becfc_1.conda#1b219fd801eddb7a94df5bd001053ad9 https://conda.anaconda.org/conda-forge/linux-aarch64/libdeflate-1.20-h31becfc_0.conda#018592a3d691662f451f89d0de474a20 https://conda.anaconda.org/conda-forge/linux-aarch64/libffi-3.4.2-h3557bc0_5.tar.bz2#dddd85f4d52121fab0a8b099c5e06501 -https://conda.anaconda.org/conda-forge/linux-aarch64/libgfortran5-13.2.0-h582850c_5.conda#547486aac825d236de3beecb927b389c +https://conda.anaconda.org/conda-forge/linux-aarch64/libgfortran5-13.2.0-h87d9d71_6.conda#a3fdb6378e561e73c735ec30207daa15 https://conda.anaconda.org/conda-forge/linux-aarch64/libjpeg-turbo-3.0.0-h31becfc_1.conda#ed24e702928be089d9ba3f05618515c6 https://conda.anaconda.org/conda-forge/linux-aarch64/libnsl-2.0.1-h31becfc_0.conda#c14f32510f694e3185704d89967ec422 https://conda.anaconda.org/conda-forge/linux-aarch64/libuuid-2.38.1-hb4cce97_0.conda#000e30b09db0b7c775b21695dff30969 @@ -23,26 +23,26 @@ https://conda.anaconda.org/conda-forge/linux-aarch64/libxcrypt-4.4.36-h31becfc_1 https://conda.anaconda.org/conda-forge/linux-aarch64/libzlib-1.2.13-h31becfc_5.conda#b213aa87eea9491ef7b129179322e955 https://conda.anaconda.org/conda-forge/linux-aarch64/ncurses-6.4.20240210-h0425590_0.conda#c1a1612ddaee95c83abfa0b2ec858626 https://conda.anaconda.org/conda-forge/linux-aarch64/ninja-1.12.0-h2a328a1_0.conda#c0f3f508baf69c8db8142466beaa0ccc -https://conda.anaconda.org/conda-forge/linux-aarch64/openssl-3.2.1-h31becfc_1.conda#e95eb18d256edc72058e0dc9be5338a0 +https://conda.anaconda.org/conda-forge/linux-aarch64/openssl-3.3.0-h31becfc_0.conda#36ca60a3afaf2ea2c460daeebd67430e https://conda.anaconda.org/conda-forge/linux-aarch64/pthread-stubs-0.4-hb9de7d4_1001.tar.bz2#d0183ec6ce0b5aaa3486df25fa5f0ded https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxau-1.0.11-h31becfc_0.conda#13de34f69cb73165dbe08c1e9148bedb https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxdmcp-1.1.3-h3557bc0_0.tar.bz2#a6c9016ae1ca5c47a3603ed4cd65fedd https://conda.anaconda.org/conda-forge/linux-aarch64/xz-5.2.6-h9cdd2b7_0.tar.bz2#83baad393a31d59c20b63ba4da6592df https://conda.anaconda.org/conda-forge/linux-aarch64/libbrotlidec-1.1.0-h31becfc_1.conda#8db7cff89510bec0b863a0a8ee6a7bce https://conda.anaconda.org/conda-forge/linux-aarch64/libbrotlienc-1.1.0-h31becfc_1.conda#ad3d3a826b5848d99936e4466ebbaa26 -https://conda.anaconda.org/conda-forge/linux-aarch64/libgfortran-ng-13.2.0-he9431aa_5.conda#fab7c6a8c84492e18cbe578820e97a56 +https://conda.anaconda.org/conda-forge/linux-aarch64/libgfortran-ng-13.2.0-he9431aa_6.conda#c8ab19934c000ea8cc9cf1fc6c2aa83d https://conda.anaconda.org/conda-forge/linux-aarch64/libpng-1.6.43-h194ca79_0.conda#1123e504d9254dd9494267ab9aba95f0 https://conda.anaconda.org/conda-forge/linux-aarch64/libsqlite-3.45.3-h194ca79_0.conda#fb35b8afbe9e92467ac7b5608d60b775 https://conda.anaconda.org/conda-forge/linux-aarch64/libxcb-1.15-h2a766a3_0.conda#eb3d8c8170e3d03f2564ed2024aa00c8 https://conda.anaconda.org/conda-forge/linux-aarch64/readline-8.2-h8fc344f_1.conda#105eb1e16bf83bfb2eb380a48032b655 https://conda.anaconda.org/conda-forge/linux-aarch64/tk-8.6.13-h194ca79_0.conda#f75105e0585851f818e0009dd1dde4dc -https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.5-h4c53e97_0.conda#b74eb9dbb5c3c15cb3cee7cbdf198c75 +https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.6-h02f22dd_0.conda#be8d5f8cf21aed237b8b182ea86b3dd6 https://conda.anaconda.org/conda-forge/linux-aarch64/brotli-bin-1.1.0-h31becfc_1.conda#9e4a13596ab651ea8d77aae023d0ce3f https://conda.anaconda.org/conda-forge/linux-aarch64/freetype-2.12.1-hf0a5ef3_2.conda#a5ab74c5bd158c3d5532b66d8d83d907 https://conda.anaconda.org/conda-forge/linux-aarch64/libhiredis-1.0.2-h05efe27_0.tar.bz2#a87f068744fd20334cd41489eb163bee https://conda.anaconda.org/conda-forge/linux-aarch64/libopenblas-0.3.27-pthreads_h5a5ec62_0.conda#ffecca8f4f31cd50b92c0e6e6bfe4416 https://conda.anaconda.org/conda-forge/linux-aarch64/libtiff-4.6.0-hf980d43_3.conda#b6f3abf5726ae33094bee238b4eb492f -https://conda.anaconda.org/conda-forge/linux-aarch64/llvm-openmp-18.1.3-h8b0cb96_0.conda#cd4d2b7580dd020814ea34ebbbca8c5e +https://conda.anaconda.org/conda-forge/linux-aarch64/llvm-openmp-18.1.4-h767c9be_0.conda#2572130272fb725d825c9b52e5ce096b https://conda.anaconda.org/conda-forge/linux-aarch64/python-3.9.19-h4ac3b42_0_cpython.conda#1501507cd9451472ec8900d587ce872f https://conda.anaconda.org/conda-forge/linux-aarch64/brotli-1.1.0-h31becfc_1.conda#e41f5862ac746428407f3fd44d2ed01f https://conda.anaconda.org/conda-forge/linux-aarch64/ccache-4.9.1-h6552966_0.conda#758b202f61f6bbfd2c6adf0fde043276 @@ -64,7 +64,7 @@ https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_0.conda#d3 https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.1.2-pyhd8ed1ab_0.conda#b9a4dacf97241704529131a0dfc0494f https://conda.anaconda.org/conda-forge/noarch/setuptools-69.5.1-pyhd8ed1ab_0.conda#7462280d81f639363e6e63c81276bd9e https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2#e5f25f8dbc060e9a8d912e432202afc2 -https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.4.0-pyhc1e730c_0.conda#b296278eef667c673bf51de6535bad88 +https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.5.0-pyhc1e730c_0.conda#df68d78237980a159bd7149f33c0e8fd https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2#5844808ffab9ebdb694585b50ba02a96 https://conda.anaconda.org/conda-forge/linux-aarch64/tornado-6.4-py39h7cc1d5f_0.conda#2c06a653ebfa389c18aea2d8f338df3b https://conda.anaconda.org/conda-forge/linux-aarch64/unicodedata2-15.1.0-py39h898b7ef_0.conda#8c072c9329aeea97a46005625267a851 @@ -72,7 +72,7 @@ https://conda.anaconda.org/conda-forge/noarch/wheel-0.43.0-pyhd8ed1ab_1.conda#0b https://conda.anaconda.org/conda-forge/noarch/zipp-3.17.0-pyhd8ed1ab_0.conda#2e4d6bc0b14e10f895fc6791a7d9b26a https://conda.anaconda.org/conda-forge/linux-aarch64/fonttools-4.51.0-py39h898b7ef_0.conda#7b6a069c66a729454fb4c534ed145dcd https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.4.0-pyhd8ed1ab_0.conda#c5d3907ad8bd7bf557521a1833cf7e6d -https://conda.anaconda.org/conda-forge/noarch/joblib-1.4.0-pyhd8ed1ab_0.conda#e0ed1bf13ce3a440e022157bf4764465 +https://conda.anaconda.org/conda-forge/noarch/joblib-1.4.2-pyhd8ed1ab_0.conda#25df261d4523d9f9783bcdb7208d872f https://conda.anaconda.org/conda-forge/linux-aarch64/libcblas-3.9.0-22_linuxaarch64_openblas.conda#fbe7fe553f2cc78a0311e009b26f180d https://conda.anaconda.org/conda-forge/linux-aarch64/liblapack-3.9.0-22_linuxaarch64_openblas.conda#8c709d281609792c39b1d5c0241f90f1 https://conda.anaconda.org/conda-forge/noarch/meson-1.4.0-pyhd8ed1ab_0.conda#52a0660cfa40b45bf254ecc3374cb2e0 @@ -88,7 +88,7 @@ https://conda.anaconda.org/conda-forge/linux-aarch64/numpy-1.26.4-py39h91c28bb_0 https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.5.0-pyhd8ed1ab_0.conda#d5f595da2daead898ca958ac62f0307b https://conda.anaconda.org/conda-forge/linux-aarch64/blas-devel-3.9.0-22_linuxaarch64_openblas.conda#a5b77b6c6807661afd716f33e85814b3 https://conda.anaconda.org/conda-forge/linux-aarch64/contourpy-1.2.1-py39hd16970a_0.conda#66b9718539ecdd38876b0176c315bcad -https://conda.anaconda.org/conda-forge/linux-aarch64/scipy-1.13.0-py39h91c28bb_0.conda#2b6f1ed053a61c2447304e4b810fc397 +https://conda.anaconda.org/conda-forge/linux-aarch64/scipy-1.13.0-py39hb921187_1.conda#2717303c0d13a5646308b3763bf4daa4 https://conda.anaconda.org/conda-forge/linux-aarch64/blas-2.122-openblas.conda#65bc48b3bc85f8eeeab54311443a83aa https://conda.anaconda.org/conda-forge/linux-aarch64/matplotlib-base-3.8.4-py39h8e43113_0.conda#f397ddfe5c551732de61a92106a14cf3 https://conda.anaconda.org/conda-forge/linux-aarch64/matplotlib-3.8.4-py39ha65689a_0.conda#d501bb96ff505fdd431fd8fdac8efbf9 From ac09b7bd4d21c8fcf35d094cb0cd43e9b0d5fbd4 Mon Sep 17 00:00:00 2001 From: scikit-learn-bot Date: Mon, 6 May 2024 11:01:04 +0200 Subject: [PATCH 089/344] :lock: :robot: CI Update lock files for scipy-dev CI build(s) :lock: :robot: (#28955) --- ...pylatest_pip_scipy_dev_linux-64_conda.lock | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/build_tools/azure/pylatest_pip_scipy_dev_linux-64_conda.lock b/build_tools/azure/pylatest_pip_scipy_dev_linux-64_conda.lock index dd70d9af4d30a..8324d1edb36b7 100644 --- a/build_tools/azure/pylatest_pip_scipy_dev_linux-64_conda.lock +++ b/build_tools/azure/pylatest_pip_scipy_dev_linux-64_conda.lock @@ -10,27 +10,27 @@ https://repo.anaconda.com/pkgs/main/linux-64/libgomp-11.2.0-h1234567_1.conda#b37 https://repo.anaconda.com/pkgs/main/linux-64/libstdcxx-ng-11.2.0-h1234567_1.conda#57623d10a70e09e1d048c2b2b6f4e2dd https://repo.anaconda.com/pkgs/main/linux-64/_openmp_mutex-5.1-1_gnu.conda#71d281e9c2192cb3fa425655a8defb85 https://repo.anaconda.com/pkgs/main/linux-64/libgcc-ng-11.2.0-h1234567_1.conda#a87728dabf3151fb9cfa990bd2eb0464 -https://repo.anaconda.com/pkgs/main/linux-64/bzip2-1.0.8-h5eee18b_5.conda#9c8dec113089c4aca7392c6a3864f505 +https://repo.anaconda.com/pkgs/main/linux-64/bzip2-1.0.8-h5eee18b_6.conda#f21a3ff51c1b271977f53ce956a69297 https://repo.anaconda.com/pkgs/main/linux-64/expat-2.6.2-h6a678d5_0.conda#55049db2772dae035f6b8a95f72b5970 -https://repo.anaconda.com/pkgs/main/linux-64/libffi-3.4.4-h6a678d5_0.conda#06e288f9250abef59b9a367d151fc339 +https://repo.anaconda.com/pkgs/main/linux-64/libffi-3.4.4-h6a678d5_1.conda#70646cc713f0c43926cfdcfe9b695fe0 https://repo.anaconda.com/pkgs/main/linux-64/libuuid-1.41.5-h5eee18b_0.conda#4a6a2354414c9080327274aa514e5299 https://repo.anaconda.com/pkgs/main/linux-64/ncurses-6.4-h6a678d5_0.conda#5558eec6e2191741a92f832ea826251c -https://repo.anaconda.com/pkgs/main/linux-64/openssl-3.0.13-h7f8727e_0.conda#c73d46a4d666da0ae3dcd3fd8f805122 -https://repo.anaconda.com/pkgs/main/linux-64/xz-5.4.6-h5eee18b_0.conda#81a9916f581d4da15a3839216a487c66 -https://repo.anaconda.com/pkgs/main/linux-64/zlib-1.2.13-h5eee18b_0.conda#333e31fbfbb5057c92fa845ad6adef93 +https://repo.anaconda.com/pkgs/main/linux-64/openssl-3.0.13-h7f8727e_1.conda#d1d1fc47640fe0d9f7fa64c0a054bfd8 +https://repo.anaconda.com/pkgs/main/linux-64/xz-5.4.6-h5eee18b_1.conda#1562802f843297ee776a50b9329597ed +https://repo.anaconda.com/pkgs/main/linux-64/zlib-1.2.13-h5eee18b_1.conda#92e42d8310108b0a440fb2e60b2b2a25 https://repo.anaconda.com/pkgs/main/linux-64/ccache-3.7.9-hfe4627d_0.conda#bef6fc681c273bb7bd0c67d1a591365e https://repo.anaconda.com/pkgs/main/linux-64/readline-8.2-h5eee18b_0.conda#be42180685cce6e6b0329201d9f48efb https://repo.anaconda.com/pkgs/main/linux-64/tk-8.6.12-h1ccaba5_0.conda#fa10ff4aa631fa4aa090a6234d7770b9 -https://repo.anaconda.com/pkgs/main/linux-64/sqlite-3.41.2-h5eee18b_0.conda#c7086c9ceb6cfe1c4c729a774a2d88a5 +https://repo.anaconda.com/pkgs/main/linux-64/sqlite-3.45.3-h5eee18b_0.conda#acf93d6aceb74d6110e20b44cc45939e https://repo.anaconda.com/pkgs/main/linux-64/python-3.12.3-h996f2a0_0.conda#77af2bd351a8311d1e780bcfa7819bb8 https://repo.anaconda.com/pkgs/main/linux-64/setuptools-68.2.2-py312h06a4308_0.conda#83ba634cde4f30d9e0b88e4ac9716ca4 -https://repo.anaconda.com/pkgs/main/linux-64/wheel-0.41.2-py312h06a4308_0.conda#b2c4f82880d58d679f3982370d80c0e2 +https://repo.anaconda.com/pkgs/main/linux-64/wheel-0.43.0-py312h06a4308_0.conda#18d5f3b68a175c72576876db4afc9e9e https://repo.anaconda.com/pkgs/main/linux-64/pip-23.3.1-py312h06a4308_0.conda#e1d44bca4a257e84af33503233491107 # pip alabaster @ https://files.pythonhosted.org/packages/32/34/d4e1c02d3bee589efb5dfa17f88ea08bdb3e3eac12bc475462aec52ed223/alabaster-0.7.16-py3-none-any.whl#sha256=b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92 -# pip babel @ https://files.pythonhosted.org/packages/0d/35/4196b21041e29a42dc4f05866d0c94fa26c9da88ce12c38c2265e42c82fb/Babel-2.14.0-py3-none-any.whl#sha256=efb1a25b7118e67ce3a259bed20545c29cb68be8ad2c784c83689981b7a57287 +# pip babel @ https://files.pythonhosted.org/packages/27/45/377f7e32a5c93d94cd56542349b34efab5ca3f9e2fd5a68c5e93169aa32d/Babel-2.15.0-py3-none-any.whl#sha256=08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb # pip certifi @ https://files.pythonhosted.org/packages/ba/06/a07f096c664aeb9f01624f858c3add0a4e913d6c96257acb4fce61e7de14/certifi-2024.2.2-py3-none-any.whl#sha256=dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1 # pip charset-normalizer @ https://files.pythonhosted.org/packages/ee/fb/14d30eb4956408ee3ae09ad34299131fb383c47df355ddb428a7331cfa1e/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b -# pip coverage @ https://files.pythonhosted.org/packages/fa/d9/ec4ba0913195d240d026670d41b91f3e5b9a8a143a385f93a09e97c90f5c/coverage-7.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=adf032b6c105881f9d77fa17d9eebe0ad1f9bfb2ad25777811f97c5362aa07f2 +# pip coverage @ https://files.pythonhosted.org/packages/3f/4f/fcad903698f02ac0d7501432449db12e15fbe5ecfbc01e363eb752c65cbd/coverage-7.5.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=8748731ad392d736cc9ccac03c9845b13bb07d020a33423fa5b3a36521ac6e4e # pip docutils @ https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl#sha256=dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2 # pip execnet @ https://files.pythonhosted.org/packages/43/09/2aea36ff60d16dd8879bdb2f5b3ee0ba8d08cbbdcdfe870e695ce3784385/execnet-2.1.1-py3-none-any.whl#sha256=26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc # pip idna @ https://files.pythonhosted.org/packages/e5/3e/741d8c82801c347547f8a2a06aa57dbb1992be9e948df2ea0eda2c8b79e8/idna-3.7-py3-none-any.whl#sha256=82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0 @@ -42,7 +42,7 @@ https://repo.anaconda.com/pkgs/main/linux-64/pip-23.3.1-py312h06a4308_0.conda#e1 # pip packaging @ https://files.pythonhosted.org/packages/49/df/1fceb2f8900f8639e278b056416d49134fb8d84c5942ffaa01ad34782422/packaging-24.0-py3-none-any.whl#sha256=2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5 # pip platformdirs @ https://files.pythonhosted.org/packages/b0/15/1691fa5aaddc0c4ea4901c26f6137c29d5f6673596fe960a0340e8c308e1/platformdirs-4.2.1-py3-none-any.whl#sha256=17d5a1161b3fd67b390023cb2d3b026bbd40abde6fdb052dfbd3a29c3ba22ee1 # pip pluggy @ https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl#sha256=44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 -# pip pygments @ https://files.pythonhosted.org/packages/97/9c/372fef8377a6e340b1704768d20daaded98bf13282b5327beb2e2fe2c7ef/pygments-2.17.2-py3-none-any.whl#sha256=b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c +# pip pygments @ https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl#sha256=b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a # pip six @ https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl#sha256=8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 # pip snowballstemmer @ https://files.pythonhosted.org/packages/ed/dc/c02e01294f7265e63a7315fe086dd1df7dacb9f840a804da846b96d01b96/snowballstemmer-2.2.0-py2.py3-none-any.whl#sha256=c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a # pip sphinxcontrib-applehelp @ https://files.pythonhosted.org/packages/56/89/fea3fbf6785b388e6cb8a1beaf62f96e80b37311bdeed6e133388a732426/sphinxcontrib_applehelp-1.0.8-py3-none-any.whl#sha256=cb61eb0ec1b61f349e5cc36b2028e9e7ca765be05e49641c97241274753067b4 @@ -52,9 +52,9 @@ https://repo.anaconda.com/pkgs/main/linux-64/pip-23.3.1-py312h06a4308_0.conda#e1 # pip sphinxcontrib-qthelp @ https://files.pythonhosted.org/packages/80/b3/1beac14a88654d2e5120d0143b49be5ad450b86eb1963523d8dbdcc51eb2/sphinxcontrib_qthelp-1.0.7-py3-none-any.whl#sha256=e2ae3b5c492d58fcbd73281fbd27e34b8393ec34a073c792642cd8e529288182 # pip sphinxcontrib-serializinghtml @ https://files.pythonhosted.org/packages/38/24/228bb903ea87b9e08ab33470e6102402a644127108c7117ac9c00d849f82/sphinxcontrib_serializinghtml-1.1.10-py3-none-any.whl#sha256=326369b8df80a7d2d8d7f99aa5ac577f51ea51556ed974e7716cfd4fca3f6cb7 # pip tabulate @ https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl#sha256=024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f -# pip threadpoolctl @ https://files.pythonhosted.org/packages/1e/84/ccd9b08653022b7785b6e3ee070ffb2825841e0dc119be22f0840b2b35cb/threadpoolctl-3.4.0-py3-none-any.whl#sha256=8f4c689a65b23e5ed825c8436a92b818aac005e0f3715f6a1664d7c7ee29d262 +# pip threadpoolctl @ https://files.pythonhosted.org/packages/4b/2c/ffbf7a134b9ab11a67b0cf0726453cedd9c5043a4fe7a35d1cefa9a1bcfb/threadpoolctl-3.5.0-py3-none-any.whl#sha256=56c1e26c150397e58c4926da8eeee87533b1e32bef131bd4bf6a2f45f3185467 # pip urllib3 @ https://files.pythonhosted.org/packages/a2/73/a68704750a7679d0b6d3ad7aa8d4da8e14e151ae82e6fee774e6e0d05ec8/urllib3-2.2.1-py3-none-any.whl#sha256=450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d -# pip jinja2 @ https://files.pythonhosted.org/packages/30/6d/6de6be2d02603ab56e72997708809e8a5b0fbfee080735109b40a3564843/Jinja2-3.1.3-py3-none-any.whl#sha256=7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa +# pip jinja2 @ https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl#sha256=bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d # pip pyproject-metadata @ https://files.pythonhosted.org/packages/aa/5f/bb5970d3d04173b46c9037109f7f05fc8904ff5be073ee49bb6ff00301bc/pyproject_metadata-0.8.0-py3-none-any.whl#sha256=ad858d448e1d3a1fb408ac5bac9ea7743e7a8bbb472f2693aaa334d2db42f526 # pip pytest @ https://files.pythonhosted.org/packages/51/ff/f6e8b8f39e08547faece4bd80f89d5a8de68a38b2d179cc1c4490ffa3286/pytest-7.4.4-py3-none-any.whl#sha256=b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8 # pip python-dateutil @ https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl#sha256=a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 From 3915d1d5ab56fd38ecb37345c49055926f175c7a Mon Sep 17 00:00:00 2001 From: scikit-learn-bot Date: Mon, 6 May 2024 11:03:03 +0200 Subject: [PATCH 090/344] :lock: :robot: CI Update lock files for main CI build(s) :lock: :robot: (#28957) --- build_tools/azure/debian_atlas_32bit_lock.txt | 2 +- ...latest_conda_forge_mkl_linux-64_conda.lock | 30 ++++---- ...pylatest_conda_forge_mkl_osx-64_conda.lock | 38 +++++----- ...test_conda_mkl_no_openmp_osx-64_conda.lock | 32 ++++----- ...st_pip_openblas_pandas_linux-64_conda.lock | 26 +++---- ...onda_defaults_openblas_linux-64_conda.lock | 24 +++---- .../pymin_conda_forge_mkl_win-64_conda.lock | 24 +++---- ...e_openblas_ubuntu_2204_linux-64_conda.lock | 28 ++++---- build_tools/circle/doc_linux-64_conda.lock | 70 +++++++++---------- .../doc_min_dependencies_linux-64_conda.lock | 50 ++++++------- 10 files changed, 159 insertions(+), 165 deletions(-) diff --git a/build_tools/azure/debian_atlas_32bit_lock.txt b/build_tools/azure/debian_atlas_32bit_lock.txt index 61ad07e857cb8..7971e64b72560 100644 --- a/build_tools/azure/debian_atlas_32bit_lock.txt +++ b/build_tools/azure/debian_atlas_32bit_lock.txt @@ -6,7 +6,7 @@ # attrs==23.2.0 # via pytest -coverage==7.5.0 +coverage==7.5.1 # via pytest-cov cython==3.0.10 # via -r build_tools/azure/debian_atlas_32bit_requirements.txt diff --git a/build_tools/azure/pylatest_conda_forge_mkl_linux-64_conda.lock b/build_tools/azure/pylatest_conda_forge_mkl_linux-64_conda.lock index 3194bf106d6c2..932fc6ad670f7 100644 --- a/build_tools/azure/pylatest_conda_forge_mkl_linux-64_conda.lock +++ b/build_tools/azure/pylatest_conda_forge_mkl_linux-64_conda.lock @@ -7,15 +7,15 @@ https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2024.2.2-hbcca05 https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2#0c96522c6bdaed4b1566d11387caaf45 https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2#34893075a5c9e55cdafac56607368fc6 https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2#4d59c254e01d9cde7957100457e2d5fb -https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_1.conda#6185f640c43843e5ad6fd1c5372c3f80 +https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_2.conda#cbbe59391138ea5ad3658c76912e147f https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.40-h55db66e_0.conda#10569984e7db886e4f1abc2b47ad79a1 -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-13.2.0-h95c4c6d_6.conda#3cfab3e709f77e9f1b3d380eb622494a +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-13.2.0-hc0a3c3a_6.conda#2f18345bbc433c8a1ed887d7161e86a6 https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.11-4_cp311.conda#d786502c97404c94d7d58d258a445a65 https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda#161081fc7cec0bfda0d86d7cb595f8d8 https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2#f766549260d6815b0c52253f1fb1bb29 https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2#fee5683a3f04bd15cbd8318b096a27ab https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_kmp_llvm.tar.bz2#562b26ba2e19059551a811e72ab7f793 -https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-hc881cc4_6.conda#df88796bd09a0d2ed292e59101478ad8 +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-h77fa898_6.conda#4398809ac84d0b8c28beebaaa83277f5 https://conda.anaconda.org/conda-forge/linux-64/alsa-lib-1.2.11-hd590300_1.conda#0bb492cca54017ea314b809b1ee3a176 https://conda.anaconda.org/conda-forge/linux-64/attr-2.5.1-h166bdaf_1.tar.bz2#d9c69a24ad678ffce24c6543a0176b00 https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.9.0-hd590300_0.conda#71b89db63b5b504e7afc8ad901172e1e @@ -41,7 +41,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-13.2.0-h43f5ff8_6.c https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.17-hd590300_2.conda#d66573916ffcf376178462f1b61c941e https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.0.0-hd590300_1.conda#ea25936bb4080d843790b586850f82b8 https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda#30fd6e37fe21f86f4bd26d6ee73eeec7 -https://conda.anaconda.org/conda-forge/linux-64/libnuma-2.0.18-hd590300_0.conda#8feeecae73aeef0a2985af46b5a2c1df +https://conda.anaconda.org/conda-forge/linux-64/libnuma-2.0.18-h4ab18f5_2.conda#a263760479dbc7bc1f3df12707bd90dc https://conda.anaconda.org/conda-forge/linux-64/libogg-1.3.4-h7f98852_1.tar.bz2#6e8cc2173440d77708196c5b93771680 https://conda.anaconda.org/conda-forge/linux-64/libopus-1.3.1-h7f98852_1.tar.bz2#15345e56d527b330e1cacbdf58676e8f https://conda.anaconda.org/conda-forge/linux-64/libutf8proc-2.8.0-h166bdaf_0.tar.bz2#ede4266dc02e875fe1ea77b25dd43747 @@ -54,7 +54,7 @@ https://conda.anaconda.org/conda-forge/linux-64/mpg123-1.32.6-h59595ed_0.conda#9 https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.4.20240210-h59595ed_0.conda#97da8860a0da5413c7c98a3b3838a645 https://conda.anaconda.org/conda-forge/linux-64/ninja-1.12.0-h00ab1b0_0.conda#b048701d52e7cbb5f59ddd4d3b17bbf5 https://conda.anaconda.org/conda-forge/linux-64/nspr-4.35-h27087fc_0.conda#da0ec11a6454ae19bff5b02ed881a2b1 -https://conda.anaconda.org/conda-forge/linux-64/openssl-3.2.1-hd590300_1.conda#9d731343cff6ee2e5a25c4a091bf8e2a +https://conda.anaconda.org/conda-forge/linux-64/openssl-3.3.0-hd590300_0.conda#c0f3abb4a16477208bbd43a39bd56f18 https://conda.anaconda.org/conda-forge/linux-64/pixman-0.43.2-h59595ed_0.conda#71004cbf7924e19c02746ccde9fd7123 https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-h36c2ea0_1001.tar.bz2#22dad4df6e8630e8dff2428f6f6a7036 https://conda.anaconda.org/conda-forge/linux-64/rdma-core-28.9-h59595ed_1.conda#aeffb7c06b5f65e55e6c637408dc4100 @@ -100,7 +100,7 @@ https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.con https://conda.anaconda.org/conda-forge/linux-64/ucx-1.14.1-h64cca9d_5.conda#39aa3b356d10d7e5add0c540945a0944 https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.4-h7391055_0.conda#93ee23f12bc2e684548181256edd2cf6 https://conda.anaconda.org/conda-forge/linux-64/zlib-1.2.13-hd590300_5.conda#68c34ec6149623be41a1933ab996a209 -https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.5-hfc55251_0.conda#04b88013080254850d6c01ed54810589 +https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.6-ha6fb4c9_0.conda#4d056880988120e29d75bfff282e0f45 https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.13.32-he9a53bd_1.conda#8a24e5820f4a0ffd2ed9c4722cd5d7ca https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.0.9-h166bdaf_9.conda#d47dee1856d9cb955b8076eeff304a5b https://conda.anaconda.org/conda-forge/linux-64/freetype-2.12.1-h267a509_2.conda#9ae35c3d96db2c94ce0cef86efdfa2cb @@ -111,10 +111,10 @@ https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.54.3-hb20ce57_0.conda# https://conda.anaconda.org/conda-forge/linux-64/libhiredis-1.0.2-h2cc385e_0.tar.bz2#b34907d3a81a3cd8095ee83d174c074a https://conda.anaconda.org/conda-forge/linux-64/libhwloc-2.10.0-default_h2fb2949_1000.conda#7e3726e647a619c6ce5939014dfde86d https://conda.anaconda.org/conda-forge/linux-64/libllvm15-15.0.7-hb3ce162_4.conda#8a35df3cbc0c8b12cc8af9473ae75eef -https://conda.anaconda.org/conda-forge/linux-64/libllvm18-18.1.3-h2448989_0.conda#927b6d6e80b2c0d4405a58b61ca248a3 +https://conda.anaconda.org/conda-forge/linux-64/libllvm18-18.1.5-hb77312f_0.conda#efd221d3668077ca067a206269418dec https://conda.anaconda.org/conda-forge/linux-64/libthrift-0.18.1-h8fd135c_2.conda#bbf65f7688512872f063810623b755dc https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.6.0-h1dd3fc0_3.conda#66f03896ffbe1a110ffda05c7a856504 -https://conda.anaconda.org/conda-forge/linux-64/llvm-openmp-18.1.3-h4dfa4b3_0.conda#d39965123dffcad4d750989be65bcb7c +https://conda.anaconda.org/conda-forge/linux-64/llvm-openmp-18.1.4-ha31de31_0.conda#48b9991e66abc186a7ad7975e97bd4d0 https://conda.anaconda.org/conda-forge/linux-64/mysql-libs-8.3.0-hca2cd23_4.conda#1b50eebe2a738a3146c154d2eceaa8b6 https://conda.anaconda.org/conda-forge/linux-64/nss-3.98-h1d7d5a4_0.conda#54b56c2fdf973656b748e0378900ec13 https://conda.anaconda.org/conda-forge/linux-64/orc-1.9.0-h2f23424_1.conda#9571eb3eb0f7fe8b59956a7786babbcd @@ -142,7 +142,7 @@ https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_0.conda https://conda.anaconda.org/conda-forge/linux-64/kiwisolver-1.4.5-py311h9547e67_1.conda#2c65bdf442b0d37aad080c8a4e0d452f https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.16-hb7c19ff_0.conda#51bb7010fc86f70eee639b4bb7a894f5 https://conda.anaconda.org/conda-forge/linux-64/libclang-cpp15-15.0.7-default_h127d8a8_5.conda#d0a9633b53cdc319b8a1a532ae7822b8 -https://conda.anaconda.org/conda-forge/linux-64/libclang13-18.1.3-default_h5d6823c_0.conda#5fff487759736b275dc3e4a263cac666 +https://conda.anaconda.org/conda-forge/linux-64/libclang13-18.1.5-default_h5d6823c_0.conda#60c39a00b694c98da03f67a3ba1d7499 https://conda.anaconda.org/conda-forge/linux-64/libcups-2.3.3-h4637d8d_4.conda#d4529f4dff3057982a7617c7ac58fde3 https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.7.1-hca28451_0.conda#755c7f876815003337d2c61ff5d047e5 https://conda.anaconda.org/conda-forge/linux-64/libflac-1.4.3-h59595ed_0.conda#ee48bf17cc83a00f59ca1494d5646869 @@ -159,7 +159,7 @@ https://conda.anaconda.org/conda-forge/noarch/pytz-2024.1-pyhd8ed1ab_0.conda#3ee https://conda.anaconda.org/conda-forge/noarch/setuptools-69.5.1-pyhd8ed1ab_0.conda#7462280d81f639363e6e63c81276bd9e https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2#e5f25f8dbc060e9a8d912e432202afc2 https://conda.anaconda.org/conda-forge/linux-64/tbb-2021.12.0-h00ab1b0_0.conda#f1b776cff1b426e7e7461a8502a3b731 -https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.4.0-pyhc1e730c_0.conda#b296278eef667c673bf51de6535bad88 +https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.5.0-pyhc1e730c_0.conda#df68d78237980a159bd7149f33c0e8fd https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_0.tar.bz2#f832c45a477c78bebd107098db465095 https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2#5844808ffab9ebdb694585b50ba02a96 https://conda.anaconda.org/conda-forge/linux-64/tornado-6.4-py311h459d7ec_0.conda#cc7727006191b8f3630936b339a76cd0 @@ -172,10 +172,10 @@ https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.11-hd590300_ https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.7.3-h28f7589_1.conda#97503d3e565004697f1651753aa95b9e https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.9.3-hb447be9_1.conda#c520669eb0be9269a5f0d8ef62531882 https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.0-h3faef2a_0.conda#f907bb958910dc404647326ca80c263e -https://conda.anaconda.org/conda-forge/linux-64/coverage-7.5.0-py311h331c9d8_0.conda#5420e3594638adf670fca1a601d7efb9 +https://conda.anaconda.org/conda-forge/linux-64/coverage-7.5.1-py311h331c9d8_0.conda#9f35e13e3b9e05e153b78f42662061f6 https://conda.anaconda.org/conda-forge/linux-64/fonttools-4.51.0-py311h459d7ec_0.conda#17e1997cc17c571d5ad27bd0159f616c https://conda.anaconda.org/conda-forge/linux-64/glib-2.80.0-hf2295e7_6.conda#a1e026a82a562b443845db5614ca568a -https://conda.anaconda.org/conda-forge/noarch/joblib-1.4.0-pyhd8ed1ab_0.conda#e0ed1bf13ce3a440e022157bf4764465 +https://conda.anaconda.org/conda-forge/noarch/joblib-1.4.2-pyhd8ed1ab_0.conda#25df261d4523d9f9783bcdb7208d872f https://conda.anaconda.org/conda-forge/linux-64/libgcrypt-1.10.3-hd590300_0.conda#32d16ad533c59bb0a3c5ffaf16110829 https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-2.12.0-hac9eb74_1.conda#0dee716254497604762957076ac76540 https://conda.anaconda.org/conda-forge/linux-64/libsndfile-1.2.2-hc60ed4a_1.conda#ef1910918dd895516a769ed36b5b3a4e @@ -190,7 +190,7 @@ https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0-pyhd8ed1ab_0 https://conda.anaconda.org/conda-forge/linux-64/sip-6.7.12-py311hb755f60_0.conda#02336abab4cb5dd794010ef53c54bd09 https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.3.14-hf3aad02_1.conda#a968ffa7e9fe0c257628033d393e512f https://conda.anaconda.org/conda-forge/linux-64/blas-1.0-mkl.tar.bz2#349aef876b1d8c9dccae01de20d5b385 -https://conda.anaconda.org/conda-forge/linux-64/gstreamer-1.24.1-h98fc4e7_1.conda#b04b5cdf3ba01430db27979250bc5a1d +https://conda.anaconda.org/conda-forge/linux-64/gstreamer-1.24.3-haf2f30d_0.conda#f3df87cc9ef0b5113bff55aefcbcafd5 https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-8.4.0-h3d44ed6_0.conda#27f46291a6aaa3c2a4f798ebd35a7ddb https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-16_linux64_mkl.tar.bz2#85f61af03fd291dae33150ffe89dc09a https://conda.anaconda.org/conda-forge/linux-64/libsystemd0-255-h3516f8a_1.conda#3366af27f0b593544a6cd453c7932ac5 @@ -199,7 +199,7 @@ https://conda.anaconda.org/conda-forge/linux-64/pyqt5-sip-12.12.2-py311hb755f60_ https://conda.anaconda.org/conda-forge/noarch/pytest-cov-5.0.0-pyhd8ed1ab_0.conda#c54c0107057d67ddf077751339ec2c63 https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.5.0-pyhd8ed1ab_0.conda#d5f595da2daead898ca958ac62f0307b https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.21.0-hb942446_5.conda#07d92ed5403ad7b5c66ffd7d5b8f7e57 -https://conda.anaconda.org/conda-forge/linux-64/gst-plugins-base-1.24.1-hfa15dee_1.conda#a6dd2bbc684913e2bef0a54ce56fcbfb +https://conda.anaconda.org/conda-forge/linux-64/gst-plugins-base-1.24.3-h9ad1361_0.conda#8fb0e954c616bb0f9389efac4b4ed44b https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-16_linux64_mkl.tar.bz2#361bf757b95488de76c4f123805742d3 https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-16_linux64_mkl.tar.bz2#a2f166748917d6d6e4707841ca1f519e https://conda.anaconda.org/conda-forge/linux-64/pulseaudio-client-17.0-hb77b528_0.conda#07f45f1be1c25345faddb8db0de8039b @@ -213,7 +213,7 @@ https://conda.anaconda.org/conda-forge/linux-64/pandas-2.2.2-py311h320fe9a_0.con https://conda.anaconda.org/conda-forge/linux-64/polars-0.20.23-py311h00856b1_0.conda#c000e1629d890ad00bb8c20963028d9f https://conda.anaconda.org/conda-forge/linux-64/pyqt-5.15.9-py311hf0fb5b6_5.conda#ec7e45bc76d9d0b69a74a2075932b8e8 https://conda.anaconda.org/conda-forge/linux-64/pytorch-1.13.1-cpu_py311h410fd25_1.conda#ddd2fadddf89e3dc3d541a2537fce010 -https://conda.anaconda.org/conda-forge/linux-64/scipy-1.13.0-py311h64a7726_0.conda#d443c70b4a05f50236c70b9c79beff64 +https://conda.anaconda.org/conda-forge/linux-64/scipy-1.13.0-py311h517d4fd_1.conda#a86b8bea39e292a23b2cf9a750f49ea1 https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.8.4-py311h54ef318_0.conda#150186110f111b458f86c04361351337 https://conda.anaconda.org/conda-forge/linux-64/pyamg-5.1.0-py311h92ebd52_0.conda#2d415a805458e93fcf5551760fd2d287 https://conda.anaconda.org/conda-forge/linux-64/pyarrow-12.0.1-py311h39c9aba_8_cpu.conda#587370a25bb2c50cce90909ce20d38b8 diff --git a/build_tools/azure/pylatest_conda_forge_mkl_osx-64_conda.lock b/build_tools/azure/pylatest_conda_forge_mkl_osx-64_conda.lock index 86443fd97ae20..7f3e749a5728d 100644 --- a/build_tools/azure/pylatest_conda_forge_mkl_osx-64_conda.lock +++ b/build_tools/azure/pylatest_conda_forge_mkl_osx-64_conda.lock @@ -15,7 +15,6 @@ https://conda.anaconda.org/conda-forge/osx-64/libiconv-1.17-hd75f5a5_2.conda#6c3 https://conda.anaconda.org/conda-forge/osx-64/libjpeg-turbo-3.0.0-h0dc2134_1.conda#72507f8e3961bc968af17435060b6dd6 https://conda.anaconda.org/conda-forge/osx-64/libwebp-base-1.4.0-h10d778d_0.conda#b2c0047ea73819d992484faacbbe1c24 https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.2.13-h8a1eda9_5.conda#4a3ad23f6e16f99c04e166767193d700 -https://conda.anaconda.org/conda-forge/osx-64/llvm-openmp-18.1.3-hb6ac08f_0.conda#506f270f4f00980d27cc1fc127e0ed37 https://conda.anaconda.org/conda-forge/osx-64/mkl-include-2023.2.0-h6bab518_50500.conda#835abb8ded5e26f23ea6996259c7972e https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.4.20240210-h73e2aa4_0.conda#50f28c512e9ad78589e3eab34833f762 https://conda.anaconda.org/conda-forge/osx-64/pthread-stubs-0.4-hc929b4f_1001.tar.bz2#addd19059de62181cd11ae8f4ef26084 @@ -29,21 +28,21 @@ https://conda.anaconda.org/conda-forge/osx-64/isl-0.26-imath32_h2e86a7b_101.cond https://conda.anaconda.org/conda-forge/osx-64/lerc-4.0.0-hb486fe8_0.tar.bz2#f9d6a4c82889d5ecedec1d90eb673c55 https://conda.anaconda.org/conda-forge/osx-64/libbrotlidec-1.1.0-h0dc2134_1.conda#9ee0bab91b2ca579e10353738be36063 https://conda.anaconda.org/conda-forge/osx-64/libbrotlienc-1.1.0-h0dc2134_1.conda#8a421fe09c6187f0eb5e2338a8a8be6d -https://conda.anaconda.org/conda-forge/osx-64/libgfortran5-13.2.0-h2873a65_3.conda#e4fb4d23ec2870ff3c40d10afe305aec https://conda.anaconda.org/conda-forge/osx-64/libpng-1.6.43-h92b6c6a_0.conda#65dcddb15965c9de2c0365cb14910532 https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.45.3-h92b6c6a_0.conda#68e462226209f35182ef66eda0f794ff https://conda.anaconda.org/conda-forge/osx-64/libxcb-1.15-hb7f2c08_0.conda#5513f57e0238c87c12dffedbcc9c1a4a https://conda.anaconda.org/conda-forge/osx-64/libxml2-2.12.6-hc0ae0f7_2.conda#50b997370584f2c83ca0c38e9028eab9 +https://conda.anaconda.org/conda-forge/osx-64/llvm-openmp-18.1.4-h2c61cee_0.conda#0619a2dda8b7e25b78abc0b3d872744f https://conda.anaconda.org/conda-forge/osx-64/ninja-1.12.0-h7728843_0.conda#1ac079f6ecddd2c336f3acb7b371851f -https://conda.anaconda.org/conda-forge/osx-64/openssl-3.2.1-hd75f5a5_1.conda#570a6f04802df580be529f3a72d2bbf7 +https://conda.anaconda.org/conda-forge/osx-64/openssl-3.3.0-hd75f5a5_0.conda#eb8c33aa7929a7714eab8b90c1d88afe https://conda.anaconda.org/conda-forge/osx-64/readline-8.2-h9e318b2_1.conda#f17f77f2acf4d344734bda76829ce14e https://conda.anaconda.org/conda-forge/osx-64/tapi-1100.0.11-h9ce4665_0.tar.bz2#f9ff42ccf809a21ba6f8607f8de36108 https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h1abcd95_1.conda#bf830ba5afc507c6232d4ef0fb1a882d https://conda.anaconda.org/conda-forge/osx-64/zlib-1.2.13-h8a1eda9_5.conda#75a8a98b1c4671c5d2897975731da42d -https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.5-h829000d_0.conda#80abc41d0c48b82fe0f04e7f42f5cb7e +https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.6-h915ae27_0.conda#4cb2cd56f039b129bb0e491c1164167e https://conda.anaconda.org/conda-forge/osx-64/brotli-bin-1.1.0-h0dc2134_1.conda#ece565c215adcc47fc1db4e651ee094b https://conda.anaconda.org/conda-forge/osx-64/freetype-2.12.1-h60636b9_2.conda#25152fce119320c980e5470e64834b50 -https://conda.anaconda.org/conda-forge/osx-64/libgfortran-5.0.0-13_2_0_h97931a8_3.conda#0b6e23a012ee7a9a5f6b244f5a92c1d5 +https://conda.anaconda.org/conda-forge/osx-64/libgfortran5-13.2.0-h2873a65_3.conda#e4fb4d23ec2870ff3c40d10afe305aec https://conda.anaconda.org/conda-forge/osx-64/libhwloc-2.10.0-default_h1321489_1000.conda#6f5fe4374d1003e116e2573022178da6 https://conda.anaconda.org/conda-forge/osx-64/libllvm16-16.0.6-hbedff68_3.conda#8fd56c0adc07a37f93bd44aa61a97c90 https://conda.anaconda.org/conda-forge/osx-64/libtiff-4.6.0-h129831d_3.conda#568593071d2e6cea7b5fc1f75bfa10ca @@ -62,7 +61,7 @@ https://conda.anaconda.org/conda-forge/osx-64/kiwisolver-1.4.5-py312h49ebfd2_1.c https://conda.anaconda.org/conda-forge/osx-64/lcms2-2.16-ha2f27b4_0.conda#1442db8f03517834843666c422238c9b https://conda.anaconda.org/conda-forge/osx-64/ld64_osx-64-711-ha20a434_0.conda#a8b41eb97c8a9d618243a79ba78fdc3c https://conda.anaconda.org/conda-forge/osx-64/libclang-cpp16-16.0.6-default_h7151d67_6.conda#7eaad118ab797d1427f8745c861d1925 -https://conda.anaconda.org/conda-forge/osx-64/libhiredis-1.0.2-h2beb688_0.tar.bz2#524282b2c46c9dedf051b3bc2ae05494 +https://conda.anaconda.org/conda-forge/osx-64/libgfortran-5.0.0-13_2_0_h97931a8_3.conda#0b6e23a012ee7a9a5f6b244f5a92c1d5 https://conda.anaconda.org/conda-forge/osx-64/llvm-tools-16.0.6-hbedff68_3.conda#e9356b0807462e8f84c1384a8da539a5 https://conda.anaconda.org/conda-forge/osx-64/mpc-1.3.1-h81bd1dd_0.conda#c752c0eb6c250919559172c011e5f65b https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyh9f0ad1d_0.tar.bz2#2ba8498c1018c1e9c61eb99b973dfe19 @@ -75,19 +74,19 @@ https://conda.anaconda.org/conda-forge/noarch/pytz-2024.1-pyhd8ed1ab_0.conda#3ee https://conda.anaconda.org/conda-forge/noarch/setuptools-69.5.1-pyhd8ed1ab_0.conda#7462280d81f639363e6e63c81276bd9e https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2#e5f25f8dbc060e9a8d912e432202afc2 https://conda.anaconda.org/conda-forge/osx-64/tbb-2021.12.0-h7728843_0.conda#e4fb6f4700d8890c36cbf317c2c6d0cb -https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.4.0-pyhc1e730c_0.conda#b296278eef667c673bf51de6535bad88 +https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.5.0-pyhc1e730c_0.conda#df68d78237980a159bd7149f33c0e8fd https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_0.tar.bz2#f832c45a477c78bebd107098db465095 https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2#5844808ffab9ebdb694585b50ba02a96 https://conda.anaconda.org/conda-forge/osx-64/tornado-6.4-py312h41838bb_0.conda#2d2d1fde5800d45cb56218583156d23d https://conda.anaconda.org/conda-forge/noarch/wheel-0.43.0-pyhd8ed1ab_1.conda#0b5293a157c2b5cd513dd1b03d8d3aae -https://conda.anaconda.org/conda-forge/osx-64/ccache-4.9.1-h41adc32_0.conda#45aaf96b67840bd98a928de8679098fa https://conda.anaconda.org/conda-forge/osx-64/cctools_osx-64-986-ha1c5b94_0.conda#a8951de2506df5649f5a3295fdfd9f2c https://conda.anaconda.org/conda-forge/osx-64/clang-16-16.0.6-default_h7151d67_6.conda#1c298568c30efe7d9369c7c15b748461 -https://conda.anaconda.org/conda-forge/osx-64/coverage-7.5.0-py312h5fa3f64_0.conda#0ec479f31895645cfaabaa7ea318e6a5 +https://conda.anaconda.org/conda-forge/osx-64/coverage-7.5.1-py312h520dd33_0.conda#afc8c7b237683760a3c35e49bcc04deb https://conda.anaconda.org/conda-forge/osx-64/fonttools-4.51.0-py312h41838bb_0.conda#ebe40134b860cf704ddaf81f684f95a5 https://conda.anaconda.org/conda-forge/osx-64/gfortran_impl_osx-64-12.3.0-hc328e78_3.conda#b3d751dc7073bbfdfa9d863e39b9685d -https://conda.anaconda.org/conda-forge/noarch/joblib-1.4.0-pyhd8ed1ab_0.conda#e0ed1bf13ce3a440e022157bf4764465 +https://conda.anaconda.org/conda-forge/noarch/joblib-1.4.2-pyhd8ed1ab_0.conda#25df261d4523d9f9783bcdb7208d872f https://conda.anaconda.org/conda-forge/osx-64/ld64-711-ha02d983_0.conda#3ae4930ec076735cce481e906f5192e0 +https://conda.anaconda.org/conda-forge/osx-64/libhiredis-1.0.2-h2beb688_0.tar.bz2#524282b2c46c9dedf051b3bc2ae05494 https://conda.anaconda.org/conda-forge/noarch/meson-1.4.0-pyhd8ed1ab_0.conda#52a0660cfa40b45bf254ecc3374cb2e0 https://conda.anaconda.org/conda-forge/osx-64/mkl-2023.2.0-h54c2260_50500.conda#0a342ccdc79e4fcd359245ac51941e7b https://conda.anaconda.org/conda-forge/osx-64/pillow-10.3.0-py312h0c923fa_0.conda#6f0591ae972e9b815739da3392fbb3c3 @@ -95,6 +94,7 @@ https://conda.anaconda.org/conda-forge/noarch/pip-24.0-pyhd8ed1ab_0.conda#f586ac https://conda.anaconda.org/conda-forge/noarch/pyproject-metadata-0.8.0-pyhd8ed1ab_0.conda#573fe09d7bd0cd4bcc210d8369b5ca47 https://conda.anaconda.org/conda-forge/noarch/pytest-7.4.4-pyhd8ed1ab_0.conda#a9d145de8c5f064b5fa68fb34725d9f4 https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0-pyhd8ed1ab_0.conda#2cf4264fffb9e6eff6031c5b6884d61c +https://conda.anaconda.org/conda-forge/osx-64/ccache-4.9.1-h41adc32_0.conda#45aaf96b67840bd98a928de8679098fa https://conda.anaconda.org/conda-forge/osx-64/cctools-986-h40f6528_0.conda#b7a2ca0062a6ee8bc4e83ec887bef942 https://conda.anaconda.org/conda-forge/osx-64/clang-16.0.6-hdae98eb_6.conda#884e7b24306e4f21b7ee08dabadb2ecc https://conda.anaconda.org/conda-forge/osx-64/libblas-3.9.0-20_osx64_mkl.conda#160fdc97a51d66d51dc782fb67d35205 @@ -112,18 +112,18 @@ https://conda.anaconda.org/conda-forge/osx-64/blas-devel-3.9.0-20_osx64_mkl.cond https://conda.anaconda.org/conda-forge/osx-64/compiler-rt-16.0.6-ha38d28d_2.conda#3b9e8c5c63b8e86234f499490acd85c2 https://conda.anaconda.org/conda-forge/osx-64/contourpy-1.2.1-py312h9230928_0.conda#079df34ce7c71259cfdd394645370891 https://conda.anaconda.org/conda-forge/osx-64/pandas-2.2.2-py312h83c8a23_0.conda#b422a5d39ff0cd72923aef807f280145 -https://conda.anaconda.org/conda-forge/osx-64/scipy-1.13.0-py312h8adb940_0.conda#818232a7807c76970172af9c7698ba4a +https://conda.anaconda.org/conda-forge/osx-64/scipy-1.13.0-py312h741d2f9_1.conda#c416453a8ea3b38d823fe8dcecdb6a12 https://conda.anaconda.org/conda-forge/osx-64/blas-2.120-mkl.conda#b041a7677a412f3d925d8208936cb1e2 -https://conda.anaconda.org/conda-forge/osx-64/clang_impl_osx-64-16.0.6-h8787910_11.conda#ed9c90270c77481fc4cfccd0891d62a8 +https://conda.anaconda.org/conda-forge/osx-64/clang_impl_osx-64-16.0.6-h8787910_12.conda#fe1a78dddda2c0b32fac9fbd7fa05c5f https://conda.anaconda.org/conda-forge/osx-64/matplotlib-base-3.8.4-py312h1fe5000_0.conda#3e3097734a5042cb6d2675e69bf1fc5a https://conda.anaconda.org/conda-forge/osx-64/pyamg-5.1.0-py312h3db3e91_0.conda#c6d6248b99fc11b15c9becea581a1462 -https://conda.anaconda.org/conda-forge/osx-64/clang_osx-64-16.0.6-hb91bd55_11.conda#24123b15e9c0dad9c0d5fd9da0b4c7a9 +https://conda.anaconda.org/conda-forge/osx-64/clang_osx-64-16.0.6-hb91bd55_12.conda#4ef6f9a82654ad497e2334471832e774 https://conda.anaconda.org/conda-forge/osx-64/matplotlib-3.8.4-py312hb401068_0.conda#187ee42addd449b4899b55c304012436 -https://conda.anaconda.org/conda-forge/osx-64/c-compiler-1.7.0-h282daa2_0.conda#4652f33fe8d895f61177e2783b289377 -https://conda.anaconda.org/conda-forge/osx-64/clangxx_impl_osx-64-16.0.6-h6d92fbe_11.conda#a658c595675bde00373347b22a974810 +https://conda.anaconda.org/conda-forge/osx-64/c-compiler-1.7.0-h282daa2_1.conda#d27411cb82bc1b76b9f487da6ae97f1d +https://conda.anaconda.org/conda-forge/osx-64/clangxx_impl_osx-64-16.0.6-h6d92fbe_12.conda#c1b8987b40123346ee3fe120c3b66b3d https://conda.anaconda.org/conda-forge/osx-64/gfortran_osx-64-12.3.0-h18f7dce_1.conda#436af2384c47aedb94af78a128e174f1 -https://conda.anaconda.org/conda-forge/osx-64/clangxx_osx-64-16.0.6-hb91bd55_11.conda#e49aad30263abdcb785e610981b7c2c7 +https://conda.anaconda.org/conda-forge/osx-64/clangxx_osx-64-16.0.6-hb91bd55_12.conda#4e8cca2283e843a8df8b2e747d36226d https://conda.anaconda.org/conda-forge/osx-64/gfortran-12.3.0-h2c809b3_1.conda#c48adbaa8944234b80ef287c37e329b0 -https://conda.anaconda.org/conda-forge/osx-64/cxx-compiler-1.7.0-h7728843_0.conda#8abaa2694c1fba2b6bd3753d00a60415 -https://conda.anaconda.org/conda-forge/osx-64/fortran-compiler-1.7.0-h6c2ab21_0.conda#2c11db8b46df0a547997116f0fd54b8e -https://conda.anaconda.org/conda-forge/osx-64/compilers-1.7.0-h694c41f_0.conda#3576aa54986a3e2a5370e4232b35c036 +https://conda.anaconda.org/conda-forge/osx-64/cxx-compiler-1.7.0-h7728843_1.conda#e04cb15a20553b973dd068c2dc81d682 +https://conda.anaconda.org/conda-forge/osx-64/fortran-compiler-1.7.0-h6c2ab21_1.conda#48319058089f492d5059e04494b81ed9 +https://conda.anaconda.org/conda-forge/osx-64/compilers-1.7.0-h694c41f_1.conda#875e9b06186a41d55b96b9c1a52f15be diff --git a/build_tools/azure/pylatest_conda_mkl_no_openmp_osx-64_conda.lock b/build_tools/azure/pylatest_conda_mkl_no_openmp_osx-64_conda.lock index dc2fea78e7b80..c687f8fb76fb1 100644 --- a/build_tools/azure/pylatest_conda_mkl_no_openmp_osx-64_conda.lock +++ b/build_tools/azure/pylatest_conda_mkl_no_openmp_osx-64_conda.lock @@ -3,40 +3,40 @@ # input_hash: e0d2cf2593df1f2c6969d68cf849136bee785b51f6cfc50ea1bdca2143d4a051 @EXPLICIT https://repo.anaconda.com/pkgs/main/osx-64/blas-1.0-mkl.conda#cb2c87e85ac8e0ceae776d26d4214c8a -https://repo.anaconda.com/pkgs/main/osx-64/bzip2-1.0.8-h6c40b1e_5.conda#0f51dde96c82dcf58a788787fed4c5b9 +https://repo.anaconda.com/pkgs/main/osx-64/bzip2-1.0.8-h6c40b1e_6.conda#96224786021d0765ce05818fa3c59bdb https://repo.anaconda.com/pkgs/main/osx-64/ca-certificates-2024.3.11-hecd8cb5_0.conda#a2e29a11940c66baf9942912096fad5f https://repo.anaconda.com/pkgs/main/osx-64/jpeg-9e-h6c40b1e_1.conda#fc3e61fa41309946c9283fe8737d7f41 -https://repo.anaconda.com/pkgs/main/osx-64/libbrotlicommon-1.0.9-hca72f7f_7.conda#6c865b9e76fa2fad0c8ac32aa0f01f75 +https://repo.anaconda.com/pkgs/main/osx-64/libbrotlicommon-1.0.9-h6c40b1e_8.conda#8e86dfa34b08bc664b19e1499e5465b8 https://repo.anaconda.com/pkgs/main/osx-64/libcxx-14.0.6-h9765a3e_0.conda#387757bb354ae9042370452cd0fb5627 https://repo.anaconda.com/pkgs/main/osx-64/libdeflate-1.17-hb664fd8_1.conda#b6116b8db33ea6a5b5287dae70d4a913 -https://repo.anaconda.com/pkgs/main/osx-64/libffi-3.4.4-hecd8cb5_0.conda#c20b2687118c471b1d70067ef2b2703f +https://repo.anaconda.com/pkgs/main/osx-64/libffi-3.4.4-hecd8cb5_1.conda#eb7f09ada4d95f1a26f483f1009d9286 https://repo.anaconda.com/pkgs/main/osx-64/libwebp-base-1.3.2-h6c40b1e_0.conda#d8fd9f599dd4e012694e69d119016442 https://repo.anaconda.com/pkgs/main/osx-64/llvm-openmp-14.0.6-h0dcd299_0.conda#b5804d32b87dc61ca94561ade33d5f2d https://repo.anaconda.com/pkgs/main/osx-64/ncurses-6.4-hcec6c5f_0.conda#0214d1ee980e217fabc695f1e40662aa https://repo.anaconda.com/pkgs/main/noarch/tzdata-2024a-h04d1e81_0.conda#452af53adae0a5b06eb5d05c707b2f25 -https://repo.anaconda.com/pkgs/main/osx-64/xz-5.4.6-h6c40b1e_0.conda#412bf13f273c0e086da65f86567cfe80 -https://repo.anaconda.com/pkgs/main/osx-64/zlib-1.2.13-h4dc903c_0.conda#d0202dd912bfb45d3422786531717882 +https://repo.anaconda.com/pkgs/main/osx-64/xz-5.4.6-h6c40b1e_1.conda#b40d69768d28133d8be1843def4f82f5 +https://repo.anaconda.com/pkgs/main/osx-64/zlib-1.2.13-h4b97444_1.conda#38e35f7c817fac0973034bfce6706ec2 https://repo.anaconda.com/pkgs/main/osx-64/ccache-3.7.9-hf120daa_0.conda#a01515a32e721c51d631283f991bc8ea https://repo.anaconda.com/pkgs/main/osx-64/expat-2.6.2-hcec6c5f_0.conda#c748234dd7e242784198ab038372cb0c https://repo.anaconda.com/pkgs/main/osx-64/intel-openmp-2023.1.0-ha357a0b_43548.conda#ba8a89ffe593eb88e4c01334753c40c3 https://repo.anaconda.com/pkgs/main/osx-64/lerc-3.0-he9d5cce_0.conda#aec2c3dbef836849c9260f05be04f3db -https://repo.anaconda.com/pkgs/main/osx-64/libbrotlidec-1.0.9-hca72f7f_7.conda#b85983951745cc666d9a1b42894210b2 -https://repo.anaconda.com/pkgs/main/osx-64/libbrotlienc-1.0.9-hca72f7f_7.conda#e306d7a1599202a7c95762443f110832 +https://repo.anaconda.com/pkgs/main/osx-64/libbrotlidec-1.0.9-h6c40b1e_8.conda#6338cd7779e614fc16d835990e627e04 +https://repo.anaconda.com/pkgs/main/osx-64/libbrotlienc-1.0.9-h6c40b1e_8.conda#2af01a7b3fdbed47ebe5c452c34e5c5d https://repo.anaconda.com/pkgs/main/osx-64/libgfortran5-11.3.0-h9dfd629_28.conda#1fa1a27ee100b1918c3021dbfa3895a3 https://repo.anaconda.com/pkgs/main/osx-64/libpng-1.6.39-h6c40b1e_0.conda#a3c824835f53ad27aeb86d2b55e47804 -https://repo.anaconda.com/pkgs/main/osx-64/lz4-c-1.9.4-hcec6c5f_0.conda#44291e9e6920cfff30caf1299f48db38 +https://repo.anaconda.com/pkgs/main/osx-64/lz4-c-1.9.4-hcec6c5f_1.conda#aee0efbb45220e1985533dbff48551f8 https://repo.anaconda.com/pkgs/main/osx-64/ninja-base-1.10.2-haf03e11_5.conda#c857c13129710a61395270656905c4a2 -https://repo.anaconda.com/pkgs/main/osx-64/openssl-3.0.13-hca72f7f_0.conda#08b109f010b97ce6cef211e235177175 +https://repo.anaconda.com/pkgs/main/osx-64/openssl-3.0.13-hca72f7f_1.conda#e526d7e2e79132a11b4746cf305c45b5 https://repo.anaconda.com/pkgs/main/osx-64/readline-8.2-hca72f7f_0.conda#971667436260e523f6f7355fdfa238bf https://repo.anaconda.com/pkgs/main/osx-64/tbb-2021.8.0-ha357a0b_0.conda#fb48530a3eea681c11dafb95b3387c0f https://repo.anaconda.com/pkgs/main/osx-64/tk-8.6.12-h5d9f67b_0.conda#047f0af5486d19163e37fd7f8ae3d29f -https://repo.anaconda.com/pkgs/main/osx-64/brotli-bin-1.0.9-hca72f7f_7.conda#110bdca1a20710820e61f7fa3047f737 +https://repo.anaconda.com/pkgs/main/osx-64/brotli-bin-1.0.9-h6c40b1e_8.conda#11053f9c6b8d8a8348d0c33450c23ce9 https://repo.anaconda.com/pkgs/main/osx-64/freetype-2.12.1-hd8bbffd_0.conda#1f276af321375ee7fe8056843044fa76 https://repo.anaconda.com/pkgs/main/osx-64/libgfortran-5.0.0-11_3_0_hecd8cb5_28.conda#2eb13b680803f1064e53873ae0aaafb3 https://repo.anaconda.com/pkgs/main/osx-64/mkl-2023.1.0-h8e150cf_43560.conda#85d0f3431dd5c6ae44f8725fdd3d3e59 -https://repo.anaconda.com/pkgs/main/osx-64/sqlite-3.41.2-h6c40b1e_0.conda#6947a501943529c7536b7e4ba53802c1 -https://repo.anaconda.com/pkgs/main/osx-64/zstd-1.5.5-hc035e20_0.conda#5e0b7ddb1b7dc6b630e1f9a03499c19c -https://repo.anaconda.com/pkgs/main/osx-64/brotli-1.0.9-hca72f7f_7.conda#68e54d12ec67591deb2ffd70348fb00f +https://repo.anaconda.com/pkgs/main/osx-64/sqlite-3.45.3-h6c40b1e_0.conda#2edf909b937b3aad48322c9cb2e8f1a0 +https://repo.anaconda.com/pkgs/main/osx-64/zstd-1.5.5-hc035e20_2.conda#c033bf68c12f8c71fd916f000f3dc118 +https://repo.anaconda.com/pkgs/main/osx-64/brotli-1.0.9-h6c40b1e_8.conda#10f89677a3898d0113dc354adf643df3 https://repo.anaconda.com/pkgs/main/osx-64/libtiff-4.5.1-hcec6c5f_0.conda#e127a800ffd9d300ed7d5e1b026944ec https://repo.anaconda.com/pkgs/main/osx-64/python-3.12.3-hd58486a_0.conda#1a287cfa37c5a92972f5f527b6af7eed https://repo.anaconda.com/pkgs/main/osx-64/coverage-7.2.2-py312h6c40b1e_0.conda#b6e4b9fba325047c07f3c9211ae91d1c @@ -59,11 +59,11 @@ https://repo.anaconda.com/pkgs/main/noarch/six-1.16.0-pyhd3eb1b0_1.conda#3458682 https://repo.anaconda.com/pkgs/main/noarch/toml-0.10.2-pyhd3eb1b0_0.conda#cda05f5f6d8509529d1a2743288d197a https://repo.anaconda.com/pkgs/main/osx-64/tornado-6.3.3-py312h6c40b1e_0.conda#49173b5a36c9134865221f29d4a73fb6 https://repo.anaconda.com/pkgs/main/osx-64/unicodedata2-15.1.0-py312h6c40b1e_0.conda#65bd2cb787fc99662d9bb6e6520c5826 -https://repo.anaconda.com/pkgs/main/osx-64/wheel-0.41.2-py312hecd8cb5_0.conda#e7aea266d81142e2bb0bbc2280e64526 +https://repo.anaconda.com/pkgs/main/osx-64/wheel-0.43.0-py312hecd8cb5_0.conda#c0bdd5748b170523232e8ad1d667136c https://repo.anaconda.com/pkgs/main/osx-64/fonttools-4.51.0-py312h6c40b1e_0.conda#8f55fa86b73e8a7f4403503f9b7a9959 https://repo.anaconda.com/pkgs/main/osx-64/meson-1.3.1-py312hecd8cb5_0.conda#43963a2b38becce4caa95434b8c96837 https://repo.anaconda.com/pkgs/main/osx-64/numpy-base-1.26.4-py312h6f81483_0.conda#87f73efbf26ab2e2ea7c32481a71bd47 -https://repo.anaconda.com/pkgs/main/osx-64/pillow-10.2.0-py312h6c40b1e_0.conda#5a44bd28cf26fff2d6219e76a86db126 +https://repo.anaconda.com/pkgs/main/osx-64/pillow-10.3.0-py312h6c40b1e_0.conda#fe883fa4247d35fe6de49f713529ca02 https://repo.anaconda.com/pkgs/main/osx-64/pip-23.3.1-py312hecd8cb5_0.conda#efc3db40cac09f74bb480d28d3a0b260 https://repo.anaconda.com/pkgs/main/osx-64/pyproject-metadata-0.7.1-py312hecd8cb5_0.conda#e91ce37477d24dcdf7e0a8b93c5e72fd https://repo.anaconda.com/pkgs/main/osx-64/pytest-7.4.0-py312hecd8cb5_0.conda#b816a2439ba9b87524aec74d58e55b0a @@ -83,4 +83,4 @@ https://repo.anaconda.com/pkgs/main/osx-64/scipy-1.11.4-py312h81688c2_0.conda#7d https://repo.anaconda.com/pkgs/main/osx-64/pandas-2.2.1-py312he282a81_0.conda#021b70a1e40efb75b89eb8ebdb347132 https://repo.anaconda.com/pkgs/main/osx-64/pyamg-4.2.3-py312h44cbcf4_0.conda#3bdc7be74087b3a5a83c520a74e1e8eb # pip cython @ https://files.pythonhosted.org/packages/d5/6d/06c08d75adb98cdf72af18801e193d22580cc86ca553610f430f18ea26b3/Cython-3.0.10-cp312-cp312-macosx_10_9_x86_64.whl#sha256=8f2864ab5fcd27a346f0b50f901ebeb8f60b25a60a575ccfd982e7f3e9674914 -# pip threadpoolctl @ https://files.pythonhosted.org/packages/1e/84/ccd9b08653022b7785b6e3ee070ffb2825841e0dc119be22f0840b2b35cb/threadpoolctl-3.4.0-py3-none-any.whl#sha256=8f4c689a65b23e5ed825c8436a92b818aac005e0f3715f6a1664d7c7ee29d262 +# pip threadpoolctl @ https://files.pythonhosted.org/packages/4b/2c/ffbf7a134b9ab11a67b0cf0726453cedd9c5043a4fe7a35d1cefa9a1bcfb/threadpoolctl-3.5.0-py3-none-any.whl#sha256=56c1e26c150397e58c4926da8eeee87533b1e32bef131bd4bf6a2f45f3185467 diff --git a/build_tools/azure/pylatest_pip_openblas_pandas_linux-64_conda.lock b/build_tools/azure/pylatest_pip_openblas_pandas_linux-64_conda.lock index 7534de9fbd5f6..c497709ca347e 100644 --- a/build_tools/azure/pylatest_pip_openblas_pandas_linux-64_conda.lock +++ b/build_tools/azure/pylatest_pip_openblas_pandas_linux-64_conda.lock @@ -10,21 +10,21 @@ https://repo.anaconda.com/pkgs/main/linux-64/libgomp-11.2.0-h1234567_1.conda#b37 https://repo.anaconda.com/pkgs/main/linux-64/libstdcxx-ng-11.2.0-h1234567_1.conda#57623d10a70e09e1d048c2b2b6f4e2dd https://repo.anaconda.com/pkgs/main/linux-64/_openmp_mutex-5.1-1_gnu.conda#71d281e9c2192cb3fa425655a8defb85 https://repo.anaconda.com/pkgs/main/linux-64/libgcc-ng-11.2.0-h1234567_1.conda#a87728dabf3151fb9cfa990bd2eb0464 -https://repo.anaconda.com/pkgs/main/linux-64/libffi-3.4.4-h6a678d5_0.conda#06e288f9250abef59b9a367d151fc339 +https://repo.anaconda.com/pkgs/main/linux-64/libffi-3.4.4-h6a678d5_1.conda#70646cc713f0c43926cfdcfe9b695fe0 https://repo.anaconda.com/pkgs/main/linux-64/ncurses-6.4-h6a678d5_0.conda#5558eec6e2191741a92f832ea826251c -https://repo.anaconda.com/pkgs/main/linux-64/openssl-3.0.13-h7f8727e_0.conda#c73d46a4d666da0ae3dcd3fd8f805122 -https://repo.anaconda.com/pkgs/main/linux-64/xz-5.4.6-h5eee18b_0.conda#81a9916f581d4da15a3839216a487c66 -https://repo.anaconda.com/pkgs/main/linux-64/zlib-1.2.13-h5eee18b_0.conda#333e31fbfbb5057c92fa845ad6adef93 +https://repo.anaconda.com/pkgs/main/linux-64/openssl-3.0.13-h7f8727e_1.conda#d1d1fc47640fe0d9f7fa64c0a054bfd8 +https://repo.anaconda.com/pkgs/main/linux-64/xz-5.4.6-h5eee18b_1.conda#1562802f843297ee776a50b9329597ed +https://repo.anaconda.com/pkgs/main/linux-64/zlib-1.2.13-h5eee18b_1.conda#92e42d8310108b0a440fb2e60b2b2a25 https://repo.anaconda.com/pkgs/main/linux-64/ccache-3.7.9-hfe4627d_0.conda#bef6fc681c273bb7bd0c67d1a591365e https://repo.anaconda.com/pkgs/main/linux-64/readline-8.2-h5eee18b_0.conda#be42180685cce6e6b0329201d9f48efb https://repo.anaconda.com/pkgs/main/linux-64/tk-8.6.12-h1ccaba5_0.conda#fa10ff4aa631fa4aa090a6234d7770b9 -https://repo.anaconda.com/pkgs/main/linux-64/sqlite-3.41.2-h5eee18b_0.conda#c7086c9ceb6cfe1c4c729a774a2d88a5 +https://repo.anaconda.com/pkgs/main/linux-64/sqlite-3.45.3-h5eee18b_0.conda#acf93d6aceb74d6110e20b44cc45939e https://repo.anaconda.com/pkgs/main/linux-64/python-3.9.19-h955ad1f_0.conda#33cb019c40e3409df392c99e3c34f352 https://repo.anaconda.com/pkgs/main/linux-64/setuptools-68.2.2-py39h06a4308_0.conda#5b42cae5548732ae5c167bb1066085de -https://repo.anaconda.com/pkgs/main/linux-64/wheel-0.41.2-py39h06a4308_0.conda#ec1b8213c3585defaa6042ed2f95861d +https://repo.anaconda.com/pkgs/main/linux-64/wheel-0.43.0-py39h06a4308_0.conda#40bb60408c7433d767fd8c65b35bc4a0 https://repo.anaconda.com/pkgs/main/linux-64/pip-23.3.1-py39h06a4308_0.conda#685007e3dae59d211620f19926577bd6 # pip alabaster @ https://files.pythonhosted.org/packages/32/34/d4e1c02d3bee589efb5dfa17f88ea08bdb3e3eac12bc475462aec52ed223/alabaster-0.7.16-py3-none-any.whl#sha256=b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92 -# pip babel @ https://files.pythonhosted.org/packages/0d/35/4196b21041e29a42dc4f05866d0c94fa26c9da88ce12c38c2265e42c82fb/Babel-2.14.0-py3-none-any.whl#sha256=efb1a25b7118e67ce3a259bed20545c29cb68be8ad2c784c83689981b7a57287 +# pip babel @ https://files.pythonhosted.org/packages/27/45/377f7e32a5c93d94cd56542349b34efab5ca3f9e2fd5a68c5e93169aa32d/Babel-2.15.0-py3-none-any.whl#sha256=08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb # pip certifi @ https://files.pythonhosted.org/packages/ba/06/a07f096c664aeb9f01624f858c3add0a4e913d6c96257acb4fce61e7de14/certifi-2024.2.2-py3-none-any.whl#sha256=dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1 # pip charset-normalizer @ https://files.pythonhosted.org/packages/98/69/5d8751b4b670d623aa7a47bef061d69c279e9f922f6705147983aa76c3ce/charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796 # pip cycler @ https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl#sha256=85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30 @@ -36,7 +36,7 @@ https://repo.anaconda.com/pkgs/main/linux-64/pip-23.3.1-py39h06a4308_0.conda#685 # pip idna @ https://files.pythonhosted.org/packages/e5/3e/741d8c82801c347547f8a2a06aa57dbb1992be9e948df2ea0eda2c8b79e8/idna-3.7-py3-none-any.whl#sha256=82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0 # pip imagesize @ https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl#sha256=0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b # pip iniconfig @ https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl#sha256=b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 -# pip joblib @ https://files.pythonhosted.org/packages/ae/e2/4dea6313ef2b38442fccbbaf4017e50a6c3c8a50e8ee9b512783e5c90409/joblib-1.4.0-py3-none-any.whl#sha256=42942470d4062537be4d54c83511186da1fc14ba354961a2114da91efa9a4ed7 +# pip joblib @ https://files.pythonhosted.org/packages/91/29/df4b9b42f2be0b623cbd5e2140cafcaa2bef0759a00b7b70104dcfe2fb51/joblib-1.4.2-py3-none-any.whl#sha256=06d478d5674cbc267e7496a410ee875abd68e4340feff4490bcb7afb88060ae6 # pip kiwisolver @ https://files.pythonhosted.org/packages/c0/a8/841594f11d0b88d8aeb26991bc4dac38baa909dc58d0c4262a4f7893bcbf/kiwisolver-1.4.5-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl#sha256=6c3bd3cde54cafb87d74d8db50b909705c62b17c2099b8f2e25b461882e544ff # pip markupsafe @ https://files.pythonhosted.org/packages/5f/5a/360da85076688755ea0cceb92472923086993e86b5613bbae9fbc14136b0/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3 # pip meson @ https://files.pythonhosted.org/packages/33/75/b1a37fa7b2dbca8c0dbb04d5cdd7e2720c8ef6febe41b4a74866350e041c/meson-1.4.0-py3-none-any.whl#sha256=476a458d51fcfa322a6bdc64da5138997c542d08e6b2e49b9fa68c46fd7c4475 @@ -46,7 +46,7 @@ https://repo.anaconda.com/pkgs/main/linux-64/pip-23.3.1-py39h06a4308_0.conda#685 # pip packaging @ https://files.pythonhosted.org/packages/49/df/1fceb2f8900f8639e278b056416d49134fb8d84c5942ffaa01ad34782422/packaging-24.0-py3-none-any.whl#sha256=2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5 # pip pillow @ https://files.pythonhosted.org/packages/f5/6d/52e82352670e850f468de9e6bccced4202a09f58e7ea5ecdbf08283d85cb/pillow-10.3.0-cp39-cp39-manylinux_2_28_x86_64.whl#sha256=1dfc94946bc60ea375cc39cff0b8da6c7e5f8fcdc1d946beb8da5c216156ddd8 # pip pluggy @ https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl#sha256=44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 -# pip pygments @ https://files.pythonhosted.org/packages/97/9c/372fef8377a6e340b1704768d20daaded98bf13282b5327beb2e2fe2c7ef/pygments-2.17.2-py3-none-any.whl#sha256=b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c +# pip pygments @ https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl#sha256=b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a # pip pyparsing @ https://files.pythonhosted.org/packages/9d/ea/6d76df31432a0e6fdf81681a895f009a4bb47b3c39036db3e1b528191d52/pyparsing-3.1.2-py3-none-any.whl#sha256=f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742 # pip pytz @ https://files.pythonhosted.org/packages/9c/3d/a121f284241f08268b21359bd425f7d4825cffc5ac5cd0e1b3d82ffd2b10/pytz-2024.1-py2.py3-none-any.whl#sha256=328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319 # pip six @ https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl#sha256=8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 @@ -58,24 +58,24 @@ https://repo.anaconda.com/pkgs/main/linux-64/pip-23.3.1-py39h06a4308_0.conda#685 # pip sphinxcontrib-qthelp @ https://files.pythonhosted.org/packages/80/b3/1beac14a88654d2e5120d0143b49be5ad450b86eb1963523d8dbdcc51eb2/sphinxcontrib_qthelp-1.0.7-py3-none-any.whl#sha256=e2ae3b5c492d58fcbd73281fbd27e34b8393ec34a073c792642cd8e529288182 # pip sphinxcontrib-serializinghtml @ https://files.pythonhosted.org/packages/38/24/228bb903ea87b9e08ab33470e6102402a644127108c7117ac9c00d849f82/sphinxcontrib_serializinghtml-1.1.10-py3-none-any.whl#sha256=326369b8df80a7d2d8d7f99aa5ac577f51ea51556ed974e7716cfd4fca3f6cb7 # pip tabulate @ https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl#sha256=024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f -# pip threadpoolctl @ https://files.pythonhosted.org/packages/1e/84/ccd9b08653022b7785b6e3ee070ffb2825841e0dc119be22f0840b2b35cb/threadpoolctl-3.4.0-py3-none-any.whl#sha256=8f4c689a65b23e5ed825c8436a92b818aac005e0f3715f6a1664d7c7ee29d262 +# pip threadpoolctl @ https://files.pythonhosted.org/packages/4b/2c/ffbf7a134b9ab11a67b0cf0726453cedd9c5043a4fe7a35d1cefa9a1bcfb/threadpoolctl-3.5.0-py3-none-any.whl#sha256=56c1e26c150397e58c4926da8eeee87533b1e32bef131bd4bf6a2f45f3185467 # pip tomli @ https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl#sha256=939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc # pip tzdata @ https://files.pythonhosted.org/packages/65/58/f9c9e6be752e9fcb8b6a0ee9fb87e6e7a1f6bcab2cdc73f02bb7ba91ada0/tzdata-2024.1-py2.py3-none-any.whl#sha256=9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252 # pip urllib3 @ https://files.pythonhosted.org/packages/a2/73/a68704750a7679d0b6d3ad7aa8d4da8e14e151ae82e6fee774e6e0d05ec8/urllib3-2.2.1-py3-none-any.whl#sha256=450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d # pip zipp @ https://files.pythonhosted.org/packages/c2/0a/ba9d0ee9536d3ef73a3448e931776e658b36f128d344e175bc32b092a8bf/zipp-3.18.1-py3-none-any.whl#sha256=206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b # pip contourpy @ https://files.pythonhosted.org/packages/31/a2/2f12e3a6e45935ff694654b710961b03310b0e1ec997ee9f416d3c873f87/contourpy-1.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=e1d59258c3c67c865435d8fbeb35f8c59b8bef3d6f46c1f29f6123556af28445 -# pip coverage @ https://files.pythonhosted.org/packages/12/7f/9b787ffc31bc39aa9e98c7005b698e7c6539bd222043e4a9c83b83c782a2/coverage-7.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=db2de4e546f0ec4b2787d625e0b16b78e99c3e21bc1722b4977c0dddf11ca84e +# pip coverage @ https://files.pythonhosted.org/packages/c1/50/b7d6f236c20334b0378ed88078e830640a64ad8eb9f11f818b2af34d00c0/coverage-7.5.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=d21918e9ef11edf36764b93101e2ae8cc82aa5efdc7c5a4e9c6c35a48496d601 # pip imageio @ https://files.pythonhosted.org/packages/a3/b6/39c7dad203d9984225f47e0aa39ac3ba3a47c77a02d0ef2a7be691855a06/imageio-2.34.1-py3-none-any.whl#sha256=408c1d4d62f72c9e8347e7d1ca9bc11d8673328af3913868db3b828e28b40a4c # pip importlib-metadata @ https://files.pythonhosted.org/packages/2d/0a/679461c511447ffaf176567d5c496d1de27cbe34a87df6677d7171b2fbd4/importlib_metadata-7.1.0-py3-none-any.whl#sha256=30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570 # pip importlib-resources @ https://files.pythonhosted.org/packages/75/06/4df55e1b7b112d183f65db9503bff189e97179b256e1ea450a3c365241e0/importlib_resources-6.4.0-py3-none-any.whl#sha256=50d10f043df931902d4194ea07ec57960f66a80449ff867bfe782b4c486ba78c -# pip jinja2 @ https://files.pythonhosted.org/packages/30/6d/6de6be2d02603ab56e72997708809e8a5b0fbfee080735109b40a3564843/Jinja2-3.1.3-py3-none-any.whl#sha256=7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa +# pip jinja2 @ https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl#sha256=bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d # pip lazy-loader @ https://files.pythonhosted.org/packages/83/60/d497a310bde3f01cb805196ac61b7ad6dc5dcf8dce66634dc34364b20b4f/lazy_loader-0.4-py3-none-any.whl#sha256=342aa8e14d543a154047afb4ba8ef17f5563baad3fc610d7b15b213b0f119efc # pip pyproject-metadata @ https://files.pythonhosted.org/packages/aa/5f/bb5970d3d04173b46c9037109f7f05fc8904ff5be073ee49bb6ff00301bc/pyproject_metadata-0.8.0-py3-none-any.whl#sha256=ad858d448e1d3a1fb408ac5bac9ea7743e7a8bbb472f2693aaa334d2db42f526 # pip pytest @ https://files.pythonhosted.org/packages/51/ff/f6e8b8f39e08547faece4bd80f89d5a8de68a38b2d179cc1c4490ffa3286/pytest-7.4.4-py3-none-any.whl#sha256=b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8 # pip python-dateutil @ https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl#sha256=a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 # pip requests @ https://files.pythonhosted.org/packages/70/8e/0e2d847013cb52cd35b38c009bb167a1a26b2ce6cd6965bf26b47bc0bf44/requests-2.31.0-py3-none-any.whl#sha256=58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f # pip scipy @ https://files.pythonhosted.org/packages/c6/ba/a778e6c0020d728c119b0379805a357135fe8c9bc87fdb7e0750ca11319f/scipy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=28e286bf9ac422d6beb559bc61312c348ca9b0f0dae0d7c5afde7f722d6ea13d -# pip tifffile @ https://files.pythonhosted.org/packages/88/23/6398b7bca8967c853b90ba2f8da5e3ad1e9b2ca5b9f869a8c26ea41543e2/tifffile-2024.4.24-py3-none-any.whl#sha256=8d0b982f4b01ace358835ae6c2beb5a70cb7287f5d3a2e96c318bd5befa97b1f +# pip tifffile @ https://files.pythonhosted.org/packages/c1/cf/dd1cdf85db58c811816377afd6ba8a240f4611e16f4085201598fb2d5578/tifffile-2024.5.3-py3-none-any.whl#sha256=cac4d939156ff7f16d65fd689637808a7b5b3ad58f9c73327fc009b0aa32c7d5 # pip lightgbm @ https://files.pythonhosted.org/packages/ba/11/cb8b67f3cbdca05b59a032bb57963d4fe8c8d18c3870f30bed005b7f174d/lightgbm-4.3.0-py3-none-manylinux_2_28_x86_64.whl#sha256=104496a3404cb2452d3412cbddcfbfadbef9c372ea91e3a9b8794bcc5183bf07 # pip matplotlib @ https://files.pythonhosted.org/packages/5e/2c/513395a63a9e1124a5648addbf73be23cc603f955af026b04416da98dc96/matplotlib-3.8.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=606e3b90897554c989b1e38a258c626d46c873523de432b1462f295db13de6f9 # pip meson-python @ https://files.pythonhosted.org/packages/91/c0/104cb6244c83fe6bc3886f144cc433db0c0c78efac5dc00e409a5a08c87d/meson_python-0.16.0-py3-none-any.whl#sha256=842dc9f5dc29e55fc769ff1b6fe328412fe6c870220fc321060a1d2d395e69e8 diff --git a/build_tools/azure/pymin_conda_defaults_openblas_linux-64_conda.lock b/build_tools/azure/pymin_conda_defaults_openblas_linux-64_conda.lock index a1a9a668e9d2e..ff7bcd028c7f6 100644 --- a/build_tools/azure/pymin_conda_defaults_openblas_linux-64_conda.lock +++ b/build_tools/azure/pymin_conda_defaults_openblas_linux-64_conda.lock @@ -13,41 +13,41 @@ https://repo.anaconda.com/pkgs/main/linux-64/libgomp-11.2.0-h1234567_1.conda#b37 https://repo.anaconda.com/pkgs/main/linux-64/libstdcxx-ng-11.2.0-h1234567_1.conda#57623d10a70e09e1d048c2b2b6f4e2dd https://repo.anaconda.com/pkgs/main/linux-64/_openmp_mutex-5.1-1_gnu.conda#71d281e9c2192cb3fa425655a8defb85 https://repo.anaconda.com/pkgs/main/linux-64/libgcc-ng-11.2.0-h1234567_1.conda#a87728dabf3151fb9cfa990bd2eb0464 -https://repo.anaconda.com/pkgs/main/linux-64/bzip2-1.0.8-h5eee18b_5.conda#9c8dec113089c4aca7392c6a3864f505 +https://repo.anaconda.com/pkgs/main/linux-64/bzip2-1.0.8-h5eee18b_6.conda#f21a3ff51c1b271977f53ce956a69297 https://repo.anaconda.com/pkgs/main/linux-64/expat-2.6.2-h6a678d5_0.conda#55049db2772dae035f6b8a95f72b5970 https://repo.anaconda.com/pkgs/main/linux-64/fftw-3.3.9-h5eee18b_2.conda#db1df41113accc18ec59a99f1631bfcd https://repo.anaconda.com/pkgs/main/linux-64/icu-73.1-h6a678d5_0.conda#6d09df641fc23f7d277a04dc7ea32dd4 https://repo.anaconda.com/pkgs/main/linux-64/jpeg-9e-h5eee18b_1.conda#ac373800fda872108412d1ccfe3fa572 https://repo.anaconda.com/pkgs/main/linux-64/lerc-3.0-h295c915_0.conda#b97309770412f10bed8d9448f6f98f87 https://repo.anaconda.com/pkgs/main/linux-64/libdeflate-1.17-h5eee18b_1.conda#82831ef0b6c9595382d74e0c281f6742 -https://repo.anaconda.com/pkgs/main/linux-64/libffi-3.4.4-h6a678d5_0.conda#06e288f9250abef59b9a367d151fc339 -https://repo.anaconda.com/pkgs/main/linux-64/libiconv-1.16-h7f8727e_2.conda#80d4bc7d7e58b5f0be41d763f60994f5 +https://repo.anaconda.com/pkgs/main/linux-64/libffi-3.4.4-h6a678d5_1.conda#70646cc713f0c43926cfdcfe9b695fe0 +https://repo.anaconda.com/pkgs/main/linux-64/libiconv-1.16-h5eee18b_3.conda#197b1a0886a31fccab2167340528eebc https://repo.anaconda.com/pkgs/main/linux-64/libopenblas-0.3.21-h043d6bf_0.conda#7f7324dcc3c4761a14f3e4ac443235a7 https://repo.anaconda.com/pkgs/main/linux-64/libuuid-1.41.5-h5eee18b_0.conda#4a6a2354414c9080327274aa514e5299 https://repo.anaconda.com/pkgs/main/linux-64/libwebp-base-1.3.2-h5eee18b_0.conda#9179fc7baefa1e027f572edbc519d805 https://repo.anaconda.com/pkgs/main/linux-64/libxcb-1.15-h7f8727e_0.conda#ada518dcadd6aaee9aae47ba9a671553 -https://repo.anaconda.com/pkgs/main/linux-64/lz4-c-1.9.4-h6a678d5_0.conda#53915e9402180a7f22ea619c41089520 +https://repo.anaconda.com/pkgs/main/linux-64/lz4-c-1.9.4-h6a678d5_1.conda#2ee58861f2b92b868ce761abb831819d https://repo.anaconda.com/pkgs/main/linux-64/ncurses-6.4-h6a678d5_0.conda#5558eec6e2191741a92f832ea826251c -https://repo.anaconda.com/pkgs/main/linux-64/openssl-3.0.13-h7f8727e_0.conda#c73d46a4d666da0ae3dcd3fd8f805122 -https://repo.anaconda.com/pkgs/main/linux-64/xz-5.4.6-h5eee18b_0.conda#81a9916f581d4da15a3839216a487c66 -https://repo.anaconda.com/pkgs/main/linux-64/zlib-1.2.13-h5eee18b_0.conda#333e31fbfbb5057c92fa845ad6adef93 +https://repo.anaconda.com/pkgs/main/linux-64/openssl-3.0.13-h7f8727e_1.conda#d1d1fc47640fe0d9f7fa64c0a054bfd8 +https://repo.anaconda.com/pkgs/main/linux-64/xz-5.4.6-h5eee18b_1.conda#1562802f843297ee776a50b9329597ed +https://repo.anaconda.com/pkgs/main/linux-64/zlib-1.2.13-h5eee18b_1.conda#92e42d8310108b0a440fb2e60b2b2a25 https://repo.anaconda.com/pkgs/main/linux-64/ccache-3.7.9-hfe4627d_0.conda#bef6fc681c273bb7bd0c67d1a591365e https://repo.anaconda.com/pkgs/main/linux-64/libcups-2.4.2-h2d74bed_1.conda#3f265c2172a9e8c90a74037b6fa13685 https://repo.anaconda.com/pkgs/main/linux-64/libedit-3.1.20230828-h5eee18b_0.conda#850eb5a9d2d7d3c66cce12e84406ca08 https://repo.anaconda.com/pkgs/main/linux-64/libllvm14-14.0.6-hdb19cb5_3.conda#aefea2b45cf32f12b4f1ffaa70aa3201 https://repo.anaconda.com/pkgs/main/linux-64/libpng-1.6.39-h5eee18b_0.conda#f6aee38184512eb05b06c2e94d39ab22 https://repo.anaconda.com/pkgs/main/linux-64/libxml2-2.10.4-hfdd30dd_2.conda#ff7a0e3b92afb3c99b82c9f0ba8b5670 -https://repo.anaconda.com/pkgs/main/linux-64/pcre2-10.42-hebb0a14_0.conda#fca6dea6ce1eddd0876a024f62c5097a +https://repo.anaconda.com/pkgs/main/linux-64/pcre2-10.42-hebb0a14_1.conda#727e15c3cfa02b032da4eb0c1123e977 https://repo.anaconda.com/pkgs/main/linux-64/readline-8.2-h5eee18b_0.conda#be42180685cce6e6b0329201d9f48efb https://repo.anaconda.com/pkgs/main/linux-64/tk-8.6.12-h1ccaba5_0.conda#fa10ff4aa631fa4aa090a6234d7770b9 -https://repo.anaconda.com/pkgs/main/linux-64/zstd-1.5.5-hc292b87_0.conda#0f59d57dc21f585f4c282d60dfb46505 +https://repo.anaconda.com/pkgs/main/linux-64/zstd-1.5.5-hc292b87_2.conda#3b7fe809e5b429b4f90fe064842a2370 https://repo.anaconda.com/pkgs/main/linux-64/freetype-2.12.1-h4a9f257_0.conda#bdc7b5952e9c5dca01bc2f4ccef2f974 https://repo.anaconda.com/pkgs/main/linux-64/krb5-1.20.1-h143b758_1.conda#cf1accc86321fa25d6b978cc748039ae https://repo.anaconda.com/pkgs/main/linux-64/libclang13-14.0.6-default_he11475f_1.conda#44890feda1cf51639d9c94afbacce011 https://repo.anaconda.com/pkgs/main/linux-64/libglib-2.78.4-hdc74915_0.conda#2f6d27741e931d5b6ba56e1a1312aaf0 https://repo.anaconda.com/pkgs/main/linux-64/libtiff-4.5.1-h6a678d5_0.conda#235a671f74f0c4ecad9f9b3b107e3566 https://repo.anaconda.com/pkgs/main/linux-64/libxkbcommon-1.0.1-h5eee18b_1.conda#888b2e8f1bbf21017c503826e2d24b50 -https://repo.anaconda.com/pkgs/main/linux-64/sqlite-3.41.2-h5eee18b_0.conda#c7086c9ceb6cfe1c4c729a774a2d88a5 +https://repo.anaconda.com/pkgs/main/linux-64/sqlite-3.45.3-h5eee18b_0.conda#acf93d6aceb74d6110e20b44cc45939e https://repo.anaconda.com/pkgs/main/linux-64/cyrus-sasl-2.1.28-h52b45da_1.conda#d634af1577e4008f9228ae96ce671c44 https://repo.anaconda.com/pkgs/main/linux-64/fontconfig-2.14.1-h4c34cd2_2.conda#f0b472f5b544f8d57beb09ed4a2932e1 https://repo.anaconda.com/pkgs/main/linux-64/glib-tools-2.78.4-h6a678d5_0.conda#3dbe6227cd59818dca9afb75ccb70708 @@ -68,7 +68,7 @@ https://repo.anaconda.com/pkgs/main/linux-64/kiwisolver-1.4.4-py39h6a678d5_0.con https://repo.anaconda.com/pkgs/main/linux-64/mysql-5.7.24-h721c034_2.conda#dfc19ca2466d275c4c1f73b62c57f37b https://repo.anaconda.com/pkgs/main/linux-64/numpy-base-1.21.6-py39h375b286_0.conda#4ceaa5d6e6307fe06961d555f78b266f https://repo.anaconda.com/pkgs/main/linux-64/packaging-23.2-py39h06a4308_0.conda#b3f88f45f31bde016e49be3e941e5272 -https://repo.anaconda.com/pkgs/main/linux-64/pillow-10.2.0-py39h5eee18b_0.conda#fca2a1c44d16ec4b8ba71759b4ba9ba4 +https://repo.anaconda.com/pkgs/main/linux-64/pillow-10.3.0-py39h5eee18b_0.conda#b346d6c71267c1553b6c18d3db5fdf6d https://repo.anaconda.com/pkgs/main/linux-64/pluggy-1.0.0-py39h06a4308_1.conda#fb4fed11ed43cf727dbd51883cc1d9fa https://repo.anaconda.com/pkgs/main/linux-64/ply-3.11-py39h06a4308_0.conda#6c89bf6d2fdf6d24126e34cb83fd10f1 https://repo.anaconda.com/pkgs/main/linux-64/pyparsing-3.0.9-py39h06a4308_0.conda#3a0537468e59760404f63b4f04369828 @@ -78,7 +78,7 @@ https://repo.anaconda.com/pkgs/main/noarch/six-1.16.0-pyhd3eb1b0_1.conda#3458682 https://repo.anaconda.com/pkgs/main/noarch/toml-0.10.2-pyhd3eb1b0_0.conda#cda05f5f6d8509529d1a2743288d197a https://repo.anaconda.com/pkgs/main/linux-64/tomli-2.0.1-py39h06a4308_0.conda#b06dffe7ddca2645ed72f5116f0a087d https://repo.anaconda.com/pkgs/main/linux-64/tornado-6.3.3-py39h5eee18b_0.conda#9c4bd985bb8adcd12f47e790e95a9333 -https://repo.anaconda.com/pkgs/main/linux-64/wheel-0.41.2-py39h06a4308_0.conda#ec1b8213c3585defaa6042ed2f95861d +https://repo.anaconda.com/pkgs/main/linux-64/wheel-0.43.0-py39h06a4308_0.conda#40bb60408c7433d767fd8c65b35bc4a0 https://repo.anaconda.com/pkgs/main/linux-64/coverage-7.2.2-py39h5eee18b_0.conda#e9da151b7e1f56be2cb569c65949a1d2 https://repo.anaconda.com/pkgs/main/linux-64/dbus-1.13.18-hb2f20db_0.conda#6a6a6f1391f807847404344489ef6cf4 https://repo.anaconda.com/pkgs/main/linux-64/gstreamer-1.14.1-h5eee18b_1.conda#f2f26e6f869b5d87f41bd059fae47c3e diff --git a/build_tools/azure/pymin_conda_forge_mkl_win-64_conda.lock b/build_tools/azure/pymin_conda_forge_mkl_win-64_conda.lock index b98735a4336bb..88bc53dd94e1a 100644 --- a/build_tools/azure/pymin_conda_forge_mkl_win-64_conda.lock +++ b/build_tools/azure/pymin_conda_forge_mkl_win-64_conda.lock @@ -4,13 +4,11 @@ @EXPLICIT https://conda.anaconda.org/conda-forge/win-64/ca-certificates-2024.2.2-h56e8100_0.conda#63da060240ab8087b60d1357051ea7d6 https://conda.anaconda.org/conda-forge/win-64/intel-openmp-2024.1.0-h57928b3_965.conda#c66eb2fd33b999ccc258aef85689758e -https://conda.anaconda.org/conda-forge/win-64/libasprintf-0.22.5-h5728263_2.conda#75a6982b9ff0a8db0f53303527b07af8 https://conda.anaconda.org/conda-forge/win-64/mkl-include-2024.1.0-h66d3029_692.conda#60233966dc7c0261c9a443120b43c477 https://conda.anaconda.org/conda-forge/win-64/msys2-conda-epoch-20160418-1.tar.bz2#b0309b72560df66f71a9d5e34a5efdfa https://conda.anaconda.org/conda-forge/win-64/python_abi-3.9-4_cp39.conda#948b0d93d4ab1372d8fd45e1560afd47 https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda#161081fc7cec0bfda0d86d7cb595f8d8 https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.22621.0-h57928b3_0.tar.bz2#72608f6cd3e5898229c3ea16deb1ac43 -https://conda.anaconda.org/conda-forge/win-64/libasprintf-devel-0.22.5-h5728263_2.conda#8377da2cc31200d7181d2e48d60e4c7b https://conda.anaconda.org/conda-forge/win-64/m2w64-gmp-6.1.0-2.tar.bz2#53a1c73e1e3d185516d7e3af177596d9 https://conda.anaconda.org/conda-forge/win-64/m2w64-libwinpthread-git-5.0.0.4634.697f757-2.tar.bz2#774130a326dee16f1ceb05cc687ee4f0 https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.38.33130-h82b7239_18.conda#8be79fdd2725ddf7bbf8a27a4c1f79ba @@ -31,7 +29,7 @@ https://conda.anaconda.org/conda-forge/win-64/libwebp-base-1.4.0-hcfcfb64_0.cond https://conda.anaconda.org/conda-forge/win-64/libzlib-1.2.13-hcfcfb64_5.conda#5fdb9c6a113b6b6cb5e517fd972d5f41 https://conda.anaconda.org/conda-forge/win-64/m2w64-gcc-libgfortran-5.3.0-6.tar.bz2#066552ac6b907ec6d72c0ddab29050dc https://conda.anaconda.org/conda-forge/win-64/ninja-1.12.0-h91493d7_0.conda#e67ab00f4d2c089864c2b8dcccf4dc58 -https://conda.anaconda.org/conda-forge/win-64/openssl-3.2.1-hcfcfb64_1.conda#958e0418e93e50c575bff70fbcaa12d8 +https://conda.anaconda.org/conda-forge/win-64/openssl-3.3.0-hcfcfb64_0.conda#a6c544c9f060740c625dbf6d92cf3495 https://conda.anaconda.org/conda-forge/win-64/pthreads-win32-2.9.1-hfa6e2cd_3.tar.bz2#e2da8758d7d51ff6aa78a14dfb9dbed4 https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h5226925_1.conda#fc048363eb8f03cd1737600a5d08aafe https://conda.anaconda.org/conda-forge/win-64/xz-5.2.6-h8d14728_0.tar.bz2#515d77642eaa3639413c6b1bc3f94219 @@ -45,7 +43,7 @@ https://conda.anaconda.org/conda-forge/win-64/libxml2-2.12.6-hc3477c8_2.conda#ac https://conda.anaconda.org/conda-forge/win-64/m2w64-gcc-libs-5.3.0-7.tar.bz2#fe759119b8b3bfa720b8762c6fdc35de https://conda.anaconda.org/conda-forge/win-64/pcre2-10.43-h17e33f8_0.conda#d0485b8aa2cedb141a7bd27b4efa4c9c https://conda.anaconda.org/conda-forge/win-64/python-3.9.19-h4de0772_0_cpython.conda#b6999bc275e0e6beae7b1c8ea0be1e85 -https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.5-h12be248_0.conda#792bb5da68bf0a6cac6a6072ecb8dbeb +https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.6-h0ea2cb4_0.conda#9a17230f95733c04dc40a2b1e5491d74 https://conda.anaconda.org/conda-forge/win-64/brotli-bin-1.1.0-hcfcfb64_1.conda#0105229d7c5fabaa840043a86c10ec64 https://conda.anaconda.org/conda-forge/noarch/certifi-2024.2.2-pyhd8ed1ab_0.conda#0876280e409658fc6f9e75d035960333 https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz2#3faab06a954c2a04039983f2c4a50d99 @@ -54,11 +52,9 @@ https://conda.anaconda.org/conda-forge/win-64/cython-3.0.10-py39h99910a6_0.conda https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.0-pyhd8ed1ab_2.conda#8d652ea2ee8eaee02ed8dc820bc794aa https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_0.conda#15dda3cdbf330abfe9f555d22f66db46 https://conda.anaconda.org/conda-forge/win-64/freetype-2.12.1-hdaf720e_2.conda#3761b23693f768dc75a8fd0a73ca053f -https://conda.anaconda.org/conda-forge/win-64/gettext-tools-0.22.5-h7d00a51_2.conda#ef1c3bb48c013099c4872640a5f2096c https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_0.conda#f800d2da156d08e289b14e87e43c1ae5 https://conda.anaconda.org/conda-forge/win-64/kiwisolver-1.4.5-py39h1f6ef14_1.conda#4fc5bd0a7b535252028c647cc27d6c87 -https://conda.anaconda.org/conda-forge/win-64/libclang13-18.1.3-default_hf64faad_0.conda#9217c37b478ec601af909aafc954a6fc -https://conda.anaconda.org/conda-forge/win-64/libgettextpo-0.22.5-h5728263_2.conda#f4c826b19bf1ccee2a63a2c685039728 +https://conda.anaconda.org/conda-forge/win-64/libclang13-18.1.5-default_hf64faad_0.conda#8a662434c6be1f40e2d5d2506d05a41d https://conda.anaconda.org/conda-forge/win-64/libglib-2.80.0-h39d0aa6_6.conda#cd5c6efbe213c089f78575c98ab9a0ed https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.10.0-default_h2fffb23_1000.conda#ee944f0d41d9e2048f9d7492c1623ca3 https://conda.anaconda.org/conda-forge/win-64/libintl-devel-0.22.5-h5728263_2.conda#a2ad82fae23975e4ccbfab2847d31d48 @@ -71,7 +67,7 @@ https://conda.anaconda.org/conda-forge/win-64/pthread-stubs-0.4-hcd874cb_1001.ta https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.1.2-pyhd8ed1ab_0.conda#b9a4dacf97241704529131a0dfc0494f https://conda.anaconda.org/conda-forge/noarch/setuptools-69.5.1-pyhd8ed1ab_0.conda#7462280d81f639363e6e63c81276bd9e https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2#e5f25f8dbc060e9a8d912e432202afc2 -https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.4.0-pyhc1e730c_0.conda#b296278eef667c673bf51de6535bad88 +https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.5.0-pyhc1e730c_0.conda#df68d78237980a159bd7149f33c0e8fd https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_0.tar.bz2#f832c45a477c78bebd107098db465095 https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2#5844808ffab9ebdb694585b50ba02a96 https://conda.anaconda.org/conda-forge/win-64/tornado-6.4-py39ha55989b_0.conda#d8f52e8e1d02f9a5901f9224e2ddf98f @@ -81,12 +77,11 @@ https://conda.anaconda.org/conda-forge/win-64/xorg-libxau-1.0.11-hcd874cb_0.cond https://conda.anaconda.org/conda-forge/win-64/xorg-libxdmcp-1.1.3-hcd874cb_0.tar.bz2#46878ebb6b9cbd8afcf8088d7ef00ece https://conda.anaconda.org/conda-forge/noarch/zipp-3.17.0-pyhd8ed1ab_0.conda#2e4d6bc0b14e10f895fc6791a7d9b26a https://conda.anaconda.org/conda-forge/win-64/brotli-1.1.0-hcfcfb64_1.conda#f47f6db2528e38321fb00ae31674c133 -https://conda.anaconda.org/conda-forge/win-64/coverage-7.5.0-py39ha55e580_0.conda#53799e32a839e6a86e5b104a768dcd9d +https://conda.anaconda.org/conda-forge/win-64/coverage-7.5.1-py39ha55e580_0.conda#e8f43ea91f0f17d92d5575cfab41a42f https://conda.anaconda.org/conda-forge/win-64/glib-tools-2.80.0-h0a98069_6.conda#40d452e4012c00f644b1dd6319fcdbcf https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.4.0-pyhd8ed1ab_0.conda#c5d3907ad8bd7bf557521a1833cf7e6d -https://conda.anaconda.org/conda-forge/noarch/joblib-1.4.0-pyhd8ed1ab_0.conda#e0ed1bf13ce3a440e022157bf4764465 +https://conda.anaconda.org/conda-forge/noarch/joblib-1.4.2-pyhd8ed1ab_0.conda#25df261d4523d9f9783bcdb7208d872f https://conda.anaconda.org/conda-forge/win-64/lcms2-2.16-h67d730c_0.conda#d3592435917b62a8becff3a60db674f6 -https://conda.anaconda.org/conda-forge/win-64/libgettextpo-devel-0.22.5-h5728263_2.conda#6f42ec61abc6d52a4079800a640319c5 https://conda.anaconda.org/conda-forge/win-64/libxcb-1.15-hcd874cb_0.conda#090d91b69396f14afef450c285f9758c https://conda.anaconda.org/conda-forge/noarch/meson-1.4.0-pyhd8ed1ab_0.conda#52a0660cfa40b45bf254ecc3374cb2e0 https://conda.anaconda.org/conda-forge/win-64/openjpeg-2.5.2-h3d672ee_0.conda#7e7099ad94ac3b599808950cec30ad4e @@ -97,7 +92,6 @@ https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0-pyhd8ed1ab_0 https://conda.anaconda.org/conda-forge/win-64/sip-6.7.12-py39h99910a6_0.conda#0cc5774390ada632ed7975203057c91c https://conda.anaconda.org/conda-forge/win-64/tbb-2021.12.0-h91493d7_0.conda#21745fdd12f01b41178596143cbecffd https://conda.anaconda.org/conda-forge/win-64/fonttools-4.51.0-py39ha55989b_0.conda#5d19302bab29e347116b743e793aa7d6 -https://conda.anaconda.org/conda-forge/win-64/gettext-0.22.5-h5728263_2.conda#da84216f88a8c89eb943c683ceb34d7d https://conda.anaconda.org/conda-forge/win-64/glib-2.80.0-h39d0aa6_6.conda#a4036d0bc6f499ebe9fef7b887f3ca0f https://conda.anaconda.org/conda-forge/noarch/importlib-resources-6.4.0-pyhd8ed1ab_0.conda#dcbadab7a68738a028e195ab68ab2d2e https://conda.anaconda.org/conda-forge/noarch/meson-python-0.16.0-pyh0c530f3_0.conda#e16f0dbf502da873be9f9adb0dc52547 @@ -106,10 +100,10 @@ https://conda.anaconda.org/conda-forge/win-64/pillow-10.3.0-py39h9ee4981_0.conda https://conda.anaconda.org/conda-forge/win-64/pyqt5-sip-12.12.2-py39h99910a6_5.conda#dffbcea794c524c471772a5f697c2aea https://conda.anaconda.org/conda-forge/noarch/pytest-cov-5.0.0-pyhd8ed1ab_0.conda#c54c0107057d67ddf077751339ec2c63 https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.5.0-pyhd8ed1ab_0.conda#d5f595da2daead898ca958ac62f0307b -https://conda.anaconda.org/conda-forge/win-64/gstreamer-1.24.1-hb4038d2_1.conda#8a6dfe53ad02a3b151e6383a950043ee +https://conda.anaconda.org/conda-forge/win-64/gstreamer-1.24.3-h5006eae_0.conda#8c8959a520ef4911271fbf2cb2dfc3fe https://conda.anaconda.org/conda-forge/win-64/libblas-3.9.0-22_win64_mkl.conda#65c56ecdeceffd6c32d3d54db7e02c6e https://conda.anaconda.org/conda-forge/win-64/mkl-devel-2024.1.0-h57928b3_692.conda#9b3d1d4916a56fd32460f6fe784dcb51 -https://conda.anaconda.org/conda-forge/win-64/gst-plugins-base-1.24.1-h001b923_1.conda#7900eb39e6203249accb52fb705a2fb0 +https://conda.anaconda.org/conda-forge/win-64/gst-plugins-base-1.24.3-hba88be7_0.conda#1fa879c7b4868c58830762b6fac0075d https://conda.anaconda.org/conda-forge/win-64/libcblas-3.9.0-22_win64_mkl.conda#336c93ab102846c6131cf68e722a68f1 https://conda.anaconda.org/conda-forge/win-64/liblapack-3.9.0-22_win64_mkl.conda#c752cc2af9f3d8d7b2fdebb915a33ef7 https://conda.anaconda.org/conda-forge/win-64/liblapacke-3.9.0-22_win64_mkl.conda#db33ffa4bae1d2f6d5602afaa048bf6b @@ -118,7 +112,7 @@ https://conda.anaconda.org/conda-forge/win-64/qt-main-5.15.8-hcef0176_21.conda#7 https://conda.anaconda.org/conda-forge/win-64/blas-devel-3.9.0-22_win64_mkl.conda#adeb834f3b7b06f3d77cd90b7c9d08f0 https://conda.anaconda.org/conda-forge/win-64/contourpy-1.2.1-py39h1f6ef14_0.conda#03e25c6bae87f4f9595337255b44b0fb https://conda.anaconda.org/conda-forge/win-64/pyqt-5.15.9-py39hb77abff_5.conda#5ed899124a51958336371ff01482b8fd -https://conda.anaconda.org/conda-forge/win-64/scipy-1.13.0-py39hddb5d58_0.conda#cfe749056fb9ed9dbc096b5751becf34 +https://conda.anaconda.org/conda-forge/win-64/scipy-1.13.0-py39h1a10956_1.conda#5624ccefd670072fc86b2cd4ffdc6c44 https://conda.anaconda.org/conda-forge/win-64/blas-2.122-mkl.conda#aee642435696de144ddf91dc02101cf8 https://conda.anaconda.org/conda-forge/win-64/matplotlib-base-3.8.4-py39hf19769e_0.conda#7836c3dc5814f6d55a7392657c576e88 https://conda.anaconda.org/conda-forge/win-64/matplotlib-3.8.4-py39hcbf5309_0.conda#cc66c372d5eb745665da06ce56b7d72b diff --git a/build_tools/azure/pymin_conda_forge_openblas_ubuntu_2204_linux-64_conda.lock b/build_tools/azure/pymin_conda_forge_openblas_ubuntu_2204_linux-64_conda.lock index c7a155bece187..abdaeaee81527 100644 --- a/build_tools/azure/pymin_conda_forge_openblas_ubuntu_2204_linux-64_conda.lock +++ b/build_tools/azure/pymin_conda_forge_openblas_ubuntu_2204_linux-64_conda.lock @@ -7,15 +7,15 @@ https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2024.2.2-hbcca05 https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2#0c96522c6bdaed4b1566d11387caaf45 https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2#34893075a5c9e55cdafac56607368fc6 https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2#4d59c254e01d9cde7957100457e2d5fb -https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_1.conda#6185f640c43843e5ad6fd1c5372c3f80 +https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_2.conda#cbbe59391138ea5ad3658c76912e147f https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.40-h55db66e_0.conda#10569984e7db886e4f1abc2b47ad79a1 -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-13.2.0-h95c4c6d_6.conda#3cfab3e709f77e9f1b3d380eb622494a +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-13.2.0-hc0a3c3a_6.conda#2f18345bbc433c8a1ed887d7161e86a6 https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.9-4_cp39.conda#bfe4b3259a8ac6cdf0037752904da6a7 https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda#161081fc7cec0bfda0d86d7cb595f8d8 https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2#f766549260d6815b0c52253f1fb1bb29 https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2#fee5683a3f04bd15cbd8318b096a27ab https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_kmp_llvm.tar.bz2#562b26ba2e19059551a811e72ab7f793 -https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-hc881cc4_6.conda#df88796bd09a0d2ed292e59101478ad8 +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-h77fa898_6.conda#4398809ac84d0b8c28beebaaa83277f5 https://conda.anaconda.org/conda-forge/linux-64/alsa-lib-1.2.11-hd590300_1.conda#0bb492cca54017ea314b809b1ee3a176 https://conda.anaconda.org/conda-forge/linux-64/attr-2.5.1-h166bdaf_1.tar.bz2#d9c69a24ad678ffce24c6543a0176b00 https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hd590300_5.conda#69b8b6202a07720f448be700e300ccf4 @@ -46,7 +46,7 @@ https://conda.anaconda.org/conda-forge/linux-64/mpg123-1.32.6-h59595ed_0.conda#9 https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.4.20240210-h59595ed_0.conda#97da8860a0da5413c7c98a3b3838a645 https://conda.anaconda.org/conda-forge/linux-64/ninja-1.12.0-h00ab1b0_0.conda#b048701d52e7cbb5f59ddd4d3b17bbf5 https://conda.anaconda.org/conda-forge/linux-64/nspr-4.35-h27087fc_0.conda#da0ec11a6454ae19bff5b02ed881a2b1 -https://conda.anaconda.org/conda-forge/linux-64/openssl-3.2.1-hd590300_1.conda#9d731343cff6ee2e5a25c4a091bf8e2a +https://conda.anaconda.org/conda-forge/linux-64/openssl-3.3.0-hd590300_0.conda#c0f3abb4a16477208bbd43a39bd56f18 https://conda.anaconda.org/conda-forge/linux-64/pixman-0.43.2-h59595ed_0.conda#71004cbf7924e19c02746ccde9fd7123 https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-h36c2ea0_1001.tar.bz2#22dad4df6e8630e8dff2428f6f6a7036 https://conda.anaconda.org/conda-forge/linux-64/xorg-kbproto-1.0.7-h7f98852_1002.tar.bz2#4b230e8381279d76131116660f5a241a @@ -78,7 +78,7 @@ https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda#47 https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda#d453b98d9c83e71da0741bb0ff4d76bc https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.4-h7391055_0.conda#93ee23f12bc2e684548181256edd2cf6 https://conda.anaconda.org/conda-forge/linux-64/zlib-1.2.13-hd590300_5.conda#68c34ec6149623be41a1933ab996a209 -https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.5-hfc55251_0.conda#04b88013080254850d6c01ed54810589 +https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.6-ha6fb4c9_0.conda#4d056880988120e29d75bfff282e0f45 https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.1.0-hd590300_1.conda#39f910d205726805a958da408ca194ba https://conda.anaconda.org/conda-forge/linux-64/freetype-2.12.1-h267a509_2.conda#9ae35c3d96db2c94ce0cef86efdfa2cb https://conda.anaconda.org/conda-forge/linux-64/gettext-0.22.5-h59595ed_2.conda#219ba82e95d7614cf7140d2a4afc0926 @@ -86,10 +86,10 @@ https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.2-h659d440_0.conda#cd9 https://conda.anaconda.org/conda-forge/linux-64/libglib-2.80.0-hf2295e7_6.conda#9342e7c44c38bea649490f72d92c382d https://conda.anaconda.org/conda-forge/linux-64/libhiredis-1.0.2-h2cc385e_0.tar.bz2#b34907d3a81a3cd8095ee83d174c074a https://conda.anaconda.org/conda-forge/linux-64/libllvm15-15.0.7-hb3ce162_4.conda#8a35df3cbc0c8b12cc8af9473ae75eef -https://conda.anaconda.org/conda-forge/linux-64/libllvm18-18.1.3-h2448989_0.conda#927b6d6e80b2c0d4405a58b61ca248a3 +https://conda.anaconda.org/conda-forge/linux-64/libllvm18-18.1.5-hb77312f_0.conda#efd221d3668077ca067a206269418dec https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.27-pthreads_h413a1c8_0.conda#a356024784da6dfd4683dc5ecf45b155 https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.6.0-h1dd3fc0_3.conda#66f03896ffbe1a110ffda05c7a856504 -https://conda.anaconda.org/conda-forge/linux-64/llvm-openmp-18.1.3-h4dfa4b3_0.conda#d39965123dffcad4d750989be65bcb7c +https://conda.anaconda.org/conda-forge/linux-64/llvm-openmp-18.1.4-ha31de31_0.conda#48b9991e66abc186a7ad7975e97bd4d0 https://conda.anaconda.org/conda-forge/linux-64/mysql-libs-8.3.0-hca2cd23_4.conda#1b50eebe2a738a3146c154d2eceaa8b6 https://conda.anaconda.org/conda-forge/linux-64/nss-3.98-h1d7d5a4_0.conda#54b56c2fdf973656b748e0378900ec13 https://conda.anaconda.org/conda-forge/linux-64/python-3.9.19-h0755675_0_cpython.conda#d9ee3647fbd9e8595b8df759b2bbefb8 @@ -120,7 +120,7 @@ https://conda.anaconda.org/conda-forge/linux-64/kiwisolver-1.4.5-py39h7633fee_1. https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.16-hb7c19ff_0.conda#51bb7010fc86f70eee639b4bb7a894f5 https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-22_linux64_openblas.conda#1a2a0cd3153464fee6646f3dd6dad9b8 https://conda.anaconda.org/conda-forge/linux-64/libclang-cpp15-15.0.7-default_h127d8a8_5.conda#d0a9633b53cdc319b8a1a532ae7822b8 -https://conda.anaconda.org/conda-forge/linux-64/libclang13-18.1.3-default_h5d6823c_0.conda#5fff487759736b275dc3e4a263cac666 +https://conda.anaconda.org/conda-forge/linux-64/libclang13-18.1.5-default_h5d6823c_0.conda#60c39a00b694c98da03f67a3ba1d7499 https://conda.anaconda.org/conda-forge/linux-64/libcups-2.3.3-h4637d8d_4.conda#d4529f4dff3057982a7617c7ac58fde3 https://conda.anaconda.org/conda-forge/linux-64/libflac-1.4.3-h59595ed_0.conda#ee48bf17cc83a00f59ca1494d5646869 https://conda.anaconda.org/conda-forge/linux-64/libgpg-error-1.49-h4f305b6_0.conda#dfcfd72c7a430d3616763ecfbefe4ca9 @@ -132,7 +132,7 @@ https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.2-h488ebb8_0.conda# https://conda.anaconda.org/conda-forge/noarch/packaging-24.0-pyhd8ed1ab_0.conda#248f521b64ce055e7feae3105e7abeb8 https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_0.conda#d3483c8fc2dc2cc3f5cf43e26d60cabf https://conda.anaconda.org/conda-forge/noarch/ply-3.11-pyhd8ed1ab_2.conda#18c6deb6f9602e32446398203c8f0e91 -https://conda.anaconda.org/conda-forge/noarch/pygments-2.17.2-pyhd8ed1ab_0.conda#140a7f159396547e9799aa98f9f0742e +https://conda.anaconda.org/conda-forge/noarch/pygments-2.18.0-pyhd8ed1ab_0.conda#b7f5c092b8f9800150d998a71b76d5a1 https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.1.2-pyhd8ed1ab_0.conda#b9a4dacf97241704529131a0dfc0494f https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha2e5f31_6.tar.bz2#2a7de29fb590ca14b5243c4c812c8025 https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2024.1-pyhd8ed1ab_0.conda#98206ea9954216ee7540f0c773f2104d @@ -142,7 +142,7 @@ https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2#e5 https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-2.2.0-pyhd8ed1ab_0.tar.bz2#4d22a9315e78c6827f806065957d566e https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_0.conda#da1d979339e2714c30a8e806a33ec087 https://conda.anaconda.org/conda-forge/noarch/tabulate-0.9.0-pyhd8ed1ab_1.tar.bz2#4759805cce2d914c38472f70bf4d8bcb -https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.4.0-pyhc1e730c_0.conda#b296278eef667c673bf51de6535bad88 +https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.5.0-pyhc1e730c_0.conda#df68d78237980a159bd7149f33c0e8fd https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_0.tar.bz2#f832c45a477c78bebd107098db465095 https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2#5844808ffab9ebdb694585b50ba02a96 https://conda.anaconda.org/conda-forge/linux-64/tornado-6.4-py39hd1e30aa_0.conda#1e865e9188204cdfb1fd2531780add88 @@ -160,7 +160,7 @@ https://conda.anaconda.org/conda-forge/linux-64/glib-2.80.0-hf2295e7_6.conda#a1e https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-7.1.0-pyha770c72_0.conda#0896606848b2dc5cebdf111b6543aa04 https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.4.0-pyhd8ed1ab_0.conda#c5d3907ad8bd7bf557521a1833cf7e6d https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.3-pyhd8ed1ab_0.conda#e7d8df6509ba635247ff9aea31134262 -https://conda.anaconda.org/conda-forge/noarch/joblib-1.4.0-pyhd8ed1ab_0.conda#e0ed1bf13ce3a440e022157bf4764465 +https://conda.anaconda.org/conda-forge/noarch/joblib-1.4.2-pyhd8ed1ab_0.conda#25df261d4523d9f9783bcdb7208d872f https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-22_linux64_openblas.conda#4b31699e0ec5de64d5896e580389c9a1 https://conda.anaconda.org/conda-forge/linux-64/libgcrypt-1.10.3-hd590300_0.conda#32d16ad533c59bb0a3c5ffaf16110829 https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-22_linux64_openblas.conda#b083767b6c877e24ee597d93b87ab838 @@ -174,7 +174,7 @@ https://conda.anaconda.org/conda-forge/noarch/pytest-7.4.4-pyhd8ed1ab_0.conda#a9 https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0-pyhd8ed1ab_0.conda#2cf4264fffb9e6eff6031c5b6884d61c https://conda.anaconda.org/conda-forge/linux-64/sip-6.7.12-py39h3d6467e_0.conda#e667a3ab0df62c54e60e1843d2e6defb https://conda.anaconda.org/conda-forge/noarch/urllib3-2.2.1-pyhd8ed1ab_0.conda#08807a87fa7af10754d46f63b368e016 -https://conda.anaconda.org/conda-forge/linux-64/gstreamer-1.24.1-h98fc4e7_1.conda#b04b5cdf3ba01430db27979250bc5a1d +https://conda.anaconda.org/conda-forge/linux-64/gstreamer-1.24.3-haf2f30d_0.conda#f3df87cc9ef0b5113bff55aefcbcafd5 https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-8.4.0-h3d44ed6_0.conda#27f46291a6aaa3c2a4f798ebd35a7ddb https://conda.anaconda.org/conda-forge/noarch/importlib-resources-6.4.0-pyhd8ed1ab_0.conda#dcbadab7a68738a028e195ab68ab2d2e https://conda.anaconda.org/conda-forge/linux-64/liblapacke-3.9.0-22_linux64_openblas.conda#1fd156abd41a4992835952f6f4d951d0 @@ -186,10 +186,10 @@ https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.5.0-pyhd8ed1ab_0.co https://conda.anaconda.org/conda-forge/noarch/requests-2.31.0-pyhd8ed1ab_0.conda#a30144e4156cdbb236f99ebb49828f8b https://conda.anaconda.org/conda-forge/linux-64/blas-devel-3.9.0-22_linux64_openblas.conda#63ddb593595c9cf5eb08d3de54d66df8 https://conda.anaconda.org/conda-forge/linux-64/contourpy-1.2.1-py39h7633fee_0.conda#bdc188e59857d6efab332714e0d01d93 -https://conda.anaconda.org/conda-forge/linux-64/gst-plugins-base-1.24.1-hfa15dee_1.conda#a6dd2bbc684913e2bef0a54ce56fcbfb +https://conda.anaconda.org/conda-forge/linux-64/gst-plugins-base-1.24.3-h9ad1361_0.conda#8fb0e954c616bb0f9389efac4b4ed44b https://conda.anaconda.org/conda-forge/linux-64/pandas-2.2.2-py39hddac248_0.conda#259c4e76e6bda8888aefc098ae1ba749 https://conda.anaconda.org/conda-forge/linux-64/pulseaudio-client-17.0-hb77b528_0.conda#07f45f1be1c25345faddb8db0de8039b -https://conda.anaconda.org/conda-forge/linux-64/scipy-1.13.0-py39h474f0d3_0.conda#46ae0ecba9726ab4fa44c78fefa522cf +https://conda.anaconda.org/conda-forge/linux-64/scipy-1.13.0-py39haf93ffa_1.conda#57ce54e228e3fbc60e42fa368eff3251 https://conda.anaconda.org/conda-forge/linux-64/blas-2.122-openblas.conda#5065468105542a8b23ea47bd8b6fa55f https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.8.4-py39he9076e7_0.conda#1919384a8420e7bb25f6c3a582e0857c https://conda.anaconda.org/conda-forge/linux-64/pyamg-5.1.0-py39hda80f44_0.conda#f225666c47726329201b604060f1436c diff --git a/build_tools/circle/doc_linux-64_conda.lock b/build_tools/circle/doc_linux-64_conda.lock index baccc168b059d..7ca02c7cdb159 100644 --- a/build_tools/circle/doc_linux-64_conda.lock +++ b/build_tools/circle/doc_linux-64_conda.lock @@ -7,23 +7,23 @@ https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2024.2.2-hbcca05 https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2#0c96522c6bdaed4b1566d11387caaf45 https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2#34893075a5c9e55cdafac56607368fc6 https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2#4d59c254e01d9cde7957100457e2d5fb -https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_1.conda#6185f640c43843e5ad6fd1c5372c3f80 +https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_2.conda#cbbe59391138ea5ad3658c76912e147f https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-2.6.32-he073ed8_17.conda#d731b543793afc0433c4fd593e693fce https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.40-h55db66e_0.conda#10569984e7db886e4f1abc2b47ad79a1 -https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-12.3.0-h2af2641_106.conda#b97e137a252f112b8d5fadb313bd8ec9 -https://conda.anaconda.org/conda-forge/noarch/libstdcxx-devel_linux-64-12.3.0-h2af2641_106.conda#647bd9d44ad216d410329e659c898d8f -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-13.2.0-h95c4c6d_6.conda#3cfab3e709f77e9f1b3d380eb622494a +https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-12.3.0-h0223996_106.conda#304f58c690e7ba23b67a4b5c8e99a062 +https://conda.anaconda.org/conda-forge/noarch/libstdcxx-devel_linux-64-12.3.0-h0223996_106.conda#dfb9aac785d6b25b46be7850d974a72e +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-13.2.0-hc0a3c3a_6.conda#2f18345bbc433c8a1ed887d7161e86a6 https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.9-4_cp39.conda#bfe4b3259a8ac6cdf0037752904da6a7 https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda#161081fc7cec0bfda0d86d7cb595f8d8 https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2#f766549260d6815b0c52253f1fb1bb29 -https://conda.anaconda.org/conda-forge/linux-64/libgomp-13.2.0-hc881cc4_6.conda#aae89d3736661c36a5591788aebd0817 +https://conda.anaconda.org/conda-forge/linux-64/libgomp-13.2.0-h77fa898_6.conda#e733e0573651a1f0639fa8ce066a286e https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.12-he073ed8_17.conda#595db67e32b276298ff3d94d07d47fbf https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.40-ha885e6a_0.conda#800a4c872b5bc06fa83888d112fe6c4f https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2#fee5683a3f04bd15cbd8318b096a27ab https://conda.anaconda.org/conda-forge/linux-64/binutils-2.40-h4852527_0.conda#a05c7712be80622934f7011e0a1d43fc https://conda.anaconda.org/conda-forge/linux-64/binutils_linux-64-2.40-hdade7a5_3.conda#2d9a60578bc28469d9aeef9aea5520c3 https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_kmp_llvm.tar.bz2#562b26ba2e19059551a811e72ab7f793 -https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-hc881cc4_6.conda#df88796bd09a0d2ed292e59101478ad8 +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-h77fa898_6.conda#4398809ac84d0b8c28beebaaa83277f5 https://conda.anaconda.org/conda-forge/linux-64/alsa-lib-1.2.11-hd590300_1.conda#0bb492cca54017ea314b809b1ee3a176 https://conda.anaconda.org/conda-forge/linux-64/aom-3.8.2-h59595ed_0.conda#625e1fed28a5139aed71b3a76117ef84 https://conda.anaconda.org/conda-forge/linux-64/attr-2.5.1-h166bdaf_1.tar.bz2#d9c69a24ad678ffce24c6543a0176b00 @@ -52,7 +52,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.0.0-hd590300_1.c https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda#30fd6e37fe21f86f4bd26d6ee73eeec7 https://conda.anaconda.org/conda-forge/linux-64/libogg-1.3.4-h7f98852_1.tar.bz2#6e8cc2173440d77708196c5b93771680 https://conda.anaconda.org/conda-forge/linux-64/libopus-1.3.1-h7f98852_1.tar.bz2#15345e56d527b330e1cacbdf58676e8f -https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-12.3.0-h2af2641_6.conda#1cf0b420341bb1a7b7f34f6e0f4bbf2b +https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-12.3.0-hb8811af_6.conda#a9a764e2e753ed038da59343560d8a66 https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda#40b61aab5c7ba9ff276c41cfffe6b80b https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.4.0-hd590300_0.conda#b26e8aa824079e1be0294e7152ca4559 https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda#5aa797f8787fe7a17d1b0821485b5adc @@ -63,7 +63,7 @@ https://conda.anaconda.org/conda-forge/linux-64/mpg123-1.32.6-h59595ed_0.conda#9 https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.4.20240210-h59595ed_0.conda#97da8860a0da5413c7c98a3b3838a645 https://conda.anaconda.org/conda-forge/linux-64/ninja-1.12.0-h00ab1b0_0.conda#b048701d52e7cbb5f59ddd4d3b17bbf5 https://conda.anaconda.org/conda-forge/linux-64/nspr-4.35-h27087fc_0.conda#da0ec11a6454ae19bff5b02ed881a2b1 -https://conda.anaconda.org/conda-forge/linux-64/openssl-3.2.1-hd590300_1.conda#9d731343cff6ee2e5a25c4a091bf8e2a +https://conda.anaconda.org/conda-forge/linux-64/openssl-3.3.0-hd590300_0.conda#c0f3abb4a16477208bbd43a39bd56f18 https://conda.anaconda.org/conda-forge/linux-64/pixman-0.43.2-h59595ed_0.conda#71004cbf7924e19c02746ccde9fd7123 https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-h36c2ea0_1001.tar.bz2#22dad4df6e8630e8dff2428f6f6a7036 https://conda.anaconda.org/conda-forge/linux-64/rav1e-0.6.6-he8a937b_2.conda#77d9955b4abddb811cb8ab1aa7d743e4 @@ -81,7 +81,7 @@ https://conda.anaconda.org/conda-forge/linux-64/xz-5.2.6-h166bdaf_0.tar.bz2#2161 https://conda.anaconda.org/conda-forge/linux-64/zfp-1.0.1-h59595ed_0.conda#fd486bffbf0d6841cf1456a8f2e3a995 https://conda.anaconda.org/conda-forge/linux-64/zlib-ng-2.0.7-h0b41bf4_0.conda#49e8329110001f04923fe7e864990b0c https://conda.anaconda.org/conda-forge/linux-64/expat-2.6.2-h59595ed_0.conda#53fb86322bdb89496d7579fe3f02fd61 -https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-12.3.0-h1562d66_6.conda#5e4e8358a4ab43498e0ac3b6776d1c94 +https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-12.3.0-h58ffeeb_6.conda#53914a98926ce169b83726cb78366a6c https://conda.anaconda.org/conda-forge/linux-64/libasprintf-devel-0.22.5-h661eb56_2.conda#02e41ab5834dcdcc8590cf29d9526f50 https://conda.anaconda.org/conda-forge/linux-64/libavif16-1.0.4-hd9d6309_2.conda#a8c65cba5f77abc1f2e85ab9a0e614aa https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.1.0-hd590300_1.conda#f07002e225d7a60a694d42a7bf5ff53f @@ -102,7 +102,7 @@ https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda#47 https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda#d453b98d9c83e71da0741bb0ff4d76bc https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.4-h7391055_0.conda#93ee23f12bc2e684548181256edd2cf6 https://conda.anaconda.org/conda-forge/linux-64/zlib-1.2.13-hd590300_5.conda#68c34ec6149623be41a1933ab996a209 -https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.5-hfc55251_0.conda#04b88013080254850d6c01ed54810589 +https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.6-ha6fb4c9_0.conda#4d056880988120e29d75bfff282e0f45 https://conda.anaconda.org/conda-forge/linux-64/blosc-1.21.5-hc2324a3_1.conda#11d76bee958b1989bd1ac6ee7372ea6d https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.1.0-hd590300_1.conda#39f910d205726805a958da408ca194ba https://conda.anaconda.org/conda-forge/linux-64/c-blosc2-2.14.4-hb4ffafa_1.conda#84eb54e92644c328e087e1c725773317 @@ -110,16 +110,16 @@ https://conda.anaconda.org/conda-forge/linux-64/freetype-2.12.1-h267a509_2.conda https://conda.anaconda.org/conda-forge/linux-64/gcc-12.3.0-h915e2ae_6.conda#ec683e084ea08ef94528f15d30fa1e03 https://conda.anaconda.org/conda-forge/linux-64/gcc_linux-64-12.3.0-h6477408_3.conda#7a53f84c45bdf4656ba27b9e9ed68b3d https://conda.anaconda.org/conda-forge/linux-64/gettext-0.22.5-h59595ed_2.conda#219ba82e95d7614cf7140d2a4afc0926 -https://conda.anaconda.org/conda-forge/linux-64/gfortran_impl_linux-64-12.3.0-h6d6b2fb_6.conda#d6c441226a4bd0af4c024e8c0f4a47cf -https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-12.3.0-h1562d66_6.conda#5ad72ddd14e13d589dea2afe6e626619 +https://conda.anaconda.org/conda-forge/linux-64/gfortran_impl_linux-64-12.3.0-h1645026_6.conda#664d4e904674f1173752580ffdc24d46 +https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-12.3.0-h2a574ab_6.conda#aab48c86452d78a416992deeee901a52 https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.2-h659d440_0.conda#cd95826dbd331ed1be26bdf401432844 https://conda.anaconda.org/conda-forge/linux-64/libglib-2.80.0-hf2295e7_6.conda#9342e7c44c38bea649490f72d92c382d https://conda.anaconda.org/conda-forge/linux-64/libjxl-0.10.2-hcae5a98_0.conda#901db891e1e21afd8524cd636a8c8e3b https://conda.anaconda.org/conda-forge/linux-64/libllvm15-15.0.7-hb3ce162_4.conda#8a35df3cbc0c8b12cc8af9473ae75eef -https://conda.anaconda.org/conda-forge/linux-64/libllvm18-18.1.3-h2448989_0.conda#927b6d6e80b2c0d4405a58b61ca248a3 +https://conda.anaconda.org/conda-forge/linux-64/libllvm18-18.1.5-hb77312f_0.conda#efd221d3668077ca067a206269418dec https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.27-pthreads_h413a1c8_0.conda#a356024784da6dfd4683dc5ecf45b155 https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.6.0-h1dd3fc0_3.conda#66f03896ffbe1a110ffda05c7a856504 -https://conda.anaconda.org/conda-forge/linux-64/llvm-openmp-18.1.3-h4dfa4b3_0.conda#d39965123dffcad4d750989be65bcb7c +https://conda.anaconda.org/conda-forge/linux-64/llvm-openmp-18.1.4-ha31de31_0.conda#48b9991e66abc186a7ad7975e97bd4d0 https://conda.anaconda.org/conda-forge/linux-64/mysql-libs-8.3.0-hca2cd23_4.conda#1b50eebe2a738a3146c154d2eceaa8b6 https://conda.anaconda.org/conda-forge/linux-64/nss-3.98-h1d7d5a4_0.conda#54b56c2fdf973656b748e0378900ec13 https://conda.anaconda.org/conda-forge/linux-64/python-3.9.19-h0755675_0_cpython.conda#d9ee3647fbd9e8595b8df759b2bbefb8 @@ -131,7 +131,7 @@ https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.9-h8ee46fc_0.con https://conda.anaconda.org/conda-forge/noarch/alabaster-0.7.16-pyhd8ed1ab_0.conda#def531a3ac77b7fb8c21d17bb5d0badb https://conda.anaconda.org/conda-forge/linux-64/brotli-1.1.0-hd590300_1.conda#f27a24d46e3ea7b70a1f98e50c62508f https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py39h3d6467e_1.conda#c48418c8b35f1d59ae9ae1174812b40a -https://conda.anaconda.org/conda-forge/linux-64/c-compiler-1.7.0-hd590300_0.conda#fad1d0a651bf929c6c16fbf1f6ccfa7c +https://conda.anaconda.org/conda-forge/linux-64/c-compiler-1.7.0-hd590300_1.conda#e9dffe1056994133616378309f932d77 https://conda.anaconda.org/conda-forge/noarch/certifi-2024.2.2-pyhd8ed1ab_0.conda#0876280e409658fc6f9e75d035960333 https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.3.2-pyhd8ed1ab_0.conda#7f4a9e3fcff3f6356ae99244a014da6a https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz2#3faab06a954c2a04039983f2c4a50d99 @@ -154,7 +154,7 @@ https://conda.anaconda.org/conda-forge/linux-64/kiwisolver-1.4.5-py39h7633fee_1. https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.16-hb7c19ff_0.conda#51bb7010fc86f70eee639b4bb7a894f5 https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-22_linux64_openblas.conda#1a2a0cd3153464fee6646f3dd6dad9b8 https://conda.anaconda.org/conda-forge/linux-64/libclang-cpp15-15.0.7-default_h127d8a8_5.conda#d0a9633b53cdc319b8a1a532ae7822b8 -https://conda.anaconda.org/conda-forge/linux-64/libclang13-18.1.3-default_h5d6823c_0.conda#5fff487759736b275dc3e4a263cac666 +https://conda.anaconda.org/conda-forge/linux-64/libclang13-18.1.5-default_h5d6823c_0.conda#60c39a00b694c98da03f67a3ba1d7499 https://conda.anaconda.org/conda-forge/linux-64/libcups-2.3.3-h4637d8d_4.conda#d4529f4dff3057982a7617c7ac58fde3 https://conda.anaconda.org/conda-forge/linux-64/libflac-1.4.3-h59595ed_0.conda#ee48bf17cc83a00f59ca1494d5646869 https://conda.anaconda.org/conda-forge/linux-64/libgpg-error-1.49-h4f305b6_0.conda#dfcfd72c7a430d3616763ecfbefe4ca9 @@ -169,7 +169,7 @@ https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.2.1-pyhd8ed1ab_0.co https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_0.conda#d3483c8fc2dc2cc3f5cf43e26d60cabf https://conda.anaconda.org/conda-forge/noarch/ply-3.11-pyhd8ed1ab_2.conda#18c6deb6f9602e32446398203c8f0e91 https://conda.anaconda.org/conda-forge/linux-64/psutil-5.9.8-py39hd1e30aa_0.conda#ec86403fde8793ac1c36f8afa3d15902 -https://conda.anaconda.org/conda-forge/noarch/pygments-2.17.2-pyhd8ed1ab_0.conda#140a7f159396547e9799aa98f9f0742e +https://conda.anaconda.org/conda-forge/noarch/pygments-2.18.0-pyhd8ed1ab_0.conda#b7f5c092b8f9800150d998a71b76d5a1 https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.1.2-pyhd8ed1ab_0.conda#b9a4dacf97241704529131a0dfc0494f https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha2e5f31_6.tar.bz2#2a7de29fb590ca14b5243c4c812c8025 https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2024.1-pyhd8ed1ab_0.conda#98206ea9954216ee7540f0c773f2104d @@ -180,7 +180,7 @@ https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-2.2.0-pyhd8ed1ab_0 https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_0.conda#da1d979339e2714c30a8e806a33ec087 https://conda.anaconda.org/conda-forge/noarch/tabulate-0.9.0-pyhd8ed1ab_1.tar.bz2#4759805cce2d914c38472f70bf4d8bcb https://conda.anaconda.org/conda-forge/noarch/tenacity-8.2.3-pyhd8ed1ab_0.conda#1482e77f87c6a702a7e05ef22c9b197b -https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.4.0-pyhc1e730c_0.conda#b296278eef667c673bf51de6535bad88 +https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.5.0-pyhc1e730c_0.conda#df68d78237980a159bd7149f33c0e8fd https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_0.tar.bz2#f832c45a477c78bebd107098db465095 https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2#5844808ffab9ebdb694585b50ba02a96 https://conda.anaconda.org/conda-forge/linux-64/tornado-6.4-py39hd1e30aa_0.conda#1e865e9188204cdfb1fd2531780add88 @@ -195,14 +195,14 @@ https://conda.anaconda.org/conda-forge/noarch/zipp-3.17.0-pyhd8ed1ab_0.conda#2e4 https://conda.anaconda.org/conda-forge/noarch/babel-2.14.0-pyhd8ed1ab_0.conda#9669586875baeced8fc30c0826c3270e https://conda.anaconda.org/conda-forge/linux-64/brunsli-0.1-h9c3ff4c_0.tar.bz2#c1ac6229d0bfd14f8354ff9ad2a26cad https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.0-h3faef2a_0.conda#f907bb958910dc404647326ca80c263e -https://conda.anaconda.org/conda-forge/linux-64/cxx-compiler-1.7.0-h00ab1b0_0.conda#b4537c98cb59f8725b0e1e65816b4a28 +https://conda.anaconda.org/conda-forge/linux-64/cxx-compiler-1.7.0-h00ab1b0_1.conda#28de2e073db9ca9b72858bee9fb6f571 https://conda.anaconda.org/conda-forge/linux-64/fonttools-4.51.0-py39hd1e30aa_0.conda#79f5dd8778873faa54e8f7b2729fe8a6 -https://conda.anaconda.org/conda-forge/linux-64/fortran-compiler-1.7.0-heb67821_0.conda#7ef7c0f111dad1c8006504a0f1ccd820 +https://conda.anaconda.org/conda-forge/linux-64/fortran-compiler-1.7.0-heb67821_1.conda#cf4b0e7c4c78bb0662aed9b27c414a3c https://conda.anaconda.org/conda-forge/linux-64/glib-2.80.0-hf2295e7_6.conda#a1e026a82a562b443845db5614ca568a https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-7.1.0-pyha770c72_0.conda#0896606848b2dc5cebdf111b6543aa04 https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.4.0-pyhd8ed1ab_0.conda#c5d3907ad8bd7bf557521a1833cf7e6d https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.3-pyhd8ed1ab_0.conda#e7d8df6509ba635247ff9aea31134262 -https://conda.anaconda.org/conda-forge/noarch/joblib-1.4.0-pyhd8ed1ab_0.conda#e0ed1bf13ce3a440e022157bf4764465 +https://conda.anaconda.org/conda-forge/noarch/joblib-1.4.2-pyhd8ed1ab_0.conda#25df261d4523d9f9783bcdb7208d872f https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-22_linux64_openblas.conda#4b31699e0ec5de64d5896e580389c9a1 https://conda.anaconda.org/conda-forge/linux-64/libgcrypt-1.10.3-hd590300_0.conda#32d16ad533c59bb0a3c5ffaf16110829 https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-22_linux64_openblas.conda#b083767b6c877e24ee597d93b87ab838 @@ -212,14 +212,14 @@ https://conda.anaconda.org/conda-forge/noarch/memory_profiler-0.61.0-pyhd8ed1ab_ https://conda.anaconda.org/conda-forge/noarch/meson-1.4.0-pyhd8ed1ab_0.conda#52a0660cfa40b45bf254ecc3374cb2e0 https://conda.anaconda.org/conda-forge/linux-64/pillow-10.3.0-py39h90c7501_0.conda#1e3b6af9592be71ce19f0a6aae05d97b https://conda.anaconda.org/conda-forge/noarch/pip-24.0-pyhd8ed1ab_0.conda#f586ac1e56c8638b64f9c8122a7b8a67 -https://conda.anaconda.org/conda-forge/noarch/plotly-5.21.0-pyhd8ed1ab_0.conda#c8f5835e6c3a850d9a000d23056d780b +https://conda.anaconda.org/conda-forge/noarch/plotly-5.22.0-pyhd8ed1ab_0.conda#5b409a5f738e7d76c2b426eddb7e9956 https://conda.anaconda.org/conda-forge/noarch/pyproject-metadata-0.8.0-pyhd8ed1ab_0.conda#573fe09d7bd0cd4bcc210d8369b5ca47 https://conda.anaconda.org/conda-forge/noarch/pytest-7.4.4-pyhd8ed1ab_0.conda#a9d145de8c5f064b5fa68fb34725d9f4 https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0-pyhd8ed1ab_0.conda#2cf4264fffb9e6eff6031c5b6884d61c https://conda.anaconda.org/conda-forge/linux-64/sip-6.7.12-py39h3d6467e_0.conda#e667a3ab0df62c54e60e1843d2e6defb https://conda.anaconda.org/conda-forge/noarch/urllib3-2.2.1-pyhd8ed1ab_0.conda#08807a87fa7af10754d46f63b368e016 -https://conda.anaconda.org/conda-forge/linux-64/compilers-1.7.0-ha770c72_0.conda#81458b3aed8ab8711951ec3c0c04e097 -https://conda.anaconda.org/conda-forge/linux-64/gstreamer-1.24.1-h98fc4e7_1.conda#b04b5cdf3ba01430db27979250bc5a1d +https://conda.anaconda.org/conda-forge/linux-64/compilers-1.7.0-ha770c72_1.conda#d8d07866ac3b5b6937213c89a1874f08 +https://conda.anaconda.org/conda-forge/linux-64/gstreamer-1.24.3-haf2f30d_0.conda#f3df87cc9ef0b5113bff55aefcbcafd5 https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-8.4.0-h3d44ed6_0.conda#27f46291a6aaa3c2a4f798ebd35a7ddb https://conda.anaconda.org/conda-forge/noarch/importlib-resources-6.4.0-pyhd8ed1ab_0.conda#dcbadab7a68738a028e195ab68ab2d2e https://conda.anaconda.org/conda-forge/noarch/lazy_loader-0.4-pyhd8ed1ab_0.conda#a284ff318fbdb0dd83928275b4b6087c @@ -232,7 +232,7 @@ https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.5.0-pyhd8ed1ab_0.co https://conda.anaconda.org/conda-forge/noarch/requests-2.31.0-pyhd8ed1ab_0.conda#a30144e4156cdbb236f99ebb49828f8b https://conda.anaconda.org/conda-forge/linux-64/blas-devel-3.9.0-22_linux64_openblas.conda#63ddb593595c9cf5eb08d3de54d66df8 https://conda.anaconda.org/conda-forge/linux-64/contourpy-1.2.1-py39h7633fee_0.conda#bdc188e59857d6efab332714e0d01d93 -https://conda.anaconda.org/conda-forge/linux-64/gst-plugins-base-1.24.1-hfa15dee_1.conda#a6dd2bbc684913e2bef0a54ce56fcbfb +https://conda.anaconda.org/conda-forge/linux-64/gst-plugins-base-1.24.3-h9ad1361_0.conda#8fb0e954c616bb0f9389efac4b4ed44b https://conda.anaconda.org/conda-forge/linux-64/imagecodecs-2024.1.1-py39ha98d97a_6.conda#9ada409e8a8202f848abfed8e4e3f6be https://conda.anaconda.org/conda-forge/noarch/imageio-2.34.1-pyh4b66e23_0.conda#bcf6a6f4c6889ca083e8d33afbafb8d5 https://conda.anaconda.org/conda-forge/linux-64/pandas-2.2.2-py39hddac248_0.conda#259c4e76e6bda8888aefc098ae1ba749 @@ -241,21 +241,21 @@ https://conda.anaconda.org/conda-forge/linux-64/polars-0.20.23-py39ha963410_0.co https://conda.anaconda.org/conda-forge/noarch/pooch-1.8.1-pyhd8ed1ab_0.conda#d15917f33140f8d2ac9ca44db7ec8a25 https://conda.anaconda.org/conda-forge/linux-64/pulseaudio-client-17.0-hb77b528_0.conda#07f45f1be1c25345faddb8db0de8039b https://conda.anaconda.org/conda-forge/linux-64/pywavelets-1.4.1-py39h44dd56e_1.conda#d037c20e3da2e85f03ebd20ad480c359 -https://conda.anaconda.org/conda-forge/linux-64/scipy-1.13.0-py39h474f0d3_0.conda#46ae0ecba9726ab4fa44c78fefa522cf +https://conda.anaconda.org/conda-forge/linux-64/scipy-1.13.0-py39haf93ffa_1.conda#57ce54e228e3fbc60e42fa368eff3251 https://conda.anaconda.org/conda-forge/linux-64/blas-2.122-openblas.conda#5065468105542a8b23ea47bd8b6fa55f https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.8.4-py39he9076e7_0.conda#1919384a8420e7bb25f6c3a582e0857c https://conda.anaconda.org/conda-forge/linux-64/pyamg-5.1.0-py39hda80f44_0.conda#f225666c47726329201b604060f1436c https://conda.anaconda.org/conda-forge/linux-64/qt-main-5.15.8-hc9dc06e_21.conda#b325046180590c868ce0dbf267b82eb8 https://conda.anaconda.org/conda-forge/linux-64/statsmodels-0.14.1-py39h44dd56e_0.conda#dc565186b972bd87e49b9c35390ddd8c -https://conda.anaconda.org/conda-forge/noarch/tifffile-2024.4.18-pyhd8ed1ab_0.conda#9640ec921dce12e87e589ac634c7bd8a +https://conda.anaconda.org/conda-forge/noarch/tifffile-2024.5.3-pyhd8ed1ab_0.conda#0658fd78a808b6f3508917ba66b20f75 https://conda.anaconda.org/conda-forge/linux-64/pyqt-5.15.9-py39h52134e7_5.conda#e1f148e57d071b09187719df86f513c1 https://conda.anaconda.org/conda-forge/linux-64/scikit-image-0.22.0-py39hddac248_2.conda#8d502a4d2cbe5a45ff35ca8af8cbec0a -https://conda.anaconda.org/conda-forge/noarch/seaborn-base-0.13.2-pyhd8ed1ab_0.conda#0918a9201e824211cdf444dbf8d55752 +https://conda.anaconda.org/conda-forge/noarch/seaborn-base-0.13.2-pyhd8ed1ab_2.conda#b713b116feaf98acdba93ad4d7f90ca1 https://conda.anaconda.org/conda-forge/linux-64/matplotlib-3.8.4-py39hf3d152e_0.conda#c66d2da2669fddc657b679bccab95775 -https://conda.anaconda.org/conda-forge/noarch/seaborn-0.13.2-hd8ed1ab_0.conda#fd31ebf5867914de597f9961c478e482 +https://conda.anaconda.org/conda-forge/noarch/seaborn-0.13.2-hd8ed1ab_2.conda#a79d8797f62715255308d92d3a91ef2e https://conda.anaconda.org/conda-forge/noarch/numpydoc-1.7.0-pyhd8ed1ab_0.conda#1ad3afced398492586ca1bef70328be4 https://conda.anaconda.org/conda-forge/noarch/sphinx-copybutton-0.5.2-pyhd8ed1ab_0.conda#ac832cc43adc79118cf6e23f1f9b8995 -https://conda.anaconda.org/conda-forge/noarch/sphinx-gallery-0.15.0-pyhd8ed1ab_0.conda#1a49ca9515ef9a96edff2eea06143dc6 +https://conda.anaconda.org/conda-forge/noarch/sphinx-gallery-0.16.0-pyhd8ed1ab_0.conda#add28691ee89e875b190eda07929d5d4 https://conda.anaconda.org/conda-forge/noarch/sphinx-prompt-1.4.0-pyhd8ed1ab_0.tar.bz2#88ee91e8679603f2a5bd036d52919cc2 https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-1.0.8-pyhd8ed1ab_0.conda#611a35a27914fac3aa37611a6fe40bb5 https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-devhelp-1.0.6-pyhd8ed1ab_0.conda#d7e4954df0d3aea2eacc7835ad12671d @@ -299,7 +299,7 @@ https://conda.anaconda.org/conda-forge/noarch/sphinxext-opengraph-0.9.1-pyhd8ed1 # pip cffi @ https://files.pythonhosted.org/packages/ea/ac/e9e77bc385729035143e54cc8c4785bd480eaca9df17565963556b0b7a93/cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098 # pip doit @ https://files.pythonhosted.org/packages/44/83/a2960d2c975836daa629a73995134fd86520c101412578c57da3d2aa71ee/doit-0.36.0-py3-none-any.whl#sha256=ebc285f6666871b5300091c26eafdff3de968a6bd60ea35dd1e3fc6f2e32479a # pip jupyter-core @ https://files.pythonhosted.org/packages/c9/fb/108ecd1fe961941959ad0ee4e12ee7b8b1477247f30b1fdfd83ceaf017f0/jupyter_core-5.7.2-py3-none-any.whl#sha256=4f7315d2f6b4bcf2e3e7cb6e46772eba760ae459cd1f59d29eb57b0a01bd7409 -# pip referencing @ https://files.pythonhosted.org/packages/8f/ad/0a39c92d2d2769eb02adfdd50282e25341dccee3a14753c972d7327de664/referencing-0.35.0-py3-none-any.whl#sha256=8080727b30e364e5783152903672df9b6b091c926a146a759080b62ca3126cd6 +# pip referencing @ https://files.pythonhosted.org/packages/b7/59/2056f61236782a2c86b33906c025d4f4a0b17be0161b63b70fd9e8775d36/referencing-0.35.1-py3-none-any.whl#sha256=eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de # pip rfc3339-validator @ https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl#sha256=24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa # pip terminado @ https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl#sha256=a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0 # pip tinycss2 @ https://files.pythonhosted.org/packages/2c/4d/0db5b8a613d2a59bbc29bc5bb44a2f8070eb9ceab11c50d477502a8a0092/tinycss2-1.3.0-py3-none-any.whl#sha256=54a8dbdffb334d536851be0226030e9505965bb2f30f21a4a82c55fb2a80fae7 @@ -308,15 +308,15 @@ https://conda.anaconda.org/conda-forge/noarch/sphinxext-opengraph-0.9.1-pyhd8ed1 # pip jsonschema-specifications @ https://files.pythonhosted.org/packages/ee/07/44bd408781594c4d0a027666ef27fab1e441b109dc3b76b4f836f8fd04fe/jsonschema_specifications-2023.12.1-py3-none-any.whl#sha256=87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c # pip jupyter-server-terminals @ https://files.pythonhosted.org/packages/07/2d/2b32cdbe8d2a602f697a649798554e4f072115438e92249624e532e8aca6/jupyter_server_terminals-0.5.3-py3-none-any.whl#sha256=41ee0d7dc0ebf2809c668e0fc726dfaf258fcd3e769568996ca731b6194ae9aa # pip jupyterlite-core @ https://files.pythonhosted.org/packages/05/d2/1d59d9a70d684b1eb3eb3a0b80a36b4e1d691e94af5d53aee56b1ad5240b/jupyterlite_core-0.3.0-py3-none-any.whl#sha256=247cc34ae6fedda41b15ce4778997164508b2039bc92480665cadfe955193467 -# pip pyzmq @ https://files.pythonhosted.org/packages/2c/1f/044aafe62c85d579f87846f9cfd2cfce12a08ae72426ec92986171421d9f/pyzmq-26.0.2-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl#sha256=c40b09b7e184d6e3e1be1c8af2cc320c0f9f610d8a5df3dd866e6e6e4e32b235 +# pip pyzmq @ https://files.pythonhosted.org/packages/64/b8/1c181c13e118cabccfd25bd3e169e44958c649180b0d78b798a66899e08b/pyzmq-26.0.3-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl#sha256=b3cd31f859b662ac5d7f4226ec7d8bd60384fa037fc02aee6ff0b53ba29a3ba8 # pip argon2-cffi @ https://files.pythonhosted.org/packages/a4/6a/e8a041599e78b6b3752da48000b14c8d1e8a04ded09c88c714ba047f34f5/argon2_cffi-23.1.0-py3-none-any.whl#sha256=c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea -# pip jsonschema @ https://files.pythonhosted.org/packages/39/9d/b035d024c62c85f2e2d4806a59ca7b8520307f34e0932fbc8cc75fe7b2d9/jsonschema-4.21.1-py3-none-any.whl#sha256=7996507afae316306f9e2290407761157c6f78002dcf7419acb99822143d1c6f +# pip jsonschema @ https://files.pythonhosted.org/packages/c8/2f/324fab4be6fe37fb7b521546e8a557e6cf08c1c1b3d0b4839a00f589d9ef/jsonschema-4.22.0-py3-none-any.whl#sha256=ff4cfd6b1367a40e7bc6411caec72effadd3db0bbe5017de188f2d6108335802 # pip jupyter-client @ https://files.pythonhosted.org/packages/75/6d/d7b55b9c1ac802ab066b3e5015e90faab1fffbbd67a2af498ffc6cc81c97/jupyter_client-8.6.1-py3-none-any.whl#sha256=3b7bd22f058434e3b9a7ea4b1500ed47de2713872288c0d511d19926f99b459f # pip jupyterlite-pyodide-kernel @ https://files.pythonhosted.org/packages/83/bf/749279904094015d5cb7e030dd7a111f8b013b9f1809d954d04ebe0c1197/jupyterlite_pyodide_kernel-0.3.1-py3-none-any.whl#sha256=ac9d9dd95adcced57d465a7b298f220d8785845c017ad3abf2a3677ff02631c6 # pip jupyter-events @ https://files.pythonhosted.org/packages/a5/94/059180ea70a9a326e1815176b2370da56376da347a796f8c4f0b830208ef/jupyter_events-0.10.0-py3-none-any.whl#sha256=4b72130875e59d57716d327ea70d3ebc3af1944d3717e5a498b8a06c6c159960 # pip nbformat @ https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl#sha256=3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b # pip nbclient @ https://files.pythonhosted.org/packages/66/e8/00517a23d3eeaed0513e718fbc94aab26eaa1758f5690fc8578839791c79/nbclient-0.10.0-py3-none-any.whl#sha256=f13e3529332a1f1f81d82a53210322476a168bb7090a0289c795fe9cc11c9d3f -# pip nbconvert @ https://files.pythonhosted.org/packages/23/8a/8d67cbd984739247e4b205c1143e2f71b25b4f71e180fe70f7cb2cf02633/nbconvert-7.16.3-py3-none-any.whl#sha256=ddeff14beeeedf3dd0bc506623e41e4507e551736de59df69a91f86700292b3b +# pip nbconvert @ https://files.pythonhosted.org/packages/b8/bb/bb5b6a515d1584aa2fd89965b11db6632e4bdc69495a52374bcc36e56cfa/nbconvert-7.16.4-py3-none-any.whl#sha256=05873c620fe520b6322bf8a5ad562692343fe3452abda5765c7a34b7d1aa3eb3 # pip jupyter-server @ https://files.pythonhosted.org/packages/07/46/6bb926b3bf878bf687b952fb6a4c09d014b4575a25960f2cd1a61793763f/jupyter_server-2.14.0-py3-none-any.whl#sha256=fb6be52c713e80e004fac34b35a0990d6d36ba06fd0a2b2ed82b899143a64210 # pip jupyterlab-server @ https://files.pythonhosted.org/packages/2f/b9/ed4ecad7cf1863a64920dc4c19b0376628b5d6bd28d2ec1e00cbac4ba2fb/jupyterlab_server-2.27.1-py3-none-any.whl#sha256=f5e26156e5258b24d532c84e7c74cc212e203bff93eb856f81c24c16daeecc75 -# pip jupyterlite-sphinx @ https://files.pythonhosted.org/packages/38/c9/5f1142c005cf8d75830b10029e53f074324bc85cfca1f1d0f22a207b771c/jupyterlite_sphinx-0.9.3-py3-none-any.whl#sha256=be6332d16490ea2fa90b78187a2c5e1c357195966a25741d60b1790346571041 +# pip jupyterlite-sphinx @ https://files.pythonhosted.org/packages/7c/c7/5c0f4dc5408122881a32b1809529d1d7adcc60cb176c7b50725910c328cc/jupyterlite_sphinx-0.14.0-py3-none-any.whl#sha256=144edf37e8a77f49b249dd57e3a22ce19ff87805ed79b460e831dc90bf38c269 diff --git a/build_tools/circle/doc_min_dependencies_linux-64_conda.lock b/build_tools/circle/doc_min_dependencies_linux-64_conda.lock index 69eca7785d55c..dd291f8882efb 100644 --- a/build_tools/circle/doc_min_dependencies_linux-64_conda.lock +++ b/build_tools/circle/doc_min_dependencies_linux-64_conda.lock @@ -7,24 +7,24 @@ https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2024.2.2-hbcca05 https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2#0c96522c6bdaed4b1566d11387caaf45 https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2#34893075a5c9e55cdafac56607368fc6 https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2#4d59c254e01d9cde7957100457e2d5fb -https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_1.conda#6185f640c43843e5ad6fd1c5372c3f80 +https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_2.conda#cbbe59391138ea5ad3658c76912e147f https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-2.6.32-he073ed8_17.conda#d731b543793afc0433c4fd593e693fce https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.40-h55db66e_0.conda#10569984e7db886e4f1abc2b47ad79a1 -https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-12.3.0-h2af2641_106.conda#b97e137a252f112b8d5fadb313bd8ec9 -https://conda.anaconda.org/conda-forge/noarch/libstdcxx-devel_linux-64-12.3.0-h2af2641_106.conda#647bd9d44ad216d410329e659c898d8f -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-13.2.0-h95c4c6d_6.conda#3cfab3e709f77e9f1b3d380eb622494a +https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-12.3.0-h0223996_106.conda#304f58c690e7ba23b67a4b5c8e99a062 +https://conda.anaconda.org/conda-forge/noarch/libstdcxx-devel_linux-64-12.3.0-h0223996_106.conda#dfb9aac785d6b25b46be7850d974a72e +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-13.2.0-hc0a3c3a_6.conda#2f18345bbc433c8a1ed887d7161e86a6 https://conda.anaconda.org/conda-forge/linux-64/mkl-include-2024.1.0-ha957f24_692.conda#b35af3f0f25498f4e9fc4c471910346c https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.9-4_cp39.conda#bfe4b3259a8ac6cdf0037752904da6a7 https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda#161081fc7cec0bfda0d86d7cb595f8d8 https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2#f766549260d6815b0c52253f1fb1bb29 -https://conda.anaconda.org/conda-forge/linux-64/libgomp-13.2.0-hc881cc4_6.conda#aae89d3736661c36a5591788aebd0817 +https://conda.anaconda.org/conda-forge/linux-64/libgomp-13.2.0-h77fa898_6.conda#e733e0573651a1f0639fa8ce066a286e https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.12-he073ed8_17.conda#595db67e32b276298ff3d94d07d47fbf https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.40-ha885e6a_0.conda#800a4c872b5bc06fa83888d112fe6c4f https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2#fee5683a3f04bd15cbd8318b096a27ab https://conda.anaconda.org/conda-forge/linux-64/binutils-2.40-h4852527_0.conda#a05c7712be80622934f7011e0a1d43fc https://conda.anaconda.org/conda-forge/linux-64/binutils_linux-64-2.40-hdade7a5_3.conda#2d9a60578bc28469d9aeef9aea5520c3 https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_kmp_llvm.tar.bz2#562b26ba2e19059551a811e72ab7f793 -https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-hc881cc4_6.conda#df88796bd09a0d2ed292e59101478ad8 +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-h77fa898_6.conda#4398809ac84d0b8c28beebaaa83277f5 https://conda.anaconda.org/conda-forge/linux-64/alsa-lib-1.2.11-hd590300_1.conda#0bb492cca54017ea314b809b1ee3a176 https://conda.anaconda.org/conda-forge/linux-64/attr-2.5.1-h166bdaf_1.tar.bz2#d9c69a24ad678ffce24c6543a0176b00 https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hd590300_5.conda#69b8b6202a07720f448be700e300ccf4 @@ -45,7 +45,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.0.0-hd590300_1.c https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda#30fd6e37fe21f86f4bd26d6ee73eeec7 https://conda.anaconda.org/conda-forge/linux-64/libogg-1.3.4-h7f98852_1.tar.bz2#6e8cc2173440d77708196c5b93771680 https://conda.anaconda.org/conda-forge/linux-64/libopus-1.3.1-h7f98852_1.tar.bz2#15345e56d527b330e1cacbdf58676e8f -https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-12.3.0-h2af2641_6.conda#1cf0b420341bb1a7b7f34f6e0f4bbf2b +https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-12.3.0-hb8811af_6.conda#a9a764e2e753ed038da59343560d8a66 https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda#40b61aab5c7ba9ff276c41cfffe6b80b https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.4.0-hd590300_0.conda#b26e8aa824079e1be0294e7152ca4559 https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda#5aa797f8787fe7a17d1b0821485b5adc @@ -55,7 +55,7 @@ https://conda.anaconda.org/conda-forge/linux-64/mpg123-1.32.6-h59595ed_0.conda#9 https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.4.20240210-h59595ed_0.conda#97da8860a0da5413c7c98a3b3838a645 https://conda.anaconda.org/conda-forge/linux-64/ninja-1.12.0-h00ab1b0_0.conda#b048701d52e7cbb5f59ddd4d3b17bbf5 https://conda.anaconda.org/conda-forge/linux-64/nspr-4.35-h27087fc_0.conda#da0ec11a6454ae19bff5b02ed881a2b1 -https://conda.anaconda.org/conda-forge/linux-64/openssl-3.2.1-hd590300_1.conda#9d731343cff6ee2e5a25c4a091bf8e2a +https://conda.anaconda.org/conda-forge/linux-64/openssl-3.3.0-hd590300_0.conda#c0f3abb4a16477208bbd43a39bd56f18 https://conda.anaconda.org/conda-forge/linux-64/pixman-0.43.2-h59595ed_0.conda#71004cbf7924e19c02746ccde9fd7123 https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-h36c2ea0_1001.tar.bz2#22dad4df6e8630e8dff2428f6f6a7036 https://conda.anaconda.org/conda-forge/linux-64/xorg-kbproto-1.0.7-h7f98852_1002.tar.bz2#4b230e8381279d76131116660f5a241a @@ -69,7 +69,7 @@ https://conda.anaconda.org/conda-forge/linux-64/xorg-xproto-7.0.31-h7f98852_1007 https://conda.anaconda.org/conda-forge/linux-64/xz-5.2.6-h166bdaf_0.tar.bz2#2161070d867d1b1204ea749c8eec4ef0 https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h7f98852_2.tar.bz2#4cb3ad778ec2d5a7acbdf254eb1c42ae https://conda.anaconda.org/conda-forge/linux-64/expat-2.6.2-h59595ed_0.conda#53fb86322bdb89496d7579fe3f02fd61 -https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-12.3.0-h1562d66_6.conda#5e4e8358a4ab43498e0ac3b6776d1c94 +https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-12.3.0-h58ffeeb_6.conda#53914a98926ce169b83726cb78366a6c https://conda.anaconda.org/conda-forge/linux-64/libasprintf-devel-0.22.5-h661eb56_2.conda#02e41ab5834dcdcc8590cf29d9526f50 https://conda.anaconda.org/conda-forge/linux-64/libcap-2.69-h0f662aa_0.conda#25cb5999faa414e5ccb2c1388f62d3d5 https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20191231-he28a2e2_2.tar.bz2#4d331e44109e3f0e19b4cb8f9b82f3e1 @@ -87,20 +87,20 @@ https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda#47 https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda#d453b98d9c83e71da0741bb0ff4d76bc https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.4-h7391055_0.conda#93ee23f12bc2e684548181256edd2cf6 https://conda.anaconda.org/conda-forge/linux-64/zlib-1.2.13-hd590300_5.conda#68c34ec6149623be41a1933ab996a209 -https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.5-hfc55251_0.conda#04b88013080254850d6c01ed54810589 +https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.6-ha6fb4c9_0.conda#4d056880988120e29d75bfff282e0f45 https://conda.anaconda.org/conda-forge/linux-64/freetype-2.12.1-h267a509_2.conda#9ae35c3d96db2c94ce0cef86efdfa2cb https://conda.anaconda.org/conda-forge/linux-64/gcc-12.3.0-h915e2ae_6.conda#ec683e084ea08ef94528f15d30fa1e03 https://conda.anaconda.org/conda-forge/linux-64/gcc_linux-64-12.3.0-h6477408_3.conda#7a53f84c45bdf4656ba27b9e9ed68b3d https://conda.anaconda.org/conda-forge/linux-64/gettext-0.22.5-h59595ed_2.conda#219ba82e95d7614cf7140d2a4afc0926 -https://conda.anaconda.org/conda-forge/linux-64/gfortran_impl_linux-64-12.3.0-h6d6b2fb_6.conda#d6c441226a4bd0af4c024e8c0f4a47cf -https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-12.3.0-h1562d66_6.conda#5ad72ddd14e13d589dea2afe6e626619 +https://conda.anaconda.org/conda-forge/linux-64/gfortran_impl_linux-64-12.3.0-h1645026_6.conda#664d4e904674f1173752580ffdc24d46 +https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-12.3.0-h2a574ab_6.conda#aab48c86452d78a416992deeee901a52 https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.2-h659d440_0.conda#cd95826dbd331ed1be26bdf401432844 https://conda.anaconda.org/conda-forge/linux-64/libglib-2.80.0-hf2295e7_6.conda#9342e7c44c38bea649490f72d92c382d https://conda.anaconda.org/conda-forge/linux-64/libhwloc-2.10.0-default_h2fb2949_1000.conda#7e3726e647a619c6ce5939014dfde86d https://conda.anaconda.org/conda-forge/linux-64/libllvm15-15.0.7-hb3ce162_4.conda#8a35df3cbc0c8b12cc8af9473ae75eef -https://conda.anaconda.org/conda-forge/linux-64/libllvm18-18.1.3-h2448989_0.conda#927b6d6e80b2c0d4405a58b61ca248a3 +https://conda.anaconda.org/conda-forge/linux-64/libllvm18-18.1.5-hb77312f_0.conda#efd221d3668077ca067a206269418dec https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.6.0-h1dd3fc0_3.conda#66f03896ffbe1a110ffda05c7a856504 -https://conda.anaconda.org/conda-forge/linux-64/llvm-openmp-18.1.3-h4dfa4b3_0.conda#d39965123dffcad4d750989be65bcb7c +https://conda.anaconda.org/conda-forge/linux-64/llvm-openmp-18.1.4-ha31de31_0.conda#48b9991e66abc186a7ad7975e97bd4d0 https://conda.anaconda.org/conda-forge/linux-64/mysql-libs-8.3.0-hca2cd23_4.conda#1b50eebe2a738a3146c154d2eceaa8b6 https://conda.anaconda.org/conda-forge/linux-64/nss-3.98-h1d7d5a4_0.conda#54b56c2fdf973656b748e0378900ec13 https://conda.anaconda.org/conda-forge/linux-64/python-3.9.19-h0755675_0_cpython.conda#d9ee3647fbd9e8595b8df759b2bbefb8 @@ -111,7 +111,7 @@ https://conda.anaconda.org/conda-forge/linux-64/xcb-util-wm-0.4.1-h8ee46fc_1.con https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.9-h8ee46fc_0.conda#077b6e8ad6a3ddb741fce2496dd01bec https://conda.anaconda.org/conda-forge/noarch/alabaster-0.7.16-pyhd8ed1ab_0.conda#def531a3ac77b7fb8c21d17bb5d0badb https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py39h3d6467e_1.conda#c48418c8b35f1d59ae9ae1174812b40a -https://conda.anaconda.org/conda-forge/linux-64/c-compiler-1.7.0-hd590300_0.conda#fad1d0a651bf929c6c16fbf1f6ccfa7c +https://conda.anaconda.org/conda-forge/linux-64/c-compiler-1.7.0-hd590300_1.conda#e9dffe1056994133616378309f932d77 https://conda.anaconda.org/conda-forge/noarch/certifi-2024.2.2-pyhd8ed1ab_0.conda#0876280e409658fc6f9e75d035960333 https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.3.2-pyhd8ed1ab_0.conda#7f4a9e3fcff3f6356ae99244a014da6a https://conda.anaconda.org/conda-forge/noarch/click-8.1.7-unix_pyh707e725_0.conda#f3ad426304898027fc619827ff428eca @@ -136,7 +136,7 @@ https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_0.conda https://conda.anaconda.org/conda-forge/linux-64/kiwisolver-1.4.5-py39h7633fee_1.conda#c9f74d717e5a2847a9f8b779c54130f2 https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.16-hb7c19ff_0.conda#51bb7010fc86f70eee639b4bb7a894f5 https://conda.anaconda.org/conda-forge/linux-64/libclang-cpp15-15.0.7-default_h127d8a8_5.conda#d0a9633b53cdc319b8a1a532ae7822b8 -https://conda.anaconda.org/conda-forge/linux-64/libclang13-18.1.3-default_h5d6823c_0.conda#5fff487759736b275dc3e4a263cac666 +https://conda.anaconda.org/conda-forge/linux-64/libclang13-18.1.5-default_h5d6823c_0.conda#60c39a00b694c98da03f67a3ba1d7499 https://conda.anaconda.org/conda-forge/linux-64/libcups-2.3.3-h4637d8d_4.conda#d4529f4dff3057982a7617c7ac58fde3 https://conda.anaconda.org/conda-forge/linux-64/libflac-1.4.3-h59595ed_0.conda#ee48bf17cc83a00f59ca1494d5646869 https://conda.anaconda.org/conda-forge/linux-64/libgpg-error-1.49-h4f305b6_0.conda#dfcfd72c7a430d3616763ecfbefe4ca9 @@ -150,7 +150,7 @@ https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.2.1-pyhd8ed1ab_0.co https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_0.conda#d3483c8fc2dc2cc3f5cf43e26d60cabf https://conda.anaconda.org/conda-forge/noarch/ply-3.11-pyhd8ed1ab_2.conda#18c6deb6f9602e32446398203c8f0e91 https://conda.anaconda.org/conda-forge/linux-64/psutil-5.9.8-py39hd1e30aa_0.conda#ec86403fde8793ac1c36f8afa3d15902 -https://conda.anaconda.org/conda-forge/noarch/pygments-2.17.2-pyhd8ed1ab_0.conda#140a7f159396547e9799aa98f9f0742e +https://conda.anaconda.org/conda-forge/noarch/pygments-2.18.0-pyhd8ed1ab_0.conda#b7f5c092b8f9800150d998a71b76d5a1 https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.1.2-pyhd8ed1ab_0.conda#b9a4dacf97241704529131a0dfc0494f https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha2e5f31_6.tar.bz2#2a7de29fb590ca14b5243c4c812c8025 https://conda.anaconda.org/conda-forge/noarch/pytz-2024.1-pyhd8ed1ab_0.conda#3eeeeb9e4827ace8c0c1419c85d590ad @@ -161,7 +161,7 @@ https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-2.2.0-pyhd8ed1ab_0 https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_0.conda#da1d979339e2714c30a8e806a33ec087 https://conda.anaconda.org/conda-forge/linux-64/tbb-2021.12.0-h00ab1b0_0.conda#f1b776cff1b426e7e7461a8502a3b731 https://conda.anaconda.org/conda-forge/noarch/tenacity-8.2.3-pyhd8ed1ab_0.conda#1482e77f87c6a702a7e05ef22c9b197b -https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.4.0-pyhc1e730c_0.conda#b296278eef667c673bf51de6535bad88 +https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.5.0-pyhc1e730c_0.conda#df68d78237980a159bd7149f33c0e8fd https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_0.tar.bz2#f832c45a477c78bebd107098db465095 https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2#5844808ffab9ebdb694585b50ba02a96 https://conda.anaconda.org/conda-forge/noarch/toolz-0.12.1-pyhd8ed1ab_0.conda#2fcb582444635e2c402e8569bb94e039 @@ -175,13 +175,13 @@ https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.11-hd590300_ https://conda.anaconda.org/conda-forge/noarch/zipp-3.17.0-pyhd8ed1ab_0.conda#2e4d6bc0b14e10f895fc6791a7d9b26a https://conda.anaconda.org/conda-forge/noarch/babel-2.14.0-pyhd8ed1ab_0.conda#9669586875baeced8fc30c0826c3270e https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.0-h3faef2a_0.conda#f907bb958910dc404647326ca80c263e -https://conda.anaconda.org/conda-forge/linux-64/cxx-compiler-1.7.0-h00ab1b0_0.conda#b4537c98cb59f8725b0e1e65816b4a28 +https://conda.anaconda.org/conda-forge/linux-64/cxx-compiler-1.7.0-h00ab1b0_1.conda#28de2e073db9ca9b72858bee9fb6f571 https://conda.anaconda.org/conda-forge/linux-64/cytoolz-0.12.3-py39hd1e30aa_0.conda#dc0fb8e157c7caba4c98f1e1f9d2e5f4 -https://conda.anaconda.org/conda-forge/linux-64/fortran-compiler-1.7.0-heb67821_0.conda#7ef7c0f111dad1c8006504a0f1ccd820 +https://conda.anaconda.org/conda-forge/linux-64/fortran-compiler-1.7.0-heb67821_1.conda#cf4b0e7c4c78bb0662aed9b27c414a3c https://conda.anaconda.org/conda-forge/linux-64/glib-2.80.0-hf2295e7_6.conda#a1e026a82a562b443845db5614ca568a https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-7.1.0-pyha770c72_0.conda#0896606848b2dc5cebdf111b6543aa04 https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.3-pyhd8ed1ab_0.conda#e7d8df6509ba635247ff9aea31134262 -https://conda.anaconda.org/conda-forge/noarch/joblib-1.4.0-pyhd8ed1ab_0.conda#e0ed1bf13ce3a440e022157bf4764465 +https://conda.anaconda.org/conda-forge/noarch/joblib-1.4.2-pyhd8ed1ab_0.conda#25df261d4523d9f9783bcdb7208d872f https://conda.anaconda.org/conda-forge/linux-64/libgcrypt-1.10.3-hd590300_0.conda#32d16ad533c59bb0a3c5ffaf16110829 https://conda.anaconda.org/conda-forge/linux-64/libsndfile-1.2.2-hc60ed4a_1.conda#ef1910918dd895516a769ed36b5b3a4e https://conda.anaconda.org/conda-forge/linux-64/libxkbcommon-1.7.0-h662e7e4_0.conda#b32c0da42b1f24a98577bb3d7fc0b995 @@ -197,8 +197,8 @@ https://conda.anaconda.org/conda-forge/noarch/pytest-7.4.4-pyhd8ed1ab_0.conda#a9 https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0-pyhd8ed1ab_0.conda#2cf4264fffb9e6eff6031c5b6884d61c https://conda.anaconda.org/conda-forge/linux-64/sip-6.7.12-py39h3d6467e_0.conda#e667a3ab0df62c54e60e1843d2e6defb https://conda.anaconda.org/conda-forge/noarch/urllib3-2.2.1-pyhd8ed1ab_0.conda#08807a87fa7af10754d46f63b368e016 -https://conda.anaconda.org/conda-forge/linux-64/compilers-1.7.0-ha770c72_0.conda#81458b3aed8ab8711951ec3c0c04e097 -https://conda.anaconda.org/conda-forge/linux-64/gstreamer-1.24.1-h98fc4e7_1.conda#b04b5cdf3ba01430db27979250bc5a1d +https://conda.anaconda.org/conda-forge/linux-64/compilers-1.7.0-ha770c72_1.conda#d8d07866ac3b5b6937213c89a1874f08 +https://conda.anaconda.org/conda-forge/linux-64/gstreamer-1.24.3-haf2f30d_0.conda#f3df87cc9ef0b5113bff55aefcbcafd5 https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-8.4.0-h3d44ed6_0.conda#27f46291a6aaa3c2a4f798ebd35a7ddb https://conda.anaconda.org/conda-forge/noarch/importlib_metadata-7.1.0-hd8ed1ab_0.conda#6ef2b72d291b39e479d7694efa2b2b98 https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-22_linux64_mkl.conda#eb6deb4ba6f92ea3f31c09cb8b764738 @@ -208,8 +208,8 @@ https://conda.anaconda.org/conda-forge/linux-64/mkl-devel-2024.1.0-ha770c72_692. https://conda.anaconda.org/conda-forge/linux-64/pyqt5-sip-12.12.2-py39h3d6467e_5.conda#93aff412f3e49fdb43361c0215cbd72d https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.5.0-pyhd8ed1ab_0.conda#d5f595da2daead898ca958ac62f0307b https://conda.anaconda.org/conda-forge/noarch/requests-2.31.0-pyhd8ed1ab_0.conda#a30144e4156cdbb236f99ebb49828f8b -https://conda.anaconda.org/conda-forge/noarch/dask-core-2024.4.2-pyhd8ed1ab_0.conda#bb4e6c52855aa64a5443ca4eedaa6cfe -https://conda.anaconda.org/conda-forge/linux-64/gst-plugins-base-1.24.1-hfa15dee_1.conda#a6dd2bbc684913e2bef0a54ce56fcbfb +https://conda.anaconda.org/conda-forge/noarch/dask-core-2024.5.0-pyhd8ed1ab_0.conda#8472f598970b9af96ca8106fa243ab67 +https://conda.anaconda.org/conda-forge/linux-64/gst-plugins-base-1.24.3-h9ad1361_0.conda#8fb0e954c616bb0f9389efac4b4ed44b https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-22_linux64_mkl.conda#d6f942423116553f068b2f2d93ffea2e https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-22_linux64_mkl.conda#4edf2e7ce63920e4f539d12e32fb478e https://conda.anaconda.org/conda-forge/noarch/pooch-1.8.1-pyhd8ed1ab_0.conda#d15917f33140f8d2ac9ca44db7ec8a25 From ff3ad5964e61382ac4fa335217a292ce6295a139 Mon Sep 17 00:00:00 2001 From: Omar Salman Date: Mon, 6 May 2024 14:27:10 +0500 Subject: [PATCH 091/344] DOC move d2_log_loss_score in the classification metrics section (#28938) Co-authored-by: Omar Salman --- doc/modules/classes.rst | 1 + doc/modules/model_evaluation.rst | 112 +++++++++++++++---------- sklearn/tests/test_public_functions.py | 1 + 3 files changed, 69 insertions(+), 45 deletions(-) diff --git a/doc/modules/classes.rst b/doc/modules/classes.rst index 804546eababef..1da5b337ad7a4 100644 --- a/doc/modules/classes.rst +++ b/doc/modules/classes.rst @@ -982,6 +982,7 @@ details. metrics.classification_report metrics.cohen_kappa_score metrics.confusion_matrix + metrics.d2_log_loss_score metrics.dcg_score metrics.det_curve metrics.f1_score diff --git a/doc/modules/model_evaluation.rst b/doc/modules/model_evaluation.rst index 7caacd697ea1c..d2e0203424c64 100644 --- a/doc/modules/model_evaluation.rst +++ b/doc/modules/model_evaluation.rst @@ -77,6 +77,7 @@ Scoring Function 'roc_auc_ovo' :func:`metrics.roc_auc_score` 'roc_auc_ovr_weighted' :func:`metrics.roc_auc_score` 'roc_auc_ovo_weighted' :func:`metrics.roc_auc_score` +'d2_log_loss_score' :func:`metrics.d2_log_loss_score` **Clustering** 'adjusted_mutual_info_score' :func:`metrics.adjusted_mutual_info_score` @@ -377,6 +378,7 @@ Some also work in the multilabel case: recall_score roc_auc_score zero_one_loss + d2_log_loss_score And some work with binary and multilabel (but not multiclass) problems: @@ -1986,6 +1988,71 @@ see the example below. |details-end| +.. _d2_score_classification: + +D² score for classification +--------------------------- + +The D² score computes the fraction of deviance explained. +It is a generalization of R², where the squared error is generalized and replaced +by a classification deviance of choice :math:`\text{dev}(y, \hat{y})` +(e.g., Log loss). D² is a form of a *skill score*. +It is calculated as + +.. math:: + + D^2(y, \hat{y}) = 1 - \frac{\text{dev}(y, \hat{y})}{\text{dev}(y, y_{\text{null}})} \,. + +Where :math:`y_{\text{null}}` is the optimal prediction of an intercept-only model +(e.g., the per-class proportion of `y_true` in the case of the Log loss). + +Like R², 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 +:math:`y_{\text{null}}`, disregarding the input features, would get a D² score +of 0.0. + +|details-start| +**D2 log loss score** +|details-split| + +The :func:`d2_log_loss_score` function implements the special case +of D² with the log loss, see :ref:`log_loss`, i.e.: + +.. math:: + + \text{dev}(y, \hat{y}) = \text{log_loss}(y, \hat{y}). + +Here are some usage examples of the :func:`d2_log_loss_score` function:: + + >>> from sklearn.metrics import d2_log_loss_score + >>> y_true = [1, 1, 2, 3] + >>> y_pred = [ + ... [0.5, 0.25, 0.25], + ... [0.5, 0.25, 0.25], + ... [0.5, 0.25, 0.25], + ... [0.5, 0.25, 0.25], + ... ] + >>> d2_log_loss_score(y_true, y_pred) + 0.0 + >>> y_true = [1, 2, 3] + >>> y_pred = [ + ... [0.98, 0.01, 0.01], + ... [0.01, 0.98, 0.01], + ... [0.01, 0.01, 0.98], + ... ] + >>> d2_log_loss_score(y_true, y_pred) + 0.981... + >>> y_true = [1, 2, 3] + >>> y_pred = [ + ... [0.1, 0.6, 0.3], + ... [0.1, 0.6, 0.3], + ... [0.4, 0.5, 0.1], + ... ] + >>> d2_log_loss_score(y_true, y_pred) + -0.552... + +|details-end| + .. _multilabel_ranking_metrics: Multilabel ranking metrics @@ -2826,51 +2893,6 @@ Here are some usage examples of the :func:`d2_absolute_error_score` function:: |details-end| -|details-start| -**D² log loss score** -|details-split| - -The :func:`d2_log_loss_score` function implements the special case -of D² with the log loss, see :ref:`log_loss`, i.e.: - -.. math:: - - \text{dev}(y, \hat{y}) = \text{log_loss}(y, \hat{y}). - -The :math:`y_{\text{null}}` for the :func:`log_loss` is the per-class -proportion. - -Here are some usage examples of the :func:`d2_log_loss_score` function:: - - >>> from sklearn.metrics import d2_log_loss_score - >>> y_true = [1, 1, 2, 3] - >>> y_pred = [ - ... [0.5, 0.25, 0.25], - ... [0.5, 0.25, 0.25], - ... [0.5, 0.25, 0.25], - ... [0.5, 0.25, 0.25], - ... ] - >>> d2_log_loss_score(y_true, y_pred) - 0.0 - >>> y_true = [1, 2, 3] - >>> y_pred = [ - ... [0.98, 0.01, 0.01], - ... [0.01, 0.98, 0.01], - ... [0.01, 0.01, 0.98], - ... ] - >>> d2_log_loss_score(y_true, y_pred) - 0.981... - >>> y_true = [1, 2, 3] - >>> y_pred = [ - ... [0.1, 0.6, 0.3], - ... [0.1, 0.6, 0.3], - ... [0.4, 0.5, 0.1], - ... ] - >>> d2_log_loss_score(y_true, y_pred) - -0.552... - -|details-end| - .. _visualization_regression_evaluation: Visual evaluation of regression models diff --git a/sklearn/tests/test_public_functions.py b/sklearn/tests/test_public_functions.py index 41629aa189941..707aa37737c1b 100644 --- a/sklearn/tests/test_public_functions.py +++ b/sklearn/tests/test_public_functions.py @@ -234,6 +234,7 @@ def _check_function_param_validation( "sklearn.metrics.consensus_score", "sklearn.metrics.coverage_error", "sklearn.metrics.d2_absolute_error_score", + "sklearn.metrics.d2_log_loss_score", "sklearn.metrics.d2_pinball_score", "sklearn.metrics.d2_tweedie_score", "sklearn.metrics.davies_bouldin_score", From a5203e808ba134f0fd39a52e2ac818146559dd6c Mon Sep 17 00:00:00 2001 From: "Adam J. Stewart" Date: Mon, 6 May 2024 11:45:48 +0200 Subject: [PATCH 092/344] DOC update r2_score default in regression metrics tutorial (#28958) --- doc/modules/model_evaluation.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/doc/modules/model_evaluation.rst b/doc/modules/model_evaluation.rst index d2e0203424c64..056bf9a56d42c 100644 --- a/doc/modules/model_evaluation.rst +++ b/doc/modules/model_evaluation.rst @@ -2297,9 +2297,6 @@ leads to a weighting of each individual score by the variance of the corresponding target variable. This setting quantifies the globally captured unscaled variance. If the target variables are of different scale, then this score puts more importance on explaining the higher variance variables. -``multioutput='variance_weighted'`` is the default value for :func:`r2_score` -for backward compatibility. This will be changed to ``uniform_average`` in the -future. .. _r2_score: From febf1905048e605ba65ad7f75662c253b244b308 Mon Sep 17 00:00:00 2001 From: Tuhin Sharma Date: Mon, 6 May 2024 20:27:39 +0530 Subject: [PATCH 093/344] DOC add links to examples/linear_model/plot_elastic_net_precomputed_gram_matrix_with_weighted_samples.py (#28895) Co-authored-by: bivav --- doc/modules/linear_model.rst | 1 + sklearn/linear_model/_coordinate_descent.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/doc/modules/linear_model.rst b/doc/modules/linear_model.rst index dd975c4d6e417..275ee01eb022f 100644 --- a/doc/modules/linear_model.rst +++ b/doc/modules/linear_model.rst @@ -530,6 +530,7 @@ The class :class:`ElasticNetCV` can be used to set the parameters * :ref:`sphx_glr_auto_examples_linear_model_plot_lasso_and_elasticnet.py` * :ref:`sphx_glr_auto_examples_linear_model_plot_lasso_coordinate_descent_path.py` + * :ref:`sphx_glr_auto_examples_linear_model_plot_elastic_net_precomputed_gram_matrix_with_weighted_samples.py` |details-start| **References** diff --git a/sklearn/linear_model/_coordinate_descent.py b/sklearn/linear_model/_coordinate_descent.py index 45cdb8bdf2ebb..6a62fa1e245e2 100644 --- a/sklearn/linear_model/_coordinate_descent.py +++ b/sklearn/linear_model/_coordinate_descent.py @@ -776,6 +776,9 @@ class ElasticNet(MultiOutputMixin, RegressorMixin, LinearModel): Whether to use a precomputed Gram matrix to speed up 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 + ` + for details. max_iter : int, default=1000 The maximum number of iterations. From 00a8ae780df03ef6672009e088d70d21ecfef8d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20du=20Boisberranger?= Date: Tue, 7 May 2024 10:45:52 +0200 Subject: [PATCH 094/344] MAINT set version to 1.6.dev0 and add 1.6 changelog (#28962) --- doc/whats_new.rst | 1 + doc/whats_new/v1.6.rst | 42 ++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- sklearn/__init__.py | 2 +- 4 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 doc/whats_new/v1.6.rst diff --git a/doc/whats_new.rst b/doc/whats_new.rst index ecf657936186d..e659e6453f9a0 100644 --- a/doc/whats_new.rst +++ b/doc/whats_new.rst @@ -15,6 +15,7 @@ Changelogs and release notes for all scikit-learn releases are linked in this pa .. toctree:: :maxdepth: 2 + whats_new/v1.6.rst whats_new/v1.5.rst whats_new/v1.4.rst whats_new/v1.3.rst diff --git a/doc/whats_new/v1.6.rst b/doc/whats_new/v1.6.rst new file mode 100644 index 0000000000000..b90394c75b6ff --- /dev/null +++ b/doc/whats_new/v1.6.rst @@ -0,0 +1,42 @@ +.. include:: _contributors.rst + +.. currentmodule:: sklearn + +.. _release_notes_1_6: + +=========== +Version 1.6 +=========== + +.. + -- UNCOMMENT WHEN 1.6.0 IS RELEASED -- + For a short description of the main highlights of the release, please refer to + :ref:`sphx_glr_auto_examples_release_highlights_plot_release_highlights_1_6_0.py`. + +.. include:: changelog_legend.inc + +.. _changes_1_6: + +Version 1.6.0 +============= + +**In Development** + +Changelog +--------- + +.. + Entries should be grouped by module (in alphabetic order) and prefixed with + one of the labels: |MajorFeature|, |Feature|, |Efficiency|, |Enhancement|, + |Fix| or |API| (see whats_new.rst for descriptions). + Entries should be ordered by those labels (e.g. |Fix| after |Efficiency|). + Changes not specific to a module should be listed under *Multiple Modules* + or *Miscellaneous*. + Entries should end with: + :pr:`123456` by :user:`Joe Bloggs `. + where 123455 is the *pull request* number, not the issue number. + +Thanks to everyone who has contributed to the maintenance and improvement of +the project since version 1.5, including: + +TODO: update at the time of the release. diff --git a/pyproject.toml b/pyproject.toml index 69d9702716cb5..468a6a1aaf53a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "scikit-learn" -version = "1.5.dev0" +version = "1.6.dev0" description = "A set of python modules for machine learning and data mining" readme = "README.rst" maintainers = [ diff --git a/sklearn/__init__.py b/sklearn/__init__.py index 45a26334a25f5..a441845408b54 100644 --- a/sklearn/__init__.py +++ b/sklearn/__init__.py @@ -42,7 +42,7 @@ # Dev branch marker is: 'X.Y.dev' or 'X.Y.devN' where N is an integer. # 'X.Y.dev0' is the canonical version of 'X.Y.dev' # -__version__ = "1.5.dev0" +__version__ = "1.6.dev0" # On OSX, we can get a runtime error due to multiple OpenMP libraries loaded From 438f093136411d5955fcca6e7ec494196e4a7dc9 Mon Sep 17 00:00:00 2001 From: Olivier Grisel Date: Tue, 7 May 2024 11:19:03 +0200 Subject: [PATCH 095/344] TST Fix tolerance for seed-sensitive test `test_pca_solver_equivalence` (#28961) --- sklearn/decomposition/tests/test_pca.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sklearn/decomposition/tests/test_pca.py b/sklearn/decomposition/tests/test_pca.py index 59401fd8742da..bd7f60061abdc 100644 --- a/sklearn/decomposition/tests/test_pca.py +++ b/sklearn/decomposition/tests/test_pca.py @@ -309,7 +309,7 @@ def test_pca_solver_equivalence( X_train, X_test = X[:n_samples], X[n_samples:] if global_dtype == np.float32: - tols = dict(atol=1e-2, rtol=1e-5) + tols = dict(atol=3e-2, rtol=1e-5) variance_threshold = 1e-5 else: tols = dict(atol=1e-10, rtol=1e-12) From f759818a990e406da82d9f7cdd0ecd94942ba5b3 Mon Sep 17 00:00:00 2001 From: Adrin Jalali Date: Tue, 7 May 2024 13:39:52 +0200 Subject: [PATCH 096/344] DOC add Tidelift to sponsors (#28918) Co-authored-by: Arturo Amor <86408019+ArturoAmorQ@users.noreply.github.com> --- doc/about.rst | 26 +++++++++++++++++++++ doc/images/Tidelift-logo-on-light.svg | 33 +++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 doc/images/Tidelift-logo-on-light.svg diff --git a/doc/about.rst b/doc/about.rst index e7083569fd128..035bddb0ea4dc 100644 --- a/doc/about.rst +++ b/doc/about.rst @@ -347,6 +347,32 @@ and is part of the scikit-learn consortium at Inria.
+........... + +.. raw:: html + +
+
+ +`Tidelift `_ supports the project via their service +agreement. + +.. raw:: html + +
+ +
+ +.. image:: images/Tidelift-logo-on-light.svg + :width: 100pt + :align: center + :target: https://tidelift.com/ + +.. raw:: html + +
+
+ Past Sponsors ............. diff --git a/doc/images/Tidelift-logo-on-light.svg b/doc/images/Tidelift-logo-on-light.svg new file mode 100644 index 0000000000000..af12d68417235 --- /dev/null +++ b/doc/images/Tidelift-logo-on-light.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + From ffe2c1b857fa1ae1609e9d48c7a5ea25c3856716 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20du=20Boisberranger?= Date: Tue, 7 May 2024 13:49:31 +0200 Subject: [PATCH 097/344] DOC Mention the renaming of check_estimator_sparse_data in 1.5 changelog (#28968) --- doc/whats_new/v1.5.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/whats_new/v1.5.rst b/doc/whats_new/v1.5.rst index ede5d5dcbf1ec..e50309a330e39 100644 --- a/doc/whats_new/v1.5.rst +++ b/doc/whats_new/v1.5.rst @@ -527,6 +527,11 @@ Changelog `axis=0` and supports indexing polars Series. :pr:`28521` by :user:`Yao Xiao `. +- |API| :func:`utils.estimator_checks.check_estimator_sparse_data` was split into two + functions: :func:`utils.estimator_checks.check_estimator_sparse_matrix` and + :func:`utils.estimator_checks.check_estimator_sparse_array`. + :pr:`27576` by :user:`Stefanie Senger `. + .. rubric:: Code and documentation contributors Thanks to everyone who has contributed to the maintenance and improvement of From fa37777ae4f4f40798bafb0842e8916de8a90269 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20du=20Boisberranger?= Date: Tue, 7 May 2024 13:50:46 +0200 Subject: [PATCH 098/344] DOC Update release docs (#28965) --- doc/developers/maintainer.rst | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/doc/developers/maintainer.rst b/doc/developers/maintainer.rst index e82a7993997b2..70d132d2af604 100644 --- a/doc/developers/maintainer.rst +++ b/doc/developers/maintainer.rst @@ -105,14 +105,13 @@ in the description of the Pull Request to track progress. This PR will be used to push commits related to the release as explained in :ref:`making_a_release`. -You can also create a second PR from main and targeting main to increment -the ``__version__`` variable in `sklearn/__init__.py` to increment the dev -version. This means while we're in the release candidate period, the latest -stable is two versions behind the main branch, instead of one. In this PR -targeting main you should also include a new file for the matching version -under the ``doc/whats_new/`` folder so PRs that target the next version can -contribute their changelog entries to this file in parallel to the release -process. +You can also create a second PR from main and targeting main to increment the +``__version__`` variable in `sklearn/__init__.py` and in `pyproject.toml` to increment +the dev version. This means while we're in the release candidate period, the latest +stable is two versions behind the main branch, instead of one. In this PR targeting +main you should also include a new file for the matching version under the +``doc/whats_new/`` folder so PRs that target the next version can contribute their +changelog entries to this file in parallel to the release process. Minor version release (also known as bug-fix release) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -212,8 +211,8 @@ Making a release the old entries (two years or three releases are typically good enough) and to update the on-going development entry. -2. On the branch for releasing, update the version number in - ``sklearn/__init__.py``, the ``__version__``. +2. On the branch for releasing, update the version number in ``sklearn/__init__.py``, + the ``__version__`` variable, and in `pyproject.toml`. For major releases, please add a 0 at the end: `0.99.0` instead of `0.99`. From 1fa3c75e17f0327bc7cdc0eec9f132fc968e5b8a Mon Sep 17 00:00:00 2001 From: Omar Salman Date: Tue, 7 May 2024 19:16:11 +0500 Subject: [PATCH 099/344] DOC updates for d2_log_loss_score (#28969) --- sklearn/metrics/_classification.py | 6 +++--- sklearn/metrics/tests/test_classification.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/sklearn/metrics/_classification.py b/sklearn/metrics/_classification.py index 04894a4d7a7e7..b68f1593e317e 100644 --- a/sklearn/metrics/_classification.py +++ b/sklearn/metrics/_classification.py @@ -3277,10 +3277,10 @@ 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 uses the empirical mean of `y_true` as - constant prediction, disregarding the input features, gets a D^2 score of 0.0. + 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 `. + Read more in the :ref:`User Guide `. .. versionadded:: 1.5 diff --git a/sklearn/metrics/tests/test_classification.py b/sklearn/metrics/tests/test_classification.py index 40b762bfa7308..b87e76ba2fb42 100644 --- a/sklearn/metrics/tests/test_classification.py +++ b/sklearn/metrics/tests/test_classification.py @@ -3048,7 +3048,8 @@ def test_d2_log_loss_score(): def test_d2_log_loss_score_raises(): - """Test that d2_log_loss raises error on invalid input.""" + """Test that d2_log_loss_score raises the appropriate errors on + invalid inputs.""" y_true = [0, 1, 2] y_pred = [[0.2, 0.8], [0.5, 0.5], [0.4, 0.6]] err = "contain different number of classes" From e12f192c581ff78b25a6cac723a99782c9ee480d Mon Sep 17 00:00:00 2001 From: Thomas Li <47963215+lithomas1@users.noreply.github.com> Date: Wed, 8 May 2024 02:21:27 -0400 Subject: [PATCH 100/344] ENH Use Array API in mean_tweedie_deviance (#28106) --- doc/modules/array_api.rst | 1 + doc/whats_new/v1.6.rst | 18 ++++++++++++++ sklearn/metrics/_regression.py | 21 +++++++++-------- sklearn/metrics/tests/test_common.py | 35 +++++++++++++++++++++++++++- sklearn/utils/_array_api.py | 3 +++ 5 files changed, 67 insertions(+), 11 deletions(-) diff --git a/doc/modules/array_api.rst b/doc/modules/array_api.rst index 7a21274a7250f..dadae86689e08 100644 --- a/doc/modules/array_api.rst +++ b/doc/modules/array_api.rst @@ -106,6 +106,7 @@ Metrics ------- - :func:`sklearn.metrics.accuracy_score` +- :func:`sklearn.metrics.mean_tweedie_deviance` - :func:`sklearn.metrics.r2_score` - :func:`sklearn.metrics.zero_one_loss` diff --git a/doc/whats_new/v1.6.rst b/doc/whats_new/v1.6.rst index b90394c75b6ff..6eda6717b3d1b 100644 --- a/doc/whats_new/v1.6.rst +++ b/doc/whats_new/v1.6.rst @@ -22,6 +22,24 @@ Version 1.6.0 **In Development** +Support for Array API +--------------------- + +Additional estimators and functions have been updated to include support for all +`Array API `_ compliant inputs. + +See :ref:`array_api` for more details. + +**Functions:** + +- :func:`sklearn.metrics.mean_tweedie_deviance` now supports Array API compatible + inputs. + :pr:`28106` by :user:`Thomas Li ` + +**Classes:** + +- + Changelog --------- diff --git a/sklearn/metrics/_regression.py b/sklearn/metrics/_regression.py index b5605f18803ab..596a45dd3eaed 100644 --- a/sklearn/metrics/_regression.py +++ b/sklearn/metrics/_regression.py @@ -1276,13 +1276,14 @@ def max_error(y_true, y_pred): def _mean_tweedie_deviance(y_true, y_pred, sample_weight, power): """Mean Tweedie deviance regression loss.""" + xp, _ = get_namespace(y_true, y_pred) p = power if p < 0: # 'Extreme stable', y any real number, y_pred > 0 dev = 2 * ( - np.power(np.maximum(y_true, 0), 2 - p) / ((1 - p) * (2 - p)) - - y_true * np.power(y_pred, 1 - p) / (1 - p) - + np.power(y_pred, 2 - p) / (2 - p) + xp.pow(xp.where(y_true > 0, y_true, 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 @@ -1292,15 +1293,14 @@ def _mean_tweedie_deviance(y_true, y_pred, sample_weight, power): dev = 2 * (xlogy(y_true, y_true / y_pred) - y_true + y_pred) elif p == 2: # Gamma distribution - dev = 2 * (np.log(y_pred / y_true) + y_true / y_pred - 1) + dev = 2 * (xp.log(y_pred / y_true) + y_true / y_pred - 1) else: dev = 2 * ( - np.power(y_true, 2 - p) / ((1 - p) * (2 - p)) - - y_true * np.power(y_pred, 1 - p) / (1 - p) - + np.power(y_pred, 2 - p) / (2 - p) + xp.pow(y_true, 2 - p) / ((1 - p) * (2 - p)) + - y_true * xp.pow(y_pred, 1 - p) / (1 - p) + + xp.pow(y_pred, 2 - p) / (2 - p) ) - - return np.average(dev, weights=sample_weight) + return float(_average(dev, weights=sample_weight)) @validate_params( @@ -1363,8 +1363,9 @@ def mean_tweedie_deviance(y_true, y_pred, *, sample_weight=None, power=0): >>> mean_tweedie_deviance(y_true, y_pred, power=1) 1.4260... """ + xp, _ = get_namespace(y_true, y_pred) y_type, y_true, y_pred, _ = _check_reg_targets( - y_true, y_pred, None, dtype=[np.float64, np.float32] + y_true, y_pred, None, dtype=[xp.float64, xp.float32] ) if y_type == "continuous-multioutput": raise ValueError("Multioutput not supported in mean_tweedie_deviance") diff --git a/sklearn/metrics/tests/test_common.py b/sklearn/metrics/tests/test_common.py index 886f870da6adf..f00af5e160858 100644 --- a/sklearn/metrics/tests/test_common.py +++ b/sklearn/metrics/tests/test_common.py @@ -1824,6 +1824,35 @@ def check_array_api_multiclass_classification_metric( def check_array_api_regression_metric(metric, array_namespace, device, dtype_name): + y_true_np = np.array([2, 0, 1, 4], dtype=dtype_name) + y_pred_np = np.array([0.5, 0.5, 2, 2], dtype=dtype_name) + + check_array_api_metric( + metric, + array_namespace, + device, + dtype_name, + y_true_np=y_true_np, + y_pred_np=y_pred_np, + sample_weight=None, + ) + + sample_weight = np.array([0.1, 2.0, 1.5, 0.5], dtype=dtype_name) + + check_array_api_metric( + metric, + array_namespace, + device, + dtype_name, + y_true_np=y_true_np, + y_pred_np=y_pred_np, + sample_weight=sample_weight, + ) + + +def check_array_api_regression_metric_multioutput( + metric, array_namespace, device, dtype_name +): y_true_np = np.array([[1, 3], [1, 2]], dtype=dtype_name) y_pred_np = np.array([[1, 4], [1, 1]], dtype=dtype_name) @@ -1859,7 +1888,11 @@ def check_array_api_regression_metric(metric, array_namespace, device, dtype_nam check_array_api_binary_classification_metric, check_array_api_multiclass_classification_metric, ], - r2_score: [check_array_api_regression_metric], + mean_tweedie_deviance: [check_array_api_regression_metric], + r2_score: [ + check_array_api_regression_metric, + check_array_api_regression_metric_multioutput, + ], } diff --git a/sklearn/utils/_array_api.py b/sklearn/utils/_array_api.py index 7c3fd12ad4dee..a8b0363c0af38 100644 --- a/sklearn/utils/_array_api.py +++ b/sklearn/utils/_array_api.py @@ -427,6 +427,9 @@ def reshape(self, x, shape, *, copy=None): def isdtype(self, dtype, kind): return isdtype(dtype, kind, xp=self) + def pow(self, x1, x2): + return numpy.power(x1, x2) + _NUMPY_API_WRAPPER_INSTANCE = _NumPyAPIWrapper() From 1a54a11b7f2fdc09aa06057689df1a196e9fdd3c Mon Sep 17 00:00:00 2001 From: Abdulaziz Aloqeely <52792999+Aloqeely@users.noreply.github.com> Date: Fri, 10 May 2024 01:11:16 +0300 Subject: [PATCH 101/344] Update supported python versions in docs (#28986) --- doc/install.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/install.rst b/doc/install.rst index c4a3548016021..89851171f4588 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -166,7 +166,8 @@ purpose. Scikit-learn 0.22 supported Python 3.5-3.8. Scikit-learn 0.23 - 0.24 require Python 3.6 or newer. Scikit-learn 1.0 supported Python 3.7-3.10. - Scikit-learn 1.1 and later requires Python 3.8 or newer. + Scikit-learn 1.1, 1.2 and 1.3 support Python 3.8-3.12 + Scikit-learn 1.4 requires Python 3.9 or newer. .. _install_by_distribution: From 5e5cc3477025794c5b2ee6056a223d91adbfe925 Mon Sep 17 00:00:00 2001 From: Conrad Stevens Date: Sun, 12 May 2024 07:29:55 +1000 Subject: [PATCH 102/344] DOC fix gp predic doc typo (#28987) Co-authored-by: Conrad --- sklearn/gaussian_process/_gpr.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sklearn/gaussian_process/_gpr.py b/sklearn/gaussian_process/_gpr.py index 67bba2e29c857..829c1e2fad2d8 100644 --- a/sklearn/gaussian_process/_gpr.py +++ b/sklearn/gaussian_process/_gpr.py @@ -384,7 +384,7 @@ def predict(self, X, return_std=False, return_cov=False): Returns ------- y_mean : ndarray of shape (n_samples,) or (n_samples, n_targets) - Mean of predictive distribution a query points. + 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. @@ -392,7 +392,7 @@ def predict(self, X, return_std=False, return_cov=False): y_cov : ndarray of shape (n_samples, n_samples) or \ (n_samples, n_samples, n_targets), optional - Covariance of joint predictive distribution a query points. + Covariance of joint predictive distribution at query points. Only returned when `return_cov` is True. """ if return_std and return_cov: From 2d51510780a25a0186032b11fa8929d5e010eff3 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 13 May 2024 01:53:27 -0600 Subject: [PATCH 103/344] MAINT: specify C17 as C standard in meson.build (#28980) --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 3835a5099abb0..52c7deb962277 100644 --- a/meson.build +++ b/meson.build @@ -6,7 +6,7 @@ project( meson_version: '>= 1.1.0', default_options: [ 'buildtype=debugoptimized', - 'c_std=c99', + 'c_std=c17', 'cpp_std=c++14', ], ) From 2d6e1be005affcea8812b46b5d403a548df14b17 Mon Sep 17 00:00:00 2001 From: Christian Lorentzen Date: Mon, 13 May 2024 09:58:57 +0200 Subject: [PATCH 104/344] MNT remove author and license in GLM files (#28799) --- .../plot_poisson_regression_non_normal_loss.py | 7 ++----- .../plot_tweedie_regression_insurance_claims.py | 7 ++----- sklearn/linear_model/_glm/__init__.py | 4 ++-- sklearn/linear_model/_glm/_newton_solver.py | 5 ++--- sklearn/linear_model/_glm/glm.py | 6 ++---- sklearn/linear_model/_glm/tests/__init__.py | 3 ++- sklearn/linear_model/_glm/tests/test_glm.py | 6 ++---- 7 files changed, 14 insertions(+), 24 deletions(-) diff --git a/examples/linear_model/plot_poisson_regression_non_normal_loss.py b/examples/linear_model/plot_poisson_regression_non_normal_loss.py index 2a80c3db0ff40..180ee3b70671c 100644 --- a/examples/linear_model/plot_poisson_regression_non_normal_loss.py +++ b/examples/linear_model/plot_poisson_regression_non_normal_loss.py @@ -1,3 +1,5 @@ +# Authors: The scikit-learn developers +# SPDX-License-Identifier: BSD-3-Clause """ ====================================== Poisson regression and non-normal loss @@ -36,11 +38,6 @@ """ -# Authors: Christian Lorentzen -# Roman Yurchak -# Olivier Grisel -# License: BSD 3 clause - import matplotlib.pyplot as plt import numpy as np import pandas as pd diff --git a/examples/linear_model/plot_tweedie_regression_insurance_claims.py b/examples/linear_model/plot_tweedie_regression_insurance_claims.py index 96e32ee031190..31a91fb37c766 100644 --- a/examples/linear_model/plot_tweedie_regression_insurance_claims.py +++ b/examples/linear_model/plot_tweedie_regression_insurance_claims.py @@ -1,3 +1,5 @@ +# Authors: The scikit-learn developers +# SPDX-License-Identifier: BSD-3-Clause """ ====================================== Tweedie regression on insurance claims @@ -37,11 +39,6 @@ `_ """ -# Authors: Christian Lorentzen -# Roman Yurchak -# Olivier Grisel -# License: BSD 3 clause - # %% from functools import partial diff --git a/sklearn/linear_model/_glm/__init__.py b/sklearn/linear_model/_glm/__init__.py index 1b82bbd77bcf9..199b938b023d0 100644 --- a/sklearn/linear_model/_glm/__init__.py +++ b/sklearn/linear_model/_glm/__init__.py @@ -1,5 +1,5 @@ -# License: BSD 3 clause - +# Authors: The scikit-learn developers +# SPDX-License-Identifier: BSD-3-Clause from .glm import ( GammaRegressor, PoissonRegressor, diff --git a/sklearn/linear_model/_glm/_newton_solver.py b/sklearn/linear_model/_glm/_newton_solver.py index 20df35e6b48c2..b2be604d931c5 100644 --- a/sklearn/linear_model/_glm/_newton_solver.py +++ b/sklearn/linear_model/_glm/_newton_solver.py @@ -1,10 +1,9 @@ +# Authors: The scikit-learn developers +# SPDX-License-Identifier: BSD-3-Clause """ Newton solver for Generalized Linear Models """ -# Author: Christian Lorentzen -# License: BSD 3 clause - import warnings from abc import ABC, abstractmethod diff --git a/sklearn/linear_model/_glm/glm.py b/sklearn/linear_model/_glm/glm.py index 4cac889a4da51..14caa4fd733c2 100644 --- a/sklearn/linear_model/_glm/glm.py +++ b/sklearn/linear_model/_glm/glm.py @@ -1,11 +1,9 @@ +# Authors: The scikit-learn developers +# SPDX-License-Identifier: BSD-3-Clause """ Generalized Linear Models with Exponential Dispersion Family """ -# Author: Christian Lorentzen -# some parts and tricks stolen from other sklearn files. -# License: BSD 3 clause - from numbers import Integral, Real import numpy as np diff --git a/sklearn/linear_model/_glm/tests/__init__.py b/sklearn/linear_model/_glm/tests/__init__.py index 588cf7e93eef0..67dd18fb94b59 100644 --- a/sklearn/linear_model/_glm/tests/__init__.py +++ b/sklearn/linear_model/_glm/tests/__init__.py @@ -1 +1,2 @@ -# License: BSD 3 clause +# Authors: The scikit-learn developers +# SPDX-License-Identifier: BSD-3-Clause diff --git a/sklearn/linear_model/_glm/tests/test_glm.py b/sklearn/linear_model/_glm/tests/test_glm.py index 26f6bdc08d254..7f6ec64c15ad4 100644 --- a/sklearn/linear_model/_glm/tests/test_glm.py +++ b/sklearn/linear_model/_glm/tests/test_glm.py @@ -1,7 +1,5 @@ -# Authors: Christian Lorentzen -# -# License: BSD 3 clause - +# Authors: The scikit-learn developers +# SPDX-License-Identifier: BSD-3-Clause import itertools import warnings from functools import partial From 61281cf61c9fc94900fbfa280a91d3b5c0da4abb Mon Sep 17 00:00:00 2001 From: Stefanie Senger <91849487+StefanieSenger@users.noreply.github.com> Date: Mon, 13 May 2024 10:38:29 +0200 Subject: [PATCH 105/344] FEA metadata routing for `StackingClassifier` and `StackingRegressor` (#28701) Co-authored-by: Adrin Jalali Co-authored-by: Omar Salman --- doc/metadata_routing.rst | 6 +- doc/modules/ensemble.rst | 4 +- doc/whats_new/v1.6.rst | 14 +- sklearn/ensemble/_base.py | 2 +- sklearn/ensemble/_stacking.py | 228 +++++++++++++++--- sklearn/ensemble/tests/test_stacking.py | 120 +++++++++ sklearn/tests/metadata_routing_common.py | 13 +- .../test_metaestimators_metadata_routing.py | 4 - 8 files changed, 335 insertions(+), 56 deletions(-) diff --git a/doc/metadata_routing.rst b/doc/metadata_routing.rst index d319b311dddd7..0ada6ef6c4dbe 100644 --- a/doc/metadata_routing.rst +++ b/doc/metadata_routing.rst @@ -277,6 +277,8 @@ Meta-estimators and functions supporting metadata routing: - :class:`sklearn.calibration.CalibratedClassifierCV` - :class:`sklearn.compose.ColumnTransformer` - :class:`sklearn.covariance.GraphicalLassoCV` +- :class:`sklearn.ensemble.StackingClassifier` +- :class:`sklearn.ensemble.StackingRegressor` - :class:`sklearn.ensemble.VotingClassifier` - :class:`sklearn.ensemble.VotingRegressor` - :class:`sklearn.ensemble.BaggingClassifier` @@ -316,13 +318,9 @@ Meta-estimators and tools not supporting metadata routing yet: - :class:`sklearn.compose.TransformedTargetRegressor` - :class:`sklearn.ensemble.AdaBoostClassifier` - :class:`sklearn.ensemble.AdaBoostRegressor` -- :class:`sklearn.ensemble.StackingClassifier` -- :class:`sklearn.ensemble.StackingRegressor` - :class:`sklearn.feature_selection.RFE` - :class:`sklearn.feature_selection.RFECV` - :class:`sklearn.feature_selection.SequentialFeatureSelector` -- :class:`sklearn.impute.IterativeImputer` -- :class:`sklearn.linear_model.RANSACRegressor` - :class:`sklearn.model_selection.learning_curve` - :class:`sklearn.model_selection.permutation_test_score` - :class:`sklearn.model_selection.validation_curve` diff --git a/doc/modules/ensemble.rst b/doc/modules/ensemble.rst index 4237d023973f7..58c9127850f6a 100644 --- a/doc/modules/ensemble.rst +++ b/doc/modules/ensemble.rst @@ -1581,8 +1581,8 @@ availability, tested in the order of preference: `predict_proba`, `decision_function` and `predict`. A :class:`StackingRegressor` and :class:`StackingClassifier` can be used as -any other regressor or classifier, exposing a `predict`, `predict_proba`, and -`decision_function` methods, e.g.:: +any other regressor or classifier, exposing a `predict`, `predict_proba`, or +`decision_function` method, e.g.:: >>> y_pred = reg.predict(X_test) >>> from sklearn.metrics import r2_score diff --git a/doc/whats_new/v1.6.rst b/doc/whats_new/v1.6.rst index 6eda6717b3d1b..5000866b59c03 100644 --- a/doc/whats_new/v1.6.rst +++ b/doc/whats_new/v1.6.rst @@ -38,7 +38,19 @@ See :ref:`array_api` for more details. **Classes:** -- +- + +Metadata Routing +---------------- + +The following models now support metadata routing in one or more of their +methods. Refer to the :ref:`Metadata Routing User Guide ` for +more details. + +- |Feature| :class:`ensemble.StackingClassifier` and + :class:`ensemble.StackingRegressor` now support metadata routing and pass + ``**fit_params`` to the underlying estimators via their `fit` methods. + :pr:`28701` by :user:`Stefanie Senger `. Changelog --------- diff --git a/sklearn/ensemble/_base.py b/sklearn/ensemble/_base.py index 5483206de51d5..18079b02c49f1 100644 --- a/sklearn/ensemble/_base.py +++ b/sklearn/ensemble/_base.py @@ -21,7 +21,7 @@ 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 + # 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: diff --git a/sklearn/ensemble/_stacking.py b/sklearn/ensemble/_stacking.py index a18803d507ffa..9dc93b6c35975 100644 --- a/sklearn/ensemble/_stacking.py +++ b/sklearn/ensemble/_stacking.py @@ -27,8 +27,11 @@ from ..utils._estimator_html_repr import _VisualBlock from ..utils._param_validation import HasMethods, StrOptions from ..utils.metadata_routing import ( - _raise_for_unsupported_routing, - _RoutingNotSupportedMixin, + MetadataRouter, + MethodMapping, + _raise_for_params, + _routing_enabled, + process_routing, ) from ..utils.metaestimators import available_if from ..utils.multiclass import check_classification_targets, type_of_target @@ -36,6 +39,7 @@ from ..utils.validation import ( _check_feature_names_in, _check_response_method, + _deprecate_positional_args, check_is_fitted, column_or_1d, ) @@ -171,7 +175,7 @@ def _method_name(name, estimator, method): # estimators in Stacking*.estimators are not validated yet prefer_skip_nested_validation=False ) - def fit(self, X, y, sample_weight=None): + def fit(self, X, y, **fit_params): """Fit the estimators. Parameters @@ -183,14 +187,13 @@ def fit(self, X, y, sample_weight=None): y : array-like of shape (n_samples,) Target values. - sample_weight : array-like of shape (n_samples,) or default=None - Sample weights. If None, then samples are equally weighted. - Note that this is supported only if all underlying estimators - support sample weights. + **fit_params : dict + 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. - .. versionchanged:: 0.23 - when not None, `sample_weight` is passed to all underlying - estimators + .. versionadded:: 1.6 Returns ------- @@ -201,16 +204,19 @@ def fit(self, X, y, sample_weight=None): names, all_estimators = self._validate_estimators() self._validate_final_estimator() - # FIXME: when adding support for metadata routing in Stacking*. - # This is a hotfix to make StackingClassifier and StackingRegressor - # pass the tests despite not supporting metadata routing but sharing - # the same base class with VotingClassifier and VotingRegressor. - fit_params = dict() - if sample_weight is not None: - fit_params["sample_weight"] = sample_weight - stack_method = [self.stack_method] * len(all_estimators) + if _routing_enabled(): + routed_params = process_routing(self, "fit", **fit_params) + else: + routed_params = Bunch() + for name in names: + routed_params[name] = Bunch(fit={}) + if "sample_weight" in fit_params: + routed_params[name].fit["sample_weight"] = fit_params[ + "sample_weight" + ] + if self.cv == "prefit": self.estimators_ = [] for estimator in all_estimators: @@ -222,8 +228,10 @@ def fit(self, X, y, sample_weight=None): # 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, fit_params) - for est in all_estimators + delayed(_fit_single_estimator)( + clone(est), X, y, routed_params[name]["fit"] + ) + for name, est in zip(names, all_estimators) if est != "drop" ) @@ -269,10 +277,10 @@ def fit(self, X, y, sample_weight=None): cv=deepcopy(cv), method=meth, n_jobs=self.n_jobs, - params=fit_params, + params=routed_params[name]["fit"], verbose=self.verbose, ) - for est, meth in zip(all_estimators, self.stack_method_) + for name, est, meth in zip(names, all_estimators, self.stack_method_) if est != "drop" ) @@ -370,7 +378,7 @@ def predict(self, X, **predict_params): 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 - accounts for uncertainty in the final estimator. + account for uncertainty in the final estimator. Returns ------- @@ -392,8 +400,43 @@ def _sk_visual_block_with_final_estimator(self, final_estimator): ) return _VisualBlock("serial", (parallel, final_block), dash_wrapped=False) + def get_metadata_routing(self): + """Get metadata routing of this object. + + Please check :ref:`User Guide ` on how the routing + mechanism works. + + .. versionadded:: 1.6 + + Returns + ------- + routing : MetadataRouter + A :class:`~sklearn.utils.metadata_routing.MetadataRouter` encapsulating + routing information. + """ + router = MetadataRouter(owner=self.__class__.__name__) + + # `self.estimators` is a list of (name, est) tuples + for name, estimator in self.estimators: + router.add( + **{name: estimator}, + method_mapping=MethodMapping().add(callee="fit", caller="fit"), + ) + + try: + final_estimator_ = self.final_estimator_ + except AttributeError: + final_estimator_ = self.final_estimator + + router.add( + final_estimator_=final_estimator_, + method_mapping=MethodMapping().add(caller="predict", callee="predict"), + ) + + return router + -class StackingClassifier(_RoutingNotSupportedMixin, ClassifierMixin, _BaseStacking): +class StackingClassifier(ClassifierMixin, _BaseStacking): """Stack of estimators with a final classifier. Stacked generalization consists in stacking the output of individual @@ -528,7 +571,7 @@ class StackingClassifier(_RoutingNotSupportedMixin, ClassifierMixin, _BaseStacki ----- 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 + 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. @@ -629,7 +672,11 @@ def _validate_estimators(self): return names, estimators - def fit(self, X, y, sample_weight=None): + # TODO(1.7): remove `sample_weight` from the signature after deprecation + # cycle; pop it from `fit_params` before the `_raise_for_params` check and + # reinsert afterwards, for backwards compatibility + @_deprecate_positional_args(version="1.7") + def fit(self, X, y, *, sample_weight=None, **fit_params): """Fit the estimators. Parameters @@ -649,12 +696,22 @@ def fit(self, X, y, sample_weight=None): Note that this is supported only if all underlying estimators support sample weights. + **fit_params : dict + Parameters to pass to the underlying estimators. + + .. versionadded:: 1.6 + + 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 ` for + more details. + Returns ------- self : object Returns a fitted instance of estimator. """ - _raise_for_unsupported_routing(self, "fit", sample_weight=sample_weight) + _raise_for_params(fit_params, self, "fit") check_classification_targets(y) if type_of_target(y) == "multilabel-indicator": self._label_encoder = [LabelEncoder().fit(yk) for yk in y.T] @@ -669,7 +726,10 @@ def fit(self, X, y, sample_weight=None): 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, sample_weight) + + if sample_weight is not None: + fit_params["sample_weight"] = sample_weight + return super().fit(X, y_encoded, **fit_params) @available_if(_estimator_has("predict")) def predict(self, X, **predict_params): @@ -685,14 +745,33 @@ def predict(self, X, **predict_params): 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 - accounts for uncertainty in the final estimator. + account for uncertainty in the final estimator. + + - If `enable_metadata_routing=False` (default): + 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 ` for more details. + + .. versionchanged:: 1.6 + `**predict_params` can be routed via metadata routing API. Returns ------- y_pred : ndarray of shape (n_samples,) or (n_samples, n_output) Predicted targets. """ - y_pred = super().predict(X, **predict_params) + if _routing_enabled(): + routed_params = process_routing(self, "predict", **predict_params) + else: + # TODO(SLEP6): remove when metadata routing cannot be disabled. + routed_params = Bunch() + 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( @@ -775,7 +854,7 @@ def _sk_visual_block_(self): return super()._sk_visual_block_with_final_estimator(final_estimator) -class StackingRegressor(_RoutingNotSupportedMixin, RegressorMixin, _BaseStacking): +class StackingRegressor(RegressorMixin, _BaseStacking): """Stack of estimators with a final regressor. Stacked generalization consists in stacking the output of individual @@ -944,7 +1023,11 @@ def _validate_final_estimator(self): ) ) - def fit(self, X, y, sample_weight=None): + # TODO(1.7): remove `sample_weight` from the signature after deprecation + # cycle; pop it from `fit_params` before the `_raise_for_params` check and + # reinsert afterwards, for backwards compatibility + @_deprecate_positional_args(version="1.7") + def fit(self, X, y, *, sample_weight=None, **fit_params): """Fit the estimators. Parameters @@ -961,14 +1044,26 @@ def fit(self, X, y, sample_weight=None): Note that this is supported only if all underlying estimators support sample weights. + **fit_params : dict + Parameters to pass to the underlying estimators. + + .. versionadded:: 1.6 + + 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 ` for + more details. + Returns ------- self : object Returns a fitted instance. """ - _raise_for_unsupported_routing(self, "fit", sample_weight=sample_weight) + _raise_for_params(fit_params, self, "fit") y = column_or_1d(y, warn=True) - return super().fit(X, y, sample_weight) + if sample_weight is not None: + fit_params["sample_weight"] = sample_weight + return super().fit(X, y, **fit_params) def transform(self, X): """Return the predictions for X for each estimator. @@ -986,7 +1081,11 @@ def transform(self, X): """ return self._transform(X) - def fit_transform(self, X, y, sample_weight=None): + # TODO(1.7): remove `sample_weight` from the signature after deprecation + # cycle; pop it from `fit_params` before the `_raise_for_params` check and + # reinsert afterwards, for backwards compatibility + @_deprecate_positional_args(version="1.7") + def fit_transform(self, X, y, *, sample_weight=None, **fit_params): """Fit the estimators and return the predictions for X for each estimator. Parameters @@ -1003,12 +1102,69 @@ def fit_transform(self, X, y, sample_weight=None): Note that this is supported only if all underlying estimators support sample weights. + **fit_params : dict + Parameters to pass to the underlying estimators. + + .. versionadded:: 1.6 + + 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 ` for + more details. + Returns ------- y_preds : ndarray of shape (n_samples, n_estimators) Prediction outputs for each estimator. """ - return super().fit_transform(X, y, sample_weight=sample_weight) + _raise_for_params(fit_params, self, "fit") + if sample_weight is not None: + fit_params["sample_weight"] = sample_weight + return super().fit_transform(X, y, **fit_params) + + @available_if(_estimator_has("predict")) + def predict(self, X, **predict_params): + """Predict target for X. + + Parameters + ---------- + X : {array-like, sparse matrix} of shape (n_samples, n_features) + Training vectors, where `n_samples` is the number of samples and + `n_features` is the number of features. + + **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. + + - If `enable_metadata_routing=False` (default): + 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 ` for more details. + + .. versionchanged:: 1.6 + `**predict_params` can be routed via metadata routing API. + + Returns + ------- + y_pred : ndarray of shape (n_samples,) or (n_samples, n_output) + Predicted targets. + """ + if _routing_enabled(): + routed_params = process_routing(self, "predict", **predict_params) + else: + # TODO(SLEP6): remove when metadata routing cannot be disabled. + routed_params = Bunch() + routed_params.final_estimator_ = Bunch(predict={}) + routed_params.final_estimator_.predict = predict_params + + y_pred = super().predict(X, **routed_params.final_estimator_["predict"]) + + return y_pred def _sk_visual_block_(self): # If final_estimator's default changes then this should be diff --git a/sklearn/ensemble/tests/test_stacking.py b/sklearn/ensemble/tests/test_stacking.py index 300b011f661d4..1c038cd469216 100644 --- a/sklearn/ensemble/tests/test_stacking.py +++ b/sklearn/ensemble/tests/test_stacking.py @@ -3,6 +3,7 @@ # Authors: Guillaume Lemaitre # License: BSD 3 clause +import re from unittest.mock import Mock import numpy as np @@ -38,6 +39,12 @@ from sklearn.neural_network import MLPClassifier from sklearn.preprocessing import scale from sklearn.svm import SVC, LinearSVC, LinearSVR +from sklearn.tests.metadata_routing_common import ( + ConsumingClassifier, + ConsumingRegressor, + _Registry, + check_recorded_metadata, +) from sklearn.utils._mocking import CheckingClassifier from sklearn.utils._testing import ( assert_allclose, @@ -888,3 +895,116 @@ def test_stacking_final_estimator_attribute_error(): clf.fit(X, y).decision_function(X) assert isinstance(exec_info.value.__cause__, AttributeError) assert inner_msg in str(exec_info.value.__cause__) + + +# Metadata Routing Tests +# ====================== + + +@pytest.mark.parametrize( + "Estimator, Child", + [ + (StackingClassifier, ConsumingClassifier), + (StackingRegressor, ConsumingRegressor), + ], +) +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`.""" + + with pytest.raises( + 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" + ) + + +@pytest.mark.usefixtures("enable_slep006") +@pytest.mark.parametrize( + "Estimator, Child", + [ + (StackingClassifier, ConsumingClassifier), + (StackingRegressor, ConsumingRegressor), + ], +) +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() + + +@pytest.mark.usefixtures("enable_slep006") +@pytest.mark.parametrize( + "Estimator, Child", + [ + (StackingClassifier, ConsumingClassifier), + (StackingRegressor, ConsumingRegressor), + ], +) +@pytest.mark.parametrize( + "prop, prop_value", [("sample_weight", np.ones(X_iris.shape[0])), ("metadata", "a")] +) +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", + Child(registry=_Registry()).set_fit_request(**{prop: True}), + ), + ], + 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", 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", split_params=(prop), **{prop: prop_value} + ) + + +@pytest.mark.usefixtures("enable_slep006") +@pytest.mark.parametrize( + "Estimator, Child", + [ + (StackingClassifier, ConsumingClassifier), + (StackingRegressor, ConsumingRegressor), + ], +) +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" + + est = Estimator([("sub_est", Child())]) + + 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) + + +# End of Metadata Routing Tests +# ============================= diff --git a/sklearn/tests/metadata_routing_common.py b/sklearn/tests/metadata_routing_common.py index 889524bc05ddb..5091569e434a3 100644 --- a/sklearn/tests/metadata_routing_common.py +++ b/sklearn/tests/metadata_routing_common.py @@ -257,16 +257,13 @@ def predict(self, X, sample_weight="default", metadata="default"): record_metadata_not_default( self, "predict", sample_weight=sample_weight, metadata=metadata ) - return np.zeros(shape=(len(X),)) + return np.zeros(shape=(len(X),), dtype="int8") def predict_proba(self, X, sample_weight="default", metadata="default"): - pass # pragma: no cover - - # uncomment when needed - # record_metadata_not_default( - # self, "predict_proba", sample_weight=sample_weight, metadata=metadata - # ) - # return np.asarray([[0.0, 1.0]] * len(X)) + record_metadata_not_default( + self, "predict_proba", sample_weight=sample_weight, metadata=metadata + ) + return np.asarray([[0.0, 1.0]] * len(X)) def predict_log_proba(self, X, sample_weight="default", metadata="default"): pass # pragma: no cover diff --git a/sklearn/tests/test_metaestimators_metadata_routing.py b/sklearn/tests/test_metaestimators_metadata_routing.py index aa6af5bd09aac..38168f3f0261f 100644 --- a/sklearn/tests/test_metaestimators_metadata_routing.py +++ b/sklearn/tests/test_metaestimators_metadata_routing.py @@ -14,8 +14,6 @@ AdaBoostRegressor, BaggingClassifier, BaggingRegressor, - StackingClassifier, - StackingRegressor, ) from sklearn.exceptions import UnsetMetadataPassedError from sklearn.experimental import ( @@ -408,8 +406,6 @@ def enable_slep006(): RFECV(ConsumingClassifier()), SelfTrainingClassifier(ConsumingClassifier()), SequentialFeatureSelector(ConsumingClassifier()), - StackingClassifier(ConsumingClassifier()), - StackingRegressor(ConsumingRegressor()), TransformedTargetRegressor(), ] From c3d4c5165e55871d6c2d0202350d10111db42d2f Mon Sep 17 00:00:00 2001 From: Lucy Liu Date: Mon, 13 May 2024 18:49:23 +1000 Subject: [PATCH 106/344] DOC Update warm start example in ensemble user guide (#28998) --- doc/modules/ensemble.rst | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/doc/modules/ensemble.rst b/doc/modules/ensemble.rst index 58c9127850f6a..8cee8c8d403c7 100644 --- a/doc/modules/ensemble.rst +++ b/doc/modules/ensemble.rst @@ -603,7 +603,22 @@ fitted model. :: - >>> _ = est.set_params(n_estimators=200, warm_start=True) # set warm_start and new nr of trees + >>> import numpy as np + >>> from sklearn.metrics import mean_squared_error + >>> from sklearn.datasets import make_friedman1 + >>> from sklearn.ensemble import GradientBoostingRegressor + + >>> X, y = make_friedman1(n_samples=1200, random_state=0, noise=1.0) + >>> X_train, X_test = X[:200], X[200:] + >>> y_train, y_test = y[:200], y[200:] + >>> est = GradientBoostingRegressor( + ... n_estimators=100, learning_rate=0.1, max_depth=1, random_state=0, + ... loss='squared_error' + ... ) + >>> est = est.fit(X_train, y_train) # fit with 100 trees + >>> mean_squared_error(y_test, est.predict(X_test)) + 5.00... + >>> _ = est.set_params(n_estimators=200, warm_start=True) # set warm_start and increase num of trees >>> _ = est.fit(X_train, y_train) # fit additional 100 trees to est >>> mean_squared_error(y_test, est.predict(X_test)) 3.84... From c8070926d3ba8d41ca38b34039a8ece901ab1fbf Mon Sep 17 00:00:00 2001 From: Christian Veenhuis <124370897+ChVeen@users.noreply.github.com> Date: Mon, 13 May 2024 11:43:52 +0200 Subject: [PATCH 107/344] MAINT fix redirected link for `Matthews Correlation Coefficient` (#28991) --- sklearn/metrics/_classification.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sklearn/metrics/_classification.py b/sklearn/metrics/_classification.py index b68f1593e317e..1fb4c1d694be0 100644 --- a/sklearn/metrics/_classification.py +++ b/sklearn/metrics/_classification.py @@ -967,8 +967,8 @@ def matthews_corrcoef(y_true, y_pred, *, sample_weight=None): accuracy of prediction algorithms for classification: an overview. <10.1093/bioinformatics/16.5.412>` - .. [2] `Wikipedia entry for the Matthews Correlation Coefficient - `_. + .. [2] `Wikipedia entry for the Matthews Correlation Coefficient (phi coefficient) + `_. .. [3] `Gorodkin, (2004). Comparing two K-category assignments by a K-category correlation coefficient From 45ca0a764b298d205567d7fb00c48c977caf54b8 Mon Sep 17 00:00:00 2001 From: Ivan Wiryadi <44887783+strivn@users.noreply.github.com> Date: Mon, 13 May 2024 17:01:45 +0700 Subject: [PATCH 108/344] DOC Add links to digit denoising examples in docs and the user guide (#28929) --- doc/modules/decomposition.rst | 2 ++ sklearn/decomposition/_kernel_pca.py | 8 ++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/doc/modules/decomposition.rst b/doc/modules/decomposition.rst index e8241a92cfc3b..e34818a322c7d 100644 --- a/doc/modules/decomposition.rst +++ b/doc/modules/decomposition.rst @@ -291,6 +291,8 @@ prediction (kernel dependency estimation). :class:`KernelPCA` supports both .. topic:: Examples: * :ref:`sphx_glr_auto_examples_decomposition_plot_kernel_pca.py` + * :ref:`sphx_glr_auto_examples_applications_plot_digits_denoising.py` + .. topic:: References: diff --git a/sklearn/decomposition/_kernel_pca.py b/sklearn/decomposition/_kernel_pca.py index edfd49c2e87a0..0f45bc7c9239c 100644 --- a/sklearn/decomposition/_kernel_pca.py +++ b/sklearn/decomposition/_kernel_pca.py @@ -30,7 +30,7 @@ class KernelPCA(ClassNamePrefixFeaturesOutMixin, TransformerMixin, BaseEstimator): - """Kernel Principal component analysis (KPCA) [1]_. + """Kernel Principal Component Analysis (KPCA) [1]_. Non-linear dimensionality reduction through the use of kernels (see :ref:`metrics`). @@ -41,9 +41,13 @@ class KernelPCA(ClassNamePrefixFeaturesOutMixin, TransformerMixin, BaseEstimator components to extract. It can also use a randomized truncated SVD by the method proposed in [3]_, see `eigen_solver`. - For a usage example, see + 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 `. Parameters From e2c3793f9517c3ebcb08a0e6bb1a30c14d87bb8a Mon Sep 17 00:00:00 2001 From: scikit-learn-bot Date: Mon, 13 May 2024 12:22:35 +0200 Subject: [PATCH 109/344] :lock: :robot: CI Update lock files for cirrus-arm CI build(s) :lock: :robot: (#29003) --- .../pymin_conda_forge_linux-aarch64_conda.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/build_tools/cirrus/pymin_conda_forge_linux-aarch64_conda.lock b/build_tools/cirrus/pymin_conda_forge_linux-aarch64_conda.lock index 585a75c078d8c..660bc9de9ecda 100644 --- a/build_tools/cirrus/pymin_conda_forge_linux-aarch64_conda.lock +++ b/build_tools/cirrus/pymin_conda_forge_linux-aarch64_conda.lock @@ -4,25 +4,25 @@ @EXPLICIT https://conda.anaconda.org/conda-forge/linux-aarch64/ca-certificates-2024.2.2-hcefe29a_0.conda#57c226edb90c4e973b9b7503537dd339 https://conda.anaconda.org/conda-forge/linux-aarch64/ld_impl_linux-aarch64-2.40-hba4e955_0.conda#b55c1cb33c63d23b542fa53f24541e56 -https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-ng-13.2.0-h3f4de04_6.conda#dfe2ae16945dc08f163307a6bb3e70e0 +https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-ng-13.2.0-h3f4de04_7.conda#2a54872c7fab2db99b0074212d8efe64 https://conda.anaconda.org/conda-forge/linux-aarch64/python_abi-3.9-4_cp39.conda#c191905a08694e4a5cb1238e90233878 https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda#161081fc7cec0bfda0d86d7cb595f8d8 https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-2_kmp_llvm.tar.bz2#98a1185182fec3c434069fa74e6473d6 -https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-ng-13.2.0-he277a41_6.conda#5ca8651e635390d41004c847f03c2d3c +https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-ng-13.2.0-he277a41_7.conda#01c5b27ce46f50abab2dc8454842c792 https://conda.anaconda.org/conda-forge/linux-aarch64/bzip2-1.0.8-h31becfc_5.conda#a64e35f01e0b7a2a152eca87d33b9c87 https://conda.anaconda.org/conda-forge/linux-aarch64/lerc-4.0.0-h4de3ea5_0.tar.bz2#1a0ffc65e03ce81559dbcb0695ad1476 https://conda.anaconda.org/conda-forge/linux-aarch64/libbrotlicommon-1.1.0-h31becfc_1.conda#1b219fd801eddb7a94df5bd001053ad9 https://conda.anaconda.org/conda-forge/linux-aarch64/libdeflate-1.20-h31becfc_0.conda#018592a3d691662f451f89d0de474a20 https://conda.anaconda.org/conda-forge/linux-aarch64/libffi-3.4.2-h3557bc0_5.tar.bz2#dddd85f4d52121fab0a8b099c5e06501 -https://conda.anaconda.org/conda-forge/linux-aarch64/libgfortran5-13.2.0-h87d9d71_6.conda#a3fdb6378e561e73c735ec30207daa15 +https://conda.anaconda.org/conda-forge/linux-aarch64/libgfortran5-13.2.0-h87d9d71_7.conda#423eb7de085dd6b46928723edf5f8767 https://conda.anaconda.org/conda-forge/linux-aarch64/libjpeg-turbo-3.0.0-h31becfc_1.conda#ed24e702928be089d9ba3f05618515c6 https://conda.anaconda.org/conda-forge/linux-aarch64/libnsl-2.0.1-h31becfc_0.conda#c14f32510f694e3185704d89967ec422 https://conda.anaconda.org/conda-forge/linux-aarch64/libuuid-2.38.1-hb4cce97_0.conda#000e30b09db0b7c775b21695dff30969 https://conda.anaconda.org/conda-forge/linux-aarch64/libwebp-base-1.4.0-h31becfc_0.conda#5fd7ab3e5f382c70607fbac6335e6e19 https://conda.anaconda.org/conda-forge/linux-aarch64/libxcrypt-4.4.36-h31becfc_1.conda#b4df5d7d4b63579d081fd3a4cf99740e https://conda.anaconda.org/conda-forge/linux-aarch64/libzlib-1.2.13-h31becfc_5.conda#b213aa87eea9491ef7b129179322e955 -https://conda.anaconda.org/conda-forge/linux-aarch64/ncurses-6.4.20240210-h0425590_0.conda#c1a1612ddaee95c83abfa0b2ec858626 -https://conda.anaconda.org/conda-forge/linux-aarch64/ninja-1.12.0-h2a328a1_0.conda#c0f3f508baf69c8db8142466beaa0ccc +https://conda.anaconda.org/conda-forge/linux-aarch64/ncurses-6.5-h0425590_0.conda#38362af7bfac0efef69675acee564458 +https://conda.anaconda.org/conda-forge/linux-aarch64/ninja-1.12.1-h70be974_0.conda#216635cea46498d8045c7cf0f03eaf72 https://conda.anaconda.org/conda-forge/linux-aarch64/openssl-3.3.0-h31becfc_0.conda#36ca60a3afaf2ea2c460daeebd67430e https://conda.anaconda.org/conda-forge/linux-aarch64/pthread-stubs-0.4-hb9de7d4_1001.tar.bz2#d0183ec6ce0b5aaa3486df25fa5f0ded https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxau-1.0.11-h31becfc_0.conda#13de34f69cb73165dbe08c1e9148bedb @@ -30,7 +30,7 @@ https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxdmcp-1.1.3-h3557bc https://conda.anaconda.org/conda-forge/linux-aarch64/xz-5.2.6-h9cdd2b7_0.tar.bz2#83baad393a31d59c20b63ba4da6592df https://conda.anaconda.org/conda-forge/linux-aarch64/libbrotlidec-1.1.0-h31becfc_1.conda#8db7cff89510bec0b863a0a8ee6a7bce https://conda.anaconda.org/conda-forge/linux-aarch64/libbrotlienc-1.1.0-h31becfc_1.conda#ad3d3a826b5848d99936e4466ebbaa26 -https://conda.anaconda.org/conda-forge/linux-aarch64/libgfortran-ng-13.2.0-he9431aa_6.conda#c8ab19934c000ea8cc9cf1fc6c2aa83d +https://conda.anaconda.org/conda-forge/linux-aarch64/libgfortran-ng-13.2.0-he9431aa_7.conda#d714db6ba9d67d55d21cf96316714ec8 https://conda.anaconda.org/conda-forge/linux-aarch64/libpng-1.6.43-h194ca79_0.conda#1123e504d9254dd9494267ab9aba95f0 https://conda.anaconda.org/conda-forge/linux-aarch64/libsqlite-3.45.3-h194ca79_0.conda#fb35b8afbe9e92467ac7b5608d60b775 https://conda.anaconda.org/conda-forge/linux-aarch64/libxcb-1.15-h2a766a3_0.conda#eb3d8c8170e3d03f2564ed2024aa00c8 @@ -42,7 +42,7 @@ https://conda.anaconda.org/conda-forge/linux-aarch64/freetype-2.12.1-hf0a5ef3_2. https://conda.anaconda.org/conda-forge/linux-aarch64/libhiredis-1.0.2-h05efe27_0.tar.bz2#a87f068744fd20334cd41489eb163bee https://conda.anaconda.org/conda-forge/linux-aarch64/libopenblas-0.3.27-pthreads_h5a5ec62_0.conda#ffecca8f4f31cd50b92c0e6e6bfe4416 https://conda.anaconda.org/conda-forge/linux-aarch64/libtiff-4.6.0-hf980d43_3.conda#b6f3abf5726ae33094bee238b4eb492f -https://conda.anaconda.org/conda-forge/linux-aarch64/llvm-openmp-18.1.4-h767c9be_0.conda#2572130272fb725d825c9b52e5ce096b +https://conda.anaconda.org/conda-forge/linux-aarch64/llvm-openmp-18.1.5-h767c9be_0.conda#a9c2771c36671707f1992e4d0c32aa54 https://conda.anaconda.org/conda-forge/linux-aarch64/python-3.9.19-h4ac3b42_0_cpython.conda#1501507cd9451472ec8900d587ce872f https://conda.anaconda.org/conda-forge/linux-aarch64/brotli-1.1.0-h31becfc_1.conda#e41f5862ac746428407f3fd44d2ed01f https://conda.anaconda.org/conda-forge/linux-aarch64/ccache-4.9.1-h6552966_0.conda#758b202f61f6bbfd2c6adf0fde043276 From 0967ec4f5ebfea1b046d6d2012b14df24193c7d0 Mon Sep 17 00:00:00 2001 From: scikit-learn-bot Date: Mon, 13 May 2024 12:23:28 +0200 Subject: [PATCH 110/344] :lock: :robot: CI Update lock files for main CI build(s) :lock: :robot: (#29005) --- ...latest_conda_forge_mkl_linux-64_conda.lock | 26 ++++----- ...pylatest_conda_forge_mkl_osx-64_conda.lock | 38 ++++++------ ...test_conda_mkl_no_openmp_osx-64_conda.lock | 10 ++-- ...st_pip_openblas_pandas_linux-64_conda.lock | 10 ++-- ...onda_defaults_openblas_linux-64_conda.lock | 14 ++--- .../pymin_conda_forge_mkl_win-64_conda.lock | 8 +-- ...e_openblas_ubuntu_2204_linux-64_conda.lock | 26 ++++----- build_tools/circle/doc_linux-64_conda.lock | 58 +++++++++---------- .../doc_min_dependencies_linux-64_conda.lock | 50 ++++++++-------- 9 files changed, 120 insertions(+), 120 deletions(-) diff --git a/build_tools/azure/pylatest_conda_forge_mkl_linux-64_conda.lock b/build_tools/azure/pylatest_conda_forge_mkl_linux-64_conda.lock index 932fc6ad670f7..3d895fda71bc3 100644 --- a/build_tools/azure/pylatest_conda_forge_mkl_linux-64_conda.lock +++ b/build_tools/azure/pylatest_conda_forge_mkl_linux-64_conda.lock @@ -9,13 +9,13 @@ https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed3 https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2#4d59c254e01d9cde7957100457e2d5fb https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_2.conda#cbbe59391138ea5ad3658c76912e147f https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.40-h55db66e_0.conda#10569984e7db886e4f1abc2b47ad79a1 -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-13.2.0-hc0a3c3a_6.conda#2f18345bbc433c8a1ed887d7161e86a6 +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-13.2.0-hc0a3c3a_7.conda#53ebd4c833fa01cb2c6353e99f905406 https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.11-4_cp311.conda#d786502c97404c94d7d58d258a445a65 https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda#161081fc7cec0bfda0d86d7cb595f8d8 https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2#f766549260d6815b0c52253f1fb1bb29 https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2#fee5683a3f04bd15cbd8318b096a27ab https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_kmp_llvm.tar.bz2#562b26ba2e19059551a811e72ab7f793 -https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-h77fa898_6.conda#4398809ac84d0b8c28beebaaa83277f5 +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-h77fa898_7.conda#72ec1b1b04c4d15d4204ece1ecea5978 https://conda.anaconda.org/conda-forge/linux-64/alsa-lib-1.2.11-hd590300_1.conda#0bb492cca54017ea314b809b1ee3a176 https://conda.anaconda.org/conda-forge/linux-64/attr-2.5.1-h166bdaf_1.tar.bz2#d9c69a24ad678ffce24c6543a0176b00 https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.9.0-hd590300_0.conda#71b89db63b5b504e7afc8ad901172e1e @@ -37,7 +37,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda#172b https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.6.2-h59595ed_0.conda#e7ba12deb7020dd080c6c70e7b6f6a3d https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.2-h7f98852_5.tar.bz2#d645c6d2ac96843a2bfaccd2d62b3ac3 https://conda.anaconda.org/conda-forge/linux-64/libgettextpo-0.22.5-h59595ed_2.conda#172bcc51059416e7ce99e7b528cede83 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-13.2.0-h43f5ff8_6.conda#e54a5ddc67e673f9105cf2a2e9c070b0 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-13.2.0-hca663fb_7.conda#c0bd771f09a326fdcd95a60b617795bf https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.17-hd590300_2.conda#d66573916ffcf376178462f1b61c941e https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.0.0-hd590300_1.conda#ea25936bb4080d843790b586850f82b8 https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda#30fd6e37fe21f86f4bd26d6ee73eeec7 @@ -51,8 +51,8 @@ https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.cond https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.2.13-hd590300_5.conda#f36c115f1ee199da648e0597ec2047ad https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.9.4-hcb278e6_0.conda#318b08df404f9c9be5712aaa5a6f0bb0 https://conda.anaconda.org/conda-forge/linux-64/mpg123-1.32.6-h59595ed_0.conda#9160cdeb523a1b20cf8d2a0bf821f45d -https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.4.20240210-h59595ed_0.conda#97da8860a0da5413c7c98a3b3838a645 -https://conda.anaconda.org/conda-forge/linux-64/ninja-1.12.0-h00ab1b0_0.conda#b048701d52e7cbb5f59ddd4d3b17bbf5 +https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h59595ed_0.conda#fcea371545eda051b6deafb24889fc69 +https://conda.anaconda.org/conda-forge/linux-64/ninja-1.12.1-h297d8ca_0.conda#3aa1c7e292afeff25a0091ddd7c69b72 https://conda.anaconda.org/conda-forge/linux-64/nspr-4.35-h27087fc_0.conda#da0ec11a6454ae19bff5b02ed881a2b1 https://conda.anaconda.org/conda-forge/linux-64/openssl-3.3.0-hd590300_0.conda#c0f3abb4a16477208bbd43a39bd56f18 https://conda.anaconda.org/conda-forge/linux-64/pixman-0.43.2-h59595ed_0.conda#71004cbf7924e19c02746ccde9fd7123 @@ -83,7 +83,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libcap-2.69-h0f662aa_0.conda#25c https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20191231-he28a2e2_2.tar.bz2#4d331e44109e3f0e19b4cb8f9b82f3e1 https://conda.anaconda.org/conda-forge/linux-64/libevent-2.1.12-hf998b51_1.conda#a1cfcc585f0c42bf8d5546bb1dfb668d https://conda.anaconda.org/conda-forge/linux-64/libgettextpo-devel-0.22.5-h59595ed_2.conda#b63d9b6da3653179a278077f0de20014 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran-ng-13.2.0-h69a702a_6.conda#3666a850342f8f3be88f9a93d948d027 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-ng-13.2.0-h69a702a_7.conda#1b84f26d9f4f6026e179e7805d5a15cd https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.58.0-h47da74e_1.conda#700ac6ea6d53d5510591c4344d5c989a https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.43-h2797004_0.conda#009981dd9cfcaa4dbfa25ffaed86bcae https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-3.21.12-hfc55251_2.conda#e3a7d4ba09b8dc939b98fef55f539220 @@ -106,7 +106,7 @@ https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.0.9-h166bdaf_9.cond https://conda.anaconda.org/conda-forge/linux-64/freetype-2.12.1-h267a509_2.conda#9ae35c3d96db2c94ce0cef86efdfa2cb https://conda.anaconda.org/conda-forge/linux-64/gettext-0.22.5-h59595ed_2.conda#219ba82e95d7614cf7140d2a4afc0926 https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.2-h659d440_0.conda#cd95826dbd331ed1be26bdf401432844 -https://conda.anaconda.org/conda-forge/linux-64/libglib-2.80.0-hf2295e7_6.conda#9342e7c44c38bea649490f72d92c382d +https://conda.anaconda.org/conda-forge/linux-64/libglib-2.80.2-hf974151_0.conda#72724f6a78ecb15559396966226d5838 https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.54.3-hb20ce57_0.conda#7af7c59ab24db007dfd82e0a3a343f66 https://conda.anaconda.org/conda-forge/linux-64/libhiredis-1.0.2-h2cc385e_0.tar.bz2#b34907d3a81a3cd8095ee83d174c074a https://conda.anaconda.org/conda-forge/linux-64/libhwloc-2.10.0-default_h2fb2949_1000.conda#7e3726e647a619c6ce5939014dfde86d @@ -114,9 +114,9 @@ https://conda.anaconda.org/conda-forge/linux-64/libllvm15-15.0.7-hb3ce162_4.cond https://conda.anaconda.org/conda-forge/linux-64/libllvm18-18.1.5-hb77312f_0.conda#efd221d3668077ca067a206269418dec https://conda.anaconda.org/conda-forge/linux-64/libthrift-0.18.1-h8fd135c_2.conda#bbf65f7688512872f063810623b755dc https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.6.0-h1dd3fc0_3.conda#66f03896ffbe1a110ffda05c7a856504 -https://conda.anaconda.org/conda-forge/linux-64/llvm-openmp-18.1.4-ha31de31_0.conda#48b9991e66abc186a7ad7975e97bd4d0 +https://conda.anaconda.org/conda-forge/linux-64/llvm-openmp-18.1.5-ha31de31_0.conda#b923cdb6e567ada84f991ffcc5848afb https://conda.anaconda.org/conda-forge/linux-64/mysql-libs-8.3.0-hca2cd23_4.conda#1b50eebe2a738a3146c154d2eceaa8b6 -https://conda.anaconda.org/conda-forge/linux-64/nss-3.98-h1d7d5a4_0.conda#54b56c2fdf973656b748e0378900ec13 +https://conda.anaconda.org/conda-forge/linux-64/nss-3.100-hca3bf56_0.conda#949c4a82290ee58b3c970cef4bcfd4ad https://conda.anaconda.org/conda-forge/linux-64/orc-1.9.0-h2f23424_1.conda#9571eb3eb0f7fe8b59956a7786babbcd https://conda.anaconda.org/conda-forge/linux-64/python-3.11.9-hb806964_0_cpython.conda#ac68acfa8b558ed406c75e98d3428d7b https://conda.anaconda.org/conda-forge/linux-64/xcb-util-0.4.0-hd590300_1.conda#9bfac7ccd94d54fd21a0501296d60424 @@ -137,7 +137,7 @@ https://conda.anaconda.org/conda-forge/linux-64/dbus-1.13.6-h5008d03_3.tar.bz2#e https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.0-pyhd8ed1ab_2.conda#8d652ea2ee8eaee02ed8dc820bc794aa https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_0.conda#15dda3cdbf330abfe9f555d22f66db46 https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.14.2-h14ed4e7_0.conda#0f69b688f52ff6da70bccb7ff7001d1d -https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.80.0-hde27a5a_6.conda#a9d23c02485c5cf055f9ac90eb9c9c63 +https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.80.2-hb6ce0ca_0.conda#a965aeaf060289528a3fbe09326edae2 https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_0.conda#f800d2da156d08e289b14e87e43c1ae5 https://conda.anaconda.org/conda-forge/linux-64/kiwisolver-1.4.5-py311h9547e67_1.conda#2c65bdf442b0d37aad080c8a4e0d452f https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.16-hb7c19ff_0.conda#51bb7010fc86f70eee639b4bb7a894f5 @@ -147,7 +147,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libcups-2.3.3-h4637d8d_4.conda#d https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.7.1-hca28451_0.conda#755c7f876815003337d2c61ff5d047e5 https://conda.anaconda.org/conda-forge/linux-64/libflac-1.4.3-h59595ed_0.conda#ee48bf17cc83a00f59ca1494d5646869 https://conda.anaconda.org/conda-forge/linux-64/libgpg-error-1.49-h4f305b6_0.conda#dfcfd72c7a430d3616763ecfbefe4ca9 -https://conda.anaconda.org/conda-forge/linux-64/libpq-16.2-h33b98f1_1.conda#9e49ec2a61d02623b379dc332eb6889d +https://conda.anaconda.org/conda-forge/linux-64/libpq-16.3-ha72fbe1_0.conda#bac737ae28b79cfbafd515258d97d29e https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyh9f0ad1d_0.tar.bz2#2ba8498c1018c1e9c61eb99b973dfe19 https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.2-h488ebb8_0.conda#7f2e286780f072ed750df46dc2631138 https://conda.anaconda.org/conda-forge/noarch/packaging-24.0-pyhd8ed1ab_0.conda#248f521b64ce055e7feae3105e7abeb8 @@ -174,7 +174,7 @@ https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.9.3-hb447be9_1.cond https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.0-h3faef2a_0.conda#f907bb958910dc404647326ca80c263e https://conda.anaconda.org/conda-forge/linux-64/coverage-7.5.1-py311h331c9d8_0.conda#9f35e13e3b9e05e153b78f42662061f6 https://conda.anaconda.org/conda-forge/linux-64/fonttools-4.51.0-py311h459d7ec_0.conda#17e1997cc17c571d5ad27bd0159f616c -https://conda.anaconda.org/conda-forge/linux-64/glib-2.80.0-hf2295e7_6.conda#a1e026a82a562b443845db5614ca568a +https://conda.anaconda.org/conda-forge/linux-64/glib-2.80.2-hf974151_0.conda#d427988dc3dbd0a4c136f52db356cc6a https://conda.anaconda.org/conda-forge/noarch/joblib-1.4.2-pyhd8ed1ab_0.conda#25df261d4523d9f9783bcdb7208d872f https://conda.anaconda.org/conda-forge/linux-64/libgcrypt-1.10.3-hd590300_0.conda#32d16ad533c59bb0a3c5ffaf16110829 https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-2.12.0-hac9eb74_1.conda#0dee716254497604762957076ac76540 @@ -210,7 +210,7 @@ https://conda.anaconda.org/conda-forge/noarch/array-api-strict-1.1.1-pyhd8ed1ab_ https://conda.anaconda.org/conda-forge/linux-64/contourpy-1.2.1-py311h9547e67_0.conda#74ad0ae64f1ef565e27eda87fa749e84 https://conda.anaconda.org/conda-forge/linux-64/libarrow-12.0.1-hb87d912_8_cpu.conda#3f3b11398fe79b578e3c44dd00a44e4a https://conda.anaconda.org/conda-forge/linux-64/pandas-2.2.2-py311h320fe9a_0.conda#c79e96ece4110fdaf2657c9f8e16f749 -https://conda.anaconda.org/conda-forge/linux-64/polars-0.20.23-py311h00856b1_0.conda#c000e1629d890ad00bb8c20963028d9f +https://conda.anaconda.org/conda-forge/linux-64/polars-0.20.25-py311h00856b1_0.conda#84ad7fa8742f6d34784a961337622c55 https://conda.anaconda.org/conda-forge/linux-64/pyqt-5.15.9-py311hf0fb5b6_5.conda#ec7e45bc76d9d0b69a74a2075932b8e8 https://conda.anaconda.org/conda-forge/linux-64/pytorch-1.13.1-cpu_py311h410fd25_1.conda#ddd2fadddf89e3dc3d541a2537fce010 https://conda.anaconda.org/conda-forge/linux-64/scipy-1.13.0-py311h517d4fd_1.conda#a86b8bea39e292a23b2cf9a750f49ea1 diff --git a/build_tools/azure/pylatest_conda_forge_mkl_osx-64_conda.lock b/build_tools/azure/pylatest_conda_forge_mkl_osx-64_conda.lock index 7f3e749a5728d..ce2d5e2c383a3 100644 --- a/build_tools/azure/pylatest_conda_forge_mkl_osx-64_conda.lock +++ b/build_tools/azure/pylatest_conda_forge_mkl_osx-64_conda.lock @@ -6,7 +6,6 @@ https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h10d778d_5.conda#6097a https://conda.anaconda.org/conda-forge/osx-64/ca-certificates-2024.2.2-h8857fd0_0.conda#f2eacee8c33c43692f1ccfd33d0f50b1 https://conda.anaconda.org/conda-forge/osx-64/icu-73.2-hf5e326d_0.conda#5cc301d759ec03f28328428e28f65591 https://conda.anaconda.org/conda-forge/osx-64/libbrotlicommon-1.1.0-h0dc2134_1.conda#9e6c31441c9aa24e41ace40d6151aab6 -https://conda.anaconda.org/conda-forge/osx-64/libcxx-16.0.6-hd57cbcb_0.conda#7d6972792161077908b62971802f289a https://conda.anaconda.org/conda-forge/osx-64/libdeflate-1.20-h49d49c5_0.conda#d46104f6a896a0bc6a1d37b88b2edf5c https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.6.2-h73e2aa4_0.conda#3d1d51c8f716d97c864d12f7af329526 https://conda.anaconda.org/conda-forge/osx-64/libffi-3.4.2-h0d85af4_5.tar.bz2#ccb34fb14960ad8b125962d3d79b31a9 @@ -16,39 +15,38 @@ https://conda.anaconda.org/conda-forge/osx-64/libjpeg-turbo-3.0.0-h0dc2134_1.con https://conda.anaconda.org/conda-forge/osx-64/libwebp-base-1.4.0-h10d778d_0.conda#b2c0047ea73819d992484faacbbe1c24 https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.2.13-h8a1eda9_5.conda#4a3ad23f6e16f99c04e166767193d700 https://conda.anaconda.org/conda-forge/osx-64/mkl-include-2023.2.0-h6bab518_50500.conda#835abb8ded5e26f23ea6996259c7972e -https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.4.20240210-h73e2aa4_0.conda#50f28c512e9ad78589e3eab34833f762 +https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.5-h5846eda_0.conda#02a888433d165c99bf09784a7b14d900 https://conda.anaconda.org/conda-forge/osx-64/pthread-stubs-0.4-hc929b4f_1001.tar.bz2#addd19059de62181cd11ae8f4ef26084 https://conda.anaconda.org/conda-forge/osx-64/python_abi-3.12-4_cp312.conda#87201ac4314b911b74197e588cca3639 https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda#161081fc7cec0bfda0d86d7cb595f8d8 https://conda.anaconda.org/conda-forge/osx-64/xorg-libxau-1.0.11-h0dc2134_0.conda#9566b4c29274125b0266d0177b5eb97b https://conda.anaconda.org/conda-forge/osx-64/xorg-libxdmcp-1.1.3-h35c211d_0.tar.bz2#86ac76d6bf1cbb9621943eb3bd9ae36e https://conda.anaconda.org/conda-forge/osx-64/xz-5.2.6-h775f41a_0.tar.bz2#a72f9d4ea13d55d745ff1ed594747f10 -https://conda.anaconda.org/conda-forge/osx-64/gmp-6.3.0-h73e2aa4_1.conda#92f8d748d95d97f92fc26cfac9bb5b6e -https://conda.anaconda.org/conda-forge/osx-64/isl-0.26-imath32_h2e86a7b_101.conda#d06222822a9144918333346f145b68c6 -https://conda.anaconda.org/conda-forge/osx-64/lerc-4.0.0-hb486fe8_0.tar.bz2#f9d6a4c82889d5ecedec1d90eb673c55 https://conda.anaconda.org/conda-forge/osx-64/libbrotlidec-1.1.0-h0dc2134_1.conda#9ee0bab91b2ca579e10353738be36063 https://conda.anaconda.org/conda-forge/osx-64/libbrotlienc-1.1.0-h0dc2134_1.conda#8a421fe09c6187f0eb5e2338a8a8be6d +https://conda.anaconda.org/conda-forge/osx-64/libcxx-17.0.6-h88467a6_0.conda#0fe355aecb8d24b8bc07c763209adbd9 https://conda.anaconda.org/conda-forge/osx-64/libpng-1.6.43-h92b6c6a_0.conda#65dcddb15965c9de2c0365cb14910532 https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.45.3-h92b6c6a_0.conda#68e462226209f35182ef66eda0f794ff https://conda.anaconda.org/conda-forge/osx-64/libxcb-1.15-hb7f2c08_0.conda#5513f57e0238c87c12dffedbcc9c1a4a https://conda.anaconda.org/conda-forge/osx-64/libxml2-2.12.6-hc0ae0f7_2.conda#50b997370584f2c83ca0c38e9028eab9 -https://conda.anaconda.org/conda-forge/osx-64/llvm-openmp-18.1.4-h2c61cee_0.conda#0619a2dda8b7e25b78abc0b3d872744f -https://conda.anaconda.org/conda-forge/osx-64/ninja-1.12.0-h7728843_0.conda#1ac079f6ecddd2c336f3acb7b371851f +https://conda.anaconda.org/conda-forge/osx-64/llvm-openmp-18.1.5-h39e0ece_0.conda#ee12a644568269838b91f901b2537425 https://conda.anaconda.org/conda-forge/osx-64/openssl-3.3.0-hd75f5a5_0.conda#eb8c33aa7929a7714eab8b90c1d88afe https://conda.anaconda.org/conda-forge/osx-64/readline-8.2-h9e318b2_1.conda#f17f77f2acf4d344734bda76829ce14e -https://conda.anaconda.org/conda-forge/osx-64/tapi-1100.0.11-h9ce4665_0.tar.bz2#f9ff42ccf809a21ba6f8607f8de36108 https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h1abcd95_1.conda#bf830ba5afc507c6232d4ef0fb1a882d https://conda.anaconda.org/conda-forge/osx-64/zlib-1.2.13-h8a1eda9_5.conda#75a8a98b1c4671c5d2897975731da42d https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.6-h915ae27_0.conda#4cb2cd56f039b129bb0e491c1164167e https://conda.anaconda.org/conda-forge/osx-64/brotli-bin-1.1.0-h0dc2134_1.conda#ece565c215adcc47fc1db4e651ee094b https://conda.anaconda.org/conda-forge/osx-64/freetype-2.12.1-h60636b9_2.conda#25152fce119320c980e5470e64834b50 +https://conda.anaconda.org/conda-forge/osx-64/gmp-6.3.0-h73e2aa4_1.conda#92f8d748d95d97f92fc26cfac9bb5b6e +https://conda.anaconda.org/conda-forge/osx-64/isl-0.26-imath32_h2e86a7b_101.conda#d06222822a9144918333346f145b68c6 +https://conda.anaconda.org/conda-forge/osx-64/lerc-4.0.0-hb486fe8_0.tar.bz2#f9d6a4c82889d5ecedec1d90eb673c55 https://conda.anaconda.org/conda-forge/osx-64/libgfortran5-13.2.0-h2873a65_3.conda#e4fb4d23ec2870ff3c40d10afe305aec https://conda.anaconda.org/conda-forge/osx-64/libhwloc-2.10.0-default_h1321489_1000.conda#6f5fe4374d1003e116e2573022178da6 https://conda.anaconda.org/conda-forge/osx-64/libllvm16-16.0.6-hbedff68_3.conda#8fd56c0adc07a37f93bd44aa61a97c90 -https://conda.anaconda.org/conda-forge/osx-64/libtiff-4.6.0-h129831d_3.conda#568593071d2e6cea7b5fc1f75bfa10ca -https://conda.anaconda.org/conda-forge/osx-64/mpfr-4.2.1-h4f6b447_1.conda#b90df08f0deb2f58631447c1462c92a7 +https://conda.anaconda.org/conda-forge/osx-64/ninja-1.12.1-h3c5361c_0.conda#a0ebabd021c8191aeb82793fe43cfdcb https://conda.anaconda.org/conda-forge/osx-64/python-3.12.3-h1411813_0_cpython.conda#df1448ec6cbf8eceb03d29003cf72ae6 https://conda.anaconda.org/conda-forge/osx-64/sigtool-0.1.3-h88f4db0_0.tar.bz2#fbfb84b9de9a6939cb165c02c69b1865 +https://conda.anaconda.org/conda-forge/osx-64/tapi-1100.0.11-h9ce4665_0.tar.bz2#f9ff42ccf809a21ba6f8607f8de36108 https://conda.anaconda.org/conda-forge/osx-64/brotli-1.1.0-h0dc2134_1.conda#9272dd3b19c4e8212f8542cefd5c3d67 https://conda.anaconda.org/conda-forge/noarch/certifi-2024.2.2-pyhd8ed1ab_0.conda#0876280e409658fc6f9e75d035960333 https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz2#3faab06a954c2a04039983f2c4a50d99 @@ -58,14 +56,13 @@ https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.0-pyhd8ed1ab_2. https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_0.conda#15dda3cdbf330abfe9f555d22f66db46 https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_0.conda#f800d2da156d08e289b14e87e43c1ae5 https://conda.anaconda.org/conda-forge/osx-64/kiwisolver-1.4.5-py312h49ebfd2_1.conda#21f174a5cfb5964069c374171a979157 -https://conda.anaconda.org/conda-forge/osx-64/lcms2-2.16-ha2f27b4_0.conda#1442db8f03517834843666c422238c9b https://conda.anaconda.org/conda-forge/osx-64/ld64_osx-64-711-ha20a434_0.conda#a8b41eb97c8a9d618243a79ba78fdc3c https://conda.anaconda.org/conda-forge/osx-64/libclang-cpp16-16.0.6-default_h7151d67_6.conda#7eaad118ab797d1427f8745c861d1925 https://conda.anaconda.org/conda-forge/osx-64/libgfortran-5.0.0-13_2_0_h97931a8_3.conda#0b6e23a012ee7a9a5f6b244f5a92c1d5 +https://conda.anaconda.org/conda-forge/osx-64/libtiff-4.6.0-h129831d_3.conda#568593071d2e6cea7b5fc1f75bfa10ca https://conda.anaconda.org/conda-forge/osx-64/llvm-tools-16.0.6-hbedff68_3.conda#e9356b0807462e8f84c1384a8da539a5 -https://conda.anaconda.org/conda-forge/osx-64/mpc-1.3.1-h81bd1dd_0.conda#c752c0eb6c250919559172c011e5f65b +https://conda.anaconda.org/conda-forge/osx-64/mpfr-4.2.1-h4f6b447_1.conda#b90df08f0deb2f58631447c1462c92a7 https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyh9f0ad1d_0.tar.bz2#2ba8498c1018c1e9c61eb99b973dfe19 -https://conda.anaconda.org/conda-forge/osx-64/openjpeg-2.5.2-h7310d3a_0.conda#05a14cc9d725dd74995927968d6547e3 https://conda.anaconda.org/conda-forge/noarch/packaging-24.0-pyhd8ed1ab_0.conda#248f521b64ce055e7feae3105e7abeb8 https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_0.conda#d3483c8fc2dc2cc3f5cf43e26d60cabf https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.1.2-pyhd8ed1ab_0.conda#b9a4dacf97241704529131a0dfc0494f @@ -83,13 +80,14 @@ https://conda.anaconda.org/conda-forge/osx-64/cctools_osx-64-986-ha1c5b94_0.cond https://conda.anaconda.org/conda-forge/osx-64/clang-16-16.0.6-default_h7151d67_6.conda#1c298568c30efe7d9369c7c15b748461 https://conda.anaconda.org/conda-forge/osx-64/coverage-7.5.1-py312h520dd33_0.conda#afc8c7b237683760a3c35e49bcc04deb https://conda.anaconda.org/conda-forge/osx-64/fonttools-4.51.0-py312h41838bb_0.conda#ebe40134b860cf704ddaf81f684f95a5 -https://conda.anaconda.org/conda-forge/osx-64/gfortran_impl_osx-64-12.3.0-hc328e78_3.conda#b3d751dc7073bbfdfa9d863e39b9685d https://conda.anaconda.org/conda-forge/noarch/joblib-1.4.2-pyhd8ed1ab_0.conda#25df261d4523d9f9783bcdb7208d872f +https://conda.anaconda.org/conda-forge/osx-64/lcms2-2.16-ha2f27b4_0.conda#1442db8f03517834843666c422238c9b https://conda.anaconda.org/conda-forge/osx-64/ld64-711-ha02d983_0.conda#3ae4930ec076735cce481e906f5192e0 https://conda.anaconda.org/conda-forge/osx-64/libhiredis-1.0.2-h2beb688_0.tar.bz2#524282b2c46c9dedf051b3bc2ae05494 https://conda.anaconda.org/conda-forge/noarch/meson-1.4.0-pyhd8ed1ab_0.conda#52a0660cfa40b45bf254ecc3374cb2e0 https://conda.anaconda.org/conda-forge/osx-64/mkl-2023.2.0-h54c2260_50500.conda#0a342ccdc79e4fcd359245ac51941e7b -https://conda.anaconda.org/conda-forge/osx-64/pillow-10.3.0-py312h0c923fa_0.conda#6f0591ae972e9b815739da3392fbb3c3 +https://conda.anaconda.org/conda-forge/osx-64/mpc-1.3.1-h81bd1dd_0.conda#c752c0eb6c250919559172c011e5f65b +https://conda.anaconda.org/conda-forge/osx-64/openjpeg-2.5.2-h7310d3a_0.conda#05a14cc9d725dd74995927968d6547e3 https://conda.anaconda.org/conda-forge/noarch/pip-24.0-pyhd8ed1ab_0.conda#f586ac1e56c8638b64f9c8122a7b8a67 https://conda.anaconda.org/conda-forge/noarch/pyproject-metadata-0.8.0-pyhd8ed1ab_0.conda#573fe09d7bd0cd4bcc210d8369b5ca47 https://conda.anaconda.org/conda-forge/noarch/pytest-7.4.4-pyhd8ed1ab_0.conda#a9d145de8c5f064b5fa68fb34725d9f4 @@ -97,9 +95,11 @@ https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0-pyhd8ed1ab_0 https://conda.anaconda.org/conda-forge/osx-64/ccache-4.9.1-h41adc32_0.conda#45aaf96b67840bd98a928de8679098fa https://conda.anaconda.org/conda-forge/osx-64/cctools-986-h40f6528_0.conda#b7a2ca0062a6ee8bc4e83ec887bef942 https://conda.anaconda.org/conda-forge/osx-64/clang-16.0.6-hdae98eb_6.conda#884e7b24306e4f21b7ee08dabadb2ecc +https://conda.anaconda.org/conda-forge/osx-64/gfortran_impl_osx-64-12.3.0-hc328e78_3.conda#b3d751dc7073bbfdfa9d863e39b9685d https://conda.anaconda.org/conda-forge/osx-64/libblas-3.9.0-20_osx64_mkl.conda#160fdc97a51d66d51dc782fb67d35205 https://conda.anaconda.org/conda-forge/noarch/meson-python-0.16.0-pyh0c530f3_0.conda#e16f0dbf502da873be9f9adb0dc52547 https://conda.anaconda.org/conda-forge/osx-64/mkl-devel-2023.2.0-h694c41f_50500.conda#1b4d0235ef253a1e19459351badf4f9f +https://conda.anaconda.org/conda-forge/osx-64/pillow-10.3.0-py312h0c923fa_0.conda#6f0591ae972e9b815739da3392fbb3c3 https://conda.anaconda.org/conda-forge/noarch/pytest-cov-5.0.0-pyhd8ed1ab_0.conda#c54c0107057d67ddf077751339ec2c63 https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.5.0-pyhd8ed1ab_0.conda#d5f595da2daead898ca958ac62f0307b https://conda.anaconda.org/conda-forge/osx-64/clangxx-16.0.6-default_h7151d67_6.conda#cc8c007a529a7cfaa5d29d8599df3fe6 @@ -114,15 +114,15 @@ https://conda.anaconda.org/conda-forge/osx-64/contourpy-1.2.1-py312h9230928_0.co https://conda.anaconda.org/conda-forge/osx-64/pandas-2.2.2-py312h83c8a23_0.conda#b422a5d39ff0cd72923aef807f280145 https://conda.anaconda.org/conda-forge/osx-64/scipy-1.13.0-py312h741d2f9_1.conda#c416453a8ea3b38d823fe8dcecdb6a12 https://conda.anaconda.org/conda-forge/osx-64/blas-2.120-mkl.conda#b041a7677a412f3d925d8208936cb1e2 -https://conda.anaconda.org/conda-forge/osx-64/clang_impl_osx-64-16.0.6-h8787910_12.conda#fe1a78dddda2c0b32fac9fbd7fa05c5f +https://conda.anaconda.org/conda-forge/osx-64/clang_impl_osx-64-16.0.6-h8787910_14.conda#fc1a7d3f1bf236f63c58bab6e36844cb https://conda.anaconda.org/conda-forge/osx-64/matplotlib-base-3.8.4-py312h1fe5000_0.conda#3e3097734a5042cb6d2675e69bf1fc5a https://conda.anaconda.org/conda-forge/osx-64/pyamg-5.1.0-py312h3db3e91_0.conda#c6d6248b99fc11b15c9becea581a1462 -https://conda.anaconda.org/conda-forge/osx-64/clang_osx-64-16.0.6-hb91bd55_12.conda#4ef6f9a82654ad497e2334471832e774 +https://conda.anaconda.org/conda-forge/osx-64/clang_osx-64-16.0.6-hb91bd55_14.conda#3d0d9c725912bb0cb4cd301d2a5d31d7 https://conda.anaconda.org/conda-forge/osx-64/matplotlib-3.8.4-py312hb401068_0.conda#187ee42addd449b4899b55c304012436 https://conda.anaconda.org/conda-forge/osx-64/c-compiler-1.7.0-h282daa2_1.conda#d27411cb82bc1b76b9f487da6ae97f1d -https://conda.anaconda.org/conda-forge/osx-64/clangxx_impl_osx-64-16.0.6-h6d92fbe_12.conda#c1b8987b40123346ee3fe120c3b66b3d +https://conda.anaconda.org/conda-forge/osx-64/clangxx_impl_osx-64-16.0.6-h6d92fbe_14.conda#66b9f06d5f0d0ea47ffcb3a9ca65774a https://conda.anaconda.org/conda-forge/osx-64/gfortran_osx-64-12.3.0-h18f7dce_1.conda#436af2384c47aedb94af78a128e174f1 -https://conda.anaconda.org/conda-forge/osx-64/clangxx_osx-64-16.0.6-hb91bd55_12.conda#4e8cca2283e843a8df8b2e747d36226d +https://conda.anaconda.org/conda-forge/osx-64/clangxx_osx-64-16.0.6-hb91bd55_14.conda#a4504c1a7beab8875d6f765941e77248 https://conda.anaconda.org/conda-forge/osx-64/gfortran-12.3.0-h2c809b3_1.conda#c48adbaa8944234b80ef287c37e329b0 https://conda.anaconda.org/conda-forge/osx-64/cxx-compiler-1.7.0-h7728843_1.conda#e04cb15a20553b973dd068c2dc81d682 https://conda.anaconda.org/conda-forge/osx-64/fortran-compiler-1.7.0-h6c2ab21_1.conda#48319058089f492d5059e04494b81ed9 diff --git a/build_tools/azure/pylatest_conda_mkl_no_openmp_osx-64_conda.lock b/build_tools/azure/pylatest_conda_mkl_no_openmp_osx-64_conda.lock index c687f8fb76fb1..ec92612048448 100644 --- a/build_tools/azure/pylatest_conda_mkl_no_openmp_osx-64_conda.lock +++ b/build_tools/azure/pylatest_conda_mkl_no_openmp_osx-64_conda.lock @@ -29,7 +29,7 @@ https://repo.anaconda.com/pkgs/main/osx-64/ninja-base-1.10.2-haf03e11_5.conda#c8 https://repo.anaconda.com/pkgs/main/osx-64/openssl-3.0.13-hca72f7f_1.conda#e526d7e2e79132a11b4746cf305c45b5 https://repo.anaconda.com/pkgs/main/osx-64/readline-8.2-hca72f7f_0.conda#971667436260e523f6f7355fdfa238bf https://repo.anaconda.com/pkgs/main/osx-64/tbb-2021.8.0-ha357a0b_0.conda#fb48530a3eea681c11dafb95b3387c0f -https://repo.anaconda.com/pkgs/main/osx-64/tk-8.6.12-h5d9f67b_0.conda#047f0af5486d19163e37fd7f8ae3d29f +https://repo.anaconda.com/pkgs/main/osx-64/tk-8.6.14-h4d00af3_0.conda#a2c03940c2ae54614301ec82e6a98d75 https://repo.anaconda.com/pkgs/main/osx-64/brotli-bin-1.0.9-h6c40b1e_8.conda#11053f9c6b8d8a8348d0c33450c23ce9 https://repo.anaconda.com/pkgs/main/osx-64/freetype-2.12.1-hd8bbffd_0.conda#1f276af321375ee7fe8056843044fa76 https://repo.anaconda.com/pkgs/main/osx-64/libgfortran-5.0.0-11_3_0_hecd8cb5_28.conda#2eb13b680803f1064e53873ae0aaafb3 @@ -38,7 +38,7 @@ https://repo.anaconda.com/pkgs/main/osx-64/sqlite-3.45.3-h6c40b1e_0.conda#2edf90 https://repo.anaconda.com/pkgs/main/osx-64/zstd-1.5.5-hc035e20_2.conda#c033bf68c12f8c71fd916f000f3dc118 https://repo.anaconda.com/pkgs/main/osx-64/brotli-1.0.9-h6c40b1e_8.conda#10f89677a3898d0113dc354adf643df3 https://repo.anaconda.com/pkgs/main/osx-64/libtiff-4.5.1-hcec6c5f_0.conda#e127a800ffd9d300ed7d5e1b026944ec -https://repo.anaconda.com/pkgs/main/osx-64/python-3.12.3-hd58486a_0.conda#1a287cfa37c5a92972f5f527b6af7eed +https://repo.anaconda.com/pkgs/main/osx-64/python-3.12.3-hd58486a_1.conda#cdc61e8f6c2d77b3b263e720048c4b54 https://repo.anaconda.com/pkgs/main/osx-64/coverage-7.2.2-py312h6c40b1e_0.conda#b6e4b9fba325047c07f3c9211ae91d1c https://repo.anaconda.com/pkgs/main/noarch/cycler-0.11.0-pyhd3eb1b0_0.conda#f5e365d2cdb66d547eb8c3ab93843aab https://repo.anaconda.com/pkgs/main/noarch/execnet-1.9.0-pyhd3eb1b0_0.conda#f895937671af67cebb8af617494b3513 @@ -54,7 +54,7 @@ https://repo.anaconda.com/pkgs/main/osx-64/pluggy-1.0.0-py312hecd8cb5_1.conda#64 https://repo.anaconda.com/pkgs/main/osx-64/pyparsing-3.0.9-py312hecd8cb5_0.conda#d85cf2b81c6d9326a57a6418e14db258 https://repo.anaconda.com/pkgs/main/noarch/python-tzdata-2023.3-pyhd3eb1b0_0.conda#479c037de0186d114b9911158427624e https://repo.anaconda.com/pkgs/main/osx-64/pytz-2024.1-py312hecd8cb5_0.conda#2b28ec0e0d07f5c0c701f75200b1e8b6 -https://repo.anaconda.com/pkgs/main/osx-64/setuptools-68.2.2-py312hecd8cb5_0.conda#64235f0c451427d86808c70c1c31cb8b +https://repo.anaconda.com/pkgs/main/osx-64/setuptools-69.5.1-py312hecd8cb5_0.conda#5c7c7ef1e0762e3ca1f543d28310946f https://repo.anaconda.com/pkgs/main/noarch/six-1.16.0-pyhd3eb1b0_1.conda#34586824d411d36af2fa40e799c172d0 https://repo.anaconda.com/pkgs/main/noarch/toml-0.10.2-pyhd3eb1b0_0.conda#cda05f5f6d8509529d1a2743288d197a https://repo.anaconda.com/pkgs/main/osx-64/tornado-6.3.3-py312h6c40b1e_0.conda#49173b5a36c9134865221f29d4a73fb6 @@ -64,10 +64,10 @@ https://repo.anaconda.com/pkgs/main/osx-64/fonttools-4.51.0-py312h6c40b1e_0.cond https://repo.anaconda.com/pkgs/main/osx-64/meson-1.3.1-py312hecd8cb5_0.conda#43963a2b38becce4caa95434b8c96837 https://repo.anaconda.com/pkgs/main/osx-64/numpy-base-1.26.4-py312h6f81483_0.conda#87f73efbf26ab2e2ea7c32481a71bd47 https://repo.anaconda.com/pkgs/main/osx-64/pillow-10.3.0-py312h6c40b1e_0.conda#fe883fa4247d35fe6de49f713529ca02 -https://repo.anaconda.com/pkgs/main/osx-64/pip-23.3.1-py312hecd8cb5_0.conda#efc3db40cac09f74bb480d28d3a0b260 +https://repo.anaconda.com/pkgs/main/osx-64/pip-24.0-py312hecd8cb5_0.conda#7a8e0b1d3742ddf1c8aa97fbaa158039 https://repo.anaconda.com/pkgs/main/osx-64/pyproject-metadata-0.7.1-py312hecd8cb5_0.conda#e91ce37477d24dcdf7e0a8b93c5e72fd https://repo.anaconda.com/pkgs/main/osx-64/pytest-7.4.0-py312hecd8cb5_0.conda#b816a2439ba9b87524aec74d58e55b0a -https://repo.anaconda.com/pkgs/main/noarch/python-dateutil-2.8.2-pyhd3eb1b0_0.conda#211ee00320b08a1ac9fea6677649f6c9 +https://repo.anaconda.com/pkgs/main/osx-64/python-dateutil-2.9.0post0-py312hecd8cb5_0.conda#b3ed54eb118325785284dd18bfceca19 https://repo.anaconda.com/pkgs/main/osx-64/meson-python-0.15.0-py312h6c40b1e_0.conda#688ab56b9d8e5a2e3f018ca3ce34e061 https://repo.anaconda.com/pkgs/main/osx-64/pytest-cov-4.1.0-py312hecd8cb5_1.conda#a33a24eb20359f464938e75b2f57e23a https://repo.anaconda.com/pkgs/main/osx-64/pytest-xdist-3.5.0-py312hecd8cb5_0.conda#d1ecfb3691cceecb1f16bcfdf0b67bb5 diff --git a/build_tools/azure/pylatest_pip_openblas_pandas_linux-64_conda.lock b/build_tools/azure/pylatest_pip_openblas_pandas_linux-64_conda.lock index c497709ca347e..46fd0d308eaa2 100644 --- a/build_tools/azure/pylatest_pip_openblas_pandas_linux-64_conda.lock +++ b/build_tools/azure/pylatest_pip_openblas_pandas_linux-64_conda.lock @@ -17,12 +17,12 @@ https://repo.anaconda.com/pkgs/main/linux-64/xz-5.4.6-h5eee18b_1.conda#1562802f8 https://repo.anaconda.com/pkgs/main/linux-64/zlib-1.2.13-h5eee18b_1.conda#92e42d8310108b0a440fb2e60b2b2a25 https://repo.anaconda.com/pkgs/main/linux-64/ccache-3.7.9-hfe4627d_0.conda#bef6fc681c273bb7bd0c67d1a591365e https://repo.anaconda.com/pkgs/main/linux-64/readline-8.2-h5eee18b_0.conda#be42180685cce6e6b0329201d9f48efb -https://repo.anaconda.com/pkgs/main/linux-64/tk-8.6.12-h1ccaba5_0.conda#fa10ff4aa631fa4aa090a6234d7770b9 +https://repo.anaconda.com/pkgs/main/linux-64/tk-8.6.14-h39e8969_0.conda#78dbc5e3c69143ebc037fc5d5b22e597 https://repo.anaconda.com/pkgs/main/linux-64/sqlite-3.45.3-h5eee18b_0.conda#acf93d6aceb74d6110e20b44cc45939e -https://repo.anaconda.com/pkgs/main/linux-64/python-3.9.19-h955ad1f_0.conda#33cb019c40e3409df392c99e3c34f352 -https://repo.anaconda.com/pkgs/main/linux-64/setuptools-68.2.2-py39h06a4308_0.conda#5b42cae5548732ae5c167bb1066085de +https://repo.anaconda.com/pkgs/main/linux-64/python-3.9.19-h955ad1f_1.conda#4b453281859c293c9d577271f3b18a0d +https://repo.anaconda.com/pkgs/main/linux-64/setuptools-69.5.1-py39h06a4308_0.conda#3eb144d481b39c0fbbced789dd9b76b3 https://repo.anaconda.com/pkgs/main/linux-64/wheel-0.43.0-py39h06a4308_0.conda#40bb60408c7433d767fd8c65b35bc4a0 -https://repo.anaconda.com/pkgs/main/linux-64/pip-23.3.1-py39h06a4308_0.conda#685007e3dae59d211620f19926577bd6 +https://repo.anaconda.com/pkgs/main/linux-64/pip-24.0-py39h06a4308_0.conda#7f8ce3af15cfecd12e4dda8c5cef5fb7 # pip alabaster @ https://files.pythonhosted.org/packages/32/34/d4e1c02d3bee589efb5dfa17f88ea08bdb3e3eac12bc475462aec52ed223/alabaster-0.7.16-py3-none-any.whl#sha256=b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92 # pip babel @ https://files.pythonhosted.org/packages/27/45/377f7e32a5c93d94cd56542349b34efab5ca3f9e2fd5a68c5e93169aa32d/Babel-2.15.0-py3-none-any.whl#sha256=08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb # pip certifi @ https://files.pythonhosted.org/packages/ba/06/a07f096c664aeb9f01624f858c3add0a4e913d6c96257acb4fce61e7de14/certifi-2024.2.2-py3-none-any.whl#sha256=dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1 @@ -75,7 +75,7 @@ https://repo.anaconda.com/pkgs/main/linux-64/pip-23.3.1-py39h06a4308_0.conda#685 # pip python-dateutil @ https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl#sha256=a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 # pip requests @ https://files.pythonhosted.org/packages/70/8e/0e2d847013cb52cd35b38c009bb167a1a26b2ce6cd6965bf26b47bc0bf44/requests-2.31.0-py3-none-any.whl#sha256=58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f # pip scipy @ https://files.pythonhosted.org/packages/c6/ba/a778e6c0020d728c119b0379805a357135fe8c9bc87fdb7e0750ca11319f/scipy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=28e286bf9ac422d6beb559bc61312c348ca9b0f0dae0d7c5afde7f722d6ea13d -# pip tifffile @ https://files.pythonhosted.org/packages/c1/cf/dd1cdf85db58c811816377afd6ba8a240f4611e16f4085201598fb2d5578/tifffile-2024.5.3-py3-none-any.whl#sha256=cac4d939156ff7f16d65fd689637808a7b5b3ad58f9c73327fc009b0aa32c7d5 +# pip tifffile @ https://files.pythonhosted.org/packages/c1/79/29d0fa40017f7b749ce344759dcc21e2ec9bbb81fc69ca2ce06e261f83f0/tifffile-2024.5.10-py3-none-any.whl#sha256=4154f091aa24d4e75bfad9ab2d5424a68c70e67b8220188066dc61946d4551bd # pip lightgbm @ https://files.pythonhosted.org/packages/ba/11/cb8b67f3cbdca05b59a032bb57963d4fe8c8d18c3870f30bed005b7f174d/lightgbm-4.3.0-py3-none-manylinux_2_28_x86_64.whl#sha256=104496a3404cb2452d3412cbddcfbfadbef9c372ea91e3a9b8794bcc5183bf07 # pip matplotlib @ https://files.pythonhosted.org/packages/5e/2c/513395a63a9e1124a5648addbf73be23cc603f955af026b04416da98dc96/matplotlib-3.8.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=606e3b90897554c989b1e38a258c626d46c873523de432b1462f295db13de6f9 # pip meson-python @ https://files.pythonhosted.org/packages/91/c0/104cb6244c83fe6bc3886f144cc433db0c0c78efac5dc00e409a5a08c87d/meson_python-0.16.0-py3-none-any.whl#sha256=842dc9f5dc29e55fc769ff1b6fe328412fe6c870220fc321060a1d2d395e69e8 diff --git a/build_tools/azure/pymin_conda_defaults_openblas_linux-64_conda.lock b/build_tools/azure/pymin_conda_defaults_openblas_linux-64_conda.lock index ff7bcd028c7f6..6e46719df47c4 100644 --- a/build_tools/azure/pymin_conda_defaults_openblas_linux-64_conda.lock +++ b/build_tools/azure/pymin_conda_defaults_openblas_linux-64_conda.lock @@ -39,7 +39,7 @@ https://repo.anaconda.com/pkgs/main/linux-64/libpng-1.6.39-h5eee18b_0.conda#f6ae https://repo.anaconda.com/pkgs/main/linux-64/libxml2-2.10.4-hfdd30dd_2.conda#ff7a0e3b92afb3c99b82c9f0ba8b5670 https://repo.anaconda.com/pkgs/main/linux-64/pcre2-10.42-hebb0a14_1.conda#727e15c3cfa02b032da4eb0c1123e977 https://repo.anaconda.com/pkgs/main/linux-64/readline-8.2-h5eee18b_0.conda#be42180685cce6e6b0329201d9f48efb -https://repo.anaconda.com/pkgs/main/linux-64/tk-8.6.12-h1ccaba5_0.conda#fa10ff4aa631fa4aa090a6234d7770b9 +https://repo.anaconda.com/pkgs/main/linux-64/tk-8.6.14-h39e8969_0.conda#78dbc5e3c69143ebc037fc5d5b22e597 https://repo.anaconda.com/pkgs/main/linux-64/zstd-1.5.5-hc292b87_2.conda#3b7fe809e5b429b4f90fe064842a2370 https://repo.anaconda.com/pkgs/main/linux-64/freetype-2.12.1-h4a9f257_0.conda#bdc7b5952e9c5dca01bc2f4ccef2f974 https://repo.anaconda.com/pkgs/main/linux-64/krb5-1.20.1-h143b758_1.conda#cf1accc86321fa25d6b978cc748039ae @@ -55,7 +55,7 @@ https://repo.anaconda.com/pkgs/main/linux-64/lcms2-2.12-h3be6417_0.conda#719db47 https://repo.anaconda.com/pkgs/main/linux-64/libclang-14.0.6-default_hc6dbbc7_1.conda#8f12583c4027b2861cff470f6b8837c4 https://repo.anaconda.com/pkgs/main/linux-64/libpq-12.17-hdbd6064_0.conda#6bed363e25859faff66bf546a11c10e8 https://repo.anaconda.com/pkgs/main/linux-64/openjpeg-2.4.0-h3ad879b_0.conda#86baecb47ecaa7f7ff2657a1f03b90c9 -https://repo.anaconda.com/pkgs/main/linux-64/python-3.9.19-h955ad1f_0.conda#33cb019c40e3409df392c99e3c34f352 +https://repo.anaconda.com/pkgs/main/linux-64/python-3.9.19-h955ad1f_1.conda#4b453281859c293c9d577271f3b18a0d https://repo.anaconda.com/pkgs/main/linux-64/certifi-2024.2.2-py39h06a4308_0.conda#2bc1db9166ecbb968f61252e6f08c2ce https://repo.anaconda.com/pkgs/main/noarch/cycler-0.11.0-pyhd3eb1b0_0.conda#f5e365d2cdb66d547eb8c3ab93843aab https://repo.anaconda.com/pkgs/main/linux-64/cython-3.0.10-py39h5eee18b_0.conda#1419a658ed2b4d5c3ac1964f33143b64 @@ -66,14 +66,14 @@ https://repo.anaconda.com/pkgs/main/noarch/iniconfig-1.1.1-pyhd3eb1b0_0.tar.bz2# https://repo.anaconda.com/pkgs/main/linux-64/joblib-1.2.0-py39h06a4308_0.conda#ac1f5687d70aa1128cbecb26bc9e559d https://repo.anaconda.com/pkgs/main/linux-64/kiwisolver-1.4.4-py39h6a678d5_0.conda#3d57aedbfbd054ce57fb3c1e4448828c https://repo.anaconda.com/pkgs/main/linux-64/mysql-5.7.24-h721c034_2.conda#dfc19ca2466d275c4c1f73b62c57f37b -https://repo.anaconda.com/pkgs/main/linux-64/numpy-base-1.21.6-py39h375b286_0.conda#4ceaa5d6e6307fe06961d555f78b266f +https://repo.anaconda.com/pkgs/main/linux-64/numpy-base-1.21.6-py39h375b286_1.conda#0061d9193658774ab79fc85d143a94fc https://repo.anaconda.com/pkgs/main/linux-64/packaging-23.2-py39h06a4308_0.conda#b3f88f45f31bde016e49be3e941e5272 https://repo.anaconda.com/pkgs/main/linux-64/pillow-10.3.0-py39h5eee18b_0.conda#b346d6c71267c1553b6c18d3db5fdf6d https://repo.anaconda.com/pkgs/main/linux-64/pluggy-1.0.0-py39h06a4308_1.conda#fb4fed11ed43cf727dbd51883cc1d9fa https://repo.anaconda.com/pkgs/main/linux-64/ply-3.11-py39h06a4308_0.conda#6c89bf6d2fdf6d24126e34cb83fd10f1 https://repo.anaconda.com/pkgs/main/linux-64/pyparsing-3.0.9-py39h06a4308_0.conda#3a0537468e59760404f63b4f04369828 https://repo.anaconda.com/pkgs/main/linux-64/pyqt5-sip-12.13.0-py39h5eee18b_0.conda#256840c3841b52346ea5743be8490ede -https://repo.anaconda.com/pkgs/main/linux-64/setuptools-68.2.2-py39h06a4308_0.conda#5b42cae5548732ae5c167bb1066085de +https://repo.anaconda.com/pkgs/main/linux-64/setuptools-69.5.1-py39h06a4308_0.conda#3eb144d481b39c0fbbced789dd9b76b3 https://repo.anaconda.com/pkgs/main/noarch/six-1.16.0-pyhd3eb1b0_1.conda#34586824d411d36af2fa40e799c172d0 https://repo.anaconda.com/pkgs/main/noarch/toml-0.10.2-pyhd3eb1b0_0.conda#cda05f5f6d8509529d1a2743288d197a https://repo.anaconda.com/pkgs/main/linux-64/tomli-2.0.1-py39h06a4308_0.conda#b06dffe7ddca2645ed72f5116f0a087d @@ -82,10 +82,10 @@ https://repo.anaconda.com/pkgs/main/linux-64/wheel-0.43.0-py39h06a4308_0.conda#4 https://repo.anaconda.com/pkgs/main/linux-64/coverage-7.2.2-py39h5eee18b_0.conda#e9da151b7e1f56be2cb569c65949a1d2 https://repo.anaconda.com/pkgs/main/linux-64/dbus-1.13.18-hb2f20db_0.conda#6a6a6f1391f807847404344489ef6cf4 https://repo.anaconda.com/pkgs/main/linux-64/gstreamer-1.14.1-h5eee18b_1.conda#f2f26e6f869b5d87f41bd059fae47c3e -https://repo.anaconda.com/pkgs/main/linux-64/numpy-1.21.6-py39hac523dd_0.conda#a03c1fe16cf2558bca3838062c334d7d -https://repo.anaconda.com/pkgs/main/linux-64/pip-23.3.1-py39h06a4308_0.conda#685007e3dae59d211620f19926577bd6 +https://repo.anaconda.com/pkgs/main/linux-64/numpy-1.21.6-py39hac523dd_1.conda#f379f92039f666828a193fadd18c9819 +https://repo.anaconda.com/pkgs/main/linux-64/pip-24.0-py39h06a4308_0.conda#7f8ce3af15cfecd12e4dda8c5cef5fb7 https://repo.anaconda.com/pkgs/main/linux-64/pytest-7.4.0-py39h06a4308_0.conda#99d92a7a39f7e615de84f8cc5606c49a -https://repo.anaconda.com/pkgs/main/noarch/python-dateutil-2.8.2-pyhd3eb1b0_0.conda#211ee00320b08a1ac9fea6677649f6c9 +https://repo.anaconda.com/pkgs/main/linux-64/python-dateutil-2.9.0post0-py39h06a4308_0.conda#bb2c65e53e610ec258e03771cd79ad17 https://repo.anaconda.com/pkgs/main/linux-64/sip-6.7.12-py39h6a678d5_0.conda#6988a3e12fcacfedcac523c1e4c3167c https://repo.anaconda.com/pkgs/main/linux-64/gst-plugins-base-1.14.1-h6a678d5_1.conda#afd9cbe949d670d24cc0a007aaec1fe1 https://repo.anaconda.com/pkgs/main/linux-64/matplotlib-base-3.3.4-py39h62a2d02_0.conda#dbab28222c740af8e21a3e5e2882c178 diff --git a/build_tools/azure/pymin_conda_forge_mkl_win-64_conda.lock b/build_tools/azure/pymin_conda_forge_mkl_win-64_conda.lock index 88bc53dd94e1a..d95e56378ae56 100644 --- a/build_tools/azure/pymin_conda_forge_mkl_win-64_conda.lock +++ b/build_tools/azure/pymin_conda_forge_mkl_win-64_conda.lock @@ -28,7 +28,7 @@ https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.45.3-hcfcfb64_0.conda# https://conda.anaconda.org/conda-forge/win-64/libwebp-base-1.4.0-hcfcfb64_0.conda#abd61d0ab127ec5cd68f62c2969e6f34 https://conda.anaconda.org/conda-forge/win-64/libzlib-1.2.13-hcfcfb64_5.conda#5fdb9c6a113b6b6cb5e517fd972d5f41 https://conda.anaconda.org/conda-forge/win-64/m2w64-gcc-libgfortran-5.3.0-6.tar.bz2#066552ac6b907ec6d72c0ddab29050dc -https://conda.anaconda.org/conda-forge/win-64/ninja-1.12.0-h91493d7_0.conda#e67ab00f4d2c089864c2b8dcccf4dc58 +https://conda.anaconda.org/conda-forge/win-64/ninja-1.12.1-hc790b64_0.conda#a557dde55343e03c68cd7e29e7f87279 https://conda.anaconda.org/conda-forge/win-64/openssl-3.3.0-hcfcfb64_0.conda#a6c544c9f060740c625dbf6d92cf3495 https://conda.anaconda.org/conda-forge/win-64/pthreads-win32-2.9.1-hfa6e2cd_3.tar.bz2#e2da8758d7d51ff6aa78a14dfb9dbed4 https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h5226925_1.conda#fc048363eb8f03cd1737600a5d08aafe @@ -55,7 +55,7 @@ https://conda.anaconda.org/conda-forge/win-64/freetype-2.12.1-hdaf720e_2.conda#3 https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_0.conda#f800d2da156d08e289b14e87e43c1ae5 https://conda.anaconda.org/conda-forge/win-64/kiwisolver-1.4.5-py39h1f6ef14_1.conda#4fc5bd0a7b535252028c647cc27d6c87 https://conda.anaconda.org/conda-forge/win-64/libclang13-18.1.5-default_hf64faad_0.conda#8a662434c6be1f40e2d5d2506d05a41d -https://conda.anaconda.org/conda-forge/win-64/libglib-2.80.0-h39d0aa6_6.conda#cd5c6efbe213c089f78575c98ab9a0ed +https://conda.anaconda.org/conda-forge/win-64/libglib-2.80.2-h0df6a38_0.conda#ef9ae80bb2a15aee7a30180c057678ea https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.10.0-default_h2fffb23_1000.conda#ee944f0d41d9e2048f9d7492c1623ca3 https://conda.anaconda.org/conda-forge/win-64/libintl-devel-0.22.5-h5728263_2.conda#a2ad82fae23975e4ccbfab2847d31d48 https://conda.anaconda.org/conda-forge/win-64/libtiff-4.6.0-hddb2be6_3.conda#6d1828c9039929e2f185c5fa9d133018 @@ -78,7 +78,7 @@ https://conda.anaconda.org/conda-forge/win-64/xorg-libxdmcp-1.1.3-hcd874cb_0.tar https://conda.anaconda.org/conda-forge/noarch/zipp-3.17.0-pyhd8ed1ab_0.conda#2e4d6bc0b14e10f895fc6791a7d9b26a https://conda.anaconda.org/conda-forge/win-64/brotli-1.1.0-hcfcfb64_1.conda#f47f6db2528e38321fb00ae31674c133 https://conda.anaconda.org/conda-forge/win-64/coverage-7.5.1-py39ha55e580_0.conda#e8f43ea91f0f17d92d5575cfab41a42f -https://conda.anaconda.org/conda-forge/win-64/glib-tools-2.80.0-h0a98069_6.conda#40d452e4012c00f644b1dd6319fcdbcf +https://conda.anaconda.org/conda-forge/win-64/glib-tools-2.80.2-h2f9d560_0.conda#42fc785d9db7ab051a206fbf882ecf2e https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.4.0-pyhd8ed1ab_0.conda#c5d3907ad8bd7bf557521a1833cf7e6d https://conda.anaconda.org/conda-forge/noarch/joblib-1.4.2-pyhd8ed1ab_0.conda#25df261d4523d9f9783bcdb7208d872f https://conda.anaconda.org/conda-forge/win-64/lcms2-2.16-h67d730c_0.conda#d3592435917b62a8becff3a60db674f6 @@ -92,7 +92,7 @@ https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0-pyhd8ed1ab_0 https://conda.anaconda.org/conda-forge/win-64/sip-6.7.12-py39h99910a6_0.conda#0cc5774390ada632ed7975203057c91c https://conda.anaconda.org/conda-forge/win-64/tbb-2021.12.0-h91493d7_0.conda#21745fdd12f01b41178596143cbecffd https://conda.anaconda.org/conda-forge/win-64/fonttools-4.51.0-py39ha55989b_0.conda#5d19302bab29e347116b743e793aa7d6 -https://conda.anaconda.org/conda-forge/win-64/glib-2.80.0-h39d0aa6_6.conda#a4036d0bc6f499ebe9fef7b887f3ca0f +https://conda.anaconda.org/conda-forge/win-64/glib-2.80.2-h0df6a38_0.conda#a728ca6f04c33ecb0f39eeda5fbd0e23 https://conda.anaconda.org/conda-forge/noarch/importlib-resources-6.4.0-pyhd8ed1ab_0.conda#dcbadab7a68738a028e195ab68ab2d2e https://conda.anaconda.org/conda-forge/noarch/meson-python-0.16.0-pyh0c530f3_0.conda#e16f0dbf502da873be9f9adb0dc52547 https://conda.anaconda.org/conda-forge/win-64/mkl-2024.1.0-h66d3029_692.conda#b43ec7ed045323edeff31e348eea8652 diff --git a/build_tools/azure/pymin_conda_forge_openblas_ubuntu_2204_linux-64_conda.lock b/build_tools/azure/pymin_conda_forge_openblas_ubuntu_2204_linux-64_conda.lock index abdaeaee81527..231cd528ecd0e 100644 --- a/build_tools/azure/pymin_conda_forge_openblas_ubuntu_2204_linux-64_conda.lock +++ b/build_tools/azure/pymin_conda_forge_openblas_ubuntu_2204_linux-64_conda.lock @@ -9,13 +9,13 @@ https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed3 https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2#4d59c254e01d9cde7957100457e2d5fb https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_2.conda#cbbe59391138ea5ad3658c76912e147f https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.40-h55db66e_0.conda#10569984e7db886e4f1abc2b47ad79a1 -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-13.2.0-hc0a3c3a_6.conda#2f18345bbc433c8a1ed887d7161e86a6 +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-13.2.0-hc0a3c3a_7.conda#53ebd4c833fa01cb2c6353e99f905406 https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.9-4_cp39.conda#bfe4b3259a8ac6cdf0037752904da6a7 https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda#161081fc7cec0bfda0d86d7cb595f8d8 https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2#f766549260d6815b0c52253f1fb1bb29 https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2#fee5683a3f04bd15cbd8318b096a27ab https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_kmp_llvm.tar.bz2#562b26ba2e19059551a811e72ab7f793 -https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-h77fa898_6.conda#4398809ac84d0b8c28beebaaa83277f5 +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-h77fa898_7.conda#72ec1b1b04c4d15d4204ece1ecea5978 https://conda.anaconda.org/conda-forge/linux-64/alsa-lib-1.2.11-hd590300_1.conda#0bb492cca54017ea314b809b1ee3a176 https://conda.anaconda.org/conda-forge/linux-64/attr-2.5.1-h166bdaf_1.tar.bz2#d9c69a24ad678ffce24c6543a0176b00 https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hd590300_5.conda#69b8b6202a07720f448be700e300ccf4 @@ -31,7 +31,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.20-hd590300_0.conda https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.6.2-h59595ed_0.conda#e7ba12deb7020dd080c6c70e7b6f6a3d https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.2-h7f98852_5.tar.bz2#d645c6d2ac96843a2bfaccd2d62b3ac3 https://conda.anaconda.org/conda-forge/linux-64/libgettextpo-0.22.5-h59595ed_2.conda#172bcc51059416e7ce99e7b528cede83 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-13.2.0-h43f5ff8_6.conda#e54a5ddc67e673f9105cf2a2e9c070b0 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-13.2.0-hca663fb_7.conda#c0bd771f09a326fdcd95a60b617795bf https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.17-hd590300_2.conda#d66573916ffcf376178462f1b61c941e https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.0.0-hd590300_1.conda#ea25936bb4080d843790b586850f82b8 https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda#30fd6e37fe21f86f4bd26d6ee73eeec7 @@ -43,8 +43,8 @@ https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.cond https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.2.13-hd590300_5.conda#f36c115f1ee199da648e0597ec2047ad https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.9.4-hcb278e6_0.conda#318b08df404f9c9be5712aaa5a6f0bb0 https://conda.anaconda.org/conda-forge/linux-64/mpg123-1.32.6-h59595ed_0.conda#9160cdeb523a1b20cf8d2a0bf821f45d -https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.4.20240210-h59595ed_0.conda#97da8860a0da5413c7c98a3b3838a645 -https://conda.anaconda.org/conda-forge/linux-64/ninja-1.12.0-h00ab1b0_0.conda#b048701d52e7cbb5f59ddd4d3b17bbf5 +https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h59595ed_0.conda#fcea371545eda051b6deafb24889fc69 +https://conda.anaconda.org/conda-forge/linux-64/ninja-1.12.1-h297d8ca_0.conda#3aa1c7e292afeff25a0091ddd7c69b72 https://conda.anaconda.org/conda-forge/linux-64/nspr-4.35-h27087fc_0.conda#da0ec11a6454ae19bff5b02ed881a2b1 https://conda.anaconda.org/conda-forge/linux-64/openssl-3.3.0-hd590300_0.conda#c0f3abb4a16477208bbd43a39bd56f18 https://conda.anaconda.org/conda-forge/linux-64/pixman-0.43.2-h59595ed_0.conda#71004cbf7924e19c02746ccde9fd7123 @@ -66,7 +66,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libcap-2.69-h0f662aa_0.conda#25c https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20191231-he28a2e2_2.tar.bz2#4d331e44109e3f0e19b4cb8f9b82f3e1 https://conda.anaconda.org/conda-forge/linux-64/libevent-2.1.12-hf998b51_1.conda#a1cfcc585f0c42bf8d5546bb1dfb668d https://conda.anaconda.org/conda-forge/linux-64/libgettextpo-devel-0.22.5-h59595ed_2.conda#b63d9b6da3653179a278077f0de20014 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran-ng-13.2.0-h69a702a_6.conda#3666a850342f8f3be88f9a93d948d027 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-ng-13.2.0-h69a702a_7.conda#1b84f26d9f4f6026e179e7805d5a15cd https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.43-h2797004_0.conda#009981dd9cfcaa4dbfa25ffaed86bcae https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.45.3-h2797004_0.conda#b3316cbe90249da4f8e84cd66e1cc55b https://conda.anaconda.org/conda-forge/linux-64/libvorbis-1.3.7-h9c3ff4c_0.tar.bz2#309dec04b70a3cc0f1e84a4013683bc0 @@ -83,15 +83,15 @@ https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.1.0-hd590300_1.cond https://conda.anaconda.org/conda-forge/linux-64/freetype-2.12.1-h267a509_2.conda#9ae35c3d96db2c94ce0cef86efdfa2cb https://conda.anaconda.org/conda-forge/linux-64/gettext-0.22.5-h59595ed_2.conda#219ba82e95d7614cf7140d2a4afc0926 https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.2-h659d440_0.conda#cd95826dbd331ed1be26bdf401432844 -https://conda.anaconda.org/conda-forge/linux-64/libglib-2.80.0-hf2295e7_6.conda#9342e7c44c38bea649490f72d92c382d +https://conda.anaconda.org/conda-forge/linux-64/libglib-2.80.2-hf974151_0.conda#72724f6a78ecb15559396966226d5838 https://conda.anaconda.org/conda-forge/linux-64/libhiredis-1.0.2-h2cc385e_0.tar.bz2#b34907d3a81a3cd8095ee83d174c074a https://conda.anaconda.org/conda-forge/linux-64/libllvm15-15.0.7-hb3ce162_4.conda#8a35df3cbc0c8b12cc8af9473ae75eef https://conda.anaconda.org/conda-forge/linux-64/libllvm18-18.1.5-hb77312f_0.conda#efd221d3668077ca067a206269418dec https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.27-pthreads_h413a1c8_0.conda#a356024784da6dfd4683dc5ecf45b155 https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.6.0-h1dd3fc0_3.conda#66f03896ffbe1a110ffda05c7a856504 -https://conda.anaconda.org/conda-forge/linux-64/llvm-openmp-18.1.4-ha31de31_0.conda#48b9991e66abc186a7ad7975e97bd4d0 +https://conda.anaconda.org/conda-forge/linux-64/llvm-openmp-18.1.5-ha31de31_0.conda#b923cdb6e567ada84f991ffcc5848afb https://conda.anaconda.org/conda-forge/linux-64/mysql-libs-8.3.0-hca2cd23_4.conda#1b50eebe2a738a3146c154d2eceaa8b6 -https://conda.anaconda.org/conda-forge/linux-64/nss-3.98-h1d7d5a4_0.conda#54b56c2fdf973656b748e0378900ec13 +https://conda.anaconda.org/conda-forge/linux-64/nss-3.100-hca3bf56_0.conda#949c4a82290ee58b3c970cef4bcfd4ad https://conda.anaconda.org/conda-forge/linux-64/python-3.9.19-h0755675_0_cpython.conda#d9ee3647fbd9e8595b8df759b2bbefb8 https://conda.anaconda.org/conda-forge/linux-64/xcb-util-0.4.0-hd590300_1.conda#9bfac7ccd94d54fd21a0501296d60424 https://conda.anaconda.org/conda-forge/linux-64/xcb-util-keysyms-0.4.0-h8ee46fc_1.conda#632413adcd8bc16b515cab87a2932913 @@ -112,7 +112,7 @@ https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_0.conda https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.0-pyhd8ed1ab_2.conda#8d652ea2ee8eaee02ed8dc820bc794aa https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_0.conda#15dda3cdbf330abfe9f555d22f66db46 https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.14.2-h14ed4e7_0.conda#0f69b688f52ff6da70bccb7ff7001d1d -https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.80.0-hde27a5a_6.conda#a9d23c02485c5cf055f9ac90eb9c9c63 +https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.80.2-hb6ce0ca_0.conda#a965aeaf060289528a3fbe09326edae2 https://conda.anaconda.org/conda-forge/noarch/idna-3.7-pyhd8ed1ab_0.conda#c0cc1420498b17414d8617d0b9f506ca https://conda.anaconda.org/conda-forge/noarch/imagesize-1.4.1-pyhd8ed1ab_0.tar.bz2#7de5386c8fea29e76b303f37dde4c352 https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_0.conda#f800d2da156d08e289b14e87e43c1ae5 @@ -124,7 +124,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libclang13-18.1.5-default_h5d682 https://conda.anaconda.org/conda-forge/linux-64/libcups-2.3.3-h4637d8d_4.conda#d4529f4dff3057982a7617c7ac58fde3 https://conda.anaconda.org/conda-forge/linux-64/libflac-1.4.3-h59595ed_0.conda#ee48bf17cc83a00f59ca1494d5646869 https://conda.anaconda.org/conda-forge/linux-64/libgpg-error-1.49-h4f305b6_0.conda#dfcfd72c7a430d3616763ecfbefe4ca9 -https://conda.anaconda.org/conda-forge/linux-64/libpq-16.2-h33b98f1_1.conda#9e49ec2a61d02623b379dc332eb6889d +https://conda.anaconda.org/conda-forge/linux-64/libpq-16.3-ha72fbe1_0.conda#bac737ae28b79cfbafd515258d97d29e https://conda.anaconda.org/conda-forge/linux-64/markupsafe-2.1.5-py39hd1e30aa_0.conda#9a9a22eb1f83c44953319ee3b027769f https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyh9f0ad1d_0.tar.bz2#2ba8498c1018c1e9c61eb99b973dfe19 https://conda.anaconda.org/conda-forge/linux-64/openblas-0.3.27-pthreads_h7a3da1a_0.conda#4b422ebe8fc6a5320d0c1c22e5a46032 @@ -156,10 +156,10 @@ https://conda.anaconda.org/conda-forge/noarch/zipp-3.17.0-pyhd8ed1ab_0.conda#2e4 https://conda.anaconda.org/conda-forge/noarch/babel-2.14.0-pyhd8ed1ab_0.conda#9669586875baeced8fc30c0826c3270e https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.0-h3faef2a_0.conda#f907bb958910dc404647326ca80c263e https://conda.anaconda.org/conda-forge/linux-64/fonttools-4.51.0-py39hd1e30aa_0.conda#79f5dd8778873faa54e8f7b2729fe8a6 -https://conda.anaconda.org/conda-forge/linux-64/glib-2.80.0-hf2295e7_6.conda#a1e026a82a562b443845db5614ca568a +https://conda.anaconda.org/conda-forge/linux-64/glib-2.80.2-hf974151_0.conda#d427988dc3dbd0a4c136f52db356cc6a https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-7.1.0-pyha770c72_0.conda#0896606848b2dc5cebdf111b6543aa04 https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.4.0-pyhd8ed1ab_0.conda#c5d3907ad8bd7bf557521a1833cf7e6d -https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.3-pyhd8ed1ab_0.conda#e7d8df6509ba635247ff9aea31134262 +https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.4-pyhd8ed1ab_0.conda#7b86ecb7d3557821c649b3c31e3eb9f2 https://conda.anaconda.org/conda-forge/noarch/joblib-1.4.2-pyhd8ed1ab_0.conda#25df261d4523d9f9783bcdb7208d872f https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-22_linux64_openblas.conda#4b31699e0ec5de64d5896e580389c9a1 https://conda.anaconda.org/conda-forge/linux-64/libgcrypt-1.10.3-hd590300_0.conda#32d16ad533c59bb0a3c5ffaf16110829 diff --git a/build_tools/circle/doc_linux-64_conda.lock b/build_tools/circle/doc_linux-64_conda.lock index 7ca02c7cdb159..e2584c2d27333 100644 --- a/build_tools/circle/doc_linux-64_conda.lock +++ b/build_tools/circle/doc_linux-64_conda.lock @@ -10,22 +10,22 @@ https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77 https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_2.conda#cbbe59391138ea5ad3658c76912e147f https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-2.6.32-he073ed8_17.conda#d731b543793afc0433c4fd593e693fce https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.40-h55db66e_0.conda#10569984e7db886e4f1abc2b47ad79a1 -https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-12.3.0-h0223996_106.conda#304f58c690e7ba23b67a4b5c8e99a062 -https://conda.anaconda.org/conda-forge/noarch/libstdcxx-devel_linux-64-12.3.0-h0223996_106.conda#dfb9aac785d6b25b46be7850d974a72e -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-13.2.0-hc0a3c3a_6.conda#2f18345bbc433c8a1ed887d7161e86a6 +https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-12.3.0-h0223996_107.conda#851e9651c9e4cd5dc19f80398eba9a1c +https://conda.anaconda.org/conda-forge/noarch/libstdcxx-devel_linux-64-12.3.0-h0223996_107.conda#167a1f5d77d8f3c2a638f7eb418429f1 +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-13.2.0-hc0a3c3a_7.conda#53ebd4c833fa01cb2c6353e99f905406 https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.9-4_cp39.conda#bfe4b3259a8ac6cdf0037752904da6a7 https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda#161081fc7cec0bfda0d86d7cb595f8d8 https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2#f766549260d6815b0c52253f1fb1bb29 -https://conda.anaconda.org/conda-forge/linux-64/libgomp-13.2.0-h77fa898_6.conda#e733e0573651a1f0639fa8ce066a286e +https://conda.anaconda.org/conda-forge/linux-64/libgomp-13.2.0-h77fa898_7.conda#abf3fec87c2563697defa759dec3d639 https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.12-he073ed8_17.conda#595db67e32b276298ff3d94d07d47fbf https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.40-ha885e6a_0.conda#800a4c872b5bc06fa83888d112fe6c4f https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2#fee5683a3f04bd15cbd8318b096a27ab https://conda.anaconda.org/conda-forge/linux-64/binutils-2.40-h4852527_0.conda#a05c7712be80622934f7011e0a1d43fc https://conda.anaconda.org/conda-forge/linux-64/binutils_linux-64-2.40-hdade7a5_3.conda#2d9a60578bc28469d9aeef9aea5520c3 https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_kmp_llvm.tar.bz2#562b26ba2e19059551a811e72ab7f793 -https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-h77fa898_6.conda#4398809ac84d0b8c28beebaaa83277f5 +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-h77fa898_7.conda#72ec1b1b04c4d15d4204ece1ecea5978 https://conda.anaconda.org/conda-forge/linux-64/alsa-lib-1.2.11-hd590300_1.conda#0bb492cca54017ea314b809b1ee3a176 -https://conda.anaconda.org/conda-forge/linux-64/aom-3.8.2-h59595ed_0.conda#625e1fed28a5139aed71b3a76117ef84 +https://conda.anaconda.org/conda-forge/linux-64/aom-3.9.0-hac33072_0.conda#93a3bf248e5bc729807db198a9c89f07 https://conda.anaconda.org/conda-forge/linux-64/attr-2.5.1-h166bdaf_1.tar.bz2#d9c69a24ad678ffce24c6543a0176b00 https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hd590300_5.conda#69b8b6202a07720f448be700e300ccf4 https://conda.anaconda.org/conda-forge/linux-64/charls-2.4.2-h59595ed_0.conda#4336bd67920dd504cd8c6761d6a99645 @@ -45,14 +45,14 @@ https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.20-hd590300_0.conda https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.6.2-h59595ed_0.conda#e7ba12deb7020dd080c6c70e7b6f6a3d https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.2-h7f98852_5.tar.bz2#d645c6d2ac96843a2bfaccd2d62b3ac3 https://conda.anaconda.org/conda-forge/linux-64/libgettextpo-0.22.5-h59595ed_2.conda#172bcc51059416e7ce99e7b528cede83 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-13.2.0-h43f5ff8_6.conda#e54a5ddc67e673f9105cf2a2e9c070b0 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-13.2.0-hca663fb_7.conda#c0bd771f09a326fdcd95a60b617795bf https://conda.anaconda.org/conda-forge/linux-64/libhwy-1.1.0-h00ab1b0_0.conda#88928158ccfe797eac29ef5e03f7d23d https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.17-hd590300_2.conda#d66573916ffcf376178462f1b61c941e https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.0.0-hd590300_1.conda#ea25936bb4080d843790b586850f82b8 https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda#30fd6e37fe21f86f4bd26d6ee73eeec7 https://conda.anaconda.org/conda-forge/linux-64/libogg-1.3.4-h7f98852_1.tar.bz2#6e8cc2173440d77708196c5b93771680 https://conda.anaconda.org/conda-forge/linux-64/libopus-1.3.1-h7f98852_1.tar.bz2#15345e56d527b330e1cacbdf58676e8f -https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-12.3.0-hb8811af_6.conda#a9a764e2e753ed038da59343560d8a66 +https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-12.3.0-hb8811af_7.conda#ee573415c47ce17f65101d0b3fba396d https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda#40b61aab5c7ba9ff276c41cfffe6b80b https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.4.0-hd590300_0.conda#b26e8aa824079e1be0294e7152ca4559 https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda#5aa797f8787fe7a17d1b0821485b5adc @@ -60,8 +60,8 @@ https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.2.13-hd590300_5.conda# https://conda.anaconda.org/conda-forge/linux-64/libzopfli-1.0.3-h9c3ff4c_0.tar.bz2#c66fe2d123249af7651ebde8984c51c2 https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.9.4-hcb278e6_0.conda#318b08df404f9c9be5712aaa5a6f0bb0 https://conda.anaconda.org/conda-forge/linux-64/mpg123-1.32.6-h59595ed_0.conda#9160cdeb523a1b20cf8d2a0bf821f45d -https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.4.20240210-h59595ed_0.conda#97da8860a0da5413c7c98a3b3838a645 -https://conda.anaconda.org/conda-forge/linux-64/ninja-1.12.0-h00ab1b0_0.conda#b048701d52e7cbb5f59ddd4d3b17bbf5 +https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h59595ed_0.conda#fcea371545eda051b6deafb24889fc69 +https://conda.anaconda.org/conda-forge/linux-64/ninja-1.12.1-h297d8ca_0.conda#3aa1c7e292afeff25a0091ddd7c69b72 https://conda.anaconda.org/conda-forge/linux-64/nspr-4.35-h27087fc_0.conda#da0ec11a6454ae19bff5b02ed881a2b1 https://conda.anaconda.org/conda-forge/linux-64/openssl-3.3.0-hd590300_0.conda#c0f3abb4a16477208bbd43a39bd56f18 https://conda.anaconda.org/conda-forge/linux-64/pixman-0.43.2-h59595ed_0.conda#71004cbf7924e19c02746ccde9fd7123 @@ -81,16 +81,16 @@ https://conda.anaconda.org/conda-forge/linux-64/xz-5.2.6-h166bdaf_0.tar.bz2#2161 https://conda.anaconda.org/conda-forge/linux-64/zfp-1.0.1-h59595ed_0.conda#fd486bffbf0d6841cf1456a8f2e3a995 https://conda.anaconda.org/conda-forge/linux-64/zlib-ng-2.0.7-h0b41bf4_0.conda#49e8329110001f04923fe7e864990b0c https://conda.anaconda.org/conda-forge/linux-64/expat-2.6.2-h59595ed_0.conda#53fb86322bdb89496d7579fe3f02fd61 -https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-12.3.0-h58ffeeb_6.conda#53914a98926ce169b83726cb78366a6c +https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-12.3.0-h58ffeeb_7.conda#95f78565a09852783d3e90e0389cfa5f https://conda.anaconda.org/conda-forge/linux-64/libasprintf-devel-0.22.5-h661eb56_2.conda#02e41ab5834dcdcc8590cf29d9526f50 -https://conda.anaconda.org/conda-forge/linux-64/libavif16-1.0.4-hd9d6309_2.conda#a8c65cba5f77abc1f2e85ab9a0e614aa +https://conda.anaconda.org/conda-forge/linux-64/libavif16-1.0.4-hfa3d5b6_3.conda#3518d00de414c39b46d87dcc1ff65661 https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.1.0-hd590300_1.conda#f07002e225d7a60a694d42a7bf5ff53f https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.1.0-hd590300_1.conda#5fc11c6020d421960607d821310fcd4d https://conda.anaconda.org/conda-forge/linux-64/libcap-2.69-h0f662aa_0.conda#25cb5999faa414e5ccb2c1388f62d3d5 https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20191231-he28a2e2_2.tar.bz2#4d331e44109e3f0e19b4cb8f9b82f3e1 https://conda.anaconda.org/conda-forge/linux-64/libevent-2.1.12-hf998b51_1.conda#a1cfcc585f0c42bf8d5546bb1dfb668d https://conda.anaconda.org/conda-forge/linux-64/libgettextpo-devel-0.22.5-h59595ed_2.conda#b63d9b6da3653179a278077f0de20014 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran-ng-13.2.0-h69a702a_6.conda#3666a850342f8f3be88f9a93d948d027 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-ng-13.2.0-h69a702a_7.conda#1b84f26d9f4f6026e179e7805d5a15cd https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.43-h2797004_0.conda#009981dd9cfcaa4dbfa25ffaed86bcae https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.45.3-h2797004_0.conda#b3316cbe90249da4f8e84cd66e1cc55b https://conda.anaconda.org/conda-forge/linux-64/libvorbis-1.3.7-h9c3ff4c_0.tar.bz2#309dec04b70a3cc0f1e84a4013683bc0 @@ -107,21 +107,21 @@ https://conda.anaconda.org/conda-forge/linux-64/blosc-1.21.5-hc2324a3_1.conda#11 https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.1.0-hd590300_1.conda#39f910d205726805a958da408ca194ba https://conda.anaconda.org/conda-forge/linux-64/c-blosc2-2.14.4-hb4ffafa_1.conda#84eb54e92644c328e087e1c725773317 https://conda.anaconda.org/conda-forge/linux-64/freetype-2.12.1-h267a509_2.conda#9ae35c3d96db2c94ce0cef86efdfa2cb -https://conda.anaconda.org/conda-forge/linux-64/gcc-12.3.0-h915e2ae_6.conda#ec683e084ea08ef94528f15d30fa1e03 +https://conda.anaconda.org/conda-forge/linux-64/gcc-12.3.0-h915e2ae_7.conda#84b1c5cebd0a0443f3d7f90a4be93fc6 https://conda.anaconda.org/conda-forge/linux-64/gcc_linux-64-12.3.0-h6477408_3.conda#7a53f84c45bdf4656ba27b9e9ed68b3d https://conda.anaconda.org/conda-forge/linux-64/gettext-0.22.5-h59595ed_2.conda#219ba82e95d7614cf7140d2a4afc0926 -https://conda.anaconda.org/conda-forge/linux-64/gfortran_impl_linux-64-12.3.0-h1645026_6.conda#664d4e904674f1173752580ffdc24d46 -https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-12.3.0-h2a574ab_6.conda#aab48c86452d78a416992deeee901a52 +https://conda.anaconda.org/conda-forge/linux-64/gfortran_impl_linux-64-12.3.0-h1645026_7.conda#2d9d4058c433c9ce2a811c76658c4efd +https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-12.3.0-h2a574ab_7.conda#265caa78b979f112fc241cecd0015c91 https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.2-h659d440_0.conda#cd95826dbd331ed1be26bdf401432844 -https://conda.anaconda.org/conda-forge/linux-64/libglib-2.80.0-hf2295e7_6.conda#9342e7c44c38bea649490f72d92c382d +https://conda.anaconda.org/conda-forge/linux-64/libglib-2.80.2-hf974151_0.conda#72724f6a78ecb15559396966226d5838 https://conda.anaconda.org/conda-forge/linux-64/libjxl-0.10.2-hcae5a98_0.conda#901db891e1e21afd8524cd636a8c8e3b https://conda.anaconda.org/conda-forge/linux-64/libllvm15-15.0.7-hb3ce162_4.conda#8a35df3cbc0c8b12cc8af9473ae75eef https://conda.anaconda.org/conda-forge/linux-64/libllvm18-18.1.5-hb77312f_0.conda#efd221d3668077ca067a206269418dec https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.27-pthreads_h413a1c8_0.conda#a356024784da6dfd4683dc5ecf45b155 https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.6.0-h1dd3fc0_3.conda#66f03896ffbe1a110ffda05c7a856504 -https://conda.anaconda.org/conda-forge/linux-64/llvm-openmp-18.1.4-ha31de31_0.conda#48b9991e66abc186a7ad7975e97bd4d0 +https://conda.anaconda.org/conda-forge/linux-64/llvm-openmp-18.1.5-ha31de31_0.conda#b923cdb6e567ada84f991ffcc5848afb https://conda.anaconda.org/conda-forge/linux-64/mysql-libs-8.3.0-hca2cd23_4.conda#1b50eebe2a738a3146c154d2eceaa8b6 -https://conda.anaconda.org/conda-forge/linux-64/nss-3.98-h1d7d5a4_0.conda#54b56c2fdf973656b748e0378900ec13 +https://conda.anaconda.org/conda-forge/linux-64/nss-3.100-hca3bf56_0.conda#949c4a82290ee58b3c970cef4bcfd4ad https://conda.anaconda.org/conda-forge/linux-64/python-3.9.19-h0755675_0_cpython.conda#d9ee3647fbd9e8595b8df759b2bbefb8 https://conda.anaconda.org/conda-forge/linux-64/xcb-util-0.4.0-hd590300_1.conda#9bfac7ccd94d54fd21a0501296d60424 https://conda.anaconda.org/conda-forge/linux-64/xcb-util-keysyms-0.4.0-h8ee46fc_1.conda#632413adcd8bc16b515cab87a2932913 @@ -142,10 +142,10 @@ https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_0.conda https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.0-pyhd8ed1ab_2.conda#8d652ea2ee8eaee02ed8dc820bc794aa https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_0.conda#15dda3cdbf330abfe9f555d22f66db46 https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.14.2-h14ed4e7_0.conda#0f69b688f52ff6da70bccb7ff7001d1d -https://conda.anaconda.org/conda-forge/linux-64/gfortran-12.3.0-h915e2ae_6.conda#84b517f4f53e56256dbd65133aae04ac +https://conda.anaconda.org/conda-forge/linux-64/gfortran-12.3.0-h915e2ae_7.conda#8efa768f7f74085629f3e1090e7f0569 https://conda.anaconda.org/conda-forge/linux-64/gfortran_linux-64-12.3.0-h617cb40_3.conda#3a9e5b8a6f651ff14e74d896d8f04ab6 -https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.80.0-hde27a5a_6.conda#a9d23c02485c5cf055f9ac90eb9c9c63 -https://conda.anaconda.org/conda-forge/linux-64/gxx-12.3.0-h915e2ae_6.conda#0d977804df65082e17c860600ca2894b +https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.80.2-hb6ce0ca_0.conda#a965aeaf060289528a3fbe09326edae2 +https://conda.anaconda.org/conda-forge/linux-64/gxx-12.3.0-h915e2ae_7.conda#721c5433122a02bf3a081db10a2e68e2 https://conda.anaconda.org/conda-forge/linux-64/gxx_linux-64-12.3.0-h4a1b8e8_3.conda#9ec22c7c544f4a4f6d660f0a3b0fd15c https://conda.anaconda.org/conda-forge/noarch/idna-3.7-pyhd8ed1ab_0.conda#c0cc1420498b17414d8617d0b9f506ca https://conda.anaconda.org/conda-forge/noarch/imagesize-1.4.1-pyhd8ed1ab_0.tar.bz2#7de5386c8fea29e76b303f37dde4c352 @@ -158,7 +158,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libclang13-18.1.5-default_h5d682 https://conda.anaconda.org/conda-forge/linux-64/libcups-2.3.3-h4637d8d_4.conda#d4529f4dff3057982a7617c7ac58fde3 https://conda.anaconda.org/conda-forge/linux-64/libflac-1.4.3-h59595ed_0.conda#ee48bf17cc83a00f59ca1494d5646869 https://conda.anaconda.org/conda-forge/linux-64/libgpg-error-1.49-h4f305b6_0.conda#dfcfd72c7a430d3616763ecfbefe4ca9 -https://conda.anaconda.org/conda-forge/linux-64/libpq-16.2-h33b98f1_1.conda#9e49ec2a61d02623b379dc332eb6889d +https://conda.anaconda.org/conda-forge/linux-64/libpq-16.3-ha72fbe1_0.conda#bac737ae28b79cfbafd515258d97d29e https://conda.anaconda.org/conda-forge/linux-64/markupsafe-2.1.5-py39hd1e30aa_0.conda#9a9a22eb1f83c44953319ee3b027769f https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyh9f0ad1d_0.tar.bz2#2ba8498c1018c1e9c61eb99b973dfe19 https://conda.anaconda.org/conda-forge/noarch/networkx-3.2.1-pyhd8ed1ab_0.conda#425fce3b531bed6ec3c74fab3e5f0a1c @@ -179,7 +179,7 @@ https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2#e5 https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-2.2.0-pyhd8ed1ab_0.tar.bz2#4d22a9315e78c6827f806065957d566e https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_0.conda#da1d979339e2714c30a8e806a33ec087 https://conda.anaconda.org/conda-forge/noarch/tabulate-0.9.0-pyhd8ed1ab_1.tar.bz2#4759805cce2d914c38472f70bf4d8bcb -https://conda.anaconda.org/conda-forge/noarch/tenacity-8.2.3-pyhd8ed1ab_0.conda#1482e77f87c6a702a7e05ef22c9b197b +https://conda.anaconda.org/conda-forge/noarch/tenacity-8.3.0-pyhd8ed1ab_0.conda#216cfa8e32bcd1447646768351df6059 https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.5.0-pyhc1e730c_0.conda#df68d78237980a159bd7149f33c0e8fd https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_0.tar.bz2#f832c45a477c78bebd107098db465095 https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2#5844808ffab9ebdb694585b50ba02a96 @@ -198,10 +198,10 @@ https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.0-h3faef2a_0.conda#f9 https://conda.anaconda.org/conda-forge/linux-64/cxx-compiler-1.7.0-h00ab1b0_1.conda#28de2e073db9ca9b72858bee9fb6f571 https://conda.anaconda.org/conda-forge/linux-64/fonttools-4.51.0-py39hd1e30aa_0.conda#79f5dd8778873faa54e8f7b2729fe8a6 https://conda.anaconda.org/conda-forge/linux-64/fortran-compiler-1.7.0-heb67821_1.conda#cf4b0e7c4c78bb0662aed9b27c414a3c -https://conda.anaconda.org/conda-forge/linux-64/glib-2.80.0-hf2295e7_6.conda#a1e026a82a562b443845db5614ca568a +https://conda.anaconda.org/conda-forge/linux-64/glib-2.80.2-hf974151_0.conda#d427988dc3dbd0a4c136f52db356cc6a https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-7.1.0-pyha770c72_0.conda#0896606848b2dc5cebdf111b6543aa04 https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.4.0-pyhd8ed1ab_0.conda#c5d3907ad8bd7bf557521a1833cf7e6d -https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.3-pyhd8ed1ab_0.conda#e7d8df6509ba635247ff9aea31134262 +https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.4-pyhd8ed1ab_0.conda#7b86ecb7d3557821c649b3c31e3eb9f2 https://conda.anaconda.org/conda-forge/noarch/joblib-1.4.2-pyhd8ed1ab_0.conda#25df261d4523d9f9783bcdb7208d872f https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-22_linux64_openblas.conda#4b31699e0ec5de64d5896e580389c9a1 https://conda.anaconda.org/conda-forge/linux-64/libgcrypt-1.10.3-hd590300_0.conda#32d16ad533c59bb0a3c5ffaf16110829 @@ -237,7 +237,7 @@ https://conda.anaconda.org/conda-forge/linux-64/imagecodecs-2024.1.1-py39ha98d97 https://conda.anaconda.org/conda-forge/noarch/imageio-2.34.1-pyh4b66e23_0.conda#bcf6a6f4c6889ca083e8d33afbafb8d5 https://conda.anaconda.org/conda-forge/linux-64/pandas-2.2.2-py39hddac248_0.conda#259c4e76e6bda8888aefc098ae1ba749 https://conda.anaconda.org/conda-forge/noarch/patsy-0.5.6-pyhd8ed1ab_0.conda#a5b55d1cb110cdcedc748b5c3e16e687 -https://conda.anaconda.org/conda-forge/linux-64/polars-0.20.23-py39ha963410_0.conda#4871f09d653e979d598d2d4cd5fa868d +https://conda.anaconda.org/conda-forge/linux-64/polars-0.20.25-py39ha963410_0.conda#d14227f0e141af743374d845fd4f5ccd https://conda.anaconda.org/conda-forge/noarch/pooch-1.8.1-pyhd8ed1ab_0.conda#d15917f33140f8d2ac9ca44db7ec8a25 https://conda.anaconda.org/conda-forge/linux-64/pulseaudio-client-17.0-hb77b528_0.conda#07f45f1be1c25345faddb8db0de8039b https://conda.anaconda.org/conda-forge/linux-64/pywavelets-1.4.1-py39h44dd56e_1.conda#d037c20e3da2e85f03ebd20ad480c359 @@ -247,7 +247,7 @@ https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.8.4-py39he9076 https://conda.anaconda.org/conda-forge/linux-64/pyamg-5.1.0-py39hda80f44_0.conda#f225666c47726329201b604060f1436c https://conda.anaconda.org/conda-forge/linux-64/qt-main-5.15.8-hc9dc06e_21.conda#b325046180590c868ce0dbf267b82eb8 https://conda.anaconda.org/conda-forge/linux-64/statsmodels-0.14.1-py39h44dd56e_0.conda#dc565186b972bd87e49b9c35390ddd8c -https://conda.anaconda.org/conda-forge/noarch/tifffile-2024.5.3-pyhd8ed1ab_0.conda#0658fd78a808b6f3508917ba66b20f75 +https://conda.anaconda.org/conda-forge/noarch/tifffile-2024.5.10-pyhd8ed1ab_0.conda#125438a8b679e4c08ee8f244177216c9 https://conda.anaconda.org/conda-forge/linux-64/pyqt-5.15.9-py39h52134e7_5.conda#e1f148e57d071b09187719df86f513c1 https://conda.anaconda.org/conda-forge/linux-64/scikit-image-0.22.0-py39hddac248_2.conda#8d502a4d2cbe5a45ff35ca8af8cbec0a https://conda.anaconda.org/conda-forge/noarch/seaborn-base-0.13.2-pyhd8ed1ab_2.conda#b713b116feaf98acdba93ad4d7f90ca1 @@ -282,7 +282,7 @@ https://conda.anaconda.org/conda-forge/noarch/sphinxext-opengraph-0.9.1-pyhd8ed1 # pip python-json-logger @ https://files.pythonhosted.org/packages/35/a6/145655273568ee78a581e734cf35beb9e33a370b29c5d3c8fee3744de29f/python_json_logger-2.0.7-py3-none-any.whl#sha256=f380b826a991ebbe3de4d897aeec42760035ac760345e57b812938dc8b35e2bd # pip pyyaml @ https://files.pythonhosted.org/packages/7d/39/472f2554a0f1e825bd7c5afc11c817cd7a2f3657460f7159f691fbb37c51/PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c # pip rfc3986-validator @ https://files.pythonhosted.org/packages/9e/51/17023c0f8f1869d8806b979a2bffa3f861f26a3f1a66b094288323fba52f/rfc3986_validator-0.1.1-py2.py3-none-any.whl#sha256=2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9 -# pip rpds-py @ https://files.pythonhosted.org/packages/fd/ea/92231b62681961812e9fbd8ef9be7137856784406bf6a384976bb7b46472/rpds_py-0.18.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=ddc2f4dfd396c7bfa18e6ce371cba60e4cf9d2e5cdb71376aa2da264605b60b9 +# pip rpds-py @ https://files.pythonhosted.org/packages/97/b1/12238bd8cdf3cef71e85188af133399bfde1bddf319007361cc869d6f6a7/rpds_py-0.18.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=e4c39ad2f512b4041343ea3c7894339e4ca7839ac38ca83d68a832fc8b3748ab # pip send2trash @ https://files.pythonhosted.org/packages/40/b0/4562db6223154aa4e22f939003cb92514c79f3d4dccca3444253fd17f902/Send2Trash-1.8.3-py3-none-any.whl#sha256=0c31227e0bd08961c7665474a3d1ef7193929fedda4233843689baa056be46c9 # pip sniffio @ https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl#sha256=2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 # pip soupsieve @ https://files.pythonhosted.org/packages/4c/f3/038b302fdfbe3be7da016777069f26ceefe11a681055ea1f7817546508e3/soupsieve-2.5-py3-none-any.whl#sha256=eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7 diff --git a/build_tools/circle/doc_min_dependencies_linux-64_conda.lock b/build_tools/circle/doc_min_dependencies_linux-64_conda.lock index dd291f8882efb..e08a14c235079 100644 --- a/build_tools/circle/doc_min_dependencies_linux-64_conda.lock +++ b/build_tools/circle/doc_min_dependencies_linux-64_conda.lock @@ -10,21 +10,21 @@ https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77 https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_2.conda#cbbe59391138ea5ad3658c76912e147f https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-2.6.32-he073ed8_17.conda#d731b543793afc0433c4fd593e693fce https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.40-h55db66e_0.conda#10569984e7db886e4f1abc2b47ad79a1 -https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-12.3.0-h0223996_106.conda#304f58c690e7ba23b67a4b5c8e99a062 -https://conda.anaconda.org/conda-forge/noarch/libstdcxx-devel_linux-64-12.3.0-h0223996_106.conda#dfb9aac785d6b25b46be7850d974a72e -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-13.2.0-hc0a3c3a_6.conda#2f18345bbc433c8a1ed887d7161e86a6 +https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-12.3.0-h0223996_107.conda#851e9651c9e4cd5dc19f80398eba9a1c +https://conda.anaconda.org/conda-forge/noarch/libstdcxx-devel_linux-64-12.3.0-h0223996_107.conda#167a1f5d77d8f3c2a638f7eb418429f1 +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-13.2.0-hc0a3c3a_7.conda#53ebd4c833fa01cb2c6353e99f905406 https://conda.anaconda.org/conda-forge/linux-64/mkl-include-2024.1.0-ha957f24_692.conda#b35af3f0f25498f4e9fc4c471910346c https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.9-4_cp39.conda#bfe4b3259a8ac6cdf0037752904da6a7 https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda#161081fc7cec0bfda0d86d7cb595f8d8 https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2#f766549260d6815b0c52253f1fb1bb29 -https://conda.anaconda.org/conda-forge/linux-64/libgomp-13.2.0-h77fa898_6.conda#e733e0573651a1f0639fa8ce066a286e +https://conda.anaconda.org/conda-forge/linux-64/libgomp-13.2.0-h77fa898_7.conda#abf3fec87c2563697defa759dec3d639 https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.12-he073ed8_17.conda#595db67e32b276298ff3d94d07d47fbf https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.40-ha885e6a_0.conda#800a4c872b5bc06fa83888d112fe6c4f https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2#fee5683a3f04bd15cbd8318b096a27ab https://conda.anaconda.org/conda-forge/linux-64/binutils-2.40-h4852527_0.conda#a05c7712be80622934f7011e0a1d43fc https://conda.anaconda.org/conda-forge/linux-64/binutils_linux-64-2.40-hdade7a5_3.conda#2d9a60578bc28469d9aeef9aea5520c3 https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_kmp_llvm.tar.bz2#562b26ba2e19059551a811e72ab7f793 -https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-h77fa898_6.conda#4398809ac84d0b8c28beebaaa83277f5 +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-h77fa898_7.conda#72ec1b1b04c4d15d4204ece1ecea5978 https://conda.anaconda.org/conda-forge/linux-64/alsa-lib-1.2.11-hd590300_1.conda#0bb492cca54017ea314b809b1ee3a176 https://conda.anaconda.org/conda-forge/linux-64/attr-2.5.1-h166bdaf_1.tar.bz2#d9c69a24ad678ffce24c6543a0176b00 https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hd590300_5.conda#69b8b6202a07720f448be700e300ccf4 @@ -39,21 +39,21 @@ https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.20-hd590300_0.conda https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.6.2-h59595ed_0.conda#e7ba12deb7020dd080c6c70e7b6f6a3d https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.2-h7f98852_5.tar.bz2#d645c6d2ac96843a2bfaccd2d62b3ac3 https://conda.anaconda.org/conda-forge/linux-64/libgettextpo-0.22.5-h59595ed_2.conda#172bcc51059416e7ce99e7b528cede83 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-13.2.0-h43f5ff8_6.conda#e54a5ddc67e673f9105cf2a2e9c070b0 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-13.2.0-hca663fb_7.conda#c0bd771f09a326fdcd95a60b617795bf https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.17-hd590300_2.conda#d66573916ffcf376178462f1b61c941e https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.0.0-hd590300_1.conda#ea25936bb4080d843790b586850f82b8 https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda#30fd6e37fe21f86f4bd26d6ee73eeec7 https://conda.anaconda.org/conda-forge/linux-64/libogg-1.3.4-h7f98852_1.tar.bz2#6e8cc2173440d77708196c5b93771680 https://conda.anaconda.org/conda-forge/linux-64/libopus-1.3.1-h7f98852_1.tar.bz2#15345e56d527b330e1cacbdf58676e8f -https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-12.3.0-hb8811af_6.conda#a9a764e2e753ed038da59343560d8a66 +https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-12.3.0-hb8811af_7.conda#ee573415c47ce17f65101d0b3fba396d https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda#40b61aab5c7ba9ff276c41cfffe6b80b https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.4.0-hd590300_0.conda#b26e8aa824079e1be0294e7152ca4559 https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda#5aa797f8787fe7a17d1b0821485b5adc https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.2.13-hd590300_5.conda#f36c115f1ee199da648e0597ec2047ad https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.9.4-hcb278e6_0.conda#318b08df404f9c9be5712aaa5a6f0bb0 https://conda.anaconda.org/conda-forge/linux-64/mpg123-1.32.6-h59595ed_0.conda#9160cdeb523a1b20cf8d2a0bf821f45d -https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.4.20240210-h59595ed_0.conda#97da8860a0da5413c7c98a3b3838a645 -https://conda.anaconda.org/conda-forge/linux-64/ninja-1.12.0-h00ab1b0_0.conda#b048701d52e7cbb5f59ddd4d3b17bbf5 +https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h59595ed_0.conda#fcea371545eda051b6deafb24889fc69 +https://conda.anaconda.org/conda-forge/linux-64/ninja-1.12.1-h297d8ca_0.conda#3aa1c7e292afeff25a0091ddd7c69b72 https://conda.anaconda.org/conda-forge/linux-64/nspr-4.35-h27087fc_0.conda#da0ec11a6454ae19bff5b02ed881a2b1 https://conda.anaconda.org/conda-forge/linux-64/openssl-3.3.0-hd590300_0.conda#c0f3abb4a16477208bbd43a39bd56f18 https://conda.anaconda.org/conda-forge/linux-64/pixman-0.43.2-h59595ed_0.conda#71004cbf7924e19c02746ccde9fd7123 @@ -69,13 +69,13 @@ https://conda.anaconda.org/conda-forge/linux-64/xorg-xproto-7.0.31-h7f98852_1007 https://conda.anaconda.org/conda-forge/linux-64/xz-5.2.6-h166bdaf_0.tar.bz2#2161070d867d1b1204ea749c8eec4ef0 https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h7f98852_2.tar.bz2#4cb3ad778ec2d5a7acbdf254eb1c42ae https://conda.anaconda.org/conda-forge/linux-64/expat-2.6.2-h59595ed_0.conda#53fb86322bdb89496d7579fe3f02fd61 -https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-12.3.0-h58ffeeb_6.conda#53914a98926ce169b83726cb78366a6c +https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-12.3.0-h58ffeeb_7.conda#95f78565a09852783d3e90e0389cfa5f https://conda.anaconda.org/conda-forge/linux-64/libasprintf-devel-0.22.5-h661eb56_2.conda#02e41ab5834dcdcc8590cf29d9526f50 https://conda.anaconda.org/conda-forge/linux-64/libcap-2.69-h0f662aa_0.conda#25cb5999faa414e5ccb2c1388f62d3d5 https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20191231-he28a2e2_2.tar.bz2#4d331e44109e3f0e19b4cb8f9b82f3e1 https://conda.anaconda.org/conda-forge/linux-64/libevent-2.1.12-hf998b51_1.conda#a1cfcc585f0c42bf8d5546bb1dfb668d https://conda.anaconda.org/conda-forge/linux-64/libgettextpo-devel-0.22.5-h59595ed_2.conda#b63d9b6da3653179a278077f0de20014 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran-ng-13.2.0-h69a702a_6.conda#3666a850342f8f3be88f9a93d948d027 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-ng-13.2.0-h69a702a_7.conda#1b84f26d9f4f6026e179e7805d5a15cd https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.43-h2797004_0.conda#009981dd9cfcaa4dbfa25ffaed86bcae https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.45.3-h2797004_0.conda#b3316cbe90249da4f8e84cd66e1cc55b https://conda.anaconda.org/conda-forge/linux-64/libvorbis-1.3.7-h9c3ff4c_0.tar.bz2#309dec04b70a3cc0f1e84a4013683bc0 @@ -89,20 +89,20 @@ https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.4-h7391055_0.cond https://conda.anaconda.org/conda-forge/linux-64/zlib-1.2.13-hd590300_5.conda#68c34ec6149623be41a1933ab996a209 https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.6-ha6fb4c9_0.conda#4d056880988120e29d75bfff282e0f45 https://conda.anaconda.org/conda-forge/linux-64/freetype-2.12.1-h267a509_2.conda#9ae35c3d96db2c94ce0cef86efdfa2cb -https://conda.anaconda.org/conda-forge/linux-64/gcc-12.3.0-h915e2ae_6.conda#ec683e084ea08ef94528f15d30fa1e03 +https://conda.anaconda.org/conda-forge/linux-64/gcc-12.3.0-h915e2ae_7.conda#84b1c5cebd0a0443f3d7f90a4be93fc6 https://conda.anaconda.org/conda-forge/linux-64/gcc_linux-64-12.3.0-h6477408_3.conda#7a53f84c45bdf4656ba27b9e9ed68b3d https://conda.anaconda.org/conda-forge/linux-64/gettext-0.22.5-h59595ed_2.conda#219ba82e95d7614cf7140d2a4afc0926 -https://conda.anaconda.org/conda-forge/linux-64/gfortran_impl_linux-64-12.3.0-h1645026_6.conda#664d4e904674f1173752580ffdc24d46 -https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-12.3.0-h2a574ab_6.conda#aab48c86452d78a416992deeee901a52 +https://conda.anaconda.org/conda-forge/linux-64/gfortran_impl_linux-64-12.3.0-h1645026_7.conda#2d9d4058c433c9ce2a811c76658c4efd +https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-12.3.0-h2a574ab_7.conda#265caa78b979f112fc241cecd0015c91 https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.2-h659d440_0.conda#cd95826dbd331ed1be26bdf401432844 -https://conda.anaconda.org/conda-forge/linux-64/libglib-2.80.0-hf2295e7_6.conda#9342e7c44c38bea649490f72d92c382d +https://conda.anaconda.org/conda-forge/linux-64/libglib-2.80.2-hf974151_0.conda#72724f6a78ecb15559396966226d5838 https://conda.anaconda.org/conda-forge/linux-64/libhwloc-2.10.0-default_h2fb2949_1000.conda#7e3726e647a619c6ce5939014dfde86d https://conda.anaconda.org/conda-forge/linux-64/libllvm15-15.0.7-hb3ce162_4.conda#8a35df3cbc0c8b12cc8af9473ae75eef https://conda.anaconda.org/conda-forge/linux-64/libllvm18-18.1.5-hb77312f_0.conda#efd221d3668077ca067a206269418dec https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.6.0-h1dd3fc0_3.conda#66f03896ffbe1a110ffda05c7a856504 -https://conda.anaconda.org/conda-forge/linux-64/llvm-openmp-18.1.4-ha31de31_0.conda#48b9991e66abc186a7ad7975e97bd4d0 +https://conda.anaconda.org/conda-forge/linux-64/llvm-openmp-18.1.5-ha31de31_0.conda#b923cdb6e567ada84f991ffcc5848afb https://conda.anaconda.org/conda-forge/linux-64/mysql-libs-8.3.0-hca2cd23_4.conda#1b50eebe2a738a3146c154d2eceaa8b6 -https://conda.anaconda.org/conda-forge/linux-64/nss-3.98-h1d7d5a4_0.conda#54b56c2fdf973656b748e0378900ec13 +https://conda.anaconda.org/conda-forge/linux-64/nss-3.100-hca3bf56_0.conda#949c4a82290ee58b3c970cef4bcfd4ad https://conda.anaconda.org/conda-forge/linux-64/python-3.9.19-h0755675_0_cpython.conda#d9ee3647fbd9e8595b8df759b2bbefb8 https://conda.anaconda.org/conda-forge/linux-64/xcb-util-0.4.0-hd590300_1.conda#9bfac7ccd94d54fd21a0501296d60424 https://conda.anaconda.org/conda-forge/linux-64/xcb-util-keysyms-0.4.0-h8ee46fc_1.conda#632413adcd8bc16b515cab87a2932913 @@ -125,10 +125,10 @@ https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.0-pyhd8ed1ab_2. https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_0.conda#15dda3cdbf330abfe9f555d22f66db46 https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.14.2-h14ed4e7_0.conda#0f69b688f52ff6da70bccb7ff7001d1d https://conda.anaconda.org/conda-forge/noarch/fsspec-2024.3.1-pyhca7485f_0.conda#b7f0662ef2c9d4404f0af9eef5ed2fde -https://conda.anaconda.org/conda-forge/linux-64/gfortran-12.3.0-h915e2ae_6.conda#84b517f4f53e56256dbd65133aae04ac +https://conda.anaconda.org/conda-forge/linux-64/gfortran-12.3.0-h915e2ae_7.conda#8efa768f7f74085629f3e1090e7f0569 https://conda.anaconda.org/conda-forge/linux-64/gfortran_linux-64-12.3.0-h617cb40_3.conda#3a9e5b8a6f651ff14e74d896d8f04ab6 -https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.80.0-hde27a5a_6.conda#a9d23c02485c5cf055f9ac90eb9c9c63 -https://conda.anaconda.org/conda-forge/linux-64/gxx-12.3.0-h915e2ae_6.conda#0d977804df65082e17c860600ca2894b +https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.80.2-hb6ce0ca_0.conda#a965aeaf060289528a3fbe09326edae2 +https://conda.anaconda.org/conda-forge/linux-64/gxx-12.3.0-h915e2ae_7.conda#721c5433122a02bf3a081db10a2e68e2 https://conda.anaconda.org/conda-forge/linux-64/gxx_linux-64-12.3.0-h4a1b8e8_3.conda#9ec22c7c544f4a4f6d660f0a3b0fd15c https://conda.anaconda.org/conda-forge/noarch/idna-3.7-pyhd8ed1ab_0.conda#c0cc1420498b17414d8617d0b9f506ca https://conda.anaconda.org/conda-forge/noarch/imagesize-1.4.1-pyhd8ed1ab_0.tar.bz2#7de5386c8fea29e76b303f37dde4c352 @@ -140,7 +140,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libclang13-18.1.5-default_h5d682 https://conda.anaconda.org/conda-forge/linux-64/libcups-2.3.3-h4637d8d_4.conda#d4529f4dff3057982a7617c7ac58fde3 https://conda.anaconda.org/conda-forge/linux-64/libflac-1.4.3-h59595ed_0.conda#ee48bf17cc83a00f59ca1494d5646869 https://conda.anaconda.org/conda-forge/linux-64/libgpg-error-1.49-h4f305b6_0.conda#dfcfd72c7a430d3616763ecfbefe4ca9 -https://conda.anaconda.org/conda-forge/linux-64/libpq-16.2-h33b98f1_1.conda#9e49ec2a61d02623b379dc332eb6889d +https://conda.anaconda.org/conda-forge/linux-64/libpq-16.3-ha72fbe1_0.conda#bac737ae28b79cfbafd515258d97d29e https://conda.anaconda.org/conda-forge/noarch/locket-1.0.0-pyhd8ed1ab_0.tar.bz2#91e27ef3d05cc772ce627e51cff111c4 https://conda.anaconda.org/conda-forge/linux-64/markupsafe-2.1.5-py39hd1e30aa_0.conda#9a9a22eb1f83c44953319ee3b027769f https://conda.anaconda.org/conda-forge/noarch/networkx-3.2-pyhd8ed1ab_0.conda#cec8cc498664cc00a070676aa89e69a7 @@ -160,7 +160,7 @@ https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2#e5 https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-2.2.0-pyhd8ed1ab_0.tar.bz2#4d22a9315e78c6827f806065957d566e https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_0.conda#da1d979339e2714c30a8e806a33ec087 https://conda.anaconda.org/conda-forge/linux-64/tbb-2021.12.0-h00ab1b0_0.conda#f1b776cff1b426e7e7461a8502a3b731 -https://conda.anaconda.org/conda-forge/noarch/tenacity-8.2.3-pyhd8ed1ab_0.conda#1482e77f87c6a702a7e05ef22c9b197b +https://conda.anaconda.org/conda-forge/noarch/tenacity-8.3.0-pyhd8ed1ab_0.conda#216cfa8e32bcd1447646768351df6059 https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.5.0-pyhc1e730c_0.conda#df68d78237980a159bd7149f33c0e8fd https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_0.tar.bz2#f832c45a477c78bebd107098db465095 https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2#5844808ffab9ebdb694585b50ba02a96 @@ -178,9 +178,9 @@ https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.0-h3faef2a_0.conda#f9 https://conda.anaconda.org/conda-forge/linux-64/cxx-compiler-1.7.0-h00ab1b0_1.conda#28de2e073db9ca9b72858bee9fb6f571 https://conda.anaconda.org/conda-forge/linux-64/cytoolz-0.12.3-py39hd1e30aa_0.conda#dc0fb8e157c7caba4c98f1e1f9d2e5f4 https://conda.anaconda.org/conda-forge/linux-64/fortran-compiler-1.7.0-heb67821_1.conda#cf4b0e7c4c78bb0662aed9b27c414a3c -https://conda.anaconda.org/conda-forge/linux-64/glib-2.80.0-hf2295e7_6.conda#a1e026a82a562b443845db5614ca568a +https://conda.anaconda.org/conda-forge/linux-64/glib-2.80.2-hf974151_0.conda#d427988dc3dbd0a4c136f52db356cc6a https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-7.1.0-pyha770c72_0.conda#0896606848b2dc5cebdf111b6543aa04 -https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.3-pyhd8ed1ab_0.conda#e7d8df6509ba635247ff9aea31134262 +https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.4-pyhd8ed1ab_0.conda#7b86ecb7d3557821c649b3c31e3eb9f2 https://conda.anaconda.org/conda-forge/noarch/joblib-1.4.2-pyhd8ed1ab_0.conda#25df261d4523d9f9783bcdb7208d872f https://conda.anaconda.org/conda-forge/linux-64/libgcrypt-1.10.3-hd590300_0.conda#32d16ad533c59bb0a3c5ffaf16110829 https://conda.anaconda.org/conda-forge/linux-64/libsndfile-1.2.2-hc60ed4a_1.conda#ef1910918dd895516a769ed36b5b3a4e @@ -188,7 +188,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libxkbcommon-1.7.0-h662e7e4_0.co https://conda.anaconda.org/conda-forge/noarch/memory_profiler-0.61.0-pyhd8ed1ab_0.tar.bz2#8b45f9f2b2f7a98b0ec179c8991a4a9b https://conda.anaconda.org/conda-forge/noarch/meson-1.4.0-pyhd8ed1ab_0.conda#52a0660cfa40b45bf254ecc3374cb2e0 https://conda.anaconda.org/conda-forge/linux-64/mkl-2024.1.0-ha957f24_692.conda#e7f5c5cda17c6f5047db27d44367c19d -https://conda.anaconda.org/conda-forge/noarch/partd-1.4.1-pyhd8ed1ab_0.conda#acf4b7c0bcd5fa3b0e05801c4d2accd6 +https://conda.anaconda.org/conda-forge/noarch/partd-1.4.2-pyhd8ed1ab_0.conda#0badf9c54e24cecfb0ad2f99d680c163 https://conda.anaconda.org/conda-forge/linux-64/pillow-10.3.0-py39h90c7501_0.conda#1e3b6af9592be71ce19f0a6aae05d97b https://conda.anaconda.org/conda-forge/noarch/pip-24.0-pyhd8ed1ab_0.conda#f586ac1e56c8638b64f9c8122a7b8a67 https://conda.anaconda.org/conda-forge/noarch/plotly-5.14.0-pyhd8ed1ab_0.conda#6a7bcc42ef58dd6cf3da9333ea102433 From bca36349a108b9c6b372c6257d4a9837704d5a01 Mon Sep 17 00:00:00 2001 From: Olivier Grisel Date: Mon, 13 May 2024 13:28:17 +0200 Subject: [PATCH 111/344] FIX 1d sparse array validation (#28988) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jérémie du Boisberranger Co-authored-by: Christian Lorentzen --- doc/whats_new/v1.5.rst | 4 ++++ sklearn/preprocessing/tests/test_data.py | 4 ++++ sklearn/utils/tests/test_validation.py | 8 ++++++++ sklearn/utils/validation.py | 7 +++++++ 4 files changed, 23 insertions(+) diff --git a/doc/whats_new/v1.5.rst b/doc/whats_new/v1.5.rst index e50309a330e39..55a5546453f5f 100644 --- a/doc/whats_new/v1.5.rst +++ b/doc/whats_new/v1.5.rst @@ -67,6 +67,10 @@ Changes impacting many modules :class:`pipeline.Pipeline` and :class:`preprocessing.KBinsDiscretizer`. :pr:`28756` by :user:`Will Dean `. +- |Fix| Raise `ValueError` with an informative error message when passing 1D + sparse arrays to methods that expect 2D sparse inputs. + :pr:`28988` by :user:`Olivier Grisel `. + Support for Array API --------------------- diff --git a/sklearn/preprocessing/tests/test_data.py b/sklearn/preprocessing/tests/test_data.py index b7e8e4e40686e..3810e485ae301 100644 --- a/sklearn/preprocessing/tests/test_data.py +++ b/sklearn/preprocessing/tests/test_data.py @@ -595,6 +595,10 @@ def test_standard_scaler_partial_fit_numerical_stability(sparse_container): scaler_incr = StandardScaler(with_mean=False) for chunk in X: + 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 diff --git a/sklearn/utils/tests/test_validation.py b/sklearn/utils/tests/test_validation.py index 4b4eed2522102..92fff950e875e 100644 --- a/sklearn/utils/tests/test_validation.py +++ b/sklearn/utils/tests/test_validation.py @@ -361,6 +361,14 @@ def test_check_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) with pytest.raises(ValueError): diff --git a/sklearn/utils/validation.py b/sklearn/utils/validation.py index 5fac2ae6ae6c2..cdda749ec70a2 100644 --- a/sklearn/utils/validation.py +++ b/sklearn/utils/validation.py @@ -973,6 +973,13 @@ def is_sparse(dtype): estimator_name=estimator_name, input_name=input_name, ) + if ensure_2d and array.ndim < 2: + raise ValueError( + 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 From 4449ded95bdc7468f7465a3e89ea1b90b1426dfd Mon Sep 17 00:00:00 2001 From: scikit-learn-bot Date: Mon, 13 May 2024 15:19:29 +0200 Subject: [PATCH 112/344] :lock: :robot: CI Update lock files for scipy-dev CI build(s) :lock: :robot: (#29004) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jérémie du Boisberranger --- .../azure/pylatest_pip_scipy_dev_linux-64_conda.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/build_tools/azure/pylatest_pip_scipy_dev_linux-64_conda.lock b/build_tools/azure/pylatest_pip_scipy_dev_linux-64_conda.lock index 8324d1edb36b7..c1a50c7c8c140 100644 --- a/build_tools/azure/pylatest_pip_scipy_dev_linux-64_conda.lock +++ b/build_tools/azure/pylatest_pip_scipy_dev_linux-64_conda.lock @@ -20,12 +20,12 @@ https://repo.anaconda.com/pkgs/main/linux-64/xz-5.4.6-h5eee18b_1.conda#1562802f8 https://repo.anaconda.com/pkgs/main/linux-64/zlib-1.2.13-h5eee18b_1.conda#92e42d8310108b0a440fb2e60b2b2a25 https://repo.anaconda.com/pkgs/main/linux-64/ccache-3.7.9-hfe4627d_0.conda#bef6fc681c273bb7bd0c67d1a591365e https://repo.anaconda.com/pkgs/main/linux-64/readline-8.2-h5eee18b_0.conda#be42180685cce6e6b0329201d9f48efb -https://repo.anaconda.com/pkgs/main/linux-64/tk-8.6.12-h1ccaba5_0.conda#fa10ff4aa631fa4aa090a6234d7770b9 +https://repo.anaconda.com/pkgs/main/linux-64/tk-8.6.14-h39e8969_0.conda#78dbc5e3c69143ebc037fc5d5b22e597 https://repo.anaconda.com/pkgs/main/linux-64/sqlite-3.45.3-h5eee18b_0.conda#acf93d6aceb74d6110e20b44cc45939e -https://repo.anaconda.com/pkgs/main/linux-64/python-3.12.3-h996f2a0_0.conda#77af2bd351a8311d1e780bcfa7819bb8 -https://repo.anaconda.com/pkgs/main/linux-64/setuptools-68.2.2-py312h06a4308_0.conda#83ba634cde4f30d9e0b88e4ac9716ca4 +https://repo.anaconda.com/pkgs/main/linux-64/python-3.12.3-h996f2a0_1.conda#0e22ed7e6df024e4f7467e75c8575301 +https://repo.anaconda.com/pkgs/main/linux-64/setuptools-69.5.1-py312h06a4308_0.conda#ce85d9a864a73e0b12d31a97733c9fca https://repo.anaconda.com/pkgs/main/linux-64/wheel-0.43.0-py312h06a4308_0.conda#18d5f3b68a175c72576876db4afc9e9e -https://repo.anaconda.com/pkgs/main/linux-64/pip-23.3.1-py312h06a4308_0.conda#e1d44bca4a257e84af33503233491107 +https://repo.anaconda.com/pkgs/main/linux-64/pip-24.0-py312h06a4308_0.conda#6d9697bb8b9f3212be10b3b8e01a12b9 # pip alabaster @ https://files.pythonhosted.org/packages/32/34/d4e1c02d3bee589efb5dfa17f88ea08bdb3e3eac12bc475462aec52ed223/alabaster-0.7.16-py3-none-any.whl#sha256=b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92 # pip babel @ https://files.pythonhosted.org/packages/27/45/377f7e32a5c93d94cd56542349b34efab5ca3f9e2fd5a68c5e93169aa32d/Babel-2.15.0-py3-none-any.whl#sha256=08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb # pip certifi @ https://files.pythonhosted.org/packages/ba/06/a07f096c664aeb9f01624f858c3add0a4e913d6c96257acb4fce61e7de14/certifi-2024.2.2-py3-none-any.whl#sha256=dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1 From 64884f92ff38365e2df05da5fb011ee65e71dd08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20du=20Boisberranger?= Date: Mon, 13 May 2024 16:34:37 +0200 Subject: [PATCH 113/344] CI Fix wheel builder windows (#29006) --- .github/workflows/wheels.yml | 2 -- build_tools/github/repair_windows_wheels.sh | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index d30f85ff3d1e6..8bd7ffc17beca 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -53,8 +53,6 @@ jobs: matrix: include: # Window 64 bit - # Note: windows-2019 is needed for older Python versions: - # https://github.com/scikit-learn/scikit-learn/issues/22530 - os: windows-latest python: 39 platform_id: win_amd64 diff --git a/build_tools/github/repair_windows_wheels.sh b/build_tools/github/repair_windows_wheels.sh index cdd0c0c79d8c4..8f51a34d4039b 100755 --- a/build_tools/github/repair_windows_wheels.sh +++ b/build_tools/github/repair_windows_wheels.sh @@ -8,6 +8,7 @@ DEST_DIR=$2 # By default, the Windows wheels are not repaired. # In this case, we need to vendor VCRUNTIME140.dll +pip install wheel wheel unpack "$WHEEL" WHEEL_DIRNAME=$(ls -d scikit_learn-*) python build_tools/github/vendor.py "$WHEEL_DIRNAME" From 63f71ee41149899cac99c3e95f25c5496377c214 Mon Sep 17 00:00:00 2001 From: Adrin Jalali Date: Mon, 13 May 2024 16:40:26 +0200 Subject: [PATCH 114/344] DOC persistence page revamp (#28889) --- doc/model_persistence.rst | 643 +++++++++++++++++++++----------------- 1 file changed, 349 insertions(+), 294 deletions(-) diff --git a/doc/model_persistence.rst b/doc/model_persistence.rst index afd492d805e58..0c11349a68e22 100644 --- a/doc/model_persistence.rst +++ b/doc/model_persistence.rst @@ -1,294 +1,349 @@ -.. Places parent toc into the sidebar - -:parenttoc: True - -.. _model_persistence: - -================= -Model persistence -================= - -After training a scikit-learn model, it is desirable to have a way to persist -the model for future use without having to retrain. This can be accomplished -using `pickle `_, `joblib -`_, `skops -`_, `ONNX `_, -or `PMML `_. In most cases -`pickle` can be used to persist a trained scikit-learn model. Once all -transitive scikit-learn dependencies have been pinned, the trained model can -then be loaded and executed under conditions similar to those in which it was -originally pinned. The following sections will give you some hints on how to -persist a scikit-learn model and will provide details on what each alternative -can offer. - -Workflow Overview ------------------ - -In this section we present a general workflow on how to persist a -scikit-learn model. We will demonstrate this with a simple example using -Python's built-in persistence module, namely `pickle -`_. - -Storing the model in an artifact -................................ - -Once the model training process in completed, the trained model can be stored -as an artifact with the help of `pickle`. The model can be saved using the -process of serialization, where the Python object hierarchy is converted into -a byte stream. We can persist a trained model in the following manner:: - - >>> from sklearn import svm - >>> from sklearn import datasets - >>> import pickle - >>> clf = svm.SVC() - >>> X, y = datasets.load_iris(return_X_y=True) - >>> clf.fit(X, y) - SVC() - >>> s = pickle.dumps(clf) - -Replicating the training environment in production -.................................................. - -The versions of the dependencies used may differ from training to production. -This may result in unexpected behaviour and errors while using the trained -model. To prevent such situations it is recommended to use the same -dependencies and versions in both the training and production environment. -These transitive dependencies can be pinned with the help of `pip`, `conda`, -`poetry`, `conda-lock`, `pixi`, etc. - -.. note:: - - To execute a pickled scikit-learn model in a reproducible environment it is - advisable to pin all transitive scikit-learn dependencies. This prevents - any incompatibility issues that may arise while trying to load the pickled - model. You can read more about persisting models with `pickle` over - :ref:`here `. - -Loading the model artifact -.......................... - -The saved scikit-learn model can be loaded using `pickle` for future use -without having to re-train the entire model from scratch. The saved model -artifact can be unpickled by converting the byte stream into an object -hierarchy. This can be done with the help of `pickle` as follows:: - - >>> clf2 = pickle.loads(s) # doctest:+SKIP - >>> clf2.predict(X[0:1]) # doctest:+SKIP - array([0]) - >>> y[0] # doctest:+SKIP - 0 - -Serving the model artifact -.......................... - -The last step after training a scikit-learn model is serving the model. -Once the trained model is successfully loaded it can be served to manage -different prediction requests. This can involve deploying the model as a -web service using containerization, or other model deployment strategies, -according to the specifications. In the next sections, we will explore -different approaches to persist a trained scikit-learn model. - -.. _persisting_models_with_pickle: - -Persisting models with pickle ------------------------------ - -As demonstrated in the previous section, `pickle` uses serialization and -deserialization to persist scikit-learn models. Instead of using `dumps` and -`loads`, `dump` and `load` can also be used in the following way:: - - >>> from sklearn.tree import DecisionTreeClassifier - >>> from sklearn import datasets - >>> clf = DecisionTreeClassifier() - >>> X, y = datasets.load_iris(return_X_y=True) - >>> clf.fit(X, y) - DecisionTreeClassifier() - >>> from pickle import dump, load - >>> with open('filename.pkl', 'wb') as f: dump(clf, f) # doctest:+SKIP - >>> with open('filename.pkl', 'rb') as f: clf2 = load(f) # doctest:+SKIP - >>> clf2.predict(X[0:1]) # doctest:+SKIP - array([0]) - >>> y[0] - 0 - -For applications that involve writing and loading the serialized object to or -from a file, `dump` and `load` can be used instead of `dumps` and `loads`. When -file operations are not required the pickled representation of the object can -be returned as a bytes object with the help of the `dumps` function. The -reconstituted object hierarchy of the pickled data can then be returned using -the `loads` function. - -Persisting models with joblib ------------------------------ - -In the specific case of scikit-learn, it may be better to use joblib's -replacement of pickle (``dump`` & ``load``), which is more efficient on -objects that carry large numpy arrays internally as is often the case for -fitted scikit-learn estimators, but can only pickle to the disk and not to a -string:: - - >>> from joblib import dump, load - >>> dump(clf, 'filename.joblib') # doctest:+SKIP - -Later you can load back the pickled model (possibly in another Python process) -with:: - - >>> clf = load('filename.joblib') # doctest:+SKIP - -.. note:: - - ``dump`` and ``load`` functions also accept file-like object - instead of filenames. More information on data persistence with Joblib is - available `here - `_. - -|details-start| -**InconsistentVersionWarning** -|details-split| - -When an estimator is unpickled with a scikit-learn version that is inconsistent -with the version the estimator was pickled with, a -:class:`~sklearn.exceptions.InconsistentVersionWarning` is raised. This warning -can be caught to obtain the original version the estimator was pickled with:: - - from sklearn.exceptions import InconsistentVersionWarning - warnings.simplefilter("error", InconsistentVersionWarning) - - try: - est = pickle.loads("model_from_prevision_version.pickle") - except InconsistentVersionWarning as w: - print(w.original_sklearn_version) - -|details-end| - -.. _persistence_limitations: - -Security & maintainability limitations for pickle and joblib ------------------------------------------------------------- - -pickle (and joblib by extension), has some issues regarding maintainability -and security. Because of this, - -* Never unpickle untrusted data as it could lead to malicious code being - executed upon loading. -* While models saved using one version of scikit-learn might load in - other versions, this is entirely unsupported and inadvisable. It should - also be kept in mind that operations performed on such data could give - different and unexpected results. - -In order to rebuild a similar model with future versions of scikit-learn, -additional metadata should be saved along the pickled model: - -* The training data, e.g. a reference to an immutable snapshot -* The python source code used to generate the model -* The versions of scikit-learn and its dependencies -* The cross validation score obtained on the training data - -This should make it possible to check that the cross-validation score is in the -same range as before. - -Aside for a few exceptions, pickled models should be portable across -architectures assuming the same versions of dependencies and Python are used. -If you encounter an estimator that is not portable please open an issue on -GitHub. Pickled models are often deployed in production using containers, like -Docker, in order to freeze the environment and dependencies. - -If you want to know more about these issues and explore other possible -serialization methods, please refer to this -`talk by Alex Gaynor -`_. - -Persisting models with a more secure format using skops -------------------------------------------------------- - -`skops `__ provides a more secure -format via the :mod:`skops.io` module. It avoids using :mod:`pickle` and only -loads files which have types and references to functions which are trusted -either by default or by the user. - -|details-start| -**Using skops** -|details-split| - -The API is very similar to ``pickle``, and -you can persist your models as explain in the `docs -`__ using -:func:`skops.io.dump` and :func:`skops.io.dumps`:: - - import skops.io as sio - obj = sio.dumps(clf) - -And you can load them back using :func:`skops.io.load` and -:func:`skops.io.loads`. However, you need to specify the types which are -trusted by you. You can get existing unknown types in a dumped object / file -using :func:`skops.io.get_untrusted_types`, and after checking its contents, -pass it to the load function:: - - unknown_types = sio.get_untrusted_types(data=obj) - clf = sio.loads(obj, trusted=unknown_types) - -If you trust the source of the file / object, you can pass ``trusted=True``:: - - clf = sio.loads(obj, trusted=True) - -Please report issues and feature requests related to this format on the `skops -issue tracker `__. - -|details-end| - -Persisting models with interoperable formats --------------------------------------------- - -For reproducibility and quality control needs, when different architectures -and environments should be taken into account, exporting the model in -`Open Neural Network -Exchange `_ format or `Predictive Model Markup Language -(PMML) `_ format -might be a better approach than using `pickle` alone. -These are helpful where you may want to use your model for prediction in a -different environment from where the model was trained. - -ONNX is a binary serialization of the model. It has been developed to improve -the usability of the interoperable representation of data models. -It aims to facilitate the conversion of the data -models between different machine learning frameworks, and to improve their -portability on different computing architectures. More details are available -from the `ONNX tutorial `_. -To convert scikit-learn model to ONNX a specific tool `sklearn-onnx -`_ has been developed. - -PMML is an implementation of the `XML -`_ document standard -defined to represent data models together with the data used to generate them. -Being human and machine readable, -PMML is a good option for model validation on different platforms and -long term archiving. On the other hand, as XML in general, its verbosity does -not help in production when performance is critical. -To convert scikit-learn model to PMML you can use for example `sklearn2pmml -`_ distributed under the Affero GPLv3 -license. - -Summarizing the keypoints -------------------------- - -Based on the different approaches for model persistence, the keypoints for each -approach can be summarized as follows: - -* `pickle`: It is native to Python and any Python object can be serialized and - deserialized using `pickle`, including custom Python classes and objects. - While `pickle` can be used to easily save and load scikit-learn models, - unpickling of untrusted data might lead to security issues. -* `joblib`: Efficient storage and memory mapping techniques make it faster - when working with large machine learning models or large numpy arrays. However, - it may trigger the execution of malicious code while loading untrusted data. -* `skops`: Trained scikit-learn models can be easily shared and put into - production using `skops`. It is more secure compared to alternate approaches - as it allows users to load data from trusted sources. It however, does not - allow for persistence of arbitrary Python code. -* `ONNX`: It provides a uniform format for persisting any machine learning - or deep learning model (other than scikit-learn) and is useful - for model inference. It can however, result in compatibility issues with - different frameworks. -* `PMML`: Platform independent format that can be used to persist models - and reduce the risk of vendor lock-ins. The complexity and verbosity of - this format might make it harder to use for larger models. \ No newline at end of file +.. Places parent toc into the sidebar + +:parenttoc: True + +.. _model_persistence: + +================= +Model persistence +================= + +After training a scikit-learn model, it is desirable to have a way to persist +the model for future use without having to retrain. Based on your use-case, +there are a few different ways to persist a scikit-learn model, and here we +help you decide which one suits you best. In order to make a decision, you need +to answer the following questions: + +1. Do you need the Python object after persistence, or do you only need to + persist in order to serve the model and get predictions out of it? + +If you only need to serve the model and no further investigation on the Python +object itself is required, then :ref:`ONNX ` might be the +best fit for you. Note that not all models are supported by ONNX. + +In case ONNX is not suitable for your use-case, the next question is: + +2. Do you absolutely trust the source of the model, or are there any security + concerns regarding where the persisted model comes from? + +If you have security concerns, then you should consider using :ref:`skops.io +` which gives you back the Python object, but unlike +`pickle` based persistence solutions, loading the persisted model doesn't +automatically allow arbitrary code execution. Note that this requires manual +investigation of the persisted file, which :mod:`skops.io` allows you to do. + +The other solutions assume you absolutely trust the source of the file to be +loaded, as they are all susceptible to arbitrary code execution upon loading +the persisted file since they all use the pickle protocol under the hood. + +3. Do you care about the performance of loading the model, and sharing it + between processes where a memory mapped object on disk is beneficial? + +If yes, then you can consider using :ref:`joblib `. If this +is not a major concern for you, then you can use the built-in :mod:`pickle` +module. + +4. Did you try :mod:`pickle` or :mod:`joblib` and found that the model cannot + be persisted? It can happen for instance when you have user defined + functions in your model. + +If yes, then you can use `cloudpickle`_ which can serialize certain objects +which cannot be serialized by :mod:`pickle` or :mod:`joblib`. + + +Workflow Overview +----------------- + +In a typical workflow, the first step is to train the model using scikit-learn +and scikit-learn compatible libraries. Note that support for scikit-learn and +third party estimators varies across the different persistence methods. + +Train and Persist the Model +........................... + +Creating an appropriate model depends on your use-case. As an example, here we +train a :class:`sklearn.ensemble.HistGradientBoostingClassifier` on the iris +dataset:: + + >>> from sklearn import ensemble + >>> from sklearn import datasets + >>> clf = ensemble.HistGradientBoostingClassifier() + >>> X, y = datasets.load_iris(return_X_y=True) + >>> clf.fit(X, y) + HistGradientBoostingClassifier() + +Once the model is trained, you can persist it using your desired method, and +then you can load the model in a separate environment and get predictions from +it given input data. Here there are two major paths depending on how you +persist and plan to serve the model: + +- :ref:`ONNX `: You need an `ONNX` runtime and an environment + with appropriate dependencies installed to load the model and use the runtime + to get predictions. This environment can be minimal and does not necessarily + even require `python` to be installed. + +- :mod:`skops.io`, :mod:`pickle`, :mod:`joblib`, `cloudpickle`_: You need a + Python environment with the appropriate dependencies installed to load the + model and get predictions from it. This environment should have the same + **packages** and the same **versions** as the environment where the model was + trained. Note that none of these methods support loading a model trained with + a different version of scikit-learn, and possibly different versions of other + dependencies such as `numpy` and `scipy`. Another concern would be running + the persisted model on a different hardware, and in most cases you should be + able to load your persisted model on a different hardware. + + +.. _onnx_persistence: + +ONNX +---- + +`ONNX`, or `Open Neural Network Exchange `__ format is best +suitable in use-cases where one needs to persist the model and then use the +persisted artifact to get predictions without the need to load the Python +object itself. It is also useful in cases where the serving environment needs +to be lean and minimal, since the `ONNX` runtime does not require `python`. + +`ONNX` is a binary serialization of the model. It has been developed to improve +the usability of the interoperable representation of data models. It aims to +facilitate the conversion of the data models between different machine learning +frameworks, and to improve their portability on different computing +architectures. More details are available from the `ONNX tutorial +`__. To convert scikit-learn model to `ONNX` +`sklearn-onnx `__ has been developed. However, +not all scikit-learn models are supported, and it is limited to the core +scikit-learn and does not support most third party estimators. One can write a +custom converter for third party or custom estimators, but the documentation to +do that is sparse and it might be challenging to do so. + +|details-start| +**Using ONNX** +|details-split| + +To convert the model to `ONNX` format, you need to give the converter some +information about the input as well, about which you can read more `here +`__:: + + from skl2onnx import to_onnx + onx = to_onnx(clf, X[:1].astype(numpy.float32), target_opset=12) + with open("filename.onnx", "wb") as f: + f.write(onx.SerializeToString()) + +You can load the model in Python and use the `ONNX` runtime to get +predictions:: + + from onnxruntime import InferenceSession + with open("filename.onnx", "rb") as f: + onx = f.read() + sess = InferenceSession(onx, providers=["CPUExecutionProvider"]) + pred_ort = sess.run(None, {"X": X_test.astype(numpy.float32)})[0] + + +|details-end| + +.. _skops_persistence: + +`skops.io` +---------- + +:mod:`skops.io` avoids using :mod:`pickle` and only loads files which have types +and references to functions which are trusted either by default or by the user. +Therefore it provides a more secure format than :mod:`pickle`, :mod:`joblib`, +and `cloudpickle`_. + + +|details-start| +**Using skops** +|details-split| + +The API is very similar to :mod:`pickle`, and you can persist your models as +explained in the `documentation +`__ using +:func:`skops.io.dump` and :func:`skops.io.dumps`:: + + import skops.io as sio + obj = sio.dump(clf, "filename.skops") + +And you can load them back using :func:`skops.io.load` and +:func:`skops.io.loads`. However, you need to specify the types which are +trusted by you. You can get existing unknown types in a dumped object / file +using :func:`skops.io.get_untrusted_types`, and after checking its contents, +pass it to the load function:: + + unknown_types = sio.get_untrusted_types(file="filename.skops") + # investigate the contents of unknown_types, and only load if you trust + # everything you see. + clf = sio.load("filename.skops", trusted=unknown_types) + +Please report issues and feature requests related to this format on the `skops +issue tracker `__. + +|details-end| + +.. _pickle_persistence: + +`pickle`, `joblib`, and `cloudpickle` +------------------------------------- + +These three modules / packages, use the `pickle` protocol under the hood, but +come with slight variations: + +- :mod:`pickle` is a module from the Python Standard Library. It can serialize + and deserialize any Python object, including custom Python classes and + objects. +- :mod:`joblib` is more efficient than `pickle` when working with large machine + learning models or large numpy arrays. +- `cloudpickle`_ can serialize certain objects which cannot be serialized by + :mod:`pickle` or :mod:`joblib`, such as user defined functions and lambda + functions. This can happen for instance, when using a + :class:`~sklearn.preprocessing.FunctionTransformer` and using a custom + function to transform the data. + +|details-start| +**Using** ``pickle``, ``joblib``, **or** ``cloudpickle`` +|details-split| + +Depending on your use-case, you can choose one of these three methods to +persist and load your scikit-learn model, and they all follow the same API:: + + # Here you can replace pickle with joblib or cloudpickle + from pickle import dump + with open('filename.pkl', 'wb') as f: dump(clf, f) + +And later when needed, you can load the same object from the persisted file:: + + # Here you can replace pickle with joblib or cloudpickle + from pickle import load + with open('filename.pkl', 'rb') as f: clf = load(f) + +|details-end| + +.. _persistence_limitations: + +Security & Maintainability Limitations +-------------------------------------- + +:mod:`pickle` (and :mod:`joblib` and :mod:`clouldpickle` by extension), has +many documented security vulnerabilities and should only be used if the +artifact, i.e. the pickle-file, is coming from a trusted and verified source. + +Also note that arbitrary computations can be represented using the `ONNX` +format, and therefore a sandbox used to serve models using `ONNX` also needs to +safeguard against computational and memory exploits. + +Also note that there are no supported ways to load a model trained with a +different version of scikit-learn. While using :mod:`skops.io`, :mod:`joblib`, +:mod:`pickle`, or `cloudpickle`_, models saved using one version of +scikit-learn might load in other versions, however, this is entirely +unsupported and inadvisable. It should also be kept in mind that operations +performed on such data could give different and unexpected results, or even +crash your Python process. + +In order to rebuild a similar model with future versions of scikit-learn, +additional metadata should be saved along the pickled model: + +* The training data, e.g. a reference to an immutable snapshot +* The Python source code used to generate the model +* The versions of scikit-learn and its dependencies +* The cross validation score obtained on the training data + +This should make it possible to check that the cross-validation score is in the +same range as before. + +Aside for a few exceptions, persisted models should be portable across +operating systems and hardware architectures assuming the same versions of +dependencies and Python are used. If you encounter an estimator that is not +portable, please open an issue on GitHub. Persisted models are often deployed +in production using containers like Docker, in order to freeze the environment +and dependencies. + +If you want to know more about these issues, please refer to these talks: + +- `Adrin Jalali: Let's exploit pickle, and skops to the rescue! | PyData + Amsterdam 2023 `__. +- `Alex Gaynor: Pickles are for Delis, not Software - PyCon 2014 + `__. + + +.. _serving_environment: + +Replicating the training environment in production +.................................................. + +If the versions of the dependencies used may differ from training to +production, it may result in unexpected behaviour and errors while using the +trained model. To prevent such situations it is recommended to use the same +dependencies and versions in both the training and production environment. +These transitive dependencies can be pinned with the help of package management +tools like `pip`, `mamba`, `conda`, `poetry`, `conda-lock`, `pixi`, etc. + +It is not always possible to load an model trained with older versions of the +scikit-learn library and its dependencies in an updated software environment. +Instead, you might need to retrain the model with the new versions of the all +the libraries. So when training a model, it is important to record the training +recipe (e.g. a Python script) and training set information, and metadata about +all the dependencies to be able to automatically reconstruct the same training +environment for the updated software. + +|details-start| +**InconsistentVersionWarning** +|details-split| + +When an estimator is loaded with a scikit-learn version that is inconsistent +with the version the estimator was pickled with, a +:class:`~sklearn.exceptions.InconsistentVersionWarning` is raised. This warning +can be caught to obtain the original version the estimator was pickled with:: + + from sklearn.exceptions import InconsistentVersionWarning + warnings.simplefilter("error", InconsistentVersionWarning) + + try: + est = pickle.loads("model_from_prevision_version.pickle") + except InconsistentVersionWarning as w: + print(w.original_sklearn_version) + +|details-end| + + +Serving the model artifact +.......................... + +The last step after training a scikit-learn model is serving the model. +Once the trained model is successfully loaded, it can be served to manage +different prediction requests. This can involve deploying the model as a +web service using containerization, or other model deployment strategies, +according to the specifications. + + +Summarizing the key points +-------------------------- + +Based on the different approaches for model persistence, the key points for +each approach can be summarized as follows: + +* `ONNX`: It provides a uniform format for persisting any machine learning or + deep learning model (other than scikit-learn) and is useful for model + inference (predictions). It can however, result in compatibility issues with + different frameworks. +* :mod:`skops.io`: Trained scikit-learn models can be easily shared and put + into production using :mod:`skops.io`. It is more secure compared to + alternate approaches based on :mod:`pickle` because it does not load + arbitrary code unless explicitly asked for by the user. +* :mod:`joblib`: Efficient memory mapping techniques make it faster when using + the same persisted model in multiple Python processes. It also gives easy + shortcuts to compress and decompress the persisted object without the need + for extra code. However, it may trigger the execution of malicious code while + untrusted data as any other pickle-based persistence mechanism. +* :mod:`pickle`: It is native to Python and any Python object can be serialized + and deserialized using :mod:`pickle`, including custom Python classes and + objects. While :mod:`pickle` can be used to easily save and load scikit-learn + models, it may trigger the execution of malicious code while loading + untrusted data. +* `cloudpickle`_: It is slower than :mod:`pickle` and :mod:`joblib`, and is + more insecure than :mod:`pickle` and :mod:`joblib` since it can serialize + arbitrary code. However, in certain cases it might be a last resort to + persist certain models. Note that this is discouraged by `cloudpickle`_ + itself since there are no forward compatibility guarantees and you might need + the same version of `cloudpickle`_ to load the persisted model. + +.. _cloudpickle: https://github.com/cloudpipe/cloudpickle From 68b7598fe03f9b12284f94d47ea59f18b47f0dff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Est=C3=A8ve?= Date: Mon, 13 May 2024 18:06:05 +0200 Subject: [PATCH 115/344] DOC Mention that Meson is the main supported way to build scikit-learn (#29008) Co-authored-by: Tim Head --- doc/whats_new/v1.5.rst | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/doc/whats_new/v1.5.rst b/doc/whats_new/v1.5.rst index 55a5546453f5f..5fdc0707ffbee 100644 --- a/doc/whats_new/v1.5.rst +++ b/doc/whats_new/v1.5.rst @@ -95,12 +95,16 @@ See :ref:`array_api` for more details. Support for building with Meson ------------------------------- -Meson is now supported as a build backend, see :ref:`Building from source -` for more details. +From scikit-learn 1.5 onwards, Meson is the main supported way to build +scikit-learn, see :ref:`Building from source ` for more +details. -:pr:`28040` by :user:`Loïc Estève ` +Unless we discover a major blocker, setuptools support will be dropped in +scikit-learn 1.6. The 1.5.x releases will support building scikit-learn with +setuptools. -TODO Fill more details before the 1.5 release, when the Meson story has settled down. +Meson support for building scikit-learn was added in :pr:`28040` by :user:`Loïc +Estève ` Metadata Routing ---------------- From 94ad8f307d8632a702725cd1ca90ac807868c633 Mon Sep 17 00:00:00 2001 From: Olivier Grisel Date: Tue, 14 May 2024 12:17:05 +0200 Subject: [PATCH 116/344] DOC More improvements to the documentation on model persistence (#29011) Co-authored-by: Adrin Jalali --- doc/model_persistence.rst | 72 ++++++++++++++++++++++++++------------- 1 file changed, 48 insertions(+), 24 deletions(-) diff --git a/doc/model_persistence.rst b/doc/model_persistence.rst index 0c11349a68e22..0bc7384ec3d46 100644 --- a/doc/model_persistence.rst +++ b/doc/model_persistence.rst @@ -80,7 +80,9 @@ persist and plan to serve the model: - :ref:`ONNX `: You need an `ONNX` runtime and an environment with appropriate dependencies installed to load the model and use the runtime to get predictions. This environment can be minimal and does not necessarily - even require `python` to be installed. + even require Python to be installed to load the model and compute + predictions. Also note that `onnxruntime` typically requires much less RAM + than Python to to compute predictions from small models. - :mod:`skops.io`, :mod:`pickle`, :mod:`joblib`, `cloudpickle`_: You need a Python environment with the appropriate dependencies installed to load the @@ -208,13 +210,20 @@ persist and load your scikit-learn model, and they all follow the same API:: # Here you can replace pickle with joblib or cloudpickle from pickle import dump - with open('filename.pkl', 'wb') as f: dump(clf, f) + with open("filename.pkl", "wb") as f: + dump(clf, f, protocol=5) + +Using `protocol=5` is recommended to reduce memory usage and make it faster to +store and load any large NumPy array stored as a fitted attribute in the model. +You can alternatively pass `protocol=pickle.HIGHEST_PROTOCOL` which is +equivalent to `protocol=5` in Python 3.8 and later (at the time of writing). And later when needed, you can load the same object from the persisted file:: # Here you can replace pickle with joblib or cloudpickle from pickle import load - with open('filename.pkl', 'rb') as f: clf = load(f) + with open("filename.pkl", "rb") as f: + clf = load(f) |details-end| @@ -224,12 +233,14 @@ Security & Maintainability Limitations -------------------------------------- :mod:`pickle` (and :mod:`joblib` and :mod:`clouldpickle` by extension), has -many documented security vulnerabilities and should only be used if the -artifact, i.e. the pickle-file, is coming from a trusted and verified source. +many documented security vulnerabilities by design and should only be used if +the artifact, i.e. the pickle-file, is coming from a trusted and verified +source. You should never load a pickle file from an untrusted source, similarly +to how you should never execute code from an untrusted source. Also note that arbitrary computations can be represented using the `ONNX` -format, and therefore a sandbox used to serve models using `ONNX` also needs to -safeguard against computational and memory exploits. +format, and it is therefore recommended to serve models using `ONNX` in a +sandboxed environment to safeguard against computational and memory exploits. Also note that there are no supported ways to load a model trained with a different version of scikit-learn. While using :mod:`skops.io`, :mod:`joblib`, @@ -298,7 +309,8 @@ can be caught to obtain the original version the estimator was pickled with:: warnings.simplefilter("error", InconsistentVersionWarning) try: - est = pickle.loads("model_from_prevision_version.pickle") + with open("model_from_prevision_version.pickle", "rb") as f: + est = pickle.load(f) except InconsistentVersionWarning as w: print(w.original_sklearn_version) @@ -328,22 +340,34 @@ each approach can be summarized as follows: * :mod:`skops.io`: Trained scikit-learn models can be easily shared and put into production using :mod:`skops.io`. It is more secure compared to alternate approaches based on :mod:`pickle` because it does not load - arbitrary code unless explicitly asked for by the user. + arbitrary code unless explicitly asked for by the user. Such code needs to be + packaged and importable in the target Python environment. * :mod:`joblib`: Efficient memory mapping techniques make it faster when using - the same persisted model in multiple Python processes. It also gives easy - shortcuts to compress and decompress the persisted object without the need - for extra code. However, it may trigger the execution of malicious code while - untrusted data as any other pickle-based persistence mechanism. -* :mod:`pickle`: It is native to Python and any Python object can be serialized - and deserialized using :mod:`pickle`, including custom Python classes and - objects. While :mod:`pickle` can be used to easily save and load scikit-learn - models, it may trigger the execution of malicious code while loading - untrusted data. -* `cloudpickle`_: It is slower than :mod:`pickle` and :mod:`joblib`, and is - more insecure than :mod:`pickle` and :mod:`joblib` since it can serialize - arbitrary code. However, in certain cases it might be a last resort to - persist certain models. Note that this is discouraged by `cloudpickle`_ - itself since there are no forward compatibility guarantees and you might need - the same version of `cloudpickle`_ to load the persisted model. + the same persisted model in multiple Python processes when using + `mmap_mode="r"`. It also gives easy shortcuts to compress and decompress the + persisted object without the need for extra code. However, it may trigger the + execution of malicious code when loading a model from an untrusted source as + any other pickle-based persistence mechanism. +* :mod:`pickle`: It is native to Python and most Python objects can be + serialized and deserialized using :mod:`pickle`, including custom Python + classes and functions as long as they are defined in a package that can be + imported in the target environment. While :mod:`pickle` can be used to easily + save and load scikit-learn models, it may trigger the execution of malicious + code while loading a model from an untrusted source. :mod:`pickle` can also + be very efficient memorywise if the model was persisted with `protocol=5` but + it does not support memory mapping. +* `cloudpickle`_: It has comparable loading efficiency as :mod:`pickle` and + :mod:`joblib` (without memory mapping), but offers additional flexibility to + serialize custom Python code such as lambda expressions and interactively + defined functions and classes. It might be a last resort to persist pipelines + with custom Python components such as a + :class:`sklearn.preprocessing.FunctionTransformer` that wraps a function + defined in the training script itself or more generally outside of any + importable Python package. Note that `cloudpickle`_ offers no forward + compatibility guarantees and you might need the same version of + `cloudpickle`_ to load the persisted model along with the same version of all + the libraries used to define the model. As the other pickle-based persistence + mechanisms, it may trigger the execution of malicious code while loading + a model from an untrusted source. .. _cloudpickle: https://github.com/cloudpipe/cloudpickle From 3ca9fc1ad4df1b60e5652a44b8d4dc88addb79e7 Mon Sep 17 00:00:00 2001 From: Lucy Liu Date: Tue, 14 May 2024 22:30:17 +1000 Subject: [PATCH 117/344] DOC Add warm start section for tree ensembles (#29001) --- doc/modules/ensemble.rst | 37 +++++++++++++++++++++++++++++++++++++ sklearn/ensemble/_forest.py | 10 +++++----- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/doc/modules/ensemble.rst b/doc/modules/ensemble.rst index 8cee8c8d403c7..40e3894a836fc 100644 --- a/doc/modules/ensemble.rst +++ b/doc/modules/ensemble.rst @@ -1247,6 +1247,43 @@ estimation. representations of feature space, also these approaches focus also on dimensionality reduction. +.. _tree_ensemble_warm_start: + +Fitting additional trees +------------------------ + +RandomForest, Extra-Trees and :class:`RandomTreesEmbedding` estimators all support +``warm_start=True`` which allows you to add more trees to an already fitted model. + +:: + + >>> from sklearn.datasets import make_classification + >>> from sklearn.ensemble import RandomForestClassifier + + >>> X, y = make_classification(n_samples=100, random_state=1) + >>> clf = RandomForestClassifier(n_estimators=10) + >>> clf = clf.fit(X, y) # fit with 10 trees + >>> len(clf.estimators_) + 10 + >>> # set warm_start and increase num of estimators + >>> _ = clf.set_params(n_estimators=20, warm_start=True) + >>> _ = clf.fit(X, y) # fit additional 10 trees + >>> len(clf.estimators_) + 20 + +When ``random_state`` is also set, the internal random state is also preserved +between ``fit`` calls. This means that training a model once with ``n`` estimators is +the same as building the model iteratively via multiple ``fit`` calls, where the +final number of estimators is equal to ``n``. + +:: + + >>> clf = RandomForestClassifier(n_estimators=20) # set `n_estimators` to 10 + 10 + >>> _ = clf.fit(X, y) # fit `estimators_` will be the same as `clf` above + +Note that this differs from the usual behavior of :term:`random_state` in that it does +*not* result in the same result across different calls. + .. _bagging: Bagging meta-estimator diff --git a/sklearn/ensemble/_forest.py b/sklearn/ensemble/_forest.py index 6b1b842f5367b..28c404c3e406b 100644 --- a/sklearn/ensemble/_forest.py +++ b/sklearn/ensemble/_forest.py @@ -1308,7 +1308,7 @@ class RandomForestClassifier(ForestClassifier): 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 ` and - :ref:`gradient_boosting_warm_start` for details. + :ref:`tree_ensemble_warm_start` for details. class_weight : {"balanced", "balanced_subsample"}, dict or list of dicts, \ default=None @@ -1710,7 +1710,7 @@ class RandomForestRegressor(ForestRegressor): 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 ` and - :ref:`gradient_boosting_warm_start` for details. + :ref:`tree_ensemble_warm_start` for details. ccp_alpha : non-negative float, default=0.0 Complexity parameter used for Minimal Cost-Complexity Pruning. The @@ -2049,7 +2049,7 @@ class ExtraTreesClassifier(ForestClassifier): 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 ` and - :ref:`gradient_boosting_warm_start` for details. + :ref:`tree_ensemble_warm_start` for details. class_weight : {"balanced", "balanced_subsample"}, dict or list of dicts, \ default=None @@ -2434,7 +2434,7 @@ class ExtraTreesRegressor(ForestRegressor): 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 ` and - :ref:`gradient_boosting_warm_start` for details. + :ref:`tree_ensemble_warm_start` for details. ccp_alpha : non-negative float, default=0.0 Complexity parameter used for Minimal Cost-Complexity Pruning. The @@ -2727,7 +2727,7 @@ class RandomTreesEmbedding(TransformerMixin, BaseForest): 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 ` and - :ref:`gradient_boosting_warm_start` for details. + :ref:`tree_ensemble_warm_start` for details. Attributes ---------- From 00db4dfeb65a276d144b0d0b93e779eb0966634d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Est=C3=A8ve?= Date: Tue, 14 May 2024 17:26:53 +0200 Subject: [PATCH 118/344] MNT Use c11 rather than c17 in meson.build to work-around Pyodide issue (#29015) --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 52c7deb962277..b6b3652a82268 100644 --- a/meson.build +++ b/meson.build @@ -6,7 +6,7 @@ project( meson_version: '>= 1.1.0', default_options: [ 'buildtype=debugoptimized', - 'c_std=c17', + 'c_std=c11', 'cpp_std=c++14', ], ) From 28c9f50991d04a3d00913dfa19048d095446bc73 Mon Sep 17 00:00:00 2001 From: Adrin Jalali Date: Tue, 14 May 2024 22:02:20 +0200 Subject: [PATCH 119/344] DOC fix dollar sign to euro sign (#29020) Co-authored-by: Guillaume Lemaitre --- .../plot_cost_sensitive_learning.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/examples/model_selection/plot_cost_sensitive_learning.py b/examples/model_selection/plot_cost_sensitive_learning.py index 7b64af48139f2..be0900d50e4ba 100644 --- a/examples/model_selection/plot_cost_sensitive_learning.py +++ b/examples/model_selection/plot_cost_sensitive_learning.py @@ -489,7 +489,7 @@ def plot_roc_pr_curves(vanilla_model, tuned_model, *, title): _, ax = plt.subplots() ax.hist(amount_fraud, bins=100) ax.set_title("Amount of fraud transaction") -_ = ax.set_xlabel("Amount ($)") +_ = ax.set_xlabel("Amount (€)") # %% # Addressing the problem with a business metric @@ -501,8 +501,8 @@ def plot_roc_pr_curves(vanilla_model, tuned_model, *, title): # transaction result in a loss of the amount of the transaction. As stated in [2]_, the # gain and loss related to refusals (of fraudulent and legitimate transactions) are not # trivial to define. Here, we define that a refusal of a legitimate transaction is -# estimated to a loss of $5 while the refusal of a fraudulent transaction is estimated -# to a gain of $50 dollars and the amount of the transaction. Therefore, we define the +# estimated to a loss of 5€ while the refusal of a fraudulent transaction is estimated +# to a gain of 50€ and the amount of the transaction. Therefore, we define the # following function to compute the total benefit of a given decision: @@ -557,22 +557,22 @@ def business_metric(y_true, y_pred, amount): benefit_cost = business_scorer( easy_going_classifier, data_test, target_test, amount=amount_test ) -print(f"Benefit/cost of our easy-going classifier: ${benefit_cost:,.2f}") +print(f"Benefit/cost of our easy-going classifier: {benefit_cost:,.2f}€") # %% # A classifier that predict all transactions as legitimate would create a profit of -# around $220,000. We make the same evaluation for a classifier that predicts all +# around 220,000.€ We make the same evaluation for a classifier that predicts all # transactions as fraudulent. intolerant_classifier = DummyClassifier(strategy="constant", constant=1) intolerant_classifier.fit(data_train, target_train) benefit_cost = business_scorer( intolerant_classifier, data_test, target_test, amount=amount_test ) -print(f"Benefit/cost of our intolerant classifier: ${benefit_cost:,.2f}") +print(f"Benefit/cost of our intolerant classifier: {benefit_cost:,.2f}€") # %% -# Such a classifier create a loss of around $670,000. A predictive model should allow -# us to make a profit larger than $220,000. It is interesting to compare this business +# Such a classifier create a loss of around 670,000.€ A predictive model should allow +# us to make a profit larger than 220,000.€ It is interesting to compare this business # metric with another "standard" statistical metric such as the balanced accuracy. from sklearn.metrics import get_scorer @@ -607,7 +607,7 @@ def business_metric(y_true, y_pred, amount): print( "Benefit/cost of our logistic regression: " - f"${business_scorer(model, data_test, target_test, amount=amount_test):,.2f}" + f"{business_scorer(model, data_test, target_test, amount=amount_test):,.2f}€" ) print( "Balanced accuracy of our logistic regression: " @@ -645,7 +645,7 @@ def business_metric(y_true, y_pred, amount): # %% print( "Benefit/cost of our logistic regression: " - f"${business_scorer(tuned_model, data_test, target_test, amount=amount_test):,.2f}" + f"{business_scorer(tuned_model, data_test, target_test, amount=amount_test):,.2f}€" ) print( "Balanced accuracy of our logistic regression: " @@ -691,7 +691,7 @@ def business_metric(y_true, y_pred, amount): business_score = business_scorer( model_fixed_threshold, data_test, target_test, amount=amount_test ) -print(f"Benefit/cost of our logistic regression: ${business_score:,.2f}") +print(f"Benefit/cost of our logistic regression: {business_score:,.2f}€") print( "Balanced accuracy of our logistic regression: " f"{balanced_accuracy_scorer(model_fixed_threshold, data_test, target_test):.3f}" From 9f44f1ff3085467cbe268deb782333ad98084fcb Mon Sep 17 00:00:00 2001 From: Edoardo Abati <29585319+EdAbati@users.noreply.github.com> Date: Wed, 15 May 2024 12:26:15 +0200 Subject: [PATCH 120/344] ENH Add Array API compatibility to `mean_absolute_error` (#27736) --- doc/modules/array_api.rst | 1 + doc/whats_new/v1.6.rst | 1 + sklearn/metrics/_regression.py | 26 +++++++++++++++++++++----- sklearn/metrics/tests/test_common.py | 11 +++++++++++ 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/doc/modules/array_api.rst b/doc/modules/array_api.rst index dadae86689e08..3a21304a39a3e 100644 --- a/doc/modules/array_api.rst +++ b/doc/modules/array_api.rst @@ -106,6 +106,7 @@ Metrics ------- - :func:`sklearn.metrics.accuracy_score` +- :func:`sklearn.metrics.mean_absolute_error` - :func:`sklearn.metrics.mean_tweedie_deviance` - :func:`sklearn.metrics.r2_score` - :func:`sklearn.metrics.zero_one_loss` diff --git a/doc/whats_new/v1.6.rst b/doc/whats_new/v1.6.rst index 5000866b59c03..b4d26e07dffc0 100644 --- a/doc/whats_new/v1.6.rst +++ b/doc/whats_new/v1.6.rst @@ -35,6 +35,7 @@ See :ref:`array_api` for more details. - :func:`sklearn.metrics.mean_tweedie_deviance` now supports Array API compatible inputs. :pr:`28106` by :user:`Thomas Li ` +- :func:`sklearn.metrics.mean_absolute_error` :pr:`27736` by :user:`Edoardo Abati `. **Classes:** diff --git a/sklearn/metrics/_regression.py b/sklearn/metrics/_regression.py index 596a45dd3eaed..61bb1caa2d9da 100644 --- a/sklearn/metrics/_regression.py +++ b/sklearn/metrics/_regression.py @@ -189,7 +189,7 @@ def mean_absolute_error( Returns ------- - loss : float or ndarray of floats + 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 @@ -213,11 +213,19 @@ def mean_absolute_error( >>> mean_absolute_error(y_true, y_pred, multioutput=[0.3, 0.7]) 0.85... """ - y_type, y_true, y_pred, multioutput = _check_reg_targets( - y_true, y_pred, multioutput + input_arrays = [y_true, y_pred, sample_weight, multioutput] + xp, _ = get_namespace(*input_arrays) + + dtype = _find_matching_floating_dtype(y_true, y_pred, sample_weight, xp=xp) + + _, y_true, y_pred, multioutput = _check_reg_targets( + y_true, y_pred, multioutput, dtype=dtype, xp=xp ) check_consistent_length(y_true, y_pred, sample_weight) - output_errors = np.average(np.abs(y_pred - y_true), weights=sample_weight, axis=0) + + output_errors = _average( + xp.abs(y_pred - y_true), weights=sample_weight, axis=0, xp=xp + ) if isinstance(multioutput, str): if multioutput == "raw_values": return output_errors @@ -225,7 +233,15 @@ def mean_absolute_error( # pass None as weights to np.average: uniform mean multioutput = None - return np.average(output_errors, weights=multioutput) + # Average across the outputs (if needed). + mean_absolute_error = _average(output_errors, weights=multioutput) + + # Since `y_pred.ndim <= 2` and `y_true.ndim <= 2`, 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, irrespective of the + # Array API implementation. + assert mean_absolute_error.shape == () + return float(mean_absolute_error) @validate_params( diff --git a/sklearn/metrics/tests/test_common.py b/sklearn/metrics/tests/test_common.py index f00af5e160858..ae47ffe3d6a56 100644 --- a/sklearn/metrics/tests/test_common.py +++ b/sklearn/metrics/tests/test_common.py @@ -1879,6 +1879,13 @@ def check_array_api_regression_metric_multioutput( ) +def check_array_api_multioutput_regression_metric( + metric, array_namespace, device, dtype_name +): + metric = partial(metric, multioutput="raw_values") + check_array_api_regression_metric(metric, array_namespace, device, dtype_name) + + array_api_metric_checkers = { accuracy_score: [ check_array_api_binary_classification_metric, @@ -1893,6 +1900,10 @@ def check_array_api_regression_metric_multioutput( check_array_api_regression_metric, check_array_api_regression_metric_multioutput, ], + mean_absolute_error: [ + check_array_api_regression_metric, + check_array_api_multioutput_regression_metric, + ], } From 025d4b0baff58e7333ca88bb40691973cf09524d Mon Sep 17 00:00:00 2001 From: Guillaume Lemaitre Date: Wed, 15 May 2024 14:01:27 +0200 Subject: [PATCH 121/344] TST check compatibility with metadata routing for *ThresholdClassifier* (#29021) --- .../_classification_threshold.py | 19 ++++++++++------- sklearn/tests/metadata_routing_common.py | 20 ++++++++++++++---- .../test_metaestimators_metadata_routing.py | 21 +++++++++++++++++++ 3 files changed, 48 insertions(+), 12 deletions(-) diff --git a/sklearn/model_selection/_classification_threshold.py b/sklearn/model_selection/_classification_threshold.py index d5a864da10653..1f891577b4680 100644 --- a/sklearn/model_selection/_classification_threshold.py +++ b/sklearn/model_selection/_classification_threshold.py @@ -106,6 +106,14 @@ def __init__(self, estimator, *, response_method="auto"): self.estimator = estimator 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 + @_fit_context( # *ThresholdClassifier*.estimator is not validated yet prefer_skip_nested_validation=False @@ -140,11 +148,6 @@ def fit(self, X, y, **params): f"Only binary classification is supported. Unknown label type: {y_type}" ) - if self.response_method == "auto": - self._response_method = ["predict_proba", "decision_function"] - else: - self._response_method = self.response_method - self._fit(X, y, **params) if hasattr(self.estimator_, "n_features_in_"): @@ -374,7 +377,7 @@ def predict(self, X): y_score, _, response_method_used = _get_response_values_binary( self.estimator_, X, - self._response_method, + self._get_response_method(), pos_label=self.pos_label, return_response_method_used=True, ) @@ -954,7 +957,7 @@ def predict(self, X): y_score, _ = _get_response_values_binary( self.estimator_, X, - self._response_method, + self._get_response_method(), pos_label=pos_label, ) @@ -995,6 +998,6 @@ 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._response_method, self.thresholds + scoring, self._get_response_method(), self.thresholds ) return curve_scorer diff --git a/sklearn/tests/metadata_routing_common.py b/sklearn/tests/metadata_routing_common.py index 5091569e434a3..6fba2f037fd15 100644 --- a/sklearn/tests/metadata_routing_common.py +++ b/sklearn/tests/metadata_routing_common.py @@ -194,7 +194,10 @@ def decision_function(self, X): return self.predict(X) def predict(self, X): - return np.ones(len(X)) + y_pred = np.empty(shape=(len(X),)) + y_pred[: len(X) // 2] = 0 + y_pred[len(X) // 2 :] = 1 + return y_pred class NonConsumingRegressor(RegressorMixin, BaseEstimator): @@ -257,13 +260,19 @@ def predict(self, X, sample_weight="default", metadata="default"): record_metadata_not_default( self, "predict", sample_weight=sample_weight, metadata=metadata ) - return np.zeros(shape=(len(X),), dtype="int8") + 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"): record_metadata_not_default( self, "predict_proba", sample_weight=sample_weight, metadata=metadata ) - return np.asarray([[0.0, 1.0]] * len(X)) + y_proba = np.empty(shape=(len(X), 2)) + y_proba[: len(X) // 2, :] = np.asarray([1.0, 0.0]) + y_proba[len(X) // 2 :, :] = np.asarray([0.0, 1.0]) + return y_proba def predict_log_proba(self, X, sample_weight="default", metadata="default"): pass # pragma: no cover @@ -278,7 +287,10 @@ def decision_function(self, X, sample_weight="default", metadata="default"): record_metadata_not_default( self, "predict_proba", sample_weight=sample_weight, metadata=metadata ) - return np.zeros(shape=(len(X),)) + y_score = np.empty(shape=(len(X),)) + y_score[len(X) // 2 :] = 0 + y_score[: len(X) // 2] = 1 + return y_score # uncomment when needed # def score(self, X, y, sample_weight="default", metadata="default"): diff --git a/sklearn/tests/test_metaestimators_metadata_routing.py b/sklearn/tests/test_metaestimators_metadata_routing.py index 38168f3f0261f..8bfb7b0663c18 100644 --- a/sklearn/tests/test_metaestimators_metadata_routing.py +++ b/sklearn/tests/test_metaestimators_metadata_routing.py @@ -41,10 +41,12 @@ RidgeCV, ) from sklearn.model_selection import ( + FixedThresholdClassifier, GridSearchCV, HalvingGridSearchCV, HalvingRandomSearchCV, RandomizedSearchCV, + TunedThresholdClassifierCV, ) from sklearn.multiclass import ( OneVsOneClassifier, @@ -75,6 +77,7 @@ N, M = 100, 4 X = rng.rand(N, M) y = rng.randint(0, 3, size=N) +y_binary = (y >= 1).astype(int) classes = np.unique(y) y_multi = rng.randint(0, 3, size=(N, 3)) classes_multi = [np.unique(y_multi[:, i]) for i in range(y_multi.shape[1])] @@ -198,6 +201,24 @@ def enable_slep006(): "cv_name": "cv", "cv_routing_methods": ["fit"], }, + { + "metaestimator": FixedThresholdClassifier, + "estimator_name": "estimator", + "estimator": "classifier", + "X": X, + "y": y_binary, + "estimator_routing_methods": ["fit"], + "preserves_metadata": "subset", + }, + { + "metaestimator": TunedThresholdClassifierCV, + "estimator_name": "estimator", + "estimator": "classifier", + "X": X, + "y": y_binary, + "estimator_routing_methods": ["fit"], + "preserves_metadata": "subset", + }, { "metaestimator": OneVsRestClassifier, "estimator_name": "estimator", From 5c28a8e09a3956283f6e4ea6c5ece00fd33f0ae0 Mon Sep 17 00:00:00 2001 From: Christian Lorentzen Date: Wed, 15 May 2024 15:08:17 +0200 Subject: [PATCH 122/344] ENH use Scipy isotonic_regression (#28897) --- benchmarks/bench_isotonic.py | 6 +++--- sklearn/isotonic.py | 24 +++++++++++++++++------- sklearn/tests/test_isotonic.py | 32 +++++++++++++++++++------------- 3 files changed, 39 insertions(+), 23 deletions(-) diff --git a/benchmarks/bench_isotonic.py b/benchmarks/bench_isotonic.py index 556c452fa3323..be2ff6548cb92 100644 --- a/benchmarks/bench_isotonic.py +++ b/benchmarks/bench_isotonic.py @@ -13,7 +13,7 @@ import argparse import gc -from datetime import datetime +from timeit import default_timer import matplotlib.pyplot as plt import numpy as np @@ -52,9 +52,9 @@ def bench_isotonic_regression(Y): """ gc.collect() - tstart = datetime.now() + tstart = default_timer() isotonic_regression(Y) - return (datetime.now() - tstart).total_seconds() + return default_timer() - tstart if __name__ == "__main__": diff --git a/sklearn/isotonic.py b/sklearn/isotonic.py index 04456b1763791..f1c7f48966946 100644 --- a/sklearn/isotonic.py +++ b/sklearn/isotonic.py @@ -8,13 +8,14 @@ from numbers import Real import numpy as np -from scipy import interpolate +from scipy import interpolate, optimize from scipy.stats import spearmanr from ._isotonic import _inplace_contiguous_isotonic_regression, _make_unique from .base import BaseEstimator, RegressorMixin, TransformerMixin, _fit_context from .utils import check_array, check_consistent_length from .utils._param_validation import Interval, StrOptions, validate_params +from .utils.fixes import parse_version, sp_base_version from .utils.validation import _check_sample_weight, check_is_fitted __all__ = ["check_increasing", "isotonic_regression", "IsotonicRegression"] @@ -151,13 +152,22 @@ def isotonic_regression( array([2.75 , 2.75 , 2.75 , 2.75 , 7.33..., 7.33..., 7.33..., 7.33..., 7.33..., 7.33...]) """ - order = np.s_[:] if increasing else np.s_[::-1] y = check_array(y, ensure_2d=False, input_name="y", dtype=[np.float64, np.float32]) - 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]) + 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] - _inplace_contiguous_isotonic_regression(y, sample_weight) 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: @@ -165,7 +175,7 @@ def isotonic_regression( if y_max is None: y_max = np.inf np.clip(y, y_min, y_max, y) - return y[order] + return y class IsotonicRegression(RegressorMixin, TransformerMixin, BaseEstimator): diff --git a/sklearn/tests/test_isotonic.py b/sklearn/tests/test_isotonic.py index 93df0221236b8..90598b48f6434 100644 --- a/sklearn/tests/test_isotonic.py +++ b/sklearn/tests/test_isotonic.py @@ -227,7 +227,13 @@ def test_isotonic_regression_with_ties_in_differently_sized_groups(): 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)) @@ -502,25 +508,25 @@ def test_isotonic_copy_before_fit(): copy.copy(ir) -def test_isotonic_dtype(): +@pytest.mark.parametrize("dtype", [np.int32, np.int64, np.float32, np.float64]) +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 dtype in (np.int32, np.int64, np.float32, np.float64): - 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 + 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 + 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) - 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) + assert res.dtype == expected_dtype @pytest.mark.parametrize("y_dtype", [np.int32, np.int64, np.float32, np.float64]) From 25cb305e8c14d94fc33489c83cb4c5452e99f9c3 Mon Sep 17 00:00:00 2001 From: Aswathavicky Date: Thu, 16 May 2024 10:01:37 +0200 Subject: [PATCH 123/344] DOC add link to sklearn_example_ensemeble_plot_adboost_twoclass (#29023) --- sklearn/ensemble/_weight_boosting.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sklearn/ensemble/_weight_boosting.py b/sklearn/ensemble/_weight_boosting.py index 0461a397983be..6bbac0613de71 100644 --- a/sklearn/ensemble/_weight_boosting.py +++ b/sklearn/ensemble/_weight_boosting.py @@ -482,6 +482,10 @@ class AdaBoostClassifier( 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 seperable + classification dataset composed of two Gaussian quantiles clusters, please + refer to :ref:`sphx_glr_auto_examples_ensemble_plot_adaboost_twoclass.py`. """ # TODO(1.6): Modify _parameter_constraints for "algorithm" to only check From 945273d297c8c3dd578a9556b74b5e46d2270675 Mon Sep 17 00:00:00 2001 From: Tialo <65392801+Tialo@users.noreply.github.com> Date: Thu, 16 May 2024 11:11:54 +0300 Subject: [PATCH 124/344] DOC Fix default value of n in check_cv (#29024) --- sklearn/model_selection/_split.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sklearn/model_selection/_split.py b/sklearn/model_selection/_split.py index 53c11a665ccf4..1f9d78d3e4cbd 100644 --- a/sklearn/model_selection/_split.py +++ b/sklearn/model_selection/_split.py @@ -2596,7 +2596,7 @@ def check_cv(cv=5, y=None, *, classifier=False): Parameters ---------- - cv : int, cross-validation generator or an iterable, default=None + cv : int, cross-validation generator, iterable or None, default=5 Determines the cross-validation splitting strategy. Possible inputs for cv are: - None, to use the default 5-fold cross validation, From acd2d90e50852ba2afbf30b8a06b7a21431ee98a Mon Sep 17 00:00:00 2001 From: Omar Salman Date: Thu, 16 May 2024 18:47:35 +0500 Subject: [PATCH 125/344] ENH Array API support for LabelEncoder (#27381) Co-authored-by: Omar Salman Co-authored-by: Olivier Grisel --- doc/whats_new/v1.6.rst | 3 +- sklearn/preprocessing/_label.py | 23 ++-- sklearn/preprocessing/tests/test_label.py | 52 ++++++++- sklearn/utils/_array_api.py | 123 ++++++++++++++++++++++ sklearn/utils/_encode.py | 62 ++++++----- sklearn/utils/tests/test_array_api.py | 36 +++++++ 6 files changed, 261 insertions(+), 38 deletions(-) diff --git a/doc/whats_new/v1.6.rst b/doc/whats_new/v1.6.rst index b4d26e07dffc0..aff2ea2b011da 100644 --- a/doc/whats_new/v1.6.rst +++ b/doc/whats_new/v1.6.rst @@ -39,7 +39,8 @@ See :ref:`array_api` for more details. **Classes:** -- +- :class:`preprocessing.LabelEncoder` now supports Array API compatible inputs. + :pr:`27381` by :user:`Omar Salman `. Metadata Routing ---------------- diff --git a/sklearn/preprocessing/_label.py b/sklearn/preprocessing/_label.py index 301dc19bb1985..ecf0c400a2c2f 100644 --- a/sklearn/preprocessing/_label.py +++ b/sklearn/preprocessing/_label.py @@ -17,6 +17,7 @@ from ..base import BaseEstimator, TransformerMixin, _fit_context from ..utils import column_or_1d +from ..utils._array_api import _setdiff1d, device, get_namespace from ..utils._encode import _encode, _unique from ..utils._param_validation import Interval, validate_params from ..utils.multiclass import type_of_target, unique_labels @@ -129,10 +130,11 @@ def transform(self, y): Labels as normalized encodings. """ check_is_fitted(self) + xp, _ = get_namespace(y) y = column_or_1d(y, dtype=self.classes_.dtype, warn=True) # transform of empty array is empty array if _num_samples(y) == 0: - return np.array([]) + return xp.asarray([]) return _encode(y, uniques=self.classes_) @@ -141,7 +143,7 @@ def inverse_transform(self, y): Parameters ---------- - y : ndarray of shape (n_samples,) + y : array-like of shape (n_samples,) Target values. Returns @@ -150,19 +152,24 @@ def inverse_transform(self, y): Original encoding. """ check_is_fitted(self) + xp, _ = get_namespace(y) y = column_or_1d(y, warn=True) # inverse transform of empty array is empty array if _num_samples(y) == 0: - return np.array([]) + return xp.asarray([]) - diff = np.setdiff1d(y, np.arange(len(self.classes_))) - if len(diff): + diff = _setdiff1d( + ar1=y, + ar2=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)) - y = np.asarray(y) - return self.classes_[y] + y = xp.asarray(y) + return xp.take(self.classes_, y, axis=0) def _more_tags(self): - return {"X_types": ["1dlabels"]} + return {"X_types": ["1dlabels"], "array_api_support": True} class LabelBinarizer(TransformerMixin, BaseEstimator, auto_wrap_output_keys=None): diff --git a/sklearn/preprocessing/tests/test_label.py b/sklearn/preprocessing/tests/test_label.py index e438805df1254..90e3aa210eebb 100644 --- a/sklearn/preprocessing/tests/test_label.py +++ b/sklearn/preprocessing/tests/test_label.py @@ -2,7 +2,7 @@ import pytest from scipy.sparse import issparse -from sklearn import datasets +from sklearn import config_context, datasets from sklearn.preprocessing._label import ( LabelBinarizer, LabelEncoder, @@ -11,7 +11,16 @@ _inverse_binarize_thresholding, label_binarize, ) -from sklearn.utils._testing import assert_array_equal, ignore_warnings +from sklearn.utils._array_api import ( + _convert_to_numpy, + get_namespace, + yield_namespace_device_dtype_combinations, +) +from sklearn.utils._testing import ( + _array_api_for_tests, + assert_array_equal, + ignore_warnings, +) from sklearn.utils.fixes import ( COO_CONTAINERS, CSC_CONTAINERS, @@ -697,3 +706,42 @@ def test_label_encoders_do_not_have_set_output(encoder): 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) + + +@pytest.mark.parametrize( + "array_namespace, device, dtype", yield_namespace_device_dtype_combinations() +) +@pytest.mark.parametrize( + "y", + [ + np.array([2, 1, 3, 1, 3]), + np.array([1, 1, 4, 5, -1, 0]), + np.array([3, 5, 9, 5, 9, 3]), + ], +) +def test_label_encoder_array_api_compliance(y, array_namespace, device, dtype): + xp = _array_api_for_tests(array_namespace, device) + xp_y = xp.asarray(y, device=device) + with config_context(array_api_dispatch=True): + 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_label = LabelEncoder() + np_label = LabelEncoder() + xp_transformed = xp_label.fit_transform(xp_y) + np_transformed = np_label.fit_transform(y) + assert get_namespace(xp_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_label.classes_, xp), np_label.classes_) diff --git a/sklearn/utils/_array_api.py b/sklearn/utils/_array_api.py index a8b0363c0af38..8374dc35ff4f0 100644 --- a/sklearn/utils/_array_api.py +++ b/sklearn/utils/_array_api.py @@ -406,6 +406,11 @@ def unique_counts(self, x): def unique_values(self, x): return numpy.unique(x) + def unique_all(self, x): + return numpy.unique( + x, return_index=True, return_inverse=True, return_counts=True + ) + def concat(self, arrays, *, axis=None): return numpy.concatenate(arrays, axis=axis) @@ -839,3 +844,121 @@ def indexing_dtype(xp): # 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(xp, a, v, *, side="left", sorter=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 # noqa + 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 _setdiff1d(ar1, ar2, xp, assume_unique=False): + """Find the set difference of two arrays. + + Return the unique values in `ar1` that are not in `ar2`. + """ + if _is_numpy_namespace(xp): + return xp.asarray( + numpy.setdiff1d( + ar1=ar1, + ar2=ar2, + assume_unique=assume_unique, + ) + ) + + if assume_unique: + ar1 = xp.reshape(ar1, (-1,)) + else: + ar1 = xp.unique_values(ar1) + ar2 = xp.unique_values(ar2) + return ar1[_in1d(ar1=ar1, ar2=ar2, xp=xp, assume_unique=True, invert=True)] + + +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. + """ + if _is_numpy_namespace(xp): + 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, + ) + + +# Note: This is a helper for the functions `_isin` and +# `_setdiff1d`. It is not meant to be called directly. +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. + + 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 + """ + xp, _ = get_namespace(ar1, ar2, xp=xp) + + # This code is run to make the code significantly faster + 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)) + for a in ar2: + mask |= ar1 == a + return mask + + if not assume_unique: + ar1, rev_idx = xp.unique_inverse(ar1) + ar2 = xp.unique_values(ar2) + + ar = xp.concat((ar1, ar2)) + device_ = 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) + if invert: + bool_ar = sar[1:] != sar[:-1] + else: + bool_ar = sar[1:] == sar[:-1] + flag = xp.concat((bool_ar, xp.asarray([invert], device=device_))) + ret = xp.take(flag, reverse_order, axis=0) + + if assume_unique: + return ret[: ar1.shape[0]] + else: + return xp.take(ret, rev_idx, axis=0) diff --git a/sklearn/utils/_encode.py b/sklearn/utils/_encode.py index a468af43f857d..3fd4d45f522e6 100644 --- a/sklearn/utils/_encode.py +++ b/sklearn/utils/_encode.py @@ -4,6 +4,13 @@ import numpy as np +from ._array_api import ( + _isin, + _searchsorted, + _setdiff1d, + device, + get_namespace, +) from ._missing import is_scalar_nan @@ -51,31 +58,29 @@ def _unique(values, *, return_inverse=False, return_counts=False): 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.""" - uniques = np.unique( - values, return_inverse=return_inverse, return_counts=return_counts - ) + xp, _ = get_namespace(values) inverse, counts = None, None - if return_counts: - *uniques, counts = uniques - - if return_inverse: - *uniques, inverse = uniques - - if return_counts or return_inverse: - uniques = uniques[0] + 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 = np.searchsorted(uniques, np.nan) + nan_idx = _searchsorted(xp, uniques, xp.nan) uniques = uniques[: nan_idx + 1] if return_inverse: inverse[inverse > nan_idx] = nan_idx if return_counts: - counts[nan_idx] = np.sum(counts[nan_idx:]) + counts[nan_idx] = xp.sum(counts[nan_idx:]) counts = counts[: nan_idx + 1] ret = (uniques,) @@ -161,8 +166,9 @@ def __missing__(self, 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 np.array([table[v] for v in values]) + return xp.asarray([table[v] for v in values], device=device(values)) def _unique_python(values, *, return_inverse, return_counts): @@ -220,7 +226,8 @@ def _encode(values, *, uniques, check_unknown=True): encoded : ndarray Encoded values """ - if values.dtype.kind in "OUS": + xp, _ = get_namespace(values, uniques) + if not xp.isdtype(values.dtype, "numeric"): try: return _map_to_integer(values, uniques) except KeyError as e: @@ -230,7 +237,7 @@ def _encode(values, *, uniques, check_unknown=True): diff = _check_unknown(values, uniques) if diff: raise ValueError(f"y contains previously unseen labels: {str(diff)}") - return np.searchsorted(uniques, values) + return _searchsorted(xp, uniques, values) def _check_unknown(values, known_values, return_mask=False): @@ -258,9 +265,10 @@ def _check_unknown(values, known_values, return_mask=False): Additionally returned if ``return_mask=True``. """ + xp, _ = get_namespace(values, known_values) valid_mask = None - if values.dtype.kind in "OUS": + if not xp.isdtype(values.dtype, "numeric"): values_set = set(values) values_set, missing_in_values = _extract_missing(values_set) @@ -282,9 +290,9 @@ def is_valid(value): if return_mask: if diff or nan_in_diff or none_in_diff: - valid_mask = np.array([is_valid(value) for value in values]) + valid_mask = xp.array([is_valid(value) for value in values]) else: - valid_mask = np.ones(len(values), dtype=bool) + valid_mask = xp.ones(len(values), dtype=xp.bool) diff = list(diff) if none_in_diff: @@ -292,21 +300,21 @@ def is_valid(value): if nan_in_diff: diff.append(np.nan) else: - unique_values = np.unique(values) - diff = np.setdiff1d(unique_values, known_values, assume_unique=True) + unique_values = xp.unique_values(values) + diff = _setdiff1d(unique_values, known_values, xp, assume_unique=True) if return_mask: if diff.size: - valid_mask = np.isin(values, known_values) + valid_mask = _isin(values, known_values, xp) else: - valid_mask = np.ones(len(values), dtype=bool) + valid_mask = xp.ones(len(values), dtype=xp.bool) # check for nans in the known_values - if np.isnan(known_values).any(): - diff_is_nan = np.isnan(diff) - if diff_is_nan.any(): + 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 = np.isnan(values) + is_nan = xp.isnan(values) valid_mask[is_nan] = 1 # remove nan from diff diff --git a/sklearn/utils/tests/test_array_api.py b/sklearn/utils/tests/test_array_api.py index d0b368cd7fe91..30fc88c539fc8 100644 --- a/sklearn/utils/tests/test_array_api.py +++ b/sklearn/utils/tests/test_array_api.py @@ -15,6 +15,7 @@ _convert_to_numpy, _estimator_with_converted_arrays, _is_numpy_namespace, + _isin, _nanmax, _nanmin, _NumPyAPIWrapper, @@ -27,6 +28,7 @@ ) from sklearn.utils._testing import ( _array_api_for_tests, + assert_array_equal, skip_if_array_api_compat_not_configured, ) from sklearn.utils.fixes import _IS_32BIT @@ -504,3 +506,37 @@ def test_indexing_dtype(namespace, _device, _dtype): assert indexing_dtype(xp) == xp.int32 else: assert indexing_dtype(xp) == xp.int64 + + +@pytest.mark.parametrize( + "array_namespace, device, _", yield_namespace_device_dtype_combinations() +) +@pytest.mark.parametrize("invert", [True, False]) +@pytest.mark.parametrize("assume_unique", [True, False]) +@pytest.mark.parametrize("element_size", [6, 10, 14]) +@pytest.mark.parametrize("int_dtype", ["int16", "int32", "int64", "uint8"]) +def test_isin( + array_namespace, device, _, invert, assume_unique, element_size, int_dtype +): + xp = _array_api_for_tests(array_namespace, device) + 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, + ) + with config_context(array_api_dispatch=True): + result = _isin( + element=element_xp, + test_elements=test_elements_xp, + xp=xp, + assume_unique=assume_unique, + invert=invert, + ) + + assert_array_equal(_convert_to_numpy(result, xp=xp), expected) From e82550268fce38b38c09d90483cdef8c9bde846f Mon Sep 17 00:00:00 2001 From: raisadz <34237447+raisadz@users.noreply.github.com> Date: Fri, 17 May 2024 09:13:51 +0100 Subject: [PATCH 126/344] DOC replace pandas with Polars in examples/gaussian_process/plot_gpr_co2.py (#28804) Co-authored-by: raisa <> Co-authored-by: Adrin Jalali --- ...latest_conda_forge_mkl_linux-64_conda.lock | 6 +-- ...pylatest_conda_forge_mkl_osx-64_conda.lock | 2 +- ...pylatest_pip_scipy_dev_linux-64_conda.lock | 2 +- .../pymin_conda_forge_mkl_win-64_conda.lock | 2 +- ...e_openblas_ubuntu_2204_linux-64_conda.lock | 4 +- build_tools/azure/pypy3_linux-64_conda.lock | 34 +++++++-------- build_tools/circle/doc_linux-64_conda.lock | 8 ++-- .../doc_min_dependencies_environment.yml | 2 +- .../doc_min_dependencies_linux-64_conda.lock | 10 ++--- examples/gaussian_process/plot_gpr_co2.py | 41 ++++++++++--------- pyproject.toml | 4 +- sklearn/_min_dependencies.py | 2 +- sklearn/tests/test_base.py | 2 +- 13 files changed, 61 insertions(+), 58 deletions(-) diff --git a/build_tools/azure/pylatest_conda_forge_mkl_linux-64_conda.lock b/build_tools/azure/pylatest_conda_forge_mkl_linux-64_conda.lock index 3d895fda71bc3..bf5bcd3daff08 100644 --- a/build_tools/azure/pylatest_conda_forge_mkl_linux-64_conda.lock +++ b/build_tools/azure/pylatest_conda_forge_mkl_linux-64_conda.lock @@ -91,7 +91,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.45.3-h2797004_0.cond https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.0-h0841786_0.conda#1f5a58e686b13bcfde88b93f547d23fe https://conda.anaconda.org/conda-forge/linux-64/libvorbis-1.3.7-h9c3ff4c_0.tar.bz2#309dec04b70a3cc0f1e84a4013683bc0 https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.15-h0b41bf4_0.conda#33277193f5b92bad9fdd230eb700929c -https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.12.6-h232c23b_2.conda#9a3a42df8a95f65334dfc7b80da1195d +https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.12.7-hc051c1a_0.conda#5d801a4906adc712d480afc362623b59 https://conda.anaconda.org/conda-forge/linux-64/mysql-common-8.3.0-hf1915f5_4.conda#784a4df6676c581ca624fbe460703a6d https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.43-hcad00b1_0.conda#8292dea9e022d9610a11fce5e0896ed8 https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda#47d31b792659ce70f470b5c82fdfb7a4 @@ -191,7 +191,7 @@ https://conda.anaconda.org/conda-forge/linux-64/sip-6.7.12-py311hb755f60_0.conda https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.3.14-hf3aad02_1.conda#a968ffa7e9fe0c257628033d393e512f https://conda.anaconda.org/conda-forge/linux-64/blas-1.0-mkl.tar.bz2#349aef876b1d8c9dccae01de20d5b385 https://conda.anaconda.org/conda-forge/linux-64/gstreamer-1.24.3-haf2f30d_0.conda#f3df87cc9ef0b5113bff55aefcbcafd5 -https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-8.4.0-h3d44ed6_0.conda#27f46291a6aaa3c2a4f798ebd35a7ddb +https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-8.5.0-hfac3d4d_0.conda#f5126317dd0ce0ba26945e411ecc6960 https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-16_linux64_mkl.tar.bz2#85f61af03fd291dae33150ffe89dc09a https://conda.anaconda.org/conda-forge/linux-64/libsystemd0-255-h3516f8a_1.conda#3366af27f0b593544a6cd453c7932ac5 https://conda.anaconda.org/conda-forge/noarch/meson-python-0.16.0-pyh0c530f3_0.conda#e16f0dbf502da873be9f9adb0dc52547 @@ -210,7 +210,7 @@ https://conda.anaconda.org/conda-forge/noarch/array-api-strict-1.1.1-pyhd8ed1ab_ https://conda.anaconda.org/conda-forge/linux-64/contourpy-1.2.1-py311h9547e67_0.conda#74ad0ae64f1ef565e27eda87fa749e84 https://conda.anaconda.org/conda-forge/linux-64/libarrow-12.0.1-hb87d912_8_cpu.conda#3f3b11398fe79b578e3c44dd00a44e4a https://conda.anaconda.org/conda-forge/linux-64/pandas-2.2.2-py311h320fe9a_0.conda#c79e96ece4110fdaf2657c9f8e16f749 -https://conda.anaconda.org/conda-forge/linux-64/polars-0.20.25-py311h00856b1_0.conda#84ad7fa8742f6d34784a961337622c55 +https://conda.anaconda.org/conda-forge/linux-64/polars-0.20.26-py311h00856b1_0.conda#d9002441c9b75b188f9cdc51bf4f22c7 https://conda.anaconda.org/conda-forge/linux-64/pyqt-5.15.9-py311hf0fb5b6_5.conda#ec7e45bc76d9d0b69a74a2075932b8e8 https://conda.anaconda.org/conda-forge/linux-64/pytorch-1.13.1-cpu_py311h410fd25_1.conda#ddd2fadddf89e3dc3d541a2537fce010 https://conda.anaconda.org/conda-forge/linux-64/scipy-1.13.0-py311h517d4fd_1.conda#a86b8bea39e292a23b2cf9a750f49ea1 diff --git a/build_tools/azure/pylatest_conda_forge_mkl_osx-64_conda.lock b/build_tools/azure/pylatest_conda_forge_mkl_osx-64_conda.lock index ce2d5e2c383a3..c0e54faa37bc6 100644 --- a/build_tools/azure/pylatest_conda_forge_mkl_osx-64_conda.lock +++ b/build_tools/azure/pylatest_conda_forge_mkl_osx-64_conda.lock @@ -28,7 +28,7 @@ https://conda.anaconda.org/conda-forge/osx-64/libcxx-17.0.6-h88467a6_0.conda#0fe https://conda.anaconda.org/conda-forge/osx-64/libpng-1.6.43-h92b6c6a_0.conda#65dcddb15965c9de2c0365cb14910532 https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.45.3-h92b6c6a_0.conda#68e462226209f35182ef66eda0f794ff https://conda.anaconda.org/conda-forge/osx-64/libxcb-1.15-hb7f2c08_0.conda#5513f57e0238c87c12dffedbcc9c1a4a -https://conda.anaconda.org/conda-forge/osx-64/libxml2-2.12.6-hc0ae0f7_2.conda#50b997370584f2c83ca0c38e9028eab9 +https://conda.anaconda.org/conda-forge/osx-64/libxml2-2.12.7-h3e169fe_0.conda#4c04ba47fdd2ebecc1d3b6a77534d9ef https://conda.anaconda.org/conda-forge/osx-64/llvm-openmp-18.1.5-h39e0ece_0.conda#ee12a644568269838b91f901b2537425 https://conda.anaconda.org/conda-forge/osx-64/openssl-3.3.0-hd75f5a5_0.conda#eb8c33aa7929a7714eab8b90c1d88afe https://conda.anaconda.org/conda-forge/osx-64/readline-8.2-h9e318b2_1.conda#f17f77f2acf4d344734bda76829ce14e diff --git a/build_tools/azure/pylatest_pip_scipy_dev_linux-64_conda.lock b/build_tools/azure/pylatest_pip_scipy_dev_linux-64_conda.lock index c1a50c7c8c140..e4305c97b76bc 100644 --- a/build_tools/azure/pylatest_pip_scipy_dev_linux-64_conda.lock +++ b/build_tools/azure/pylatest_pip_scipy_dev_linux-64_conda.lock @@ -40,7 +40,7 @@ https://repo.anaconda.com/pkgs/main/linux-64/pip-24.0-py312h06a4308_0.conda#6d96 # pip meson @ https://files.pythonhosted.org/packages/33/75/b1a37fa7b2dbca8c0dbb04d5cdd7e2720c8ef6febe41b4a74866350e041c/meson-1.4.0-py3-none-any.whl#sha256=476a458d51fcfa322a6bdc64da5138997c542d08e6b2e49b9fa68c46fd7c4475 # pip ninja @ https://files.pythonhosted.org/packages/6d/92/8d7aebd4430ab5ff65df2bfee6d5745f95c004284db2d8ca76dcbfd9de47/ninja-1.11.1.1-py2.py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl#sha256=84502ec98f02a037a169c4b0d5d86075eaf6afc55e1879003d6cab51ced2ea4b # pip packaging @ https://files.pythonhosted.org/packages/49/df/1fceb2f8900f8639e278b056416d49134fb8d84c5942ffaa01ad34782422/packaging-24.0-py3-none-any.whl#sha256=2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5 -# pip platformdirs @ https://files.pythonhosted.org/packages/b0/15/1691fa5aaddc0c4ea4901c26f6137c29d5f6673596fe960a0340e8c308e1/platformdirs-4.2.1-py3-none-any.whl#sha256=17d5a1161b3fd67b390023cb2d3b026bbd40abde6fdb052dfbd3a29c3ba22ee1 +# pip platformdirs @ https://files.pythonhosted.org/packages/68/13/2aa1f0e1364feb2c9ef45302f387ac0bd81484e9c9a4c5688a322fbdfd08/platformdirs-4.2.2-py3-none-any.whl#sha256=2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee # pip pluggy @ https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl#sha256=44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 # pip pygments @ https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl#sha256=b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a # pip six @ https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl#sha256=8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 diff --git a/build_tools/azure/pymin_conda_forge_mkl_win-64_conda.lock b/build_tools/azure/pymin_conda_forge_mkl_win-64_conda.lock index d95e56378ae56..8f0a473c031ca 100644 --- a/build_tools/azure/pymin_conda_forge_mkl_win-64_conda.lock +++ b/build_tools/azure/pymin_conda_forge_mkl_win-64_conda.lock @@ -39,7 +39,7 @@ https://conda.anaconda.org/conda-forge/win-64/libbrotlienc-1.1.0-hcfcfb64_1.cond https://conda.anaconda.org/conda-forge/win-64/libintl-0.22.5-h5728263_2.conda#aa622c938af057adc119f8b8eecada01 https://conda.anaconda.org/conda-forge/win-64/libpng-1.6.43-h19919ed_0.conda#77e398acc32617a0384553aea29e866b https://conda.anaconda.org/conda-forge/win-64/libvorbis-1.3.7-h0e60522_0.tar.bz2#e1a22282de0169c93e4ffe6ce6acc212 -https://conda.anaconda.org/conda-forge/win-64/libxml2-2.12.6-hc3477c8_2.conda#ac7af7a949db01dae61ddc48f4a93d79 +https://conda.anaconda.org/conda-forge/win-64/libxml2-2.12.7-h283a6d9_0.conda#1451be68a5549561979125c1827b79ed https://conda.anaconda.org/conda-forge/win-64/m2w64-gcc-libs-5.3.0-7.tar.bz2#fe759119b8b3bfa720b8762c6fdc35de https://conda.anaconda.org/conda-forge/win-64/pcre2-10.43-h17e33f8_0.conda#d0485b8aa2cedb141a7bd27b4efa4c9c https://conda.anaconda.org/conda-forge/win-64/python-3.9.19-h4de0772_0_cpython.conda#b6999bc275e0e6beae7b1c8ea0be1e85 diff --git a/build_tools/azure/pymin_conda_forge_openblas_ubuntu_2204_linux-64_conda.lock b/build_tools/azure/pymin_conda_forge_openblas_ubuntu_2204_linux-64_conda.lock index 231cd528ecd0e..1a4d0feae1773 100644 --- a/build_tools/azure/pymin_conda_forge_openblas_ubuntu_2204_linux-64_conda.lock +++ b/build_tools/azure/pymin_conda_forge_openblas_ubuntu_2204_linux-64_conda.lock @@ -71,7 +71,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.43-h2797004_0.conda#0 https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.45.3-h2797004_0.conda#b3316cbe90249da4f8e84cd66e1cc55b https://conda.anaconda.org/conda-forge/linux-64/libvorbis-1.3.7-h9c3ff4c_0.tar.bz2#309dec04b70a3cc0f1e84a4013683bc0 https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.15-h0b41bf4_0.conda#33277193f5b92bad9fdd230eb700929c -https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.12.6-h232c23b_2.conda#9a3a42df8a95f65334dfc7b80da1195d +https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.12.7-hc051c1a_0.conda#5d801a4906adc712d480afc362623b59 https://conda.anaconda.org/conda-forge/linux-64/mysql-common-8.3.0-hf1915f5_4.conda#784a4df6676c581ca624fbe460703a6d https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.43-hcad00b1_0.conda#8292dea9e022d9610a11fce5e0896ed8 https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda#47d31b792659ce70f470b5c82fdfb7a4 @@ -175,7 +175,7 @@ https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0-pyhd8ed1ab_0 https://conda.anaconda.org/conda-forge/linux-64/sip-6.7.12-py39h3d6467e_0.conda#e667a3ab0df62c54e60e1843d2e6defb https://conda.anaconda.org/conda-forge/noarch/urllib3-2.2.1-pyhd8ed1ab_0.conda#08807a87fa7af10754d46f63b368e016 https://conda.anaconda.org/conda-forge/linux-64/gstreamer-1.24.3-haf2f30d_0.conda#f3df87cc9ef0b5113bff55aefcbcafd5 -https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-8.4.0-h3d44ed6_0.conda#27f46291a6aaa3c2a4f798ebd35a7ddb +https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-8.5.0-hfac3d4d_0.conda#f5126317dd0ce0ba26945e411ecc6960 https://conda.anaconda.org/conda-forge/noarch/importlib-resources-6.4.0-pyhd8ed1ab_0.conda#dcbadab7a68738a028e195ab68ab2d2e https://conda.anaconda.org/conda-forge/linux-64/liblapacke-3.9.0-22_linux64_openblas.conda#1fd156abd41a4992835952f6f4d951d0 https://conda.anaconda.org/conda-forge/linux-64/libsystemd0-255-h3516f8a_1.conda#3366af27f0b593544a6cd453c7932ac5 diff --git a/build_tools/azure/pypy3_linux-64_conda.lock b/build_tools/azure/pypy3_linux-64_conda.lock index 23710cfe35cb8..ab6a908edf340 100644 --- a/build_tools/azure/pypy3_linux-64_conda.lock +++ b/build_tools/azure/pypy3_linux-64_conda.lock @@ -4,24 +4,24 @@ @EXPLICIT https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2024.2.2-hbcca054_0.conda#2f4327a1cbe7f022401b236e915a5fef -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-13.2.0-h7e041cc_5.conda#f6f6600d18a4047b54f803cf708b868a +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-13.2.0-hc0a3c3a_7.conda#53ebd4c833fa01cb2c6353e99f905406 https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.9-4_pypy39_pp73.conda#c1b2f29111681a4036ed21eaa3f44620 https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda#161081fc7cec0bfda0d86d7cb595f8d8 https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_kmp_llvm.tar.bz2#562b26ba2e19059551a811e72ab7f793 -https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-h807b86a_5.conda#d4ff227c46917d3b4565302a2bbb276b +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-h77fa898_7.conda#72ec1b1b04c4d15d4204ece1ecea5978 https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hd590300_5.conda#69b8b6202a07720f448be700e300ccf4 https://conda.anaconda.org/conda-forge/linux-64/lerc-4.0.0-h27087fc_0.tar.bz2#76bbff344f0134279f225174e9064c8f https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.1.0-hd590300_1.conda#aec6c91c7371c26392a06708a73c70e5 https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.20-hd590300_0.conda#8e88f9389f1165d7c0936fe40d9a9a79 https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.6.2-h59595ed_0.conda#e7ba12deb7020dd080c6c70e7b6f6a3d https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.2-h7f98852_5.tar.bz2#d645c6d2ac96843a2bfaccd2d62b3ac3 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-13.2.0-ha4646dd_5.conda#7a6bd7a12a4bd359e2afe6c0fa1acace +https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-13.2.0-hca663fb_7.conda#c0bd771f09a326fdcd95a60b617795bf https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.0.0-hd590300_1.conda#ea25936bb4080d843790b586850f82b8 -https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.3.2-hd590300_1.conda#049b7df8bae5e184d1de42cdf64855f8 +https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.4.0-hd590300_0.conda#b26e8aa824079e1be0294e7152ca4559 https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.2.13-hd590300_5.conda#f36c115f1ee199da648e0597ec2047ad -https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.4.20240210-h59595ed_0.conda#97da8860a0da5413c7c98a3b3838a645 -https://conda.anaconda.org/conda-forge/linux-64/ninja-1.11.1-h924138e_0.conda#73a4953a2d9c115bdc10ff30a52f675f -https://conda.anaconda.org/conda-forge/linux-64/openssl-3.2.1-hd590300_1.conda#9d731343cff6ee2e5a25c4a091bf8e2a +https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h59595ed_0.conda#fcea371545eda051b6deafb24889fc69 +https://conda.anaconda.org/conda-forge/linux-64/ninja-1.12.1-h297d8ca_0.conda#3aa1c7e292afeff25a0091ddd7c69b72 +https://conda.anaconda.org/conda-forge/linux-64/openssl-3.3.0-hd590300_0.conda#c0f3abb4a16477208bbd43a39bd56f18 https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-h36c2ea0_1001.tar.bz2#22dad4df6e8630e8dff2428f6f6a7036 https://conda.anaconda.org/conda-forge/linux-64/xorg-kbproto-1.0.7-h7f98852_1002.tar.bz2#4b230e8381279d76131116660f5a241a https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.11-hd590300_0.conda#2c80dc38fface310c9bd81b17037fee5 @@ -32,22 +32,22 @@ https://conda.anaconda.org/conda-forge/linux-64/xz-5.2.6-h166bdaf_0.tar.bz2#2161 https://conda.anaconda.org/conda-forge/linux-64/expat-2.6.2-h59595ed_0.conda#53fb86322bdb89496d7579fe3f02fd61 https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.1.0-hd590300_1.conda#f07002e225d7a60a694d42a7bf5ff53f https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.1.0-hd590300_1.conda#5fc11c6020d421960607d821310fcd4d -https://conda.anaconda.org/conda-forge/linux-64/libgfortran-ng-13.2.0-h69a702a_5.conda#e73e9cfd1191783392131e6238bdb3e9 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-ng-13.2.0-h69a702a_7.conda#1b84f26d9f4f6026e179e7805d5a15cd https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.43-h2797004_0.conda#009981dd9cfcaa4dbfa25ffaed86bcae -https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.45.2-h2797004_0.conda#866983a220e27a80cb75e85cb30466a1 +https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.45.3-h2797004_0.conda#b3316cbe90249da4f8e84cd66e1cc55b https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.15-h0b41bf4_0.conda#33277193f5b92bad9fdd230eb700929c https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda#47d31b792659ce70f470b5c82fdfb7a4 https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda#d453b98d9c83e71da0741bb0ff4d76bc https://conda.anaconda.org/conda-forge/linux-64/zlib-1.2.13-hd590300_5.conda#68c34ec6149623be41a1933ab996a209 -https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.5-hfc55251_0.conda#04b88013080254850d6c01ed54810589 +https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.6-ha6fb4c9_0.conda#4d056880988120e29d75bfff282e0f45 https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.1.0-hd590300_1.conda#39f910d205726805a958da408ca194ba https://conda.anaconda.org/conda-forge/linux-64/freetype-2.12.1-h267a509_2.conda#9ae35c3d96db2c94ce0cef86efdfa2cb https://conda.anaconda.org/conda-forge/linux-64/gdbm-1.18-h0a1914f_2.tar.bz2#b77bc399b07a19c00fe12fdc95ee0297 https://conda.anaconda.org/conda-forge/linux-64/libhiredis-1.0.2-h2cc385e_0.tar.bz2#b34907d3a81a3cd8095ee83d174c074a https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.27-pthreads_h413a1c8_0.conda#a356024784da6dfd4683dc5ecf45b155 https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.6.0-h1dd3fc0_3.conda#66f03896ffbe1a110ffda05c7a856504 -https://conda.anaconda.org/conda-forge/linux-64/llvm-openmp-18.1.3-h4dfa4b3_0.conda#d39965123dffcad4d750989be65bcb7c -https://conda.anaconda.org/conda-forge/linux-64/sqlite-3.45.2-h2c6b66d_0.conda#1423efca06ed343c1da0fc429bae0779 +https://conda.anaconda.org/conda-forge/linux-64/llvm-openmp-18.1.5-ha31de31_0.conda#b923cdb6e567ada84f991ffcc5848afb +https://conda.anaconda.org/conda-forge/linux-64/sqlite-3.45.3-h2c6b66d_0.conda#be7d70f2db41b674733667bdd69bd000 https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.9-h8ee46fc_0.conda#077b6e8ad6a3ddb741fce2496dd01bec https://conda.anaconda.org/conda-forge/linux-64/brotli-1.1.0-hd590300_1.conda#f27a24d46e3ea7b70a1f98e50c62508f https://conda.anaconda.org/conda-forge/linux-64/ccache-4.9.1-h1fcd64f_0.conda#3620f564bcf28c3524951b6f64f5c5ac @@ -72,12 +72,12 @@ https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyh9f0ad1d_0.tar.bz2 https://conda.anaconda.org/conda-forge/linux-64/numpy-1.26.4-py39h6dedee3_0.conda#557d64563e84ff21b14f586c7f662b7f https://conda.anaconda.org/conda-forge/noarch/packaging-24.0-pyhd8ed1ab_0.conda#248f521b64ce055e7feae3105e7abeb8 https://conda.anaconda.org/conda-forge/linux-64/pillow-10.3.0-py39h90a76f3_0.conda#799e6519cfffe2784db27b1db2ef33f3 -https://conda.anaconda.org/conda-forge/noarch/pluggy-1.4.0-pyhd8ed1ab_0.conda#139e9feb65187e916162917bb2484976 +https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_0.conda#d3483c8fc2dc2cc3f5cf43e26d60cabf https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.1.2-pyhd8ed1ab_0.conda#b9a4dacf97241704529131a0dfc0494f https://conda.anaconda.org/conda-forge/noarch/pypy-7.3.15-1_pypy39.conda#a418a6c16bd6f7ed56b92194214791a0 https://conda.anaconda.org/conda-forge/noarch/setuptools-69.5.1-pyhd8ed1ab_0.conda#7462280d81f639363e6e63c81276bd9e https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2#e5f25f8dbc060e9a8d912e432202afc2 -https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.4.0-pyhc1e730c_0.conda#b296278eef667c673bf51de6535bad88 +https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.5.0-pyhc1e730c_0.conda#df68d78237980a159bd7149f33c0e8fd https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2#5844808ffab9ebdb694585b50ba02a96 https://conda.anaconda.org/conda-forge/linux-64/tornado-6.4-py39hf860d4a_0.conda#e7fded713fb466e1e0670afce1761b47 https://conda.anaconda.org/conda-forge/linux-64/unicodedata2-15.1.0-py39hf860d4a_0.conda#f699157518d28d00c87542b4ec1273be @@ -87,16 +87,16 @@ https://conda.anaconda.org/conda-forge/linux-64/blas-devel-3.9.0-22_linux64_open https://conda.anaconda.org/conda-forge/linux-64/contourpy-1.2.1-py39ha90811c_0.conda#07ed14c8326da42356514bcbc0b04802 https://conda.anaconda.org/conda-forge/linux-64/fonttools-4.51.0-py39hf860d4a_0.conda#63421b4dd7222fad555e34ec9af015a1 https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.4.0-pyhd8ed1ab_0.conda#c5d3907ad8bd7bf557521a1833cf7e6d -https://conda.anaconda.org/conda-forge/noarch/joblib-1.4.0-pyhd8ed1ab_0.conda#e0ed1bf13ce3a440e022157bf4764465 +https://conda.anaconda.org/conda-forge/noarch/joblib-1.4.2-pyhd8ed1ab_0.conda#25df261d4523d9f9783bcdb7208d872f https://conda.anaconda.org/conda-forge/noarch/meson-1.4.0-pyhd8ed1ab_0.conda#52a0660cfa40b45bf254ecc3374cb2e0 https://conda.anaconda.org/conda-forge/noarch/pip-24.0-pyhd8ed1ab_0.conda#f586ac1e56c8638b64f9c8122a7b8a67 -https://conda.anaconda.org/conda-forge/noarch/pyproject-metadata-0.7.1-pyhd8ed1ab_0.conda#dcb27826ffc94d5f04e241322239983b +https://conda.anaconda.org/conda-forge/noarch/pyproject-metadata-0.8.0-pyhd8ed1ab_0.conda#573fe09d7bd0cd4bcc210d8369b5ca47 https://conda.anaconda.org/conda-forge/noarch/pytest-7.4.4-pyhd8ed1ab_0.conda#a9d145de8c5f064b5fa68fb34725d9f4 https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0-pyhd8ed1ab_0.conda#2cf4264fffb9e6eff6031c5b6884d61c https://conda.anaconda.org/conda-forge/linux-64/scipy-1.12.0-py39h6dedee3_2.conda#6c5d74bac41838f4377dfd45085e1fec https://conda.anaconda.org/conda-forge/linux-64/blas-2.122-openblas.conda#5065468105542a8b23ea47bd8b6fa55f https://conda.anaconda.org/conda-forge/noarch/importlib-resources-6.4.0-pyhd8ed1ab_0.conda#dcbadab7a68738a028e195ab68ab2d2e -https://conda.anaconda.org/conda-forge/noarch/meson-python-0.15.0-pyh0c530f3_0.conda#3bc64565ca78ce3bb80248d09926d8f9 +https://conda.anaconda.org/conda-forge/noarch/meson-python-0.16.0-pyh0c530f3_0.conda#e16f0dbf502da873be9f9adb0dc52547 https://conda.anaconda.org/conda-forge/linux-64/pyamg-5.1.0-py39h5fd064f_0.conda#04676d2a49da3cb608af77e04b796ce1 https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.5.0-pyhd8ed1ab_0.conda#d5f595da2daead898ca958ac62f0307b https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.8.4-py39h4e7d633_0.conda#58272019e595dde98d0844ae3ebf0cfe diff --git a/build_tools/circle/doc_linux-64_conda.lock b/build_tools/circle/doc_linux-64_conda.lock index e2584c2d27333..34ec64ad5863b 100644 --- a/build_tools/circle/doc_linux-64_conda.lock +++ b/build_tools/circle/doc_linux-64_conda.lock @@ -95,7 +95,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.43-h2797004_0.conda#0 https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.45.3-h2797004_0.conda#b3316cbe90249da4f8e84cd66e1cc55b https://conda.anaconda.org/conda-forge/linux-64/libvorbis-1.3.7-h9c3ff4c_0.tar.bz2#309dec04b70a3cc0f1e84a4013683bc0 https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.15-h0b41bf4_0.conda#33277193f5b92bad9fdd230eb700929c -https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.12.6-h232c23b_2.conda#9a3a42df8a95f65334dfc7b80da1195d +https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.12.7-hc051c1a_0.conda#5d801a4906adc712d480afc362623b59 https://conda.anaconda.org/conda-forge/linux-64/mysql-common-8.3.0-hf1915f5_4.conda#784a4df6676c581ca624fbe460703a6d https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.43-hcad00b1_0.conda#8292dea9e022d9610a11fce5e0896ed8 https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda#47d31b792659ce70f470b5c82fdfb7a4 @@ -165,7 +165,7 @@ https://conda.anaconda.org/conda-forge/noarch/networkx-3.2.1-pyhd8ed1ab_0.conda# https://conda.anaconda.org/conda-forge/linux-64/openblas-0.3.27-pthreads_h7a3da1a_0.conda#4b422ebe8fc6a5320d0c1c22e5a46032 https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.2-h488ebb8_0.conda#7f2e286780f072ed750df46dc2631138 https://conda.anaconda.org/conda-forge/noarch/packaging-24.0-pyhd8ed1ab_0.conda#248f521b64ce055e7feae3105e7abeb8 -https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.2.1-pyhd8ed1ab_0.conda#d478a8a3044cdff1aa6e62f9269cefe0 +https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.2.2-pyhd8ed1ab_0.conda#6f6cf28bf8e021933869bae3f84b8fc9 https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_0.conda#d3483c8fc2dc2cc3f5cf43e26d60cabf https://conda.anaconda.org/conda-forge/noarch/ply-3.11-pyhd8ed1ab_2.conda#18c6deb6f9602e32446398203c8f0e91 https://conda.anaconda.org/conda-forge/linux-64/psutil-5.9.8-py39hd1e30aa_0.conda#ec86403fde8793ac1c36f8afa3d15902 @@ -220,7 +220,7 @@ https://conda.anaconda.org/conda-forge/linux-64/sip-6.7.12-py39h3d6467e_0.conda# https://conda.anaconda.org/conda-forge/noarch/urllib3-2.2.1-pyhd8ed1ab_0.conda#08807a87fa7af10754d46f63b368e016 https://conda.anaconda.org/conda-forge/linux-64/compilers-1.7.0-ha770c72_1.conda#d8d07866ac3b5b6937213c89a1874f08 https://conda.anaconda.org/conda-forge/linux-64/gstreamer-1.24.3-haf2f30d_0.conda#f3df87cc9ef0b5113bff55aefcbcafd5 -https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-8.4.0-h3d44ed6_0.conda#27f46291a6aaa3c2a4f798ebd35a7ddb +https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-8.5.0-hfac3d4d_0.conda#f5126317dd0ce0ba26945e411ecc6960 https://conda.anaconda.org/conda-forge/noarch/importlib-resources-6.4.0-pyhd8ed1ab_0.conda#dcbadab7a68738a028e195ab68ab2d2e https://conda.anaconda.org/conda-forge/noarch/lazy_loader-0.4-pyhd8ed1ab_0.conda#a284ff318fbdb0dd83928275b4b6087c https://conda.anaconda.org/conda-forge/linux-64/liblapacke-3.9.0-22_linux64_openblas.conda#1fd156abd41a4992835952f6f4d951d0 @@ -237,7 +237,7 @@ https://conda.anaconda.org/conda-forge/linux-64/imagecodecs-2024.1.1-py39ha98d97 https://conda.anaconda.org/conda-forge/noarch/imageio-2.34.1-pyh4b66e23_0.conda#bcf6a6f4c6889ca083e8d33afbafb8d5 https://conda.anaconda.org/conda-forge/linux-64/pandas-2.2.2-py39hddac248_0.conda#259c4e76e6bda8888aefc098ae1ba749 https://conda.anaconda.org/conda-forge/noarch/patsy-0.5.6-pyhd8ed1ab_0.conda#a5b55d1cb110cdcedc748b5c3e16e687 -https://conda.anaconda.org/conda-forge/linux-64/polars-0.20.25-py39ha963410_0.conda#d14227f0e141af743374d845fd4f5ccd +https://conda.anaconda.org/conda-forge/linux-64/polars-0.20.26-py39ha963410_0.conda#d138679a254e4e0918cfc1114c928bb8 https://conda.anaconda.org/conda-forge/noarch/pooch-1.8.1-pyhd8ed1ab_0.conda#d15917f33140f8d2ac9ca44db7ec8a25 https://conda.anaconda.org/conda-forge/linux-64/pulseaudio-client-17.0-hb77b528_0.conda#07f45f1be1c25345faddb8db0de8039b https://conda.anaconda.org/conda-forge/linux-64/pywavelets-1.4.1-py39h44dd56e_1.conda#d037c20e3da2e85f03ebd20ad480c359 diff --git a/build_tools/circle/doc_min_dependencies_environment.yml b/build_tools/circle/doc_min_dependencies_environment.yml index 298a60e8ec4ff..14f4485295455 100644 --- a/build_tools/circle/doc_min_dependencies_environment.yml +++ b/build_tools/circle/doc_min_dependencies_environment.yml @@ -30,7 +30,7 @@ dependencies: - numpydoc=1.2.0 # min - sphinx-prompt=1.3.0 # min - plotly=5.14.0 # min - - polars=0.19.12 # min + - polars=0.20.23 # min - pooch - pip - pip: diff --git a/build_tools/circle/doc_min_dependencies_linux-64_conda.lock b/build_tools/circle/doc_min_dependencies_linux-64_conda.lock index e08a14c235079..043587152c63b 100644 --- a/build_tools/circle/doc_min_dependencies_linux-64_conda.lock +++ b/build_tools/circle/doc_min_dependencies_linux-64_conda.lock @@ -1,6 +1,6 @@ # Generated by conda-lock. # platform: linux-64 -# input_hash: 32601810330a8200864f7908d07d870a3a58931be4f833691b2b5c7937f2d330 +# input_hash: 08b61aae27c59a8d35d008fa2f947440f3cbcbc41622112e33e68f90d69b621c @EXPLICIT https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2024.2.2-hbcca054_0.conda#2f4327a1cbe7f022401b236e915a5fef @@ -80,7 +80,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.43-h2797004_0.conda#0 https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.45.3-h2797004_0.conda#b3316cbe90249da4f8e84cd66e1cc55b https://conda.anaconda.org/conda-forge/linux-64/libvorbis-1.3.7-h9c3ff4c_0.tar.bz2#309dec04b70a3cc0f1e84a4013683bc0 https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.15-h0b41bf4_0.conda#33277193f5b92bad9fdd230eb700929c -https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.12.6-h232c23b_2.conda#9a3a42df8a95f65334dfc7b80da1195d +https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.12.7-hc051c1a_0.conda#5d801a4906adc712d480afc362623b59 https://conda.anaconda.org/conda-forge/linux-64/mysql-common-8.3.0-hf1915f5_4.conda#784a4df6676c581ca624fbe460703a6d https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.43-hcad00b1_0.conda#8292dea9e022d9610a11fce5e0896ed8 https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda#47d31b792659ce70f470b5c82fdfb7a4 @@ -146,7 +146,7 @@ https://conda.anaconda.org/conda-forge/linux-64/markupsafe-2.1.5-py39hd1e30aa_0. https://conda.anaconda.org/conda-forge/noarch/networkx-3.2-pyhd8ed1ab_0.conda#cec8cc498664cc00a070676aa89e69a7 https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.2-h488ebb8_0.conda#7f2e286780f072ed750df46dc2631138 https://conda.anaconda.org/conda-forge/noarch/packaging-24.0-pyhd8ed1ab_0.conda#248f521b64ce055e7feae3105e7abeb8 -https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.2.1-pyhd8ed1ab_0.conda#d478a8a3044cdff1aa6e62f9269cefe0 +https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.2.2-pyhd8ed1ab_0.conda#6f6cf28bf8e021933869bae3f84b8fc9 https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_0.conda#d3483c8fc2dc2cc3f5cf43e26d60cabf https://conda.anaconda.org/conda-forge/noarch/ply-3.11-pyhd8ed1ab_2.conda#18c6deb6f9602e32446398203c8f0e91 https://conda.anaconda.org/conda-forge/linux-64/psutil-5.9.8-py39hd1e30aa_0.conda#ec86403fde8793ac1c36f8afa3d15902 @@ -199,7 +199,7 @@ https://conda.anaconda.org/conda-forge/linux-64/sip-6.7.12-py39h3d6467e_0.conda# https://conda.anaconda.org/conda-forge/noarch/urllib3-2.2.1-pyhd8ed1ab_0.conda#08807a87fa7af10754d46f63b368e016 https://conda.anaconda.org/conda-forge/linux-64/compilers-1.7.0-ha770c72_1.conda#d8d07866ac3b5b6937213c89a1874f08 https://conda.anaconda.org/conda-forge/linux-64/gstreamer-1.24.3-haf2f30d_0.conda#f3df87cc9ef0b5113bff55aefcbcafd5 -https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-8.4.0-h3d44ed6_0.conda#27f46291a6aaa3c2a4f798ebd35a7ddb +https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-8.5.0-hfac3d4d_0.conda#f5126317dd0ce0ba26945e411ecc6960 https://conda.anaconda.org/conda-forge/noarch/importlib_metadata-7.1.0-hd8ed1ab_0.conda#6ef2b72d291b39e479d7694efa2b2b98 https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-22_linux64_mkl.conda#eb6deb4ba6f92ea3f31c09cb8b764738 https://conda.anaconda.org/conda-forge/linux-64/libsystemd0-255-h3516f8a_1.conda#3366af27f0b593544a6cd453c7932ac5 @@ -223,7 +223,7 @@ https://conda.anaconda.org/conda-forge/noarch/imageio-2.34.1-pyh4b66e23_0.conda# https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.3.4-py39h2fa2bec_0.tar.bz2#9ec0b2186fab9121c54f4844f93ee5b7 https://conda.anaconda.org/conda-forge/linux-64/pandas-1.1.5-py39hde0f152_0.tar.bz2#79fc4b5b3a865b90dd3701cecf1ad33c https://conda.anaconda.org/conda-forge/noarch/patsy-0.5.6-pyhd8ed1ab_0.conda#a5b55d1cb110cdcedc748b5c3e16e687 -https://conda.anaconda.org/conda-forge/linux-64/polars-0.19.12-py39h90d8ae4_0.conda#191828961c95f8d59fa2b86a590f9905 +https://conda.anaconda.org/conda-forge/linux-64/polars-0.20.23-py39ha963410_0.conda#4871f09d653e979d598d2d4cd5fa868d https://conda.anaconda.org/conda-forge/linux-64/pyqt-5.15.9-py39h52134e7_5.conda#e1f148e57d071b09187719df86f513c1 https://conda.anaconda.org/conda-forge/linux-64/pywavelets-1.3.0-py39hd257fcd_1.tar.bz2#c4b698994b2d8d2e659ae02202e6abe4 https://conda.anaconda.org/conda-forge/linux-64/scipy-1.6.0-py39hee8e79c_0.tar.bz2#3afcb78281836e61351a2924f3230060 diff --git a/examples/gaussian_process/plot_gpr_co2.py b/examples/gaussian_process/plot_gpr_co2.py index 33b0ab7271549..b3da30daa0f6d 100644 --- a/examples/gaussian_process/plot_gpr_co2.py +++ b/examples/gaussian_process/plot_gpr_co2.py @@ -33,24 +33,25 @@ # We will derive a dataset from the Mauna Loa Observatory that collected air # samples. We are interested in estimating the concentration of CO2 and # extrapolate it for further year. First, we load the original dataset available -# in OpenML. +# in OpenML as a pandas dataframe. This will be replaced with Polars +# once `fetch_openml` adds a native support for it. from sklearn.datasets import fetch_openml co2 = fetch_openml(data_id=41187, as_frame=True) co2.frame.head() # %% -# First, we process the original dataframe to create a date index and select -# only the CO2 column. -import pandas as pd +# First, we process the original dataframe to create a date column and select +# it along with the CO2 column. +import polars as pl -co2_data = co2.frame -co2_data["date"] = pd.to_datetime(co2_data[["year", "month", "day"]]) -co2_data = co2_data[["date", "co2"]].set_index("date") +co2_data = pl.DataFrame(co2.frame[["year", "month", "day", "co2"]]).select( + pl.date("year", "month", "day"), "co2" +) co2_data.head() # %% -co2_data.index.min(), co2_data.index.max() +co2_data["date"].min(), co2_data["date"].max() # %% # We see that we get CO2 concentration for some days from March, 1958 to @@ -58,7 +59,8 @@ # understanding. import matplotlib.pyplot as plt -co2_data.plot() +plt.plot(co2_data["date"], co2_data["co2"]) +plt.xlabel("date") plt.ylabel("CO$_2$ concentration (ppm)") _ = plt.title("Raw air samples measurements from the Mauna Loa Observatory") @@ -67,15 +69,14 @@ # for which no measurements were collected. Such a processing will have an # smoothing effect on the data. -try: - co2_data_resampled_monthly = co2_data.resample("ME") -except ValueError: - # pandas < 2.2 uses M instead of ME - co2_data_resampled_monthly = co2_data.resample("M") - - -co2_data = co2_data_resampled_monthly.mean().dropna(axis="index", how="any") -co2_data.plot() +co2_data = ( + co2_data.sort(by="date") + .group_by_dynamic("date", every="1mo") + .agg(pl.col("co2").mean()) + .drop_nulls() +) +plt.plot(co2_data["date"], co2_data["co2"]) +plt.xlabel("date") plt.ylabel("Monthly average of CO$_2$ concentration (ppm)") _ = plt.title( "Monthly average of air samples measurements\nfrom the Mauna Loa Observatory" @@ -88,7 +89,9 @@ # # As a first step, we will divide the data and the target to estimate. The data # being a date, we will convert it into a numeric. -X = (co2_data.index.year + co2_data.index.month / 12).to_numpy().reshape(-1, 1) +X = co2_data.select( + pl.col("date").dt.year() + pl.col("date").dt.month() / 12 +).to_numpy() y = co2_data["co2"].to_numpy() # %% diff --git a/pyproject.toml b/pyproject.toml index 468a6a1aaf53a..0c4e4c17c68e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,7 +62,7 @@ docs = [ "sphinx-prompt>=1.3.0", "sphinxext-opengraph>=0.4.2", "plotly>=5.14.0", - "polars>=0.19.12" + "polars>=0.20.23" ] examples = [ "matplotlib>=3.3.4", @@ -82,7 +82,7 @@ tests = [ "black>=24.3.0", "mypy>=1.9", "pyamg>=4.0.0", - "polars>=0.19.12", + "polars>=0.20.23", "pyarrow>=12.0.0", "numpydoc>=1.2.0", "pooch>=1.6.0", diff --git a/sklearn/_min_dependencies.py b/sklearn/_min_dependencies.py index 00315f31d4c3f..0b1a96748a588 100644 --- a/sklearn/_min_dependencies.py +++ b/sklearn/_min_dependencies.py @@ -33,7 +33,7 @@ "black": ("24.3.0", "tests"), "mypy": ("1.9", "tests"), "pyamg": ("4.0.0", "tests"), - "polars": ("0.19.12", "docs, tests"), + "polars": ("0.20.23", "docs, tests"), "pyarrow": ("12.0.0", "tests"), "sphinx": ("6.0.0", "docs"), "sphinx-copybutton": ("0.5.2", "docs"), diff --git a/sklearn/tests/test_base.py b/sklearn/tests/test_base.py index 3bbc236e703df..a1cd3b8fc8c7b 100644 --- a/sklearn/tests/test_base.py +++ b/sklearn/tests/test_base.py @@ -834,7 +834,7 @@ class Estimator(BaseEstimator, WithSlots): [ ("dataframe", "1.5.0"), ("pyarrow", "12.0.0"), - ("polars", "0.19.12"), + ("polars", "0.20.23"), ], ) def test_dataframe_protocol(constructor_name, minversion): From b461547fc2f089c5f21a233c17304034f1258d8f Mon Sep 17 00:00:00 2001 From: Edoardo Abati <29585319+EdAbati@users.noreply.github.com> Date: Fri, 17 May 2024 12:37:59 +0200 Subject: [PATCH 127/344] ENH Add Array API compatibility to `cosine_similarity` (#29014) --- doc/modules/array_api.rst | 1 + doc/whats_new/v1.6.rst | 6 ++- sklearn/metrics/pairwise.py | 11 ++++- sklearn/metrics/tests/test_common.py | 64 ++++++++++++++++++---------- 4 files changed, 56 insertions(+), 26 deletions(-) diff --git a/doc/modules/array_api.rst b/doc/modules/array_api.rst index 3a21304a39a3e..310df6b12a6ec 100644 --- a/doc/modules/array_api.rst +++ b/doc/modules/array_api.rst @@ -108,6 +108,7 @@ Metrics - :func:`sklearn.metrics.accuracy_score` - :func:`sklearn.metrics.mean_absolute_error` - :func:`sklearn.metrics.mean_tweedie_deviance` +- :func:`sklearn.metrics.pairwise.cosine_similarity`` - :func:`sklearn.metrics.r2_score` - :func:`sklearn.metrics.zero_one_loss` diff --git a/doc/whats_new/v1.6.rst b/doc/whats_new/v1.6.rst index aff2ea2b011da..601868a9a9581 100644 --- a/doc/whats_new/v1.6.rst +++ b/doc/whats_new/v1.6.rst @@ -34,8 +34,10 @@ See :ref:`array_api` for more details. - :func:`sklearn.metrics.mean_tweedie_deviance` now supports Array API compatible inputs. - :pr:`28106` by :user:`Thomas Li ` -- :func:`sklearn.metrics.mean_absolute_error` :pr:`27736` by :user:`Edoardo Abati `. + :pr:`28106` by :user:`Thomas Li `; +- :func:`sklearn.metrics.mean_absolute_error` :pr:`27736` by :user:`Edoardo Abati `; +- :func:`sklearn.metrics.pairwise.cosine_similarity` :pr:`29014` by :user:`Edoardo Abati `. + **Classes:** diff --git a/sklearn/metrics/pairwise.py b/sklearn/metrics/pairwise.py index d30c1775823a5..ff158825cc0f9 100644 --- a/sklearn/metrics/pairwise.py +++ b/sklearn/metrics/pairwise.py @@ -25,6 +25,11 @@ gen_batches, gen_even_slices, ) +from ..utils._array_api import ( + _find_matching_floating_dtype, + _is_numpy_namespace, + get_namespace, +) from ..utils._chunking import get_chunk_n_rows from ..utils._mask import _get_mask from ..utils._missing import is_scalar_nan @@ -154,7 +159,11 @@ def check_pairwise_arrays( 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. """ - X, Y, dtype_float = _return_float_dtype(X, Y) + 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": diff --git a/sklearn/metrics/tests/test_common.py b/sklearn/metrics/tests/test_common.py index ae47ffe3d6a56..9e94b9241de7a 100644 --- a/sklearn/metrics/tests/test_common.py +++ b/sklearn/metrics/tests/test_common.py @@ -51,6 +51,7 @@ zero_one_loss, ) from sklearn.metrics._base import _average_binary_score +from sklearn.metrics.pairwise import cosine_similarity from sklearn.preprocessing import LabelBinarizer from sklearn.utils import shuffle from sklearn.utils._array_api import ( @@ -1743,20 +1744,22 @@ def test_metrics_pos_label_error_str(metric, y_pred_threshold, dtype_y_str): def check_array_api_metric( - metric, array_namespace, device, dtype_name, y_true_np, y_pred_np, sample_weight + metric, array_namespace, device, dtype_name, a_np, b_np, **metric_kwargs ): xp = _array_api_for_tests(array_namespace, device) - y_true_xp = xp.asarray(y_true_np, device=device) - y_pred_xp = xp.asarray(y_pred_np, device=device) + a_xp = xp.asarray(a_np, device=device) + b_xp = xp.asarray(b_np, device=device) - metric_np = metric(y_true_np, y_pred_np, sample_weight=sample_weight) + metric_np = metric(a_np, b_np, **metric_kwargs) - if sample_weight is not None: - sample_weight = xp.asarray(sample_weight, device=device) + if metric_kwargs.get("sample_weight") is not None: + metric_kwargs["sample_weight"] = xp.asarray( + metric_kwargs["sample_weight"], device=device + ) with config_context(array_api_dispatch=True): - metric_xp = metric(y_true_xp, y_pred_xp, sample_weight=sample_weight) + metric_xp = metric(a_xp, b_xp, **metric_kwargs) assert_allclose( _convert_to_numpy(xp.asarray(metric_xp), xp), @@ -1776,8 +1779,8 @@ def check_array_api_binary_classification_metric( array_namespace, device, dtype_name, - y_true_np=y_true_np, - y_pred_np=y_pred_np, + a_np=y_true_np, + b_np=y_pred_np, sample_weight=None, ) @@ -1788,8 +1791,8 @@ def check_array_api_binary_classification_metric( array_namespace, device, dtype_name, - y_true_np=y_true_np, - y_pred_np=y_pred_np, + a_np=y_true_np, + b_np=y_pred_np, sample_weight=sample_weight, ) @@ -1805,8 +1808,8 @@ def check_array_api_multiclass_classification_metric( array_namespace, device, dtype_name, - y_true_np=y_true_np, - y_pred_np=y_pred_np, + a_np=y_true_np, + b_np=y_pred_np, sample_weight=None, ) @@ -1817,8 +1820,8 @@ def check_array_api_multiclass_classification_metric( array_namespace, device, dtype_name, - y_true_np=y_true_np, - y_pred_np=y_pred_np, + a_np=y_true_np, + b_np=y_pred_np, sample_weight=sample_weight, ) @@ -1832,8 +1835,8 @@ def check_array_api_regression_metric(metric, array_namespace, device, dtype_nam array_namespace, device, dtype_name, - y_true_np=y_true_np, - y_pred_np=y_pred_np, + a_np=y_true_np, + b_np=y_pred_np, sample_weight=None, ) @@ -1844,8 +1847,8 @@ def check_array_api_regression_metric(metric, array_namespace, device, dtype_nam array_namespace, device, dtype_name, - y_true_np=y_true_np, - y_pred_np=y_pred_np, + a_np=y_true_np, + b_np=y_pred_np, sample_weight=sample_weight, ) @@ -1861,8 +1864,8 @@ def check_array_api_regression_metric_multioutput( array_namespace, device, dtype_name, - y_true_np=y_true_np, - y_pred_np=y_pred_np, + a_np=y_true_np, + b_np=y_pred_np, sample_weight=None, ) @@ -1873,8 +1876,8 @@ def check_array_api_regression_metric_multioutput( array_namespace, device, dtype_name, - y_true_np=y_true_np, - y_pred_np=y_pred_np, + a_np=y_true_np, + b_np=y_pred_np, sample_weight=sample_weight, ) @@ -1886,6 +1889,20 @@ def check_array_api_multioutput_regression_metric( check_array_api_regression_metric(metric, array_namespace, device, dtype_name) +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) + + metric_kwargs = {} + if "dense_output" in signature(metric).parameters: + metric_kwargs["dense_output"] = True + + check_array_api_metric( + metric, array_namespace, device, dtype_name, a_np=X_np, b_np=Y_np + ) + + array_api_metric_checkers = { accuracy_score: [ check_array_api_binary_classification_metric, @@ -1900,6 +1917,7 @@ def check_array_api_multioutput_regression_metric( check_array_api_regression_metric, check_array_api_regression_metric_multioutput, ], + cosine_similarity: [check_array_api_metric_pairwise], mean_absolute_error: [ check_array_api_regression_metric, check_array_api_multioutput_regression_metric, From 77fc72c39834ddc3ff404e0eb0306406801bc4a8 Mon Sep 17 00:00:00 2001 From: Stefanie Senger <91849487+StefanieSenger@users.noreply.github.com> Date: Fri, 17 May 2024 14:44:37 +0200 Subject: [PATCH 128/344] FEA SLEP006: Metadata routing for `learning_curve` (#28975) --- doc/metadata_routing.rst | 4 +- doc/whats_new/v1.6.rst | 4 + sklearn/model_selection/_validation.py | 133 ++++++++++++++---- .../model_selection/tests/test_validation.py | 93 ++++++++---- sklearn/utils/_metadata_requests.py | 5 +- sklearn/utils/validation.py | 2 +- 6 files changed, 182 insertions(+), 59 deletions(-) diff --git a/doc/metadata_routing.rst b/doc/metadata_routing.rst index 0ada6ef6c4dbe..27000a192ab21 100644 --- a/doc/metadata_routing.rst +++ b/doc/metadata_routing.rst @@ -292,6 +292,7 @@ Meta-estimators and functions supporting metadata routing: - :class:`sklearn.linear_model.LogisticRegressionCV` - :class:`sklearn.linear_model.MultiTaskElasticNetCV` - :class:`sklearn.linear_model.MultiTaskLassoCV` +- :class:`sklearn.linear_model.OrthogonalMatchingPursuitCV` - :class:`sklearn.linear_model.RANSACRegressor` - :class:`sklearn.linear_model.RidgeClassifierCV` - :class:`sklearn.linear_model.RidgeCV` @@ -302,13 +303,13 @@ Meta-estimators and functions supporting metadata routing: - :func:`sklearn.model_selection.cross_validate` - :func:`sklearn.model_selection.cross_val_score` - :func:`sklearn.model_selection.cross_val_predict` +- :class:`sklearn.model_selection.learning_curve` - :class:`sklearn.multiclass.OneVsOneClassifier` - :class:`sklearn.multiclass.OneVsRestClassifier` - :class:`sklearn.multiclass.OutputCodeClassifier` - :class:`sklearn.multioutput.ClassifierChain` - :class:`sklearn.multioutput.MultiOutputClassifier` - :class:`sklearn.multioutput.MultiOutputRegressor` -- :class:`sklearn.linear_model.OrthogonalMatchingPursuitCV` - :class:`sklearn.multioutput.RegressorChain` - :class:`sklearn.pipeline.FeatureUnion` - :class:`sklearn.pipeline.Pipeline` @@ -321,7 +322,6 @@ Meta-estimators and tools not supporting metadata routing yet: - :class:`sklearn.feature_selection.RFE` - :class:`sklearn.feature_selection.RFECV` - :class:`sklearn.feature_selection.SequentialFeatureSelector` -- :class:`sklearn.model_selection.learning_curve` - :class:`sklearn.model_selection.permutation_test_score` - :class:`sklearn.model_selection.validation_curve` - :class:`sklearn.semi_supervised.SelfTrainingClassifier` diff --git a/doc/whats_new/v1.6.rst b/doc/whats_new/v1.6.rst index 601868a9a9581..0e6844155c6fa 100644 --- a/doc/whats_new/v1.6.rst +++ b/doc/whats_new/v1.6.rst @@ -51,6 +51,10 @@ The following models now support metadata routing in one or more of their methods. Refer to the :ref:`Metadata Routing User Guide ` for more details. +- |Feature| :func:`model_selection.learning_curve` now supports metadata routing for the + `fit` method of its estimator and for its underlying CV splitter and scorer. + :pr:`28975` by :user:`Stefanie Senger `. + - |Feature| :class:`ensemble.StackingClassifier` and :class:`ensemble.StackingRegressor` now support metadata routing and pass ``**fit_params`` to the underlying estimators via their `fit` methods. diff --git a/sklearn/model_selection/_validation.py b/sklearn/model_selection/_validation.py index 176627ace91d4..83d289d36efb2 100644 --- a/sklearn/model_selection/_validation.py +++ b/sklearn/model_selection/_validation.py @@ -58,21 +58,22 @@ ] -def _check_params_groups_deprecation(fit_params, params, groups): +def _check_params_groups_deprecation(fit_params, params, groups, version): """A helper function to check deprecations on `groups` and `fit_params`. - To be removed when set_config(enable_metadata_routing=False) is not possible. + # 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: raise ValueError( "`params` and `fit_params` cannot both be provided. Pass parameters " "via `params`. `fit_params` is deprecated and will be removed in " - "version 1.6." + f"version {version}." ) elif fit_params is not None: warnings.warn( ( - "`fit_params` is deprecated and will be removed in version 1.6. " + "`fit_params` is deprecated and will be removed in version {version}. " "Pass parameters via `params` instead." ), FutureWarning, @@ -346,7 +347,7 @@ def cross_validate( >>> print(scores['train_r2']) [0.28009951 0.3908844 0.22784907] """ - params = _check_params_groups_deprecation(fit_params, params, groups) + params = _check_params_groups_deprecation(fit_params, params, groups, "1.6") X, y = indexable(X, y) @@ -602,10 +603,8 @@ def cross_val_score( ``cross_val_score(..., params={'groups': groups})``. scoring : str or callable, default=None - A str (see model evaluation documentation) or - a scorer callable object / function with signature - ``scorer(estimator, X, y)`` which should return only - a single value. + A str (see :ref:`scoring_parameter`) or a scorer callable object / function with + signature ``scorer(estimator, X, y)`` which should return only a single value. Similar to :func:`cross_validate` but only a single metric is permitted. @@ -1206,7 +1205,7 @@ def cross_val_predict( >>> lasso = linear_model.Lasso() >>> y_pred = cross_val_predict(lasso, X, y, cv=3) """ - params = _check_params_groups_deprecation(fit_params, params, groups) + params = _check_params_groups_deprecation(fit_params, params, groups, "1.6") X, y = indexable(X, y) if _routing_enabled(): @@ -1718,6 +1717,7 @@ def _shuffle(y, groups, random_state): "error_score": [StrOptions({"raise"}), Real], "return_times": ["boolean"], "fit_params": [dict, None], + "params": [dict, None], }, prefer_skip_nested_validation=False, # estimator is not validated yet ) @@ -1739,6 +1739,7 @@ def learning_curve( error_score=np.nan, return_times=False, fit_params=None, + params=None, ): """Learning curve. @@ -1773,6 +1774,13 @@ def learning_curve( train/test set. Only used in conjunction with a "Group" :term:`cv` instance (e.g., :class:`GroupKFold`). + .. versionchanged:: 1.6 + ``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.: + ``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 @@ -1780,7 +1788,7 @@ def learning_curve( 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 + Note that for classification the number of samples usually has to be big enough to contain at least one sample from each class. cv : int, cross-validation generator or an iterable, default=None @@ -1804,9 +1812,8 @@ def learning_curve( ``cv`` default value if None changed from 3-fold to 5-fold. scoring : str or callable, default=None - A str (see model evaluation documentation) or - a scorer callable object / function with signature - ``scorer(estimator, X, y)``. + A str (see :ref:`scoring_parameter`) or a scorer callable object / function with + signature ``scorer(estimator, X, y)``. exploit_incremental_learning : bool, default=False If the estimator supports incremental learning, this will be @@ -1849,7 +1856,22 @@ def learning_curve( fit_params : dict, default=None Parameters to pass to the fit method of the estimator. - .. versionadded:: 0.24 + .. deprecated:: 1.6 + This parameter is deprecated and will be removed in version 1.6. Use + ``params`` instead. + + params : dict, default=None + Parameters to pass to the `fit` method of the estimator and to the scorer. + + - 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. + See :ref:`Metadata Routing User Guide ` for more + details. + + .. versionadded:: 1.6 Returns ------- @@ -1903,14 +1925,69 @@ def learning_curve( "An estimator must support the partial_fit interface " "to exploit incremental learning" ) + + params = _check_params_groups_deprecation(fit_params, params, groups, "1.8") + X, y, groups = indexable(X, y, groups) cv = check_cv(cv, y, classifier=is_classifier(estimator)) - # Store it as list as we will be iterating over the list multiple times - cv_iter = list(cv.split(X, y, groups)) scorer = check_scoring(estimator, scoring=scoring) + if _routing_enabled(): + router = ( + MetadataRouter(owner="learning_curve") + .add( + estimator=estimator, + # TODO(SLEP6): also pass metadata to the predict method for + # scoring? + method_mapping=MethodMapping() + .add(caller="fit", callee="fit") + .add(caller="fit", callee="partial_fit"), + ) + .add( + splitter=cv, + method_mapping=MethodMapping().add(caller="fit", callee="split"), + ) + .add( + scorer=scorer, + method_mapping=MethodMapping().add(caller="fit", callee="score"), + ) + ) + + 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. + unrequested_params = sorted(e.unrequested_params) + raise UnsetMetadataPassedError( + message=( + f"{unrequested_params} are passed to `learning_curve` but are not" + " explicitly set as requested or not requested for learning_curve's" + f" estimator: {estimator.__class__.__name__}. Call" + " `.set_fit_request({{metadata}}=True)` on the estimator for" + f" each metadata in {unrequested_params} that you" + " want to use and `metadata=False` for not using it. See the" + " Metadata Routing User guide" + " for more" + " information." + ), + unrequested_params=e.unrequested_params, + routed_params=e.routed_params, + ) + + else: + routed_params = Bunch() + routed_params.estimator = Bunch(fit=params, partial_fit=params) + routed_params.splitter = Bunch(split={"groups": groups}) + routed_params.scorer = Bunch(score={}) + + # 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 @@ -1940,7 +2017,8 @@ def learning_curve( scorer, return_times, error_score=error_score, - fit_params=fit_params, + fit_params=routed_params.estimator.partial_fit, + score_params=routed_params.scorer.score, ) for train, test in cv_iter ) @@ -1961,9 +2039,8 @@ def learning_curve( test=test, verbose=verbose, parameters=None, - fit_params=fit_params, - # TODO(SLEP6): support score params here - score_params=None, + fit_params=routed_params.estimator.fit, + score_params=routed_params.scorer.score, return_train_score=True, error_score=error_score, return_times=return_times, @@ -2069,6 +2146,7 @@ def _incremental_fit_estimator( return_times, error_score, fit_params, + score_params, ): """Train estimator on training subsets incrementally and compute scores.""" train_scores, test_scores, fit_times, score_times = [], [], [], [] @@ -2079,6 +2157,9 @@ def _incremental_fit_estimator( partial_fit_func = partial(estimator.partial_fit, **fit_params) else: partial_fit_func = partial(estimator.partial_fit, classes=classes, **fit_params) + 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) for n_train_samples, partial_train in partitions: train_subset = train[:n_train_samples] @@ -2095,14 +2176,13 @@ def _incremental_fit_estimator( start_score = time.time() - # TODO(SLEP6): support score params in the following two calls test_scores.append( _score( estimator, X_test, y_test, scorer, - score_params=None, + score_params=score_params_test, error_score=error_score, ) ) @@ -2112,7 +2192,7 @@ def _incremental_fit_estimator( X_train, y_train, scorer, - score_params=None, + score_params=score_params_train, error_score=error_score, ) ) @@ -2220,9 +2300,8 @@ def validation_curve( ``cv`` default value if None changed from 3-fold to 5-fold. scoring : str or callable, default=None - A str (see model evaluation documentation) or - a scorer callable object / function with signature - ``scorer(estimator, X, y)``. + A str (see :ref:`scoring_parameter`) or a scorer callable object / function with + signature ``scorer(estimator, X, y)``. n_jobs : int, default=None Number of jobs to run in parallel. Training the estimator and computing diff --git a/sklearn/model_selection/tests/test_validation.py b/sklearn/model_selection/tests/test_validation.py index a1a860b243249..679c0052e3956 100644 --- a/sklearn/model_selection/tests/test_validation.py +++ b/sklearn/model_selection/tests/test_validation.py @@ -1535,7 +1535,7 @@ def test_learning_curve_with_shuffle(): ) -def test_learning_curve_fit_params(): +def test_learning_curve_params(): X = np.arange(100).reshape(10, 10) y = np.array([0] * 5 + [1] * 5) clf = CheckingClassifier(expected_sample_weight=True) @@ -1547,14 +1547,14 @@ def test_learning_curve_fit_params(): err_msg = r"sample_weight.shape == \(1,\), expected \(2,\)!" with pytest.raises(ValueError, match=err_msg): learning_curve( - clf, X, y, error_score="raise", fit_params={"sample_weight": np.ones(1)} + clf, X, y, error_score="raise", params={"sample_weight": np.ones(1)} ) learning_curve( - clf, X, y, error_score="raise", fit_params={"sample_weight": np.ones(10)} + clf, X, y, error_score="raise", params={"sample_weight": np.ones(10)} ) -def test_learning_curve_incremental_learning_fit_params(): +def test_learning_curve_incremental_learning_params(): X, y = make_classification( n_samples=30, n_features=1, @@ -1587,7 +1587,7 @@ def test_learning_curve_incremental_learning_fit_params(): exploit_incremental_learning=True, train_sizes=np.linspace(0.1, 1.0, 10), error_score="raise", - fit_params={"sample_weight": np.ones(3)}, + params={"sample_weight": np.ones(3)}, ) learning_curve( @@ -1598,7 +1598,7 @@ def test_learning_curve_incremental_learning_fit_params(): exploit_incremental_learning=True, train_sizes=np.linspace(0.1, 1.0, 10), error_score="raise", - fit_params={"sample_weight": np.ones(2)}, + params={"sample_weight": np.ones(2)}, ) @@ -2481,34 +2481,34 @@ def test_cross_validate_return_indices(global_random_seed): assert_array_equal(test_indices[split_idx], expected_test_idx) -# Tests for metadata routing in cross_val* -# ======================================== +# Tests for metadata routing in cross_val* and learning_curve +# =========================================================== -# TODO(1.6): remove this test in 1.6 -def test_cross_validate_fit_param_deprecation(): +# TODO(1.6): remove `cross_validate` and `cross_val_predict` from this test in 1.6 and +# `learning_curve` in 1.8 +@pytest.mark.parametrize("func", [cross_validate, cross_val_predict, learning_curve]) +def test_fit_param_deprecation(func): """Check that we warn about deprecating `fit_params`.""" with pytest.warns(FutureWarning, match="`fit_params` is deprecated"): - cross_validate(estimator=ConsumingClassifier(), X=X, y=y, cv=2, fit_params={}) + func(estimator=ConsumingClassifier(), X=X, y=y, cv=2, fit_params={}) with pytest.raises( ValueError, match="`params` and `fit_params` cannot both be provided" ): - cross_validate( - estimator=ConsumingClassifier(), X=X, y=y, fit_params={}, params={} - ) + func(estimator=ConsumingClassifier(), X=X, y=y, fit_params={}, params={}) @pytest.mark.usefixtures("enable_slep006") @pytest.mark.parametrize( - "cv_method", [cross_validate, cross_val_score, cross_val_predict] + "func", [cross_validate, cross_val_score, cross_val_predict, learning_curve] ) -def test_groups_with_routing_validation(cv_method): +def test_groups_with_routing_validation(func): """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"): - cv_method( + func( estimator=ConsumingClassifier(), X=X, y=y, @@ -2518,14 +2518,14 @@ def test_groups_with_routing_validation(cv_method): @pytest.mark.usefixtures("enable_slep006") @pytest.mark.parametrize( - "cv_method", [cross_validate, cross_val_score, cross_val_predict] + "func", [cross_validate, cross_val_score, cross_val_predict, learning_curve] ) -def test_passed_unrequested_metadata(cv_method): +def test_passed_unrequested_metadata(func): """Check that we raise an error when passing metadata that is not requested.""" err_msg = re.escape("but are not explicitly set as requested or not requested") with pytest.raises(ValueError, match=err_msg): - cv_method( + func( estimator=ConsumingClassifier(), X=X, y=y, @@ -2535,9 +2535,9 @@ def test_passed_unrequested_metadata(cv_method): @pytest.mark.usefixtures("enable_slep006") @pytest.mark.parametrize( - "cv_method", [cross_validate, cross_val_score, cross_val_predict] + "func", [cross_validate, cross_val_score, cross_val_predict, learning_curve] ) -def test_cross_validate_routing(cv_method): +def test_validation_functions_routing(func): """Check that the respective cv method is properly dispatching the metadata to the consumer.""" scorer_registry = _Registry() @@ -2552,6 +2552,7 @@ def test_cross_validate_routing(cv_method): estimator = ConsumingClassifier(registry=estimator_registry).set_fit_request( sample_weight="fit_sample_weight", metadata="fit_metadata" ) + n_samples = _num_samples(X) rng = np.random.RandomState(0) score_weights = rng.rand(n_samples) @@ -2563,8 +2564,9 @@ def test_cross_validate_routing(cv_method): extra_params = { cross_validate: dict(scoring=dict(my_scorer=scorer, accuracy="accuracy")), - # cross_val_score doesn't support multiple scorers + # cross_val_score and learning_curve don't support multiple scorers: cross_val_score: dict(scoring=scorer), + learning_curve: dict(scoring=scorer), # cross_val_predict doesn't need a scorer cross_val_predict: dict(), } @@ -2576,22 +2578,22 @@ def test_cross_validate_routing(cv_method): fit_metadata=fit_metadata, ) - if cv_method is not cross_val_predict: + if func is not cross_val_predict: params.update( score_weights=score_weights, score_metadata=score_metadata, ) - cv_method( + func( estimator, X=X, y=y, cv=splitter, - **extra_params[cv_method], + **extra_params[func], params=params, ) - if cv_method is not cross_val_predict: + if func is not cross_val_predict: # cross_val_predict doesn't need a scorer assert len(scorer_registry) for _scorer in scorer_registry: @@ -2623,5 +2625,42 @@ def test_cross_validate_routing(cv_method): ) +@pytest.mark.usefixtures("enable_slep006") +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`.""" + + n_samples = _num_samples(X) + rng = np.random.RandomState(0) + fit_sample_weight = rng.rand(n_samples) + fit_metadata = rng.rand(n_samples) + + estimator_registry = _Registry() + estimator = ConsumingClassifier( + registry=estimator_registry + ).set_partial_fit_request( + sample_weight="fit_sample_weight", metadata="fit_metadata" + ) + + learning_curve( + estimator, + X=X, + y=y, + cv=ConsumingSplitter(), + exploit_incremental_learning=True, + params=dict(fit_sample_weight=fit_sample_weight, fit_metadata=fit_metadata), + ) + + assert len(estimator_registry) + for _estimator in estimator_registry: + check_recorded_metadata( + obj=_estimator, + method="partial_fit", + split_params=("sample_weight", "metadata"), + sample_weight=fit_sample_weight, + metadata=fit_metadata, + ) + + # End of metadata routing tests # ============================= diff --git a/sklearn/utils/_metadata_requests.py b/sklearn/utils/_metadata_requests.py index f730539621177..02a79bb8a6f20 100644 --- a/sklearn/utils/_metadata_requests.py +++ b/sklearn/utils/_metadata_requests.py @@ -999,8 +999,9 @@ def _route_params(self, *, params, method, parent, caller): def route_params(self, *, caller, params): """Return the input parameters requested by child objects. - The output of this method is a bunch, which includes the metadata for all - methods of each child object that is used in the router's `caller` method. + 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. diff --git a/sklearn/utils/validation.py b/sklearn/utils/validation.py index cdda749ec70a2..4e25750290a7a 100644 --- a/sklearn/utils/validation.py +++ b/sklearn/utils/validation.py @@ -488,7 +488,7 @@ def indexable(*iterables): Checks consistent length, passes through None, and ensures that everything can be indexed by converting sparse matrices to csr and converting - non-interable objects to arrays. + non-iterable objects to arrays. Parameters ---------- From 87ceec2f159da0d2f0860ce0cd2ec302f1d371a2 Mon Sep 17 00:00:00 2001 From: Guillaume Lemaitre Date: Fri, 17 May 2024 15:14:33 +0200 Subject: [PATCH 129/344] FIX add long long for int32/int64 windows compat in NumPy 2.0 (#29029) --- sklearn/utils/arrayfuncs.pyx | 1 + sklearn/utils/tests/test_arrayfuncs.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/sklearn/utils/arrayfuncs.pyx b/sklearn/utils/arrayfuncs.pyx index 346531d325ca5..1ad5804770358 100644 --- a/sklearn/utils/arrayfuncs.pyx +++ b/sklearn/utils/arrayfuncs.pyx @@ -16,6 +16,7 @@ ctypedef fused real_numeric: short int long + long long float double diff --git a/sklearn/utils/tests/test_arrayfuncs.py b/sklearn/utils/tests/test_arrayfuncs.py index 4a80a4c1edefd..a5c99427cbd00 100644 --- a/sklearn/utils/tests/test_arrayfuncs.py +++ b/sklearn/utils/tests/test_arrayfuncs.py @@ -26,7 +26,9 @@ def test_min_pos_no_positive(dtype): assert min_pos(X) == np.finfo(dtype).max -@pytest.mark.parametrize("dtype", [np.int16, np.int32, np.float32, np.float64]) +@pytest.mark.parametrize( + "dtype", [np.int16, np.int32, np.int64, np.float32, np.float64] +) @pytest.mark.parametrize("value", [0, 1.5, -1]) def test_all_with_any_reduction_axis_1(dtype, value): # Check that return value is False when there is no row equal to `value` From e796d0ae6801486ec65c31a386846d0bd56a201a Mon Sep 17 00:00:00 2001 From: Akihiro Kuno Date: Fri, 17 May 2024 23:57:47 +0900 Subject: [PATCH 130/344] FIX convergence criterion of MeanShift (#28951) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Olivier Grisel Co-authored-by: Jérémie du Boisberranger --- doc/whats_new/v1.5.rst | 3 +++ sklearn/cluster/_mean_shift.py | 2 +- sklearn/cluster/tests/test_mean_shift.py | 9 +++++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/doc/whats_new/v1.5.rst b/doc/whats_new/v1.5.rst index 5fdc0707ffbee..6dc76ceefaf5f 100644 --- a/doc/whats_new/v1.5.rst +++ b/doc/whats_new/v1.5.rst @@ -183,6 +183,9 @@ Changelog :mod:`sklearn.cluster` ...................... +- |Fix| The :class:`cluster.MeanShift` class now properly converges for constant data. + :pr:`28951` by :user:`Akihiro Kuno `. + - |FIX| Create copy of precomputed sparse matrix within the `fit` method of :class:`~cluster.OPTICS` to avoid in-place modification of the sparse matrix. :pr:`28491` by :user:`Thanh Lam Dang `. diff --git a/sklearn/cluster/_mean_shift.py b/sklearn/cluster/_mean_shift.py index fae11cca7df23..a99a607f3cf0d 100644 --- a/sklearn/cluster/_mean_shift.py +++ b/sklearn/cluster/_mean_shift.py @@ -122,7 +122,7 @@ def _mean_shift_single_seed(my_mean, X, nbrs, max_iter): 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 + np.linalg.norm(my_mean - my_old_mean) <= stop_thresh or completed_iterations == max_iter ): break diff --git a/sklearn/cluster/tests/test_mean_shift.py b/sklearn/cluster/tests/test_mean_shift.py index 265c72d0c4ce1..d2d73ba11a3ec 100644 --- a/sklearn/cluster/tests/test_mean_shift.py +++ b/sklearn/cluster/tests/test_mean_shift.py @@ -25,6 +25,15 @@ ) +def test_convergence_of_1d_constant_data(): + # Test convergence using 1D constant data + # Non-regression test for: + # 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) From 48669a514d076e6a0407ca67c8edcfa566fdc6cf Mon Sep 17 00:00:00 2001 From: Omar Salman Date: Fri, 17 May 2024 21:08:24 +0500 Subject: [PATCH 131/344] Fix codecov in tests for array api in pairwise metrics (#29036) --- sklearn/metrics/tests/test_common.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sklearn/metrics/tests/test_common.py b/sklearn/metrics/tests/test_common.py index 9e94b9241de7a..096fc82ae56e3 100644 --- a/sklearn/metrics/tests/test_common.py +++ b/sklearn/metrics/tests/test_common.py @@ -1896,6 +1896,10 @@ def check_array_api_metric_pairwise(metric, array_namespace, device, dtype_name) metric_kwargs = {} if "dense_output" in signature(metric).parameters: + metric_kwargs["dense_output"] = False + check_array_api_metric( + metric, array_namespace, device, dtype_name, a_np=X_np, b_np=Y_np + ) metric_kwargs["dense_output"] = True check_array_api_metric( From d88b41324ff507b610463f045d57823b6f215af5 Mon Sep 17 00:00:00 2001 From: jpienaar-tuks <112702520+jpienaar-tuks@users.noreply.github.com> Date: Sat, 18 May 2024 14:46:56 +0200 Subject: [PATCH 132/344] DOC Fix time complexity of MLP (#28592) Co-authored-by: Johann --- doc/modules/neural_networks_supervised.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/modules/neural_networks_supervised.rst b/doc/modules/neural_networks_supervised.rst index 95d0a1be38238..7ee2387068c81 100644 --- a/doc/modules/neural_networks_supervised.rst +++ b/doc/modules/neural_networks_supervised.rst @@ -229,7 +229,7 @@ Complexity Suppose there are :math:`n` training samples, :math:`m` features, :math:`k` hidden layers, each containing :math:`h` neurons - for simplicity, and :math:`o` output neurons. The time complexity of backpropagation is -:math:`O(n\cdot m \cdot h^k \cdot o \cdot i)`, where :math:`i` is the number +:math:`O(i \cdot n \cdot (m \cdot h + (k - 1) \cdot h \cdot h + h \cdot o))`, where :math:`i` is the number of iterations. Since backpropagation has a high time complexity, it is advisable to start with smaller number of hidden neurons and few hidden layers for training. From 18c1972eb9637034b8e9fbd0df966c10058770f5 Mon Sep 17 00:00:00 2001 From: Adrin Jalali Date: Sun, 19 May 2024 21:14:50 +0200 Subject: [PATCH 133/344] DOC remove obsolete SVM example (#27108) --- doc/conf.py | 1 + examples/svm/plot_svm_kernels.py | 59 +++++++++++++++++++++++------- examples/svm/plot_svm_nonlinear.py | 45 ----------------------- 3 files changed, 46 insertions(+), 59 deletions(-) delete mode 100644 examples/svm/plot_svm_nonlinear.py diff --git a/doc/conf.py b/doc/conf.py index 9d77fc68d0f71..0587e98130118 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -301,6 +301,7 @@ "auto_examples/decomposition/plot_beta_divergence": ( "auto_examples/applications/plot_topics_extraction_with_nmf_lda" ), + "auto_examples/svm/plot_svm_nonlinear": "auto_examples/svm/plot_svm_kernels", "auto_examples/ensemble/plot_adaboost_hastie_10_2": ( "auto_examples/ensemble/plot_adaboost_multiclass" ), diff --git a/examples/svm/plot_svm_kernels.py b/examples/svm/plot_svm_kernels.py index d801e2477e682..a63de6765f083 100644 --- a/examples/svm/plot_svm_kernels.py +++ b/examples/svm/plot_svm_kernels.py @@ -110,12 +110,15 @@ from sklearn.inspection import DecisionBoundaryDisplay -def plot_training_data_with_decision_boundary(kernel): +def plot_training_data_with_decision_boundary( + kernel, ax=None, long_title=True, support_vectors=True +): # Train the SVC clf = svm.SVC(kernel=kernel, gamma=2).fit(X, y) # Settings for plotting - _, ax = plt.subplots(figsize=(4, 3)) + if ax is None: + _, ax = plt.subplots(figsize=(4, 3)) x_min, x_max, y_min, y_max = -3, 3, -3, 3 ax.set(xlim=(x_min, x_max), ylim=(y_min, y_max)) @@ -136,20 +139,26 @@ def plot_training_data_with_decision_boundary(kernel): linestyles=["--", "-", "--"], ) - # Plot bigger circles around samples that serve as support vectors - ax.scatter( - clf.support_vectors_[:, 0], - clf.support_vectors_[:, 1], - s=250, - facecolors="none", - edgecolors="k", - ) + if support_vectors: + # Plot bigger circles around samples that serve as support vectors + ax.scatter( + clf.support_vectors_[:, 0], + clf.support_vectors_[:, 1], + s=150, + facecolors="none", + edgecolors="k", + ) + # Plot samples by color and add legend - ax.scatter(X[:, 0], X[:, 1], c=y, s=150, edgecolors="k") + ax.scatter(X[:, 0], X[:, 1], c=y, s=30, edgecolors="k") ax.legend(*scatter.legend_elements(), loc="upper right", title="Classes") - ax.set_title(f" Decision boundaries of {kernel} kernel in SVC") + if long_title: + ax.set_title(f" Decision boundaries of {kernel} kernel in SVC") + else: + ax.set_title(kernel) - _ = plt.show() + if ax is None: + plt.show() # %% @@ -237,7 +246,6 @@ def plot_training_data_with_decision_boundary(kernel): # using the hyperbolic tangent function (:math:`\tanh`). The kernel function # scales and possibly shifts the dot product of the two points # (:math:`\mathbf{x}_1` and :math:`\mathbf{x}_2`). - plot_training_data_with_decision_boundary("sigmoid") # %% @@ -271,3 +279,26 @@ def plot_training_data_with_decision_boundary(kernel): # parameters using techniques such as # :class:`~sklearn.model_selection.GridSearchCV` is recommended to capture the # underlying structures within the data. + +# %% +# XOR dataset +# ----------- +# A classical example of a dataset which is not linearly separable is the XOR +# pattern. HEre we demonstrate how different kernels work on such a dataset. + +xx, yy = np.meshgrid(np.linspace(-3, 3, 500), np.linspace(-3, 3, 500)) +np.random.seed(0) +X = np.random.randn(300, 2) +y = np.logical_xor(X[:, 0] > 0, X[:, 1] > 0) + +_, ax = plt.subplots(2, 2, figsize=(8, 8)) +args = dict(long_title=False, support_vectors=False) +plot_training_data_with_decision_boundary("linear", ax[0, 0], **args) +plot_training_data_with_decision_boundary("poly", ax[0, 1], **args) +plot_training_data_with_decision_boundary("rbf", ax[1, 0], **args) +plot_training_data_with_decision_boundary("sigmoid", ax[1, 1], **args) +plt.show() + +# %% +# As you can see from the plots above, only the `rbf` kernel can find a +# reasonable decision boundary for the above dataset. diff --git a/examples/svm/plot_svm_nonlinear.py b/examples/svm/plot_svm_nonlinear.py deleted file mode 100644 index 4990e509661a1..0000000000000 --- a/examples/svm/plot_svm_nonlinear.py +++ /dev/null @@ -1,45 +0,0 @@ -""" -============== -Non-linear SVM -============== - -Perform binary classification using non-linear SVC -with RBF kernel. The target to predict is a XOR of the -inputs. - -The color map illustrates the decision function learned by the SVC. - -""" - -import matplotlib.pyplot as plt -import numpy as np - -from sklearn import svm - -xx, yy = np.meshgrid(np.linspace(-3, 3, 500), np.linspace(-3, 3, 500)) -np.random.seed(0) -X = np.random.randn(300, 2) -Y = np.logical_xor(X[:, 0] > 0, X[:, 1] > 0) - -# fit the model -clf = svm.NuSVC(gamma="auto") -clf.fit(X, Y) - -# plot the decision function for each datapoint on the grid -Z = clf.decision_function(np.c_[xx.ravel(), yy.ravel()]) -Z = Z.reshape(xx.shape) - -plt.imshow( - Z, - interpolation="nearest", - extent=(xx.min(), xx.max(), yy.min(), yy.max()), - aspect="auto", - origin="lower", - cmap=plt.cm.PuOr_r, -) -contours = plt.contour(xx, yy, Z, levels=[0], linewidths=2, linestyles="dashed") -plt.scatter(X[:, 0], X[:, 1], s=30, c=Y, cmap=plt.cm.Paired, edgecolors="k") -plt.xticks(()) -plt.yticks(()) -plt.axis([-3, 3, -3, 3]) -plt.show() From 3a023b07eddf4e29abba923760ded10cc3a9e2b0 Mon Sep 17 00:00:00 2001 From: Edoardo Abati <29585319+EdAbati@users.noreply.github.com> Date: Mon, 20 May 2024 11:20:21 +0200 Subject: [PATCH 134/344] Fix use metric_kwargs in check_array_api_metric_pairwise (#29045) --- sklearn/metrics/tests/test_common.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/sklearn/metrics/tests/test_common.py b/sklearn/metrics/tests/test_common.py index 096fc82ae56e3..42f2a36445642 100644 --- a/sklearn/metrics/tests/test_common.py +++ b/sklearn/metrics/tests/test_common.py @@ -1898,12 +1898,24 @@ def check_array_api_metric_pairwise(metric, array_namespace, device, dtype_name) if "dense_output" in signature(metric).parameters: metric_kwargs["dense_output"] = False check_array_api_metric( - metric, array_namespace, device, dtype_name, a_np=X_np, b_np=Y_np + metric, + array_namespace, + device, + dtype_name, + a_np=X_np, + b_np=Y_np, + **metric_kwargs, ) metric_kwargs["dense_output"] = True check_array_api_metric( - metric, array_namespace, device, dtype_name, a_np=X_np, b_np=Y_np + metric, + array_namespace, + device, + dtype_name, + a_np=X_np, + b_np=Y_np, + **metric_kwargs, ) From d03c351780d32d06be592cec612a6223b962c459 Mon Sep 17 00:00:00 2001 From: scikit-learn-bot Date: Mon, 20 May 2024 11:46:54 +0200 Subject: [PATCH 135/344] :lock: :robot: CI Update lock files for main CI build(s) :lock: :robot: (#29053) Co-authored-by: Lock file bot --- ...ylatest_conda_forge_mkl_linux-64_conda.lock | 14 +++++++------- .../pylatest_conda_forge_mkl_osx-64_conda.lock | 12 ++++++------ ...est_pip_openblas_pandas_linux-64_conda.lock | 4 ++-- .../pymin_conda_forge_mkl_win-64_conda.lock | 8 ++++---- ...ge_openblas_ubuntu_2204_linux-64_conda.lock | 8 ++++---- build_tools/circle/doc_linux-64_conda.lock | 18 +++++++++--------- .../doc_min_dependencies_linux-64_conda.lock | 10 +++++----- 7 files changed, 37 insertions(+), 37 deletions(-) diff --git a/build_tools/azure/pylatest_conda_forge_mkl_linux-64_conda.lock b/build_tools/azure/pylatest_conda_forge_mkl_linux-64_conda.lock index bf5bcd3daff08..752e32b1d6220 100644 --- a/build_tools/azure/pylatest_conda_forge_mkl_linux-64_conda.lock +++ b/build_tools/azure/pylatest_conda_forge_mkl_linux-64_conda.lock @@ -109,7 +109,7 @@ https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.2-h659d440_0.conda#cd9 https://conda.anaconda.org/conda-forge/linux-64/libglib-2.80.2-hf974151_0.conda#72724f6a78ecb15559396966226d5838 https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.54.3-hb20ce57_0.conda#7af7c59ab24db007dfd82e0a3a343f66 https://conda.anaconda.org/conda-forge/linux-64/libhiredis-1.0.2-h2cc385e_0.tar.bz2#b34907d3a81a3cd8095ee83d174c074a -https://conda.anaconda.org/conda-forge/linux-64/libhwloc-2.10.0-default_h2fb2949_1000.conda#7e3726e647a619c6ce5939014dfde86d +https://conda.anaconda.org/conda-forge/linux-64/libhwloc-2.10.0-default_h5622ce7_1001.conda#fc2d5b79c2d3f8568fbab31db7ae02f3 https://conda.anaconda.org/conda-forge/linux-64/libllvm15-15.0.7-hb3ce162_4.conda#8a35df3cbc0c8b12cc8af9473ae75eef https://conda.anaconda.org/conda-forge/linux-64/libllvm18-18.1.5-hb77312f_0.conda#efd221d3668077ca067a206269418dec https://conda.anaconda.org/conda-forge/linux-64/libthrift-0.18.1-h8fd135c_2.conda#bbf65f7688512872f063810623b755dc @@ -158,7 +158,7 @@ https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2024.1-pyhd8ed1ab_0. https://conda.anaconda.org/conda-forge/noarch/pytz-2024.1-pyhd8ed1ab_0.conda#3eeeeb9e4827ace8c0c1419c85d590ad https://conda.anaconda.org/conda-forge/noarch/setuptools-69.5.1-pyhd8ed1ab_0.conda#7462280d81f639363e6e63c81276bd9e https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2#e5f25f8dbc060e9a8d912e432202afc2 -https://conda.anaconda.org/conda-forge/linux-64/tbb-2021.12.0-h00ab1b0_0.conda#f1b776cff1b426e7e7461a8502a3b731 +https://conda.anaconda.org/conda-forge/linux-64/tbb-2021.12.0-h297d8ca_1.conda#3ff978d8994f591818a506640c6a7071 https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.5.0-pyhc1e730c_0.conda#df68d78237980a159bd7149f33c0e8fd https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_0.tar.bz2#f832c45a477c78bebd107098db465095 https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2#5844808ffab9ebdb694585b50ba02a96 @@ -209,13 +209,13 @@ https://conda.anaconda.org/conda-forge/linux-64/qt-main-5.15.8-hc9dc06e_21.conda https://conda.anaconda.org/conda-forge/noarch/array-api-strict-1.1.1-pyhd8ed1ab_0.conda#941bbcd64d1a7b44aeb497f468fc85b4 https://conda.anaconda.org/conda-forge/linux-64/contourpy-1.2.1-py311h9547e67_0.conda#74ad0ae64f1ef565e27eda87fa749e84 https://conda.anaconda.org/conda-forge/linux-64/libarrow-12.0.1-hb87d912_8_cpu.conda#3f3b11398fe79b578e3c44dd00a44e4a -https://conda.anaconda.org/conda-forge/linux-64/pandas-2.2.2-py311h320fe9a_0.conda#c79e96ece4110fdaf2657c9f8e16f749 -https://conda.anaconda.org/conda-forge/linux-64/polars-0.20.26-py311h00856b1_0.conda#d9002441c9b75b188f9cdc51bf4f22c7 +https://conda.anaconda.org/conda-forge/linux-64/pandas-2.2.2-py311h14de704_1.conda#84e2dd379d4edec4dd6382861486104d +https://conda.anaconda.org/conda-forge/linux-64/polars-0.20.26-py311h00856b1_1.conda#4d1715777d0485d6e455b64f72794aa4 https://conda.anaconda.org/conda-forge/linux-64/pyqt-5.15.9-py311hf0fb5b6_5.conda#ec7e45bc76d9d0b69a74a2075932b8e8 https://conda.anaconda.org/conda-forge/linux-64/pytorch-1.13.1-cpu_py311h410fd25_1.conda#ddd2fadddf89e3dc3d541a2537fce010 https://conda.anaconda.org/conda-forge/linux-64/scipy-1.13.0-py311h517d4fd_1.conda#a86b8bea39e292a23b2cf9a750f49ea1 -https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.8.4-py311h54ef318_0.conda#150186110f111b458f86c04361351337 -https://conda.anaconda.org/conda-forge/linux-64/pyamg-5.1.0-py311h92ebd52_0.conda#2d415a805458e93fcf5551760fd2d287 +https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.8.4-py311ha4ca890_2.conda#0848e2084cbb57014f232f48568561af +https://conda.anaconda.org/conda-forge/linux-64/pyamg-5.1.0-py311h5510f57_1.conda#734865cccfb0a27b433ea31bd178d0e3 https://conda.anaconda.org/conda-forge/linux-64/pyarrow-12.0.1-py311h39c9aba_8_cpu.conda#587370a25bb2c50cce90909ce20d38b8 https://conda.anaconda.org/conda-forge/linux-64/pytorch-cpu-1.13.1-cpu_py311hdb170b5_1.conda#a805d5f103e493f207613283d8acbbe1 -https://conda.anaconda.org/conda-forge/linux-64/matplotlib-3.8.4-py311h38be061_0.conda#fd6fc4385d0eb6b00c46c4c0d28f5c48 +https://conda.anaconda.org/conda-forge/linux-64/matplotlib-3.8.4-py311h38be061_2.conda#7667100b9559c1b7a40c728cd72dabdf diff --git a/build_tools/azure/pylatest_conda_forge_mkl_osx-64_conda.lock b/build_tools/azure/pylatest_conda_forge_mkl_osx-64_conda.lock index c0e54faa37bc6..5e83abb9667a2 100644 --- a/build_tools/azure/pylatest_conda_forge_mkl_osx-64_conda.lock +++ b/build_tools/azure/pylatest_conda_forge_mkl_osx-64_conda.lock @@ -41,7 +41,7 @@ https://conda.anaconda.org/conda-forge/osx-64/gmp-6.3.0-h73e2aa4_1.conda#92f8d74 https://conda.anaconda.org/conda-forge/osx-64/isl-0.26-imath32_h2e86a7b_101.conda#d06222822a9144918333346f145b68c6 https://conda.anaconda.org/conda-forge/osx-64/lerc-4.0.0-hb486fe8_0.tar.bz2#f9d6a4c82889d5ecedec1d90eb673c55 https://conda.anaconda.org/conda-forge/osx-64/libgfortran5-13.2.0-h2873a65_3.conda#e4fb4d23ec2870ff3c40d10afe305aec -https://conda.anaconda.org/conda-forge/osx-64/libhwloc-2.10.0-default_h1321489_1000.conda#6f5fe4374d1003e116e2573022178da6 +https://conda.anaconda.org/conda-forge/osx-64/libhwloc-2.10.0-default_h456cccd_1001.conda#d2dc768b14cdf226a30a8eab15641305 https://conda.anaconda.org/conda-forge/osx-64/libllvm16-16.0.6-hbedff68_3.conda#8fd56c0adc07a37f93bd44aa61a97c90 https://conda.anaconda.org/conda-forge/osx-64/ninja-1.12.1-h3c5361c_0.conda#a0ebabd021c8191aeb82793fe43cfdcb https://conda.anaconda.org/conda-forge/osx-64/python-3.12.3-h1411813_0_cpython.conda#df1448ec6cbf8eceb03d29003cf72ae6 @@ -70,7 +70,7 @@ https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2024.1-pyhd8ed1ab_0. https://conda.anaconda.org/conda-forge/noarch/pytz-2024.1-pyhd8ed1ab_0.conda#3eeeeb9e4827ace8c0c1419c85d590ad https://conda.anaconda.org/conda-forge/noarch/setuptools-69.5.1-pyhd8ed1ab_0.conda#7462280d81f639363e6e63c81276bd9e https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2#e5f25f8dbc060e9a8d912e432202afc2 -https://conda.anaconda.org/conda-forge/osx-64/tbb-2021.12.0-h7728843_0.conda#e4fb6f4700d8890c36cbf317c2c6d0cb +https://conda.anaconda.org/conda-forge/osx-64/tbb-2021.12.0-h3c5361c_1.conda#e23dd312f13ffe470cc4fdeaddc7a32e https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.5.0-pyhc1e730c_0.conda#df68d78237980a159bd7149f33c0e8fd https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_0.tar.bz2#f832c45a477c78bebd107098db465095 https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2#5844808ffab9ebdb694585b50ba02a96 @@ -111,14 +111,14 @@ https://conda.anaconda.org/conda-forge/osx-64/numpy-1.26.4-py312he3a82b2_0.conda https://conda.anaconda.org/conda-forge/osx-64/blas-devel-3.9.0-20_osx64_mkl.conda#cc3260179093918b801e373c6e888e02 https://conda.anaconda.org/conda-forge/osx-64/compiler-rt-16.0.6-ha38d28d_2.conda#3b9e8c5c63b8e86234f499490acd85c2 https://conda.anaconda.org/conda-forge/osx-64/contourpy-1.2.1-py312h9230928_0.conda#079df34ce7c71259cfdd394645370891 -https://conda.anaconda.org/conda-forge/osx-64/pandas-2.2.2-py312h83c8a23_0.conda#b422a5d39ff0cd72923aef807f280145 +https://conda.anaconda.org/conda-forge/osx-64/pandas-2.2.2-py312h1171441_1.conda#240737937f1f046b0e03ecc11ac4ec98 https://conda.anaconda.org/conda-forge/osx-64/scipy-1.13.0-py312h741d2f9_1.conda#c416453a8ea3b38d823fe8dcecdb6a12 https://conda.anaconda.org/conda-forge/osx-64/blas-2.120-mkl.conda#b041a7677a412f3d925d8208936cb1e2 https://conda.anaconda.org/conda-forge/osx-64/clang_impl_osx-64-16.0.6-h8787910_14.conda#fc1a7d3f1bf236f63c58bab6e36844cb -https://conda.anaconda.org/conda-forge/osx-64/matplotlib-base-3.8.4-py312h1fe5000_0.conda#3e3097734a5042cb6d2675e69bf1fc5a -https://conda.anaconda.org/conda-forge/osx-64/pyamg-5.1.0-py312h3db3e91_0.conda#c6d6248b99fc11b15c9becea581a1462 +https://conda.anaconda.org/conda-forge/osx-64/matplotlib-base-3.8.4-py312hb6d62fa_2.conda#6c5cf505d118f4b58961191fd5e0d030 +https://conda.anaconda.org/conda-forge/osx-64/pyamg-5.1.0-py312h44e70fa_1.conda#ffbfe3b3d5e9675541ee516badfb7729 https://conda.anaconda.org/conda-forge/osx-64/clang_osx-64-16.0.6-hb91bd55_14.conda#3d0d9c725912bb0cb4cd301d2a5d31d7 -https://conda.anaconda.org/conda-forge/osx-64/matplotlib-3.8.4-py312hb401068_0.conda#187ee42addd449b4899b55c304012436 +https://conda.anaconda.org/conda-forge/osx-64/matplotlib-3.8.4-py312hb401068_2.conda#456c057a3e2dcac3d02f4b9d25e277f5 https://conda.anaconda.org/conda-forge/osx-64/c-compiler-1.7.0-h282daa2_1.conda#d27411cb82bc1b76b9f487da6ae97f1d https://conda.anaconda.org/conda-forge/osx-64/clangxx_impl_osx-64-16.0.6-h6d92fbe_14.conda#66b9f06d5f0d0ea47ffcb3a9ca65774a https://conda.anaconda.org/conda-forge/osx-64/gfortran_osx-64-12.3.0-h18f7dce_1.conda#436af2384c47aedb94af78a128e174f1 diff --git a/build_tools/azure/pylatest_pip_openblas_pandas_linux-64_conda.lock b/build_tools/azure/pylatest_pip_openblas_pandas_linux-64_conda.lock index 46fd0d308eaa2..afccc559e409a 100644 --- a/build_tools/azure/pylatest_pip_openblas_pandas_linux-64_conda.lock +++ b/build_tools/azure/pylatest_pip_openblas_pandas_linux-64_conda.lock @@ -62,7 +62,7 @@ https://repo.anaconda.com/pkgs/main/linux-64/pip-24.0-py39h06a4308_0.conda#7f8ce # pip tomli @ https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl#sha256=939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc # pip tzdata @ https://files.pythonhosted.org/packages/65/58/f9c9e6be752e9fcb8b6a0ee9fb87e6e7a1f6bcab2cdc73f02bb7ba91ada0/tzdata-2024.1-py2.py3-none-any.whl#sha256=9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252 # pip urllib3 @ https://files.pythonhosted.org/packages/a2/73/a68704750a7679d0b6d3ad7aa8d4da8e14e151ae82e6fee774e6e0d05ec8/urllib3-2.2.1-py3-none-any.whl#sha256=450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d -# pip zipp @ https://files.pythonhosted.org/packages/c2/0a/ba9d0ee9536d3ef73a3448e931776e658b36f128d344e175bc32b092a8bf/zipp-3.18.1-py3-none-any.whl#sha256=206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b +# pip zipp @ https://files.pythonhosted.org/packages/da/55/a03fd7240714916507e1fcf7ae355bd9d9ed2e6db492595f1a67f61681be/zipp-3.18.2-py3-none-any.whl#sha256=dce197b859eb796242b0622af1b8beb0a722d52aa2f57133ead08edd5bf5374e # pip contourpy @ https://files.pythonhosted.org/packages/31/a2/2f12e3a6e45935ff694654b710961b03310b0e1ec997ee9f416d3c873f87/contourpy-1.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=e1d59258c3c67c865435d8fbeb35f8c59b8bef3d6f46c1f29f6123556af28445 # pip coverage @ https://files.pythonhosted.org/packages/c1/50/b7d6f236c20334b0378ed88078e830640a64ad8eb9f11f818b2af34d00c0/coverage-7.5.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=d21918e9ef11edf36764b93101e2ae8cc82aa5efdc7c5a4e9c6c35a48496d601 # pip imageio @ https://files.pythonhosted.org/packages/a3/b6/39c7dad203d9984225f47e0aa39ac3ba3a47c77a02d0ef2a7be691855a06/imageio-2.34.1-py3-none-any.whl#sha256=408c1d4d62f72c9e8347e7d1ca9bc11d8673328af3913868db3b828e28b40a4c @@ -77,7 +77,7 @@ https://repo.anaconda.com/pkgs/main/linux-64/pip-24.0-py39h06a4308_0.conda#7f8ce # pip scipy @ https://files.pythonhosted.org/packages/c6/ba/a778e6c0020d728c119b0379805a357135fe8c9bc87fdb7e0750ca11319f/scipy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=28e286bf9ac422d6beb559bc61312c348ca9b0f0dae0d7c5afde7f722d6ea13d # pip tifffile @ https://files.pythonhosted.org/packages/c1/79/29d0fa40017f7b749ce344759dcc21e2ec9bbb81fc69ca2ce06e261f83f0/tifffile-2024.5.10-py3-none-any.whl#sha256=4154f091aa24d4e75bfad9ab2d5424a68c70e67b8220188066dc61946d4551bd # pip lightgbm @ https://files.pythonhosted.org/packages/ba/11/cb8b67f3cbdca05b59a032bb57963d4fe8c8d18c3870f30bed005b7f174d/lightgbm-4.3.0-py3-none-manylinux_2_28_x86_64.whl#sha256=104496a3404cb2452d3412cbddcfbfadbef9c372ea91e3a9b8794bcc5183bf07 -# pip matplotlib @ https://files.pythonhosted.org/packages/5e/2c/513395a63a9e1124a5648addbf73be23cc603f955af026b04416da98dc96/matplotlib-3.8.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=606e3b90897554c989b1e38a258c626d46c873523de432b1462f295db13de6f9 +# pip matplotlib @ https://files.pythonhosted.org/packages/d3/6d/45837c5b3d0005a5a9b04729b218a16bf3aa195701c6b33b2cc39ae943b6/matplotlib-3.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=81c40af649d19c85f8073e25e5806926986806fa6d54be506fbf02aef47d5a89 # pip meson-python @ https://files.pythonhosted.org/packages/91/c0/104cb6244c83fe6bc3886f144cc433db0c0c78efac5dc00e409a5a08c87d/meson_python-0.16.0-py3-none-any.whl#sha256=842dc9f5dc29e55fc769ff1b6fe328412fe6c870220fc321060a1d2d395e69e8 # pip pandas @ https://files.pythonhosted.org/packages/bb/30/f6f1f1ac36250f50c421b1b6af08c35e5a8b5a84385ef928625336b93e6f/pandas-2.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=66b479b0bd07204e37583c191535505410daa8df638fd8e75ae1b383851fe921 # pip pyamg @ https://files.pythonhosted.org/packages/68/a9/aed9f557e7eb779d2cb4fa090663f8540979e0c04dadd16e9a0bdc9632c5/pyamg-5.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=5817d4567fb240dab4779bb1630bbb3035b3827731fcdaeb9ecc9c8814319995 diff --git a/build_tools/azure/pymin_conda_forge_mkl_win-64_conda.lock b/build_tools/azure/pymin_conda_forge_mkl_win-64_conda.lock index 8f0a473c031ca..206a72f334f6e 100644 --- a/build_tools/azure/pymin_conda_forge_mkl_win-64_conda.lock +++ b/build_tools/azure/pymin_conda_forge_mkl_win-64_conda.lock @@ -56,7 +56,7 @@ https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_0.conda https://conda.anaconda.org/conda-forge/win-64/kiwisolver-1.4.5-py39h1f6ef14_1.conda#4fc5bd0a7b535252028c647cc27d6c87 https://conda.anaconda.org/conda-forge/win-64/libclang13-18.1.5-default_hf64faad_0.conda#8a662434c6be1f40e2d5d2506d05a41d https://conda.anaconda.org/conda-forge/win-64/libglib-2.80.2-h0df6a38_0.conda#ef9ae80bb2a15aee7a30180c057678ea -https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.10.0-default_h2fffb23_1000.conda#ee944f0d41d9e2048f9d7492c1623ca3 +https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.10.0-default_h8125262_1001.conda#e761885eb4c181074d172220d46319a0 https://conda.anaconda.org/conda-forge/win-64/libintl-devel-0.22.5-h5728263_2.conda#a2ad82fae23975e4ccbfab2847d31d48 https://conda.anaconda.org/conda-forge/win-64/libtiff-4.6.0-hddb2be6_3.conda#6d1828c9039929e2f185c5fa9d133018 https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyh9f0ad1d_0.tar.bz2#2ba8498c1018c1e9c61eb99b973dfe19 @@ -90,7 +90,7 @@ https://conda.anaconda.org/conda-forge/noarch/pyproject-metadata-0.8.0-pyhd8ed1a https://conda.anaconda.org/conda-forge/noarch/pytest-7.4.4-pyhd8ed1ab_0.conda#a9d145de8c5f064b5fa68fb34725d9f4 https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0-pyhd8ed1ab_0.conda#2cf4264fffb9e6eff6031c5b6884d61c https://conda.anaconda.org/conda-forge/win-64/sip-6.7.12-py39h99910a6_0.conda#0cc5774390ada632ed7975203057c91c -https://conda.anaconda.org/conda-forge/win-64/tbb-2021.12.0-h91493d7_0.conda#21745fdd12f01b41178596143cbecffd +https://conda.anaconda.org/conda-forge/win-64/tbb-2021.12.0-hc790b64_1.conda#e98333643abc739ebea1bac97a479828 https://conda.anaconda.org/conda-forge/win-64/fonttools-4.51.0-py39ha55989b_0.conda#5d19302bab29e347116b743e793aa7d6 https://conda.anaconda.org/conda-forge/win-64/glib-2.80.2-h0df6a38_0.conda#a728ca6f04c33ecb0f39eeda5fbd0e23 https://conda.anaconda.org/conda-forge/noarch/importlib-resources-6.4.0-pyhd8ed1ab_0.conda#dcbadab7a68738a028e195ab68ab2d2e @@ -114,5 +114,5 @@ https://conda.anaconda.org/conda-forge/win-64/contourpy-1.2.1-py39h1f6ef14_0.con https://conda.anaconda.org/conda-forge/win-64/pyqt-5.15.9-py39hb77abff_5.conda#5ed899124a51958336371ff01482b8fd https://conda.anaconda.org/conda-forge/win-64/scipy-1.13.0-py39h1a10956_1.conda#5624ccefd670072fc86b2cd4ffdc6c44 https://conda.anaconda.org/conda-forge/win-64/blas-2.122-mkl.conda#aee642435696de144ddf91dc02101cf8 -https://conda.anaconda.org/conda-forge/win-64/matplotlib-base-3.8.4-py39hf19769e_0.conda#7836c3dc5814f6d55a7392657c576e88 -https://conda.anaconda.org/conda-forge/win-64/matplotlib-3.8.4-py39hcbf5309_0.conda#cc66c372d5eb745665da06ce56b7d72b +https://conda.anaconda.org/conda-forge/win-64/matplotlib-base-3.8.4-py39he1095e7_2.conda#5c813b5da86f186d8026b6de6429c212 +https://conda.anaconda.org/conda-forge/win-64/matplotlib-3.8.4-py39hcbf5309_2.conda#1ecee90b529cb69ec4e95add23323110 diff --git a/build_tools/azure/pymin_conda_forge_openblas_ubuntu_2204_linux-64_conda.lock b/build_tools/azure/pymin_conda_forge_openblas_ubuntu_2204_linux-64_conda.lock index 1a4d0feae1773..d0f35d5f79808 100644 --- a/build_tools/azure/pymin_conda_forge_openblas_ubuntu_2204_linux-64_conda.lock +++ b/build_tools/azure/pymin_conda_forge_openblas_ubuntu_2204_linux-64_conda.lock @@ -187,15 +187,15 @@ https://conda.anaconda.org/conda-forge/noarch/requests-2.31.0-pyhd8ed1ab_0.conda https://conda.anaconda.org/conda-forge/linux-64/blas-devel-3.9.0-22_linux64_openblas.conda#63ddb593595c9cf5eb08d3de54d66df8 https://conda.anaconda.org/conda-forge/linux-64/contourpy-1.2.1-py39h7633fee_0.conda#bdc188e59857d6efab332714e0d01d93 https://conda.anaconda.org/conda-forge/linux-64/gst-plugins-base-1.24.3-h9ad1361_0.conda#8fb0e954c616bb0f9389efac4b4ed44b -https://conda.anaconda.org/conda-forge/linux-64/pandas-2.2.2-py39hddac248_0.conda#259c4e76e6bda8888aefc098ae1ba749 +https://conda.anaconda.org/conda-forge/linux-64/pandas-2.2.2-py39hfc16268_1.conda#8b23d2b425035a7468d17e6fe1d54124 https://conda.anaconda.org/conda-forge/linux-64/pulseaudio-client-17.0-hb77b528_0.conda#07f45f1be1c25345faddb8db0de8039b https://conda.anaconda.org/conda-forge/linux-64/scipy-1.13.0-py39haf93ffa_1.conda#57ce54e228e3fbc60e42fa368eff3251 https://conda.anaconda.org/conda-forge/linux-64/blas-2.122-openblas.conda#5065468105542a8b23ea47bd8b6fa55f -https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.8.4-py39he9076e7_0.conda#1919384a8420e7bb25f6c3a582e0857c -https://conda.anaconda.org/conda-forge/linux-64/pyamg-5.1.0-py39hda80f44_0.conda#f225666c47726329201b604060f1436c +https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.8.4-py39h10d1fc8_2.conda#c9fb6571b93b1dd490ea627af7344f36 +https://conda.anaconda.org/conda-forge/linux-64/pyamg-5.1.0-py39h85c637f_1.conda#b2b15112d019e27e62f9433e31607d08 https://conda.anaconda.org/conda-forge/linux-64/qt-main-5.15.8-hc9dc06e_21.conda#b325046180590c868ce0dbf267b82eb8 https://conda.anaconda.org/conda-forge/linux-64/pyqt-5.15.9-py39h52134e7_5.conda#e1f148e57d071b09187719df86f513c1 -https://conda.anaconda.org/conda-forge/linux-64/matplotlib-3.8.4-py39hf3d152e_0.conda#c66d2da2669fddc657b679bccab95775 +https://conda.anaconda.org/conda-forge/linux-64/matplotlib-3.8.4-py39hf3d152e_2.conda#bd956c7563b6a6b27521b83623c74e22 https://conda.anaconda.org/conda-forge/noarch/numpydoc-1.7.0-pyhd8ed1ab_0.conda#1ad3afced398492586ca1bef70328be4 https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-1.0.8-pyhd8ed1ab_0.conda#611a35a27914fac3aa37611a6fe40bb5 https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-devhelp-1.0.6-pyhd8ed1ab_0.conda#d7e4954df0d3aea2eacc7835ad12671d diff --git a/build_tools/circle/doc_linux-64_conda.lock b/build_tools/circle/doc_linux-64_conda.lock index 34ec64ad5863b..3483e48208b45 100644 --- a/build_tools/circle/doc_linux-64_conda.lock +++ b/build_tools/circle/doc_linux-64_conda.lock @@ -68,7 +68,7 @@ https://conda.anaconda.org/conda-forge/linux-64/pixman-0.43.2-h59595ed_0.conda#7 https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-h36c2ea0_1001.tar.bz2#22dad4df6e8630e8dff2428f6f6a7036 https://conda.anaconda.org/conda-forge/linux-64/rav1e-0.6.6-he8a937b_2.conda#77d9955b4abddb811cb8ab1aa7d743e4 https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.0-hdb0a2a9_1.conda#843bbb8ace1d64ac50d64639ff38b014 -https://conda.anaconda.org/conda-forge/linux-64/svt-av1-2.0.0-h59595ed_0.conda#207e01ffa0eb2d2efb83fb6f46365a21 +https://conda.anaconda.org/conda-forge/linux-64/svt-av1-2.1.0-hac33072_0.conda#2a08edb7cd75e56623f2712292a97325 https://conda.anaconda.org/conda-forge/linux-64/xorg-kbproto-1.0.7-h7f98852_1002.tar.bz2#4b230e8381279d76131116660f5a241a https://conda.anaconda.org/conda-forge/linux-64/xorg-libice-1.1.1-hd590300_0.conda#b462a33c0be1421532f28bfe8f4a7514 https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.11-hd590300_0.conda#2c80dc38fface310c9bd81b17037fee5 @@ -83,7 +83,7 @@ https://conda.anaconda.org/conda-forge/linux-64/zlib-ng-2.0.7-h0b41bf4_0.conda#4 https://conda.anaconda.org/conda-forge/linux-64/expat-2.6.2-h59595ed_0.conda#53fb86322bdb89496d7579fe3f02fd61 https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-12.3.0-h58ffeeb_7.conda#95f78565a09852783d3e90e0389cfa5f https://conda.anaconda.org/conda-forge/linux-64/libasprintf-devel-0.22.5-h661eb56_2.conda#02e41ab5834dcdcc8590cf29d9526f50 -https://conda.anaconda.org/conda-forge/linux-64/libavif16-1.0.4-hfa3d5b6_3.conda#3518d00de414c39b46d87dcc1ff65661 +https://conda.anaconda.org/conda-forge/linux-64/libavif16-1.0.4-hd2f8ffe_4.conda#cb911b3e0d863ca9caafd767525f7cac https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.1.0-hd590300_1.conda#f07002e225d7a60a694d42a7bf5ff53f https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.1.0-hd590300_1.conda#5fc11c6020d421960607d821310fcd4d https://conda.anaconda.org/conda-forge/linux-64/libcap-2.69-h0f662aa_0.conda#25cb5999faa414e5ccb2c1388f62d3d5 @@ -235,23 +235,23 @@ https://conda.anaconda.org/conda-forge/linux-64/contourpy-1.2.1-py39h7633fee_0.c https://conda.anaconda.org/conda-forge/linux-64/gst-plugins-base-1.24.3-h9ad1361_0.conda#8fb0e954c616bb0f9389efac4b4ed44b https://conda.anaconda.org/conda-forge/linux-64/imagecodecs-2024.1.1-py39ha98d97a_6.conda#9ada409e8a8202f848abfed8e4e3f6be https://conda.anaconda.org/conda-forge/noarch/imageio-2.34.1-pyh4b66e23_0.conda#bcf6a6f4c6889ca083e8d33afbafb8d5 -https://conda.anaconda.org/conda-forge/linux-64/pandas-2.2.2-py39hddac248_0.conda#259c4e76e6bda8888aefc098ae1ba749 +https://conda.anaconda.org/conda-forge/linux-64/pandas-2.2.2-py39hfc16268_1.conda#8b23d2b425035a7468d17e6fe1d54124 https://conda.anaconda.org/conda-forge/noarch/patsy-0.5.6-pyhd8ed1ab_0.conda#a5b55d1cb110cdcedc748b5c3e16e687 -https://conda.anaconda.org/conda-forge/linux-64/polars-0.20.26-py39ha963410_0.conda#d138679a254e4e0918cfc1114c928bb8 +https://conda.anaconda.org/conda-forge/linux-64/polars-0.20.26-py39ha963410_1.conda#da14fdc7c8400f6b6505b710dd129636 https://conda.anaconda.org/conda-forge/noarch/pooch-1.8.1-pyhd8ed1ab_0.conda#d15917f33140f8d2ac9ca44db7ec8a25 https://conda.anaconda.org/conda-forge/linux-64/pulseaudio-client-17.0-hb77b528_0.conda#07f45f1be1c25345faddb8db0de8039b https://conda.anaconda.org/conda-forge/linux-64/pywavelets-1.4.1-py39h44dd56e_1.conda#d037c20e3da2e85f03ebd20ad480c359 https://conda.anaconda.org/conda-forge/linux-64/scipy-1.13.0-py39haf93ffa_1.conda#57ce54e228e3fbc60e42fa368eff3251 https://conda.anaconda.org/conda-forge/linux-64/blas-2.122-openblas.conda#5065468105542a8b23ea47bd8b6fa55f -https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.8.4-py39he9076e7_0.conda#1919384a8420e7bb25f6c3a582e0857c -https://conda.anaconda.org/conda-forge/linux-64/pyamg-5.1.0-py39hda80f44_0.conda#f225666c47726329201b604060f1436c +https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.8.4-py39h10d1fc8_2.conda#c9fb6571b93b1dd490ea627af7344f36 +https://conda.anaconda.org/conda-forge/linux-64/pyamg-5.1.0-py39h85c637f_1.conda#b2b15112d019e27e62f9433e31607d08 https://conda.anaconda.org/conda-forge/linux-64/qt-main-5.15.8-hc9dc06e_21.conda#b325046180590c868ce0dbf267b82eb8 -https://conda.anaconda.org/conda-forge/linux-64/statsmodels-0.14.1-py39h44dd56e_0.conda#dc565186b972bd87e49b9c35390ddd8c +https://conda.anaconda.org/conda-forge/linux-64/statsmodels-0.14.2-py39hd92a3bb_0.conda#2f6c03d60e71f13d92d511b06193f007 https://conda.anaconda.org/conda-forge/noarch/tifffile-2024.5.10-pyhd8ed1ab_0.conda#125438a8b679e4c08ee8f244177216c9 https://conda.anaconda.org/conda-forge/linux-64/pyqt-5.15.9-py39h52134e7_5.conda#e1f148e57d071b09187719df86f513c1 https://conda.anaconda.org/conda-forge/linux-64/scikit-image-0.22.0-py39hddac248_2.conda#8d502a4d2cbe5a45ff35ca8af8cbec0a https://conda.anaconda.org/conda-forge/noarch/seaborn-base-0.13.2-pyhd8ed1ab_2.conda#b713b116feaf98acdba93ad4d7f90ca1 -https://conda.anaconda.org/conda-forge/linux-64/matplotlib-3.8.4-py39hf3d152e_0.conda#c66d2da2669fddc657b679bccab95775 +https://conda.anaconda.org/conda-forge/linux-64/matplotlib-3.8.4-py39hf3d152e_2.conda#bd956c7563b6a6b27521b83623c74e22 https://conda.anaconda.org/conda-forge/noarch/seaborn-0.13.2-hd8ed1ab_2.conda#a79d8797f62715255308d92d3a91ef2e https://conda.anaconda.org/conda-forge/noarch/numpydoc-1.7.0-pyhd8ed1ab_0.conda#1ad3afced398492586ca1bef70328be4 https://conda.anaconda.org/conda-forge/noarch/sphinx-copybutton-0.5.2-pyhd8ed1ab_0.conda#ac832cc43adc79118cf6e23f1f9b8995 @@ -319,4 +319,4 @@ https://conda.anaconda.org/conda-forge/noarch/sphinxext-opengraph-0.9.1-pyhd8ed1 # pip nbconvert @ https://files.pythonhosted.org/packages/b8/bb/bb5b6a515d1584aa2fd89965b11db6632e4bdc69495a52374bcc36e56cfa/nbconvert-7.16.4-py3-none-any.whl#sha256=05873c620fe520b6322bf8a5ad562692343fe3452abda5765c7a34b7d1aa3eb3 # pip jupyter-server @ https://files.pythonhosted.org/packages/07/46/6bb926b3bf878bf687b952fb6a4c09d014b4575a25960f2cd1a61793763f/jupyter_server-2.14.0-py3-none-any.whl#sha256=fb6be52c713e80e004fac34b35a0990d6d36ba06fd0a2b2ed82b899143a64210 # pip jupyterlab-server @ https://files.pythonhosted.org/packages/2f/b9/ed4ecad7cf1863a64920dc4c19b0376628b5d6bd28d2ec1e00cbac4ba2fb/jupyterlab_server-2.27.1-py3-none-any.whl#sha256=f5e26156e5258b24d532c84e7c74cc212e203bff93eb856f81c24c16daeecc75 -# pip jupyterlite-sphinx @ https://files.pythonhosted.org/packages/7c/c7/5c0f4dc5408122881a32b1809529d1d7adcc60cb176c7b50725910c328cc/jupyterlite_sphinx-0.14.0-py3-none-any.whl#sha256=144edf37e8a77f49b249dd57e3a22ce19ff87805ed79b460e831dc90bf38c269 +# pip jupyterlite-sphinx @ https://files.pythonhosted.org/packages/71/2c/bd797dc46a7281d43444c79ff312d4f8d27d41a0de05f48cad81c7939966/jupyterlite_sphinx-0.15.0-py3-none-any.whl#sha256=344d1f9ee5a20b141a4a4139874eae30a68216f0c995d03ea2e3b3e9d29c4cd5 diff --git a/build_tools/circle/doc_min_dependencies_linux-64_conda.lock b/build_tools/circle/doc_min_dependencies_linux-64_conda.lock index 043587152c63b..8bc3e84fde36f 100644 --- a/build_tools/circle/doc_min_dependencies_linux-64_conda.lock +++ b/build_tools/circle/doc_min_dependencies_linux-64_conda.lock @@ -96,7 +96,7 @@ https://conda.anaconda.org/conda-forge/linux-64/gfortran_impl_linux-64-12.3.0-h1 https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-12.3.0-h2a574ab_7.conda#265caa78b979f112fc241cecd0015c91 https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.2-h659d440_0.conda#cd95826dbd331ed1be26bdf401432844 https://conda.anaconda.org/conda-forge/linux-64/libglib-2.80.2-hf974151_0.conda#72724f6a78ecb15559396966226d5838 -https://conda.anaconda.org/conda-forge/linux-64/libhwloc-2.10.0-default_h2fb2949_1000.conda#7e3726e647a619c6ce5939014dfde86d +https://conda.anaconda.org/conda-forge/linux-64/libhwloc-2.10.0-default_h5622ce7_1001.conda#fc2d5b79c2d3f8568fbab31db7ae02f3 https://conda.anaconda.org/conda-forge/linux-64/libllvm15-15.0.7-hb3ce162_4.conda#8a35df3cbc0c8b12cc8af9473ae75eef https://conda.anaconda.org/conda-forge/linux-64/libllvm18-18.1.5-hb77312f_0.conda#efd221d3668077ca067a206269418dec https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.6.0-h1dd3fc0_3.conda#66f03896ffbe1a110ffda05c7a856504 @@ -124,7 +124,7 @@ https://conda.anaconda.org/conda-forge/linux-64/docutils-0.19-py39hf3d152e_1.tar https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.0-pyhd8ed1ab_2.conda#8d652ea2ee8eaee02ed8dc820bc794aa https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_0.conda#15dda3cdbf330abfe9f555d22f66db46 https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.14.2-h14ed4e7_0.conda#0f69b688f52ff6da70bccb7ff7001d1d -https://conda.anaconda.org/conda-forge/noarch/fsspec-2024.3.1-pyhca7485f_0.conda#b7f0662ef2c9d4404f0af9eef5ed2fde +https://conda.anaconda.org/conda-forge/noarch/fsspec-2024.5.0-pyhff2d567_0.conda#d73e9932511ef7670b2cc0ebd9dfbd30 https://conda.anaconda.org/conda-forge/linux-64/gfortran-12.3.0-h915e2ae_7.conda#8efa768f7f74085629f3e1090e7f0569 https://conda.anaconda.org/conda-forge/linux-64/gfortran_linux-64-12.3.0-h617cb40_3.conda#3a9e5b8a6f651ff14e74d896d8f04ab6 https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.80.2-hb6ce0ca_0.conda#a965aeaf060289528a3fbe09326edae2 @@ -159,7 +159,7 @@ https://conda.anaconda.org/conda-forge/linux-64/setuptools-59.8.0-py39hf3d152e_1 https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2#e5f25f8dbc060e9a8d912e432202afc2 https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-2.2.0-pyhd8ed1ab_0.tar.bz2#4d22a9315e78c6827f806065957d566e https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_0.conda#da1d979339e2714c30a8e806a33ec087 -https://conda.anaconda.org/conda-forge/linux-64/tbb-2021.12.0-h00ab1b0_0.conda#f1b776cff1b426e7e7461a8502a3b731 +https://conda.anaconda.org/conda-forge/linux-64/tbb-2021.12.0-h297d8ca_1.conda#3ff978d8994f591818a506640c6a7071 https://conda.anaconda.org/conda-forge/noarch/tenacity-8.3.0-pyhd8ed1ab_0.conda#216cfa8e32bcd1447646768351df6059 https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.5.0-pyhc1e730c_0.conda#df68d78237980a159bd7149f33c0e8fd https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_0.tar.bz2#f832c45a477c78bebd107098db465095 @@ -208,7 +208,7 @@ https://conda.anaconda.org/conda-forge/linux-64/mkl-devel-2024.1.0-ha770c72_692. https://conda.anaconda.org/conda-forge/linux-64/pyqt5-sip-12.12.2-py39h3d6467e_5.conda#93aff412f3e49fdb43361c0215cbd72d https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.5.0-pyhd8ed1ab_0.conda#d5f595da2daead898ca958ac62f0307b https://conda.anaconda.org/conda-forge/noarch/requests-2.31.0-pyhd8ed1ab_0.conda#a30144e4156cdbb236f99ebb49828f8b -https://conda.anaconda.org/conda-forge/noarch/dask-core-2024.5.0-pyhd8ed1ab_0.conda#8472f598970b9af96ca8106fa243ab67 +https://conda.anaconda.org/conda-forge/noarch/dask-core-2024.5.1-pyhd8ed1ab_0.conda#d4f60ccc5421472d2583efd9ce39d8b1 https://conda.anaconda.org/conda-forge/linux-64/gst-plugins-base-1.24.3-h9ad1361_0.conda#8fb0e954c616bb0f9389efac4b4ed44b https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-22_linux64_mkl.conda#d6f942423116553f068b2f2d93ffea2e https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-22_linux64_mkl.conda#4edf2e7ce63920e4f539d12e32fb478e @@ -218,7 +218,7 @@ https://conda.anaconda.org/conda-forge/linux-64/liblapacke-3.9.0-22_linux64_mkl. https://conda.anaconda.org/conda-forge/linux-64/numpy-1.19.5-py39hd249d9e_3.tar.bz2#0cf333996ebdeeba8d1c8c1c0ee9eff9 https://conda.anaconda.org/conda-forge/linux-64/qt-main-5.15.8-hc9dc06e_21.conda#b325046180590c868ce0dbf267b82eb8 https://conda.anaconda.org/conda-forge/linux-64/blas-devel-3.9.0-22_linux64_mkl.conda#3cb0e51433c88d2f4cdfb50c5c08a683 -https://conda.anaconda.org/conda-forge/linux-64/imagecodecs-lite-2019.12.3-py39hd257fcd_5.tar.bz2#32dba66d6abc2b4b5b019c9e54307312 +https://conda.anaconda.org/conda-forge/linux-64/imagecodecs-lite-2019.12.3-py39hd92a3bb_8.conda#5eb64443d4d973c31e179a498e1bb4a2 https://conda.anaconda.org/conda-forge/noarch/imageio-2.34.1-pyh4b66e23_0.conda#bcf6a6f4c6889ca083e8d33afbafb8d5 https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.3.4-py39h2fa2bec_0.tar.bz2#9ec0b2186fab9121c54f4844f93ee5b7 https://conda.anaconda.org/conda-forge/linux-64/pandas-1.1.5-py39hde0f152_0.tar.bz2#79fc4b5b3a865b90dd3701cecf1ad33c From 751829cbb007c39a43f88fe90769af9559fb56ac Mon Sep 17 00:00:00 2001 From: scikit-learn-bot Date: Mon, 20 May 2024 11:47:21 +0200 Subject: [PATCH 136/344] :lock: :robot: CI Update lock files for cirrus-arm CI build(s) :lock: :robot: (#29052) Co-authored-by: Lock file bot --- build_tools/cirrus/pymin_conda_forge_linux-aarch64_conda.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build_tools/cirrus/pymin_conda_forge_linux-aarch64_conda.lock b/build_tools/cirrus/pymin_conda_forge_linux-aarch64_conda.lock index 660bc9de9ecda..8db59cbb2c373 100644 --- a/build_tools/cirrus/pymin_conda_forge_linux-aarch64_conda.lock +++ b/build_tools/cirrus/pymin_conda_forge_linux-aarch64_conda.lock @@ -90,5 +90,5 @@ https://conda.anaconda.org/conda-forge/linux-aarch64/blas-devel-3.9.0-22_linuxaa https://conda.anaconda.org/conda-forge/linux-aarch64/contourpy-1.2.1-py39hd16970a_0.conda#66b9718539ecdd38876b0176c315bcad https://conda.anaconda.org/conda-forge/linux-aarch64/scipy-1.13.0-py39hb921187_1.conda#2717303c0d13a5646308b3763bf4daa4 https://conda.anaconda.org/conda-forge/linux-aarch64/blas-2.122-openblas.conda#65bc48b3bc85f8eeeab54311443a83aa -https://conda.anaconda.org/conda-forge/linux-aarch64/matplotlib-base-3.8.4-py39h8e43113_0.conda#f397ddfe5c551732de61a92106a14cf3 -https://conda.anaconda.org/conda-forge/linux-aarch64/matplotlib-3.8.4-py39ha65689a_0.conda#d501bb96ff505fdd431fd8fdac8efbf9 +https://conda.anaconda.org/conda-forge/linux-aarch64/matplotlib-base-3.8.4-py39hf44f4b6_2.conda#fadf734d38ed608c9f0b5c91fe79cfb4 +https://conda.anaconda.org/conda-forge/linux-aarch64/matplotlib-3.8.4-py39ha65689a_2.conda#c0472e3c4b3f007de6d643317c30963b From be5316fb57ac5dfe429d7a994a4ef34aaa0d79c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20du=20Boisberranger?= Date: Mon, 20 May 2024 14:31:55 +0200 Subject: [PATCH 137/344] DOC Release highlights 1.5 (#29007) Co-authored-by: Tim Head Co-authored-by: Guillaume Lemaitre Co-authored-by: Christian Lorentzen --- .../plot_release_highlights_1_5_0.py | 183 ++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 examples/release_highlights/plot_release_highlights_1_5_0.py diff --git a/examples/release_highlights/plot_release_highlights_1_5_0.py b/examples/release_highlights/plot_release_highlights_1_5_0.py new file mode 100644 index 0000000000000..0acc6fda6589d --- /dev/null +++ b/examples/release_highlights/plot_release_highlights_1_5_0.py @@ -0,0 +1,183 @@ +# ruff: noqa +""" +======================================= +Release Highlights for scikit-learn 1.5 +======================================= + +.. currentmodule:: sklearn + +We are pleased to announce the release of scikit-learn 1.5! Many bug fixes +and improvements were added, as well as some key new features. Below we +detail the highlights of this release. **For an exhaustive list of +all the changes**, please refer to the :ref:`release notes `. + +To install the latest version (with pip):: + + pip install --upgrade scikit-learn + +or with conda:: + + conda install -c conda-forge scikit-learn + +""" + +# %% +# FixedThresholdClassifier: Setting the decision threshold of a binary classifier +# ------------------------------------------------------------------------------- +# All binary classifiers of scikit-learn use a fixed decision threshold of 0.5 to +# convert probability estimates (i.e. output of `predict_proba`) into class +# predictions. However, 0.5 is almost never the desired threshold for a given problem. +# :class:`~model_selection.FixedThresholdClassifier` allows to wrap any binary +# classifier and set a custom decision threshold. +from sklearn.datasets import make_classification +from sklearn.linear_model import LogisticRegression +from sklearn.metrics import confusion_matrix + +X, y = make_classification(n_samples=1_000, weights=[0.9, 0.1], random_state=0) +classifier = LogisticRegression(random_state=0).fit(X, y) + +print("confusion matrix:\n", confusion_matrix(y, classifier.predict(X))) + +# %% +# Lowering the threshold, i.e. allowing more samples to be classified as the positive +# class, increases the number of true positives at the cost of more false positives +# (as is well known from the concavity of the ROC curve). +from sklearn.model_selection import FixedThresholdClassifier + +wrapped_classifier = FixedThresholdClassifier(classifier, threshold=0.1).fit(X, y) + +print("confusion matrix:\n", confusion_matrix(y, wrapped_classifier.predict(X))) + +# %% +# TunedThresholdClassifierCV: Tuning the decision threshold of a binary classifier +# -------------------------------------------------------------------------------- +# The decision threshold of a binary classifier can be tuned to optimize a given +# metric, using :class:`~model_selection.TunedThresholdClassifierCV`. +from sklearn.metrics import balanced_accuracy_score + +# Due to the class imbalance, the balanced accuracy is not optimal for the default +# threshold. The classifier tends to over predict the majority class. +print(f"balanced accuracy: {balanced_accuracy_score(y, classifier.predict(X)):.2f}") + +# %% +# Tuning the threshold to optimize the balanced accuracy gives a smaller threshold +# that allows more samples to be classified as the positive class. +from sklearn.model_selection import TunedThresholdClassifierCV + +tuned_classifier = TunedThresholdClassifierCV( + classifier, cv=5, scoring="balanced_accuracy" +).fit(X, y) + +print(f"new threshold: {tuned_classifier.best_threshold_:.4f}") +print( + f"balanced accuracy: {balanced_accuracy_score(y, tuned_classifier.predict(X)):.2f}" +) + +# %% +# :class:`~model_selection.TunedThresholdClassifierCV` also benefits from the +# metadata routing support (:ref:`Metadata Routing User Guide`) +# allowing to optimze complex business metrics, detailed +# in :ref:`Post-tuning the decision threshold for cost-sensitive learning +# `. + +# %% +# Performance improvements in PCA +# ------------------------------- +# :class:`~decomposition.PCA` has a new solver, "covariance_eigh", which is faster +# and more memory efficient than the other solvers for datasets with a large number +# of samples and a small number of features. +from sklearn.datasets import make_low_rank_matrix +from sklearn.decomposition import PCA + +X = make_low_rank_matrix( + n_samples=10_000, n_features=100, tail_strength=0.1, random_state=0 +) + +pca = PCA(n_components=10).fit(X) + +print(f"explained variance: {pca.explained_variance_ratio_.sum():.2f}") + +# %% +# The "full" solver has also been improved to use less memory and allows to +# transform faster. The "auto" option for the solver takes advantage of the +# new solver and is now able to select an appropriate solver for sparse +# datasets. +from scipy.sparse import random + +X = random(10000, 100, format="csr", random_state=0) + +pca = PCA(n_components=10, svd_solver="auto").fit(X) + +# %% +# ColumnTransformer is subscriptable +# ---------------------------------- +# The transformers of a :class:`~compose.ColumnTransformer` can now be directly +# accessed using indexing by name. +import numpy as np +from sklearn.compose import ColumnTransformer +from sklearn.preprocessing import StandardScaler, OneHotEncoder + +X = np.array([[0, 1, 2], [3, 4, 5]]) +column_transformer = ColumnTransformer( + [("std_scaler", StandardScaler(), [0]), ("one_hot", OneHotEncoder(), [1, 2])] +) + +column_transformer.fit(X) + +print(column_transformer["std_scaler"]) +print(column_transformer["one_hot"]) + +# %% +# Custom imputation strategies for the SimpleImputer +# -------------------------------------------------- +# :class:`~impute.SimpleImputer` now supports custom strategies for imputation, +# using a callable that computes a scalar value from the non missing values of +# a column vector. +from sklearn.impute import SimpleImputer + +X = np.array( + [ + [-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], + ] +) + + +def smallest_abs(arr): + """Return the smallest absolute value of a 1D array.""" + return np.min(np.abs(arr)) + + +imputer = SimpleImputer(strategy=smallest_abs) + +imputer.fit_transform(X) + +# %% +# Pairwise distances with non-numeric arrays +# ------------------------------------------ +# :func:`~metrics.pairwise_distances` can now compute distances between +# non-numeric arrays using a callable metric. +from sklearn.metrics import pairwise_distances + +X = ["cat", "dog"] +Y = ["cat", "fox"] + + +def levenshtein_distance(x, y): + """Return the Levenshtein distance between two strings.""" + if x == "" or y == "": + return max(len(x), len(y)) + if x[0] == y[0]: + return levenshtein_distance(x[1:], y[1:]) + return 1 + min( + levenshtein_distance(x[1:], y), + levenshtein_distance(x, y[1:]), + levenshtein_distance(x[1:], y[1:]), + ) + + +pairwise_distances(X, Y, metric=levenshtein_distance) From 0e0033a385ccc50b90be169d249cebc0338210b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Morais?= <15629444+ojoaomorais@users.noreply.github.com> Date: Mon, 20 May 2024 11:05:26 -0300 Subject: [PATCH 138/344] DOC Add plot face recognition example to API docs (#29049) --- sklearn/datasets/_lfw.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sklearn/datasets/_lfw.py b/sklearn/datasets/_lfw.py index cb62288646d23..be72baa981da7 100644 --- a/sklearn/datasets/_lfw.py +++ b/sklearn/datasets/_lfw.py @@ -281,6 +281,9 @@ def fetch_lfw_people( 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 `. Parameters From 5a6ad81946cae496892a36e0b26c3f468fa8088a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20du=20Boisberranger?= Date: Mon, 20 May 2024 16:13:08 +0200 Subject: [PATCH 139/344] MAINT Reoder what's new for 1.5 (#29039) --- doc/templates/index.html | 10 +-- doc/whats_new/v1.5.rst | 143 +++++++++++++++++++++++---------------- 2 files changed, 88 insertions(+), 65 deletions(-) diff --git a/doc/templates/index.html b/doc/templates/index.html index 5b3a61a5b98bb..74816a4b473d3 100644 --- a/doc/templates/index.html +++ b/doc/templates/index.html @@ -167,7 +167,9 @@

Machine Learning in

News

  • On-going development: - scikit-learn 1.5 (Changelog) + scikit-learn 1.6 (Changelog) +
  • +
  • May 2024. scikit-learn 1.5.0 is available for download (Changelog).
  • April 2024. scikit-learn 1.4.2 is available for download (Changelog).
  • @@ -175,12 +177,6 @@

    News

  • January 2024. scikit-learn 1.4.0 is available for download (Changelog).
  • -
  • October 2023. scikit-learn 1.3.2 is available for download (Changelog). -
  • -
  • September 2023. scikit-learn 1.3.1 is available for download (Changelog). -
  • -
  • June 2023. scikit-learn 1.3.0 is available for download (Changelog). -
  • All releases: What's new (Changelog)
  • diff --git a/doc/whats_new/v1.5.rst b/doc/whats_new/v1.5.rst index 6dc76ceefaf5f..c2c64e24ba9e0 100644 --- a/doc/whats_new/v1.5.rst +++ b/doc/whats_new/v1.5.rst @@ -8,10 +8,8 @@ Version 1.5 =========== -.. - -- UNCOMMENT WHEN 1.5.0 IS RELEASED -- - For a short description of the main highlights of the release, please refer to - :ref:`sphx_glr_auto_examples_release_highlights_plot_release_highlights_1_5_0.py`. +For a short description of the main highlights of the release, please refer to +:ref:`sphx_glr_auto_examples_release_highlights_plot_release_highlights_1_5_0.py`. .. include:: changelog_legend.inc @@ -20,7 +18,7 @@ Version 1.5 Version 1.5.0 ============= -**In Development** +**May 2024** Security -------- @@ -59,6 +57,10 @@ Changed models Changes impacting many modules ------------------------------ +- |Fix| Raise `ValueError` with an informative error message when passing 1D + sparse arrays to methods that expect 2D sparse inputs. + :pr:`28988` by :user:`Olivier Grisel `. + - |API| The name of the input of the `inverse_transform` method of estimators has been standardized to `X`. As a consequence, `Xt` is deprecated and will be removed in version 1.7 in the following estimators: :class:`cluster.FeatureAgglomeration`, @@ -67,10 +69,6 @@ Changes impacting many modules :class:`pipeline.Pipeline` and :class:`preprocessing.KBinsDiscretizer`. :pr:`28756` by :user:`Will Dean `. -- |Fix| Raise `ValueError` with an informative error message when passing 1D - sparse arrays to methods that expect 2D sparse inputs. - :pr:`28988` by :user:`Olivier Grisel `. - Support for Array API --------------------- @@ -82,8 +80,8 @@ See :ref:`array_api` for more details. **Functions:** - :func:`sklearn.metrics.r2_score` now supports Array API compliant inputs. - :pr:`27904` by :user:`Eric Lindgren `, `Franck Charras `, - `Olivier Grisel ` and `Tim Head `. + :pr:`27904` by :user:`Eric Lindgren `, :user:`Franck Charras `, + :user:`Olivier Grisel ` and :user:`Tim Head `. **Classes:** @@ -103,8 +101,8 @@ Unless we discover a major blocker, setuptools support will be dropped in scikit-learn 1.6. The 1.5.x releases will support building scikit-learn with setuptools. -Meson support for building scikit-learn was added in :pr:`28040` by :user:`Loïc -Estève ` +Meson support for building scikit-learn was added in :pr:`28040` by +:user:`Loïc Estève ` Metadata Routing ---------------- @@ -120,7 +118,8 @@ more details. now support metadata routing. The fit methods now accept ``**fit_params`` which are passed to the underlying estimators via their `fit` methods. - :pr:`28432` by :user:`Adam Li ` and :user:`Benjamin Bossan `. + :pr:`28432` by :user:`Adam Li ` and + :user:`Benjamin Bossan `. - |Feature| :class:`linear_model.RidgeCV` and :class:`linear_model.RidgeClassifierCV` now support metadata routing in @@ -144,8 +143,8 @@ more details. - |Feature| :class:`pipeline.FeatureUnion` now supports metadata routing in its ``fit`` and ``fit_transform`` methods and route metadata to the underlying - transformers' ``fit`` and ``fit_transform``. :pr:`28205` by :user:`Stefanie - Senger `. + transformers' ``fit`` and ``fit_transform``. + :pr:`28205` by :user:`Stefanie Senger `. - |Fix| Fix an issue when resolving default routing requests set via class attributes. @@ -156,8 +155,8 @@ more details. :pr:`28651` by `Adrin Jalali`_. - |FIX| Prevent a `RecursionError` when estimators with the default `scoring` - param (`None`) route metadata. :pr:`28712` by :user:`Stefanie Senger - `. + param (`None`) route metadata. + :pr:`28712` by :user:`Stefanie Senger `. Changelog --------- @@ -217,7 +216,13 @@ Changelog :mod:`sklearn.cross_decomposition` .................................. -- |API| Deprecates `Y` in favor of `y` in the methods fit, transform and inverse_transform of: +- |Fix| The `coef_` fitted attribute of :class:`cross_decomposition.PLSRegression` + now takes into account both the scale of `X` and `Y` when `scale=True`. Note that + the previous predicted values were not affected by this bug. + :pr:`28612` by :user:`Guillaume Lemaitre `. + +- |API| Deprecates `Y` in favor of `y` in the methods fit, transform and + inverse_transform of: :class:`cross_decomposition.PLSRegression`. :class:`cross_decomposition.PLSCanonical`, :class:`cross_decomposition.CCA`, @@ -225,11 +230,6 @@ Changelog `Y` will be removed in version 1.7. :pr:`28604` by :user:`David Leon `. -- |Fix| The `coef_` fitted attribute of :class:`cross_decomposition.PLSRegression` - now takes into account both the scale of `X` and `Y` when `scale=True`. Note that - the previous predicted values were not affected by this bug. - :pr:`28612` by :user:`Guillaume Lemaitre `. - :mod:`sklearn.datasets` ....................... @@ -245,7 +245,8 @@ Changelog :func:`datasets.fetch_rcv1`, and :func:`datasets.fetch_species_distributions`. By default, the functions will retry up to 3 times in case of network failures. - :pr:`28160` by :user:`Zhehao Liu ` and :user:`Filip Karlo Došilović `. + :pr:`28160` by :user:`Zhehao Liu ` and + :user:`Filip Karlo Došilović `. :mod:`sklearn.decomposition` ............................ @@ -350,13 +351,8 @@ Changelog - |Fix| :class:`linear_model.ElasticNet`, :class:`linear_model.ElasticNetCV`, :class:`linear_model.Lasso` and :class:`linear_model.LassoCV` now explicitly don't - accept large sparse data formats. :pr:`27576` by :user:`Stefanie Senger - `. - -- |API| :class:`linear_model.RidgeCV` and :class:`linear_model.RidgeClassifierCV` - will now allow `alpha=0` when `cv != None`, which is consistent with - :class:`linear_model.Ridge` and :class:`linear_model.RidgeClassifier`. - :pr:`28425` by :user:`Lucy Liu `. + accept large sparse data formats. + :pr:`27576` by :user:`Stefanie Senger `. - |Fix| :class:`linear_model.RidgeCV` and :class:`RidgeClassifierCV` correctly pass `sample_weight` to the underlying scorer when `cv` is None. @@ -366,6 +362,11 @@ Changelog will now always be `None` when `tol` is set, as `n_nonzero_coefs` is ignored in this case. :pr:`28557` by :user:`Lucy Liu `. +- |API| :class:`linear_model.RidgeCV` and :class:`linear_model.RidgeClassifierCV` + will now allow `alpha=0` when `cv != None`, which is consistent with + :class:`linear_model.Ridge` and :class:`linear_model.RidgeClassifier`. + :pr:`28425` by :user:`Lucy Liu `. + - |API| Passing `average=0` to disable averaging is deprecated in :class:`linear_model.PassiveAggressiveClassifier`, :class:`linear_model.PassiveAggressiveRegressor`, @@ -382,7 +383,8 @@ Changelog :pr:`28703` by :user:`Christian Lorentzen `. - |API| `store_cv_values` and `cv_values_` are deprecated in favor of - `store_cv_results` and `cv_results_` in `RidgeCV` and `RidgeClassifierCV`. + `store_cv_results` and `cv_results_` in `~linear_model.RidgeCV` and + `~linear_model.RidgeClassifierCV`. :pr:`28915` by :user:`Lucy Liu `. :mod:`sklearn.manifold` @@ -401,8 +403,15 @@ Changelog :pr:`27456` by :user:`Venkatachalam N `, :user:`Kshitij Mathur ` and :user:`Julian Libiseller-Egger `. +- |Feature| :func:`sklearn.metrics.check_scoring` now returns a multi-metric scorer + when `scoring` as a `dict`, `set`, `tuple`, or `list`. :pr:`28360` by `Thomas Fan`_. + +- |Feature| :func:`metrics.d2_log_loss_score` has been added which + calculates the D^2 score for the log loss. + :pr:`28351` by :user:`Omar Salman `. + - |Efficiency| Improve efficiency of functions :func:`~metrics.brier_score_loss`, - :func:`~metrics.calibration_curve`, :func:`~metrics.det_curve`, + :func:`~calibration.calibration_curve`, :func:`~metrics.det_curve`, :func:`~metrics.precision_recall_curve`, :func:`~metrics.roc_curve` when `pos_label` argument is specified. Also improve efficiency of methods `from_estimator` @@ -411,9 +420,6 @@ Changelog :class:`~calibration.CalibrationDisplay`. :pr:`28051` by :user:`Pierre de Fréminville `. -- |Feature| :func:`sklearn.metrics.check_scoring` now returns a multi-metric scorer - when `scoring` as a `dict`, `set`, `tuple`, or `list`. :pr:`28360` by `Thomas Fan`_. - - |Fix|:class:`metrics.classification_report` now shows only accuracy and not micro-average when input is a subset of labels. :pr:`28399` by :user:`Vineet Joshi `. @@ -422,8 +428,8 @@ Changelog computation. This is likely to affect neighbor-based algorithms. :pr:`28692` by :user:`Loïc Estève `. -- |API| :func:`metrics.precision_recall_curve` deprecated the keyword argument `probas_pred` - in favor of `y_score`. `probas_pred` will be removed in version 1.7. +- |API| :func:`metrics.precision_recall_curve` deprecated the keyword argument + `probas_pred` in favor of `y_score`. `probas_pred` will be removed in version 1.7. :pr:`28092` by :user:`Adam Li `. - |API| :func:`metrics.brier_score_loss` deprecated the keyword argument `y_prob` @@ -434,10 +440,6 @@ Changelog is deprecated and will raise an error in v1.7. :pr:`18555` by :user:`Kaushik Amar Das `. -- |Feature| :func:`metrics.d2_log_loss_score` has been added which - calculates the D^2 score for the log loss. - :pr:`28351` by :user:`Omar Salman `. - :mod:`sklearn.mixture` ...................... @@ -460,22 +462,22 @@ Changelog raises a warning when groups are passed in to :term:`split`. :pr:`28210` by `Thomas Fan`_. +- |Enhancement| The HTML diagram representation of + :class:`~model_selection.GridSearchCV`, + :class:`~model_selection.RandomizedSearchCV`, + :class:`~model_selection.HalvingGridSearchCV`, and + :class:`~model_selection.HalvingRandomSearchCV` will show the best estimator when + `refit=True`. :pr:`28722` by :user:`Yao Xiao ` and `Thomas Fan`_. + - |Fix| the ``cv_results_`` attribute (of :class:`model_selection.GridSearchCV`) now returns masked arrays of the appropriate NumPy dtype, as opposed to always returning dtype ``object``. :pr:`28352` by :user:`Marco Gorelli`. -- |Fix| :func:`sklearn.model_selection.train_test_score` works with Array API inputs. +- |Fix| :func:`model_selection.train_test_split` works with Array API inputs. Previously indexing was not handled correctly leading to exceptions when using strict implementations of the Array API like CuPY. :pr:`28407` by :user:`Tim Head `. -- |Enhancement| The HTML diagram representation of - :class:`~model_selection.GridSearchCV`, - :class:`~model_selection.RandomizedSearchCV`, - :class:`~model_selection.HalvingGridSearchCV`, and - :class:`~model_selection.HalvingRandomSearchCV` will show the best estimator when - `refit=True`. :pr:`28722` by :user:`Yao Xiao ` and `Thomas Fan`_. - :mod:`sklearn.multioutput` .......................... @@ -518,6 +520,10 @@ Changelog :mod:`sklearn.utils` .................... +- |Fix| :func:`~utils._safe_indexing` now works correctly for polars DataFrame when + `axis=0` and supports indexing polars Series. + :pr:`28521` by :user:`Yao Xiao `. + - |API| :data:`utils.IS_PYPY` is deprecated and will be removed in version 1.7. :pr:`28768` by :user:`Jérémie du Boisberranger `. @@ -529,15 +535,11 @@ Changelog `joblib.register_parallel_backend` instead. :pr:`28847` by :user:`Jérémie du Boisberranger `. -- |API| Raise informative warning message in :func:`type_of_target` when - represented as bytes. For classifiers and classification metrics, labels encoded +- |API| Raise informative warning message in :func:`~utils.multiclass.type_of_target` + when represented as bytes. For classifiers and classification metrics, labels encoded as bytes is deprecated and will raise an error in v1.7. :pr:`18555` by :user:`Kaushik Amar Das `. -- |Fix| :func:`~utils._safe_indexing` now works correctly for polars DataFrame when - `axis=0` and supports indexing polars Series. - :pr:`28521` by :user:`Yao Xiao `. - - |API| :func:`utils.estimator_checks.check_estimator_sparse_data` was split into two functions: :func:`utils.estimator_checks.check_estimator_sparse_matrix` and :func:`utils.estimator_checks.check_estimator_sparse_array`. @@ -548,4 +550,29 @@ Changelog Thanks to everyone who has contributed to the maintenance and improvement of the project since version 1.4, including: -TODO: update at the time of the release. +101AlexMartin, Abdulaziz Aloqeely, Adam J. Stewart, Adam Li, Adarsh Wase, Adrin +Jalali, Advik Sinha, Akash Srivastava, Akihiro Kuno, Alan Guedes, Alexis +IMBERT, Ana Paula Gomes, Anderson Nelson, Andrei Dzis, Arnaud Capitaine, Arturo +Amor, Aswathavicky, Bharat Raghunathan, Brendan Lu, Bruno, Cemlyn, Christian +Lorentzen, Christian Veenhuis, Cindy Liang, Claudio Salvatore Arcidiacono, +Connor Boyle, Conrad Stevens, crispinlogan, davidleon123, DerWeh, Dipan Banik, +Duarte São José, DUONG, Eddie Bergman, Edoardo Abati, Egehan Gunduz, Emad +Izadifar, Erich Schubert, Filip Karlo Došilović, Franck Charras, Gael +Varoquaux, Gönül Aycı, Guillaume Lemaitre, Gyeongjae Choi, Harmanan Kohli, +Hong Xiang Yue, Ian Faust, itsaphel, Ivan Wiryadi, Jack Bowyer, Javier Marin +Tur, Jérémie du Boisberranger, Jérôme Dockès, Jiawei Zhang, Joel Nothman, +Johanna Bayer, John Cant, John Hopfensperger, jpcars, jpienaar-tuks, Julian +Libiseller-Egger, Julien Jerphanion, KanchiMoe, Kaushik Amar Das, keyber, +Koustav Ghosh, kraktus, Krsto Proroković, ldwy4, LeoGrin, lihaitao, Linus +Sommer, Loic Esteve, Lucy Liu, Lukas Geiger, manasimj, Manuel Labbé, Manuel +Morales, Marco Edward Gorelli, Maren Westermann, Marija Vlajic, Mark Elliot, +Mateusz Sokół, Mavs, Michael Higgins, Michael Mayer, miguelcsilva, Miki +Watanabe, Mohammed Hamdy, myenugula, Nathan Goldbaum, Naziya Mahimkar, Neto, +Olivier Grisel, Omar Salman, Patrick Wang, Pierre de Fréminville, Priyash +Shah, Puneeth K, Rahil Parikh, raisadz, Raj Pulapakura, Ralf Gommers, Ralph +Urlus, Randolf Scholz, Reshama Shaikh, Richard Barnes, Rodrigo Romero, Saad +Mahmood, Salim Dohri, Sandip Dutta, SarahRemus, scikit-learn-bot, Shaharyar +Choudhry, Shubham, sperret6, Stefanie Senger, Suha Siddiqui, Thanh Lam DANG, +thebabush, Thomas J. Fan, Thomas Lazarus, Thomas Li, Tialo, Tim Head, Tuhin +Sharma, VarunChaduvula, Vineet Joshi, virchan, Waël Boukhobza, Weyb, Will +Dean, Xavier Beltran, Xiao Yuan, Xuefeng Xu, Yao Xiao From 34db65a3addfc83d99e64ec55d5e6896ecfbb940 Mon Sep 17 00:00:00 2001 From: Adrin Jalali Date: Mon, 20 May 2024 19:36:54 +0200 Subject: [PATCH 140/344] DOC use pydata-sphinx-theme for the website (#29038) Co-authored-by: Yao Xiao <108576690+Charlie-XIAO@users.noreply.github.com> Co-authored-by: Thomas J. Fan Co-authored-by: Guillaume Lemaitre --- .gitignore | 4 + .pre-commit-config.yaml | 7 + build_tools/azure/pypy3_linux-64_conda.lock | 6 +- build_tools/circle/build_doc.sh | 8 +- build_tools/circle/doc_environment.yml | 4 + build_tools/circle/doc_linux-64_conda.lock | 13 +- .../doc_min_dependencies_environment.yml | 14 +- .../doc_min_dependencies_linux-64_conda.lock | 25 +- build_tools/circle/list_versions.py | 64 +- .../update_environments_and_lock_files.py | 24 +- doc/Makefile | 8 + doc/about.rst | 656 +- doc/api/deprecated.rst.template | 24 + doc/api/index.rst.template | 77 + doc/api/module.rst.template | 46 + doc/api_reference.py | 1336 ++++ doc/common_pitfalls.rst | 71 +- doc/computing.rst | 6 - doc/computing/computational_performance.rst | 4 - doc/computing/parallelism.rst | 4 - doc/computing/scaling_strategies.rst | 4 - doc/conf.py | 401 +- doc/contents.rst | 24 - doc/css/.gitkeep | 0 doc/data_transforms.rst | 6 - doc/datasets.rst | 6 - doc/datasets/loading_other_datasets.rst | 23 +- doc/datasets/real_world.rst | 4 - doc/datasets/sample_generators.rst | 4 - doc/datasets/toy_dataset.rst | 4 - doc/developers/contributing.rst | 520 +- doc/developers/index.rst | 7 - doc/developers/maintainer.rst | 5 +- doc/dispatching.rst | 6 - doc/faq.rst | 46 +- doc/images/ml_map.png | Bin 761071 -> 0 bytes doc/images/ml_map.svg | 4 + doc/includes/big_toc_css.rst | 40 - doc/includes/bigger_toc_css.rst | 60 - doc/index.rst.template | 25 + doc/inspection.rst | 10 +- doc/install.rst | 301 +- doc/js/scripts/api-search.js | 12 + doc/js/scripts/dropdown.js | 61 + doc/js/scripts/vendor/svg-pan-zoom.min.js | 31 + doc/js/scripts/version-switcher.js | 40 + doc/make.bat | 27 +- doc/min_dependency_substitutions.rst.template | 3 + doc/min_dependency_table.rst.template | 13 + doc/model_persistence.rst | 142 +- doc/model_selection.rst | 6 - doc/modules/array_api.rst | 4 - doc/modules/biclustering.rst | 42 +- doc/modules/calibration.rst | 96 +- doc/modules/classes.rst | 1916 ------ doc/modules/clustering.rst | 1015 ++- doc/modules/compose.rst | 215 +- doc/modules/covariance.rst | 103 +- doc/modules/cross_decomposition.rst | 70 +- doc/modules/cross_validation.rst | 140 +- doc/modules/decomposition.rst | 426 +- doc/modules/density.rst | 47 +- doc/modules/ensemble.rst | 671 +- doc/modules/feature_extraction.rst | 481 +- doc/modules/feature_selection.rst | 145 +- doc/modules/gaussian_process.rst | 225 +- doc/modules/grid_search.rst | 83 +- doc/modules/impute.rst | 12 +- doc/modules/isotonic.rst | 4 +- doc/modules/kernel_approximation.rst | 70 +- doc/modules/kernel_ridge.rst | 10 +- doc/modules/lda_qda.rst | 34 +- doc/modules/learning_curve.rst | 8 +- doc/modules/linear_model.rst | 890 ++- doc/modules/manifold.rst | 586 +- doc/modules/metrics.rst | 20 +- doc/modules/mixture.rst | 278 +- doc/modules/model_evaluation.rst | 863 ++- doc/modules/multiclass.rst | 35 +- doc/modules/naive_bayes.rst | 114 +- doc/modules/neighbors.rst | 375 +- doc/modules/neural_networks_supervised.rst | 180 +- doc/modules/neural_networks_unsupervised.rst | 22 +- doc/modules/outlier_detection.rst | 110 +- doc/modules/partial_dependence.rst | 63 +- doc/modules/permutation_importance.rst | 92 +- doc/modules/preprocessing.rst | 387 +- doc/modules/random_projection.rst | 57 +- doc/modules/semi_supervised.rst | 40 +- doc/modules/sgd.rst | 169 +- doc/modules/svm.rst | 337 +- doc/modules/tree.rst | 278 +- doc/modules/unsupervised_reduction.rst | 14 +- doc/preface.rst | 32 - doc/scss/api-search.scss | 114 + doc/scss/api.scss | 52 + doc/scss/colors.scss | 51 + doc/scss/custom.scss | 192 + doc/scss/index.scss | 175 + doc/scss/install.scss | 33 + doc/sphinxext/add_toctree_functions.py | 160 - doc/sphinxext/autoshortsummary.py | 53 + doc/sphinxext/dropdown_anchors.py | 78 + doc/sphinxext/move_gallery_links.py | 193 + doc/sphinxext/override_pst_pagetoc.py | 84 + doc/supervised_learning.rst | 6 - doc/templates/base.rst | 36 + doc/templates/class.rst | 17 - doc/templates/class_with_call.rst | 21 - doc/templates/deprecated_class.rst | 28 - doc/templates/deprecated_class_with_call.rst | 29 - .../deprecated_class_without_init.rst | 24 - doc/templates/deprecated_function.rst | 24 - doc/templates/display_all_class_methods.rst | 19 - doc/templates/display_only_from_estimator.rst | 18 - doc/templates/function.rst | 17 - doc/templates/generate_deprecated.sh | 8 - doc/templates/index.html | 369 +- doc/testimonials/testimonials.rst | 1285 ++-- .../scikit-learn-modern/javascript.html | 56 - doc/themes/scikit-learn-modern/layout.html | 150 - doc/themes/scikit-learn-modern/nav.html | 102 - doc/themes/scikit-learn-modern/search.html | 8 - .../scikit-learn-modern/static/css/theme.css | 1412 ---- .../static/css/vendor/bootstrap.min.css | 6 - .../static/js/details-permalink.js | 47 - .../static/js/vendor/bootstrap.min.js | 6 - .../static/js/vendor/jquery-3.6.3.slim.min.js | 2 - doc/themes/scikit-learn-modern/theme.conf | 10 - doc/tune_toc.rst | 131 - doc/tutorial/index.rst | 12 - .../machine_learning_map/ML_MAPS_README.txt | 93 - doc/tutorial/machine_learning_map/README.md | 17 + doc/tutorial/machine_learning_map/index.rst | 102 +- .../machine_learning_map/parse_path.py | 192 - .../machine_learning_map/pyparsing.py | 5715 ----------------- .../machine_learning_map/svg2imagemap.py | 111 - doc/tutorial/statistical_inference/index.rst | 10 +- .../statistical_inference/model_selection.rst | 67 +- .../supervised_learning.rst | 11 +- doc/unsupervised_learning.rst | 6 - doc/user_guide.rst | 12 - doc/visualizations.rst | 16 +- doc/whats_new/_contributors.rst | 12 +- doc/whats_new/older_versions.rst | 1 - examples/README.txt | 5 + .../applications/plot_digits_denoising.py | 10 +- .../covariance/plot_mahalanobis_distances.py | 18 +- examples/ensemble/plot_adaboost_multiclass.py | 22 +- examples/ensemble/plot_hgbt_regression.py | 11 +- examples/gaussian_process/plot_gpr_co2.py | 11 +- .../inspection/plot_permutation_importance.py | 6 +- examples/linear_model/plot_lasso_lars_ic.py | 10 +- .../plot_cost_sensitive_learning.py | 24 +- .../model_selection/plot_grid_search_stats.py | 48 +- .../plot_nested_cross_validation_iris.py | 12 +- ...ot_permutation_tests_for_classification.py | 10 +- .../plot_release_highlights_1_1_0.py | 4 +- .../plot_release_highlights_1_3_0.py | 8 +- pyproject.toml | 16 +- setup.cfg | 2 +- sklearn/__init__.py | 5 +- sklearn/_min_dependencies.py | 12 +- sklearn/base.py | 2 +- sklearn/calibration.py | 2 +- sklearn/cluster/__init__.py | 5 +- sklearn/compose/__init__.py | 6 +- sklearn/covariance/__init__.py | 11 +- sklearn/covariance/_shrunk_covariance.py | 4 +- sklearn/cross_decomposition/__init__.py | 2 + sklearn/datasets/__init__.py | 6 +- sklearn/datasets/descr/breast_cancer.rst | 28 +- sklearn/datasets/descr/california_housing.rst | 6 +- sklearn/datasets/descr/digits.rst | 28 +- sklearn/datasets/descr/iris.rst | 36 +- sklearn/datasets/descr/kddcup99.rst | 18 +- sklearn/datasets/descr/lfw.rst | 136 +- sklearn/datasets/descr/linnerud.rst | 10 +- sklearn/datasets/descr/rcv1.rst | 8 +- .../datasets/descr/species_distributions.rst | 8 +- sklearn/datasets/descr/twenty_newsgroups.rst | 396 +- sklearn/datasets/descr/wine_data.rst | 42 +- sklearn/decomposition/__init__.py | 8 +- sklearn/decomposition/_kernel_pca.py | 6 +- sklearn/discriminant_analysis.py | 4 +- sklearn/dummy.py | 2 + sklearn/ensemble/__init__.py | 5 +- sklearn/exceptions.py | 5 +- sklearn/experimental/__init__.py | 10 +- sklearn/feature_extraction/__init__.py | 6 +- sklearn/feature_extraction/image.py | 5 +- sklearn/feature_extraction/text.py | 6 +- sklearn/feature_selection/__init__.py | 8 +- sklearn/gaussian_process/__init__.py | 7 +- sklearn/gaussian_process/kernels.py | 5 +- sklearn/impute/__init__.py | 2 +- sklearn/inspection/__init__.py | 2 +- sklearn/isotonic.py | 2 + sklearn/kernel_approximation.py | 6 +- sklearn/kernel_ridge.py | 2 +- sklearn/linear_model/__init__.py | 4 +- sklearn/linear_model/_least_angle.py | 8 +- sklearn/manifold/__init__.py | 4 +- sklearn/metrics/__init__.py | 5 +- sklearn/metrics/cluster/__init__.py | 9 +- sklearn/metrics/cluster/_supervised.py | 4 +- sklearn/metrics/pairwise.py | 2 + sklearn/mixture/__init__.py | 4 +- sklearn/model_selection/__init__.py | 2 + sklearn/multiclass.py | 11 +- sklearn/multioutput.py | 3 +- sklearn/naive_bayes.py | 6 +- sklearn/neighbors/__init__.py | 5 +- sklearn/neighbors/_binary_tree.pxi.tp | 5 +- sklearn/neural_network/__init__.py | 5 +- sklearn/pipeline.py | 5 +- sklearn/preprocessing/__init__.py | 5 +- sklearn/random_projection.py | 7 +- sklearn/semi_supervised/__init__.py | 9 +- sklearn/svm/__init__.py | 4 +- sklearn/tree/__init__.py | 5 +- sklearn/utils/__init__.py | 4 +- sklearn/utils/arrayfuncs.pyx | 5 +- sklearn/utils/class_weight.py | 5 +- sklearn/utils/discovery.py | 5 +- sklearn/utils/estimator_checks.py | 5 +- sklearn/utils/extmath.py | 5 +- sklearn/utils/graph.py | 4 +- sklearn/utils/metadata_routing.py | 5 +- sklearn/utils/metaestimators.py | 4 +- sklearn/utils/multiclass.py | 6 +- sklearn/utils/parallel.py | 4 +- sklearn/utils/random.py | 4 +- sklearn/utils/sparsefuncs.py | 5 +- sklearn/utils/sparsefuncs_fast.pyx | 5 +- sklearn/utils/validation.py | 5 +- 236 files changed, 9384 insertions(+), 18266 deletions(-) create mode 100644 doc/api/deprecated.rst.template create mode 100644 doc/api/index.rst.template create mode 100644 doc/api/module.rst.template create mode 100644 doc/api_reference.py delete mode 100644 doc/contents.rst create mode 100644 doc/css/.gitkeep delete mode 100644 doc/images/ml_map.png create mode 100644 doc/images/ml_map.svg delete mode 100644 doc/includes/big_toc_css.rst delete mode 100644 doc/includes/bigger_toc_css.rst create mode 100644 doc/index.rst.template create mode 100644 doc/js/scripts/api-search.js create mode 100644 doc/js/scripts/dropdown.js create mode 100644 doc/js/scripts/vendor/svg-pan-zoom.min.js create mode 100644 doc/js/scripts/version-switcher.js create mode 100644 doc/min_dependency_substitutions.rst.template create mode 100644 doc/min_dependency_table.rst.template delete mode 100644 doc/modules/classes.rst delete mode 100644 doc/preface.rst create mode 100644 doc/scss/api-search.scss create mode 100644 doc/scss/api.scss create mode 100644 doc/scss/colors.scss create mode 100644 doc/scss/custom.scss create mode 100644 doc/scss/index.scss create mode 100644 doc/scss/install.scss delete mode 100644 doc/sphinxext/add_toctree_functions.py create mode 100644 doc/sphinxext/autoshortsummary.py create mode 100644 doc/sphinxext/dropdown_anchors.py create mode 100644 doc/sphinxext/move_gallery_links.py create mode 100644 doc/sphinxext/override_pst_pagetoc.py create mode 100644 doc/templates/base.rst delete mode 100644 doc/templates/class.rst delete mode 100644 doc/templates/class_with_call.rst delete mode 100644 doc/templates/deprecated_class.rst delete mode 100644 doc/templates/deprecated_class_with_call.rst delete mode 100644 doc/templates/deprecated_class_without_init.rst delete mode 100644 doc/templates/deprecated_function.rst delete mode 100644 doc/templates/display_all_class_methods.rst delete mode 100644 doc/templates/display_only_from_estimator.rst delete mode 100644 doc/templates/function.rst delete mode 100755 doc/templates/generate_deprecated.sh delete mode 100644 doc/themes/scikit-learn-modern/javascript.html delete mode 100644 doc/themes/scikit-learn-modern/layout.html delete mode 100644 doc/themes/scikit-learn-modern/nav.html delete mode 100644 doc/themes/scikit-learn-modern/search.html delete mode 100644 doc/themes/scikit-learn-modern/static/css/theme.css delete mode 100644 doc/themes/scikit-learn-modern/static/css/vendor/bootstrap.min.css delete mode 100644 doc/themes/scikit-learn-modern/static/js/details-permalink.js delete mode 100644 doc/themes/scikit-learn-modern/static/js/vendor/bootstrap.min.js delete mode 100644 doc/themes/scikit-learn-modern/static/js/vendor/jquery-3.6.3.slim.min.js delete mode 100644 doc/themes/scikit-learn-modern/theme.conf delete mode 100644 doc/tune_toc.rst delete mode 100644 doc/tutorial/machine_learning_map/ML_MAPS_README.txt create mode 100644 doc/tutorial/machine_learning_map/README.md delete mode 100644 doc/tutorial/machine_learning_map/parse_path.py delete mode 100644 doc/tutorial/machine_learning_map/pyparsing.py delete mode 100644 doc/tutorial/machine_learning_map/svg2imagemap.py diff --git a/.gitignore b/.gitignore index 9f3b453bbfd74..61c89bcb96491 100644 --- a/.gitignore +++ b/.gitignore @@ -15,9 +15,13 @@ dist/ MANIFEST doc/sg_execution_times.rst doc/_build/ +doc/api/*.rst doc/auto_examples/ +doc/css/* +!doc/css/.gitkeep doc/modules/generated/ doc/datasets/generated/ +doc/index.rst doc/min_dependency_table.rst doc/min_dependency_substitutions.rst *.pdf diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 31af43b6bbab0..abe14acc7778c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,3 +27,10 @@ repos: # TODO: add the double-quote-cython-strings hook when it's usability has improved: # possibility to pass a directory and use it as a check instead of auto-formatter. - id: cython-lint +- repo: https://github.com/pre-commit/mirrors-prettier + rev: v2.7.1 + hooks: + - id: prettier + files: ^doc/scss/|^doc/js/scripts/ + exclude: ^doc/js/scripts/vendor/ + types_or: ["scss", "javascript"] diff --git a/build_tools/azure/pypy3_linux-64_conda.lock b/build_tools/azure/pypy3_linux-64_conda.lock index ab6a908edf340..520a4935c8af5 100644 --- a/build_tools/azure/pypy3_linux-64_conda.lock +++ b/build_tools/azure/pypy3_linux-64_conda.lock @@ -97,7 +97,7 @@ https://conda.anaconda.org/conda-forge/linux-64/scipy-1.12.0-py39h6dedee3_2.cond https://conda.anaconda.org/conda-forge/linux-64/blas-2.122-openblas.conda#5065468105542a8b23ea47bd8b6fa55f https://conda.anaconda.org/conda-forge/noarch/importlib-resources-6.4.0-pyhd8ed1ab_0.conda#dcbadab7a68738a028e195ab68ab2d2e https://conda.anaconda.org/conda-forge/noarch/meson-python-0.16.0-pyh0c530f3_0.conda#e16f0dbf502da873be9f9adb0dc52547 -https://conda.anaconda.org/conda-forge/linux-64/pyamg-5.1.0-py39h5fd064f_0.conda#04676d2a49da3cb608af77e04b796ce1 +https://conda.anaconda.org/conda-forge/linux-64/pyamg-5.1.0-py39h3c335be_1.conda#7278eb55a7e97a0ba2376a6c608e7c46 https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.5.0-pyhd8ed1ab_0.conda#d5f595da2daead898ca958ac62f0307b -https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.8.4-py39h4e7d633_0.conda#58272019e595dde98d0844ae3ebf0cfe -https://conda.anaconda.org/conda-forge/linux-64/matplotlib-3.8.4-py39h4162558_0.conda#b0f7702a174422ff1db58190495fd766 +https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.8.4-py39h6fb8a73_2.conda#3212f51613e10b3ee319f3f2bf8ee5a8 +https://conda.anaconda.org/conda-forge/linux-64/matplotlib-3.8.4-py39h4162558_2.conda#05babd7bae196648bfc6b7e3d9ea7630 diff --git a/build_tools/circle/build_doc.sh b/build_tools/circle/build_doc.sh index 35fee3ae50b65..c569f4913d4d8 100755 --- a/build_tools/circle/build_doc.sh +++ b/build_tools/circle/build_doc.sh @@ -190,17 +190,13 @@ export OMP_NUM_THREADS=1 if [[ "$CIRCLE_BRANCH" =~ ^main$ && -z "$CI_PULL_REQUEST" ]] then # List available documentation versions if on main - python build_tools/circle/list_versions.py > doc/versions.rst + python build_tools/circle/list_versions.py --json doc/js/versions.json --rst doc/versions.rst fi # The pipefail is requested to propagate exit code set -o pipefail && cd doc && make $make_args 2>&1 | tee ~/log.txt -# Insert the version warning for deployment -find _build/html/stable -name "*.html" | xargs sed -i '/<\/body>/ i \ -\ ' - cd - set +o pipefail @@ -244,7 +240,7 @@ then ( echo '
      ' echo "$affected" | sed 's|.*|
    • & [dev, stable]
    • |' - echo '

    General: Home | API Reference | Examples

    ' + echo '

General: Home | API Reference | Examples

' echo 'Sphinx Warnings in affected files
    ' echo "$warnings" | sed 's/\/home\/circleci\/project\//
  • /g' echo '
' diff --git a/build_tools/circle/doc_environment.yml b/build_tools/circle/doc_environment.yml index 4df22341635a3..bc4405983a1b6 100644 --- a/build_tools/circle/doc_environment.yml +++ b/build_tools/circle/doc_environment.yml @@ -33,7 +33,11 @@ dependencies: - polars - pooch - sphinxext-opengraph + - sphinx-remove-toctrees + - sphinx-design + - pydata-sphinx-theme - pip - pip: - jupyterlite-sphinx - jupyterlite-pyodide-kernel + - sphinxcontrib-sass diff --git a/build_tools/circle/doc_linux-64_conda.lock b/build_tools/circle/doc_linux-64_conda.lock index 3483e48208b45..b959b3250c851 100644 --- a/build_tools/circle/doc_linux-64_conda.lock +++ b/build_tools/circle/doc_linux-64_conda.lock @@ -1,6 +1,6 @@ # Generated by conda-lock. # platform: linux-64 -# input_hash: b57888763997b08b2f240b5ff1ed6afcf88685f3d8c791ea8eba4d80483c43d0 +# input_hash: beab3d7262ec74c4ef8c9050098de8b9fe7910606e7bd4ff52687972bff35868 @EXPLICIT https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2024.2.2-hbcca054_0.conda#2f4327a1cbe7f022401b236e915a5fef @@ -177,6 +177,7 @@ https://conda.anaconda.org/conda-forge/noarch/pytz-2024.1-pyhd8ed1ab_0.conda#3ee https://conda.anaconda.org/conda-forge/noarch/setuptools-69.5.1-pyhd8ed1ab_0.conda#7462280d81f639363e6e63c81276bd9e https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2#e5f25f8dbc060e9a8d912e432202afc2 https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-2.2.0-pyhd8ed1ab_0.tar.bz2#4d22a9315e78c6827f806065957d566e +https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.5-pyhd8ed1ab_1.conda#3f144b2c34f8cb5a9abd9ed23a39c561 https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_0.conda#da1d979339e2714c30a8e806a33ec087 https://conda.anaconda.org/conda-forge/noarch/tabulate-0.9.0-pyhd8ed1ab_1.tar.bz2#4759805cce2d914c38472f70bf4d8bcb https://conda.anaconda.org/conda-forge/noarch/tenacity-8.3.0-pyhd8ed1ab_0.conda#216cfa8e32bcd1447646768351df6059 @@ -192,7 +193,9 @@ https://conda.anaconda.org/conda-forge/linux-64/xkeyboard-config-2.41-hd590300_0 https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.4-h0b41bf4_2.conda#82b6df12252e6f32402b96dacc656fec https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.11-hd590300_0.conda#ed67c36f215b310412b2af935bf3e530 https://conda.anaconda.org/conda-forge/noarch/zipp-3.17.0-pyhd8ed1ab_0.conda#2e4d6bc0b14e10f895fc6791a7d9b26a +https://conda.anaconda.org/conda-forge/noarch/accessible-pygments-0.0.4-pyhd8ed1ab_0.conda#46a2e6e3dfa718ce3492018d5a110dd6 https://conda.anaconda.org/conda-forge/noarch/babel-2.14.0-pyhd8ed1ab_0.conda#9669586875baeced8fc30c0826c3270e +https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.12.3-pyha770c72_0.conda#332493000404d8411859539a5a630865 https://conda.anaconda.org/conda-forge/linux-64/brunsli-0.1-h9c3ff4c_0.tar.bz2#c1ac6229d0bfd14f8354ff9ad2a26cad https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.0-h3faef2a_0.conda#f907bb958910dc404647326ca80c263e https://conda.anaconda.org/conda-forge/linux-64/cxx-compiler-1.7.0-h00ab1b0_1.conda#28de2e073db9ca9b72858bee9fb6f571 @@ -217,6 +220,7 @@ https://conda.anaconda.org/conda-forge/noarch/pyproject-metadata-0.8.0-pyhd8ed1a https://conda.anaconda.org/conda-forge/noarch/pytest-7.4.4-pyhd8ed1ab_0.conda#a9d145de8c5f064b5fa68fb34725d9f4 https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0-pyhd8ed1ab_0.conda#2cf4264fffb9e6eff6031c5b6884d61c https://conda.anaconda.org/conda-forge/linux-64/sip-6.7.12-py39h3d6467e_0.conda#e667a3ab0df62c54e60e1843d2e6defb +https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.11.0-hd8ed1ab_0.conda#471e3988f8ca5e9eb3ce6be7eac3bcee https://conda.anaconda.org/conda-forge/noarch/urllib3-2.2.1-pyhd8ed1ab_0.conda#08807a87fa7af10754d46f63b368e016 https://conda.anaconda.org/conda-forge/linux-64/compilers-1.7.0-ha770c72_1.conda#d8d07866ac3b5b6937213c89a1874f08 https://conda.anaconda.org/conda-forge/linux-64/gstreamer-1.24.3-haf2f30d_0.conda#f3df87cc9ef0b5113bff55aefcbcafd5 @@ -254,9 +258,12 @@ https://conda.anaconda.org/conda-forge/noarch/seaborn-base-0.13.2-pyhd8ed1ab_2.c https://conda.anaconda.org/conda-forge/linux-64/matplotlib-3.8.4-py39hf3d152e_2.conda#bd956c7563b6a6b27521b83623c74e22 https://conda.anaconda.org/conda-forge/noarch/seaborn-0.13.2-hd8ed1ab_2.conda#a79d8797f62715255308d92d3a91ef2e https://conda.anaconda.org/conda-forge/noarch/numpydoc-1.7.0-pyhd8ed1ab_0.conda#1ad3afced398492586ca1bef70328be4 +https://conda.anaconda.org/conda-forge/noarch/pydata-sphinx-theme-0.15.2-pyhd8ed1ab_0.conda#ce99859070b0e17ccc63234ca58f3ed8 https://conda.anaconda.org/conda-forge/noarch/sphinx-copybutton-0.5.2-pyhd8ed1ab_0.conda#ac832cc43adc79118cf6e23f1f9b8995 +https://conda.anaconda.org/conda-forge/noarch/sphinx-design-0.5.0-pyhd8ed1ab_0.conda#264b3c697fa9cdade87eb0abe4440d54 https://conda.anaconda.org/conda-forge/noarch/sphinx-gallery-0.16.0-pyhd8ed1ab_0.conda#add28691ee89e875b190eda07929d5d4 https://conda.anaconda.org/conda-forge/noarch/sphinx-prompt-1.4.0-pyhd8ed1ab_0.tar.bz2#88ee91e8679603f2a5bd036d52919cc2 +https://conda.anaconda.org/conda-forge/noarch/sphinx-remove-toctrees-1.0.0.post1-pyhd8ed1ab_0.conda#6dee8412218288a17f99f2cfffab334d https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-1.0.8-pyhd8ed1ab_0.conda#611a35a27914fac3aa37611a6fe40bb5 https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-devhelp-1.0.6-pyhd8ed1ab_0.conda#d7e4954df0d3aea2eacc7835ad12671d https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-htmlhelp-2.0.5-pyhd8ed1ab_0.conda#7e1e7437273682ada2ed5e9e9714b140 @@ -272,6 +279,7 @@ https://conda.anaconda.org/conda-forge/noarch/sphinxext-opengraph-0.9.1-pyhd8ed1 # pip json5 @ https://files.pythonhosted.org/packages/8a/3c/4f8791ee53ab9eeb0b022205aa79387119a74cc9429582ce04098e6fc540/json5-0.9.25-py3-none-any.whl#sha256=34ed7d834b1341a86987ed52f3f76cd8ee184394906b6e22a1e0deb9ab294e8f # pip jsonpointer @ https://files.pythonhosted.org/packages/12/f6/0232cc0c617e195f06f810534d00b74d2f348fe71b2118009ad8ad31f878/jsonpointer-2.4-py2.py3-none-any.whl#sha256=15d51bba20eea3165644553647711d150376234112651b4f1811022aecad7d7a # pip jupyterlab-pygments @ https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl#sha256=841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780 +# pip libsass @ https://files.pythonhosted.org/packages/fd/5a/eb5b62641df0459a3291fc206cf5bd669c0feed7814dded8edef4ade8512/libsass-0.23.0-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.whl#sha256=4a218406d605f325d234e4678bd57126a66a88841cb95bee2caeafdc6f138306 # pip mistune @ https://files.pythonhosted.org/packages/f0/74/c95adcdf032956d9ef6c89a9b8a5152bf73915f8c633f3e3d88d06bd699c/mistune-3.0.2-py3-none-any.whl#sha256=71481854c30fdbc938963d3605b72501f5c10a9320ecd412c121c163a1c7d205 # pip overrides @ https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl#sha256=c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49 # pip pandocfilters @ https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl#sha256=93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc @@ -285,7 +293,6 @@ https://conda.anaconda.org/conda-forge/noarch/sphinxext-opengraph-0.9.1-pyhd8ed1 # pip rpds-py @ https://files.pythonhosted.org/packages/97/b1/12238bd8cdf3cef71e85188af133399bfde1bddf319007361cc869d6f6a7/rpds_py-0.18.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=e4c39ad2f512b4041343ea3c7894339e4ca7839ac38ca83d68a832fc8b3748ab # pip send2trash @ https://files.pythonhosted.org/packages/40/b0/4562db6223154aa4e22f939003cb92514c79f3d4dccca3444253fd17f902/Send2Trash-1.8.3-py3-none-any.whl#sha256=0c31227e0bd08961c7665474a3d1ef7193929fedda4233843689baa056be46c9 # pip sniffio @ https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl#sha256=2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 -# pip soupsieve @ https://files.pythonhosted.org/packages/4c/f3/038b302fdfbe3be7da016777069f26ceefe11a681055ea1f7817546508e3/soupsieve-2.5-py3-none-any.whl#sha256=eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7 # pip traitlets @ https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl#sha256=b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f # pip types-python-dateutil @ https://files.pythonhosted.org/packages/c7/1b/af4f4c4f3f7339a4b7eb3c0ab13416db98f8ac09de3399129ee5fdfa282b/types_python_dateutil-2.9.0.20240316-py3-none-any.whl#sha256=6b8cb66d960771ce5ff974e9dd45e38facb81718cc1e208b10b1baccbfdbee3b # pip uri-template @ https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl#sha256=a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363 @@ -294,13 +301,13 @@ https://conda.anaconda.org/conda-forge/noarch/sphinxext-opengraph-0.9.1-pyhd8ed1 # pip websocket-client @ https://files.pythonhosted.org/packages/5a/84/44687a29792a70e111c5c477230a72c4b957d88d16141199bf9acb7537a3/websocket_client-1.8.0-py3-none-any.whl#sha256=17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526 # pip anyio @ https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl#sha256=048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8 # pip arrow @ https://files.pythonhosted.org/packages/f8/ed/e97229a566617f2ae958a6b13e7cc0f585470eac730a73e9e82c32a3cdd2/arrow-1.3.0-py3-none-any.whl#sha256=c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80 -# pip beautifulsoup4 @ https://files.pythonhosted.org/packages/b1/fe/e8c672695b37eecc5cbf43e1d0638d88d66ba3a44c4d321c796f4e59167f/beautifulsoup4-4.12.3-py3-none-any.whl#sha256=b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed # pip bleach @ https://files.pythonhosted.org/packages/ea/63/da7237f805089ecc28a3f36bca6a21c31fcbc2eb380f3b8f1be3312abd14/bleach-6.1.0-py3-none-any.whl#sha256=3225f354cfc436b9789c66c4ee030194bee0568fbf9cbdad3bc8b5c26c5f12b6 # pip cffi @ https://files.pythonhosted.org/packages/ea/ac/e9e77bc385729035143e54cc8c4785bd480eaca9df17565963556b0b7a93/cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098 # pip doit @ https://files.pythonhosted.org/packages/44/83/a2960d2c975836daa629a73995134fd86520c101412578c57da3d2aa71ee/doit-0.36.0-py3-none-any.whl#sha256=ebc285f6666871b5300091c26eafdff3de968a6bd60ea35dd1e3fc6f2e32479a # pip jupyter-core @ https://files.pythonhosted.org/packages/c9/fb/108ecd1fe961941959ad0ee4e12ee7b8b1477247f30b1fdfd83ceaf017f0/jupyter_core-5.7.2-py3-none-any.whl#sha256=4f7315d2f6b4bcf2e3e7cb6e46772eba760ae459cd1f59d29eb57b0a01bd7409 # pip referencing @ https://files.pythonhosted.org/packages/b7/59/2056f61236782a2c86b33906c025d4f4a0b17be0161b63b70fd9e8775d36/referencing-0.35.1-py3-none-any.whl#sha256=eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de # pip rfc3339-validator @ https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl#sha256=24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa +# pip sphinxcontrib-sass @ https://files.pythonhosted.org/packages/2e/87/7c2eb08e3ca1d6baae32c0a5e005330fe1cec93a36aa085e714c3b3a3c7d/sphinxcontrib_sass-0.3.4-py2.py3-none-any.whl#sha256=a0c79a44ae8b8935c02dc340ebe40c9e002c839331201c899dc93708970c355a # pip terminado @ https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl#sha256=a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0 # pip tinycss2 @ https://files.pythonhosted.org/packages/2c/4d/0db5b8a613d2a59bbc29bc5bb44a2f8070eb9ceab11c50d477502a8a0092/tinycss2-1.3.0-py3-none-any.whl#sha256=54a8dbdffb334d536851be0226030e9505965bb2f30f21a4a82c55fb2a80fae7 # pip argon2-cffi-bindings @ https://files.pythonhosted.org/packages/ec/f7/378254e6dd7ae6f31fe40c8649eea7d4832a42243acaf0f1fff9083b2bed/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae diff --git a/build_tools/circle/doc_min_dependencies_environment.yml b/build_tools/circle/doc_min_dependencies_environment.yml index 14f4485295455..8148ee330bb35 100644 --- a/build_tools/circle/doc_min_dependencies_environment.yml +++ b/build_tools/circle/doc_min_dependencies_environment.yml @@ -24,14 +24,18 @@ dependencies: - seaborn - memory_profiler - compilers - - sphinx=6.0.0 # min - - sphinx-gallery=0.15.0 # min + - sphinx=7.3.7 # min + - sphinx-gallery=0.16.0 # min - sphinx-copybutton=0.5.2 # min - numpydoc=1.2.0 # min - - sphinx-prompt=1.3.0 # min + - sphinx-prompt=1.4.0 # min - plotly=5.14.0 # min - polars=0.20.23 # min - - pooch + - pooch=1.6.0 # min + - sphinx-remove-toctrees=1.0.0.post1 # min + - sphinx-design=0.5.0 # min + - pydata-sphinx-theme=0.15.2 # min - pip - pip: - - sphinxext-opengraph==0.4.2 # min + - sphinxext-opengraph==0.9.1 # min + - sphinxcontrib-sass==0.3.4 # min diff --git a/build_tools/circle/doc_min_dependencies_linux-64_conda.lock b/build_tools/circle/doc_min_dependencies_linux-64_conda.lock index 8bc3e84fde36f..f0d02542f4b98 100644 --- a/build_tools/circle/doc_min_dependencies_linux-64_conda.lock +++ b/build_tools/circle/doc_min_dependencies_linux-64_conda.lock @@ -1,6 +1,6 @@ # Generated by conda-lock. # platform: linux-64 -# input_hash: 08b61aae27c59a8d35d008fa2f947440f3cbcbc41622112e33e68f90d69b621c +# input_hash: 6c9ff93ed18fe7c2e8387a4c3d7104701555959a32e797c1cb83593137afe155 @EXPLICIT https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2024.2.2-hbcca054_0.conda#2f4327a1cbe7f022401b236e915a5fef @@ -110,6 +110,7 @@ https://conda.anaconda.org/conda-forge/linux-64/xcb-util-renderutil-0.3.9-hd5903 https://conda.anaconda.org/conda-forge/linux-64/xcb-util-wm-0.4.1-h8ee46fc_1.conda#90108a432fb5c6150ccfee3f03388656 https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.9-h8ee46fc_0.conda#077b6e8ad6a3ddb741fce2496dd01bec https://conda.anaconda.org/conda-forge/noarch/alabaster-0.7.16-pyhd8ed1ab_0.conda#def531a3ac77b7fb8c21d17bb5d0badb +https://conda.anaconda.org/conda-forge/noarch/appdirs-1.4.4-pyh9f0ad1d_0.tar.bz2#5f095bc6454094e96f146491fd03633b https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py39h3d6467e_1.conda#c48418c8b35f1d59ae9ae1174812b40a https://conda.anaconda.org/conda-forge/linux-64/c-compiler-1.7.0-hd590300_1.conda#e9dffe1056994133616378309f932d77 https://conda.anaconda.org/conda-forge/noarch/certifi-2024.2.2-pyhd8ed1ab_0.conda#0876280e409658fc6f9e75d035960333 @@ -120,7 +121,7 @@ https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhd8ed1ab_0.conda#5cd86562580f274031ede6aa6aa24441 https://conda.anaconda.org/conda-forge/linux-64/cython-3.0.10-py39h3d6467e_0.conda#76b5d215fb735a6dc43010ffbe78040e https://conda.anaconda.org/conda-forge/linux-64/dbus-1.13.6-h5008d03_3.tar.bz2#ecfff944ba3960ecb334b9a2663d708d -https://conda.anaconda.org/conda-forge/linux-64/docutils-0.19-py39hf3d152e_1.tar.bz2#adb733ec2ee669f6d010758d054da60f +https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_0.conda#e8cd5d629f65bdf0f3bb312cde14659e https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.0-pyhd8ed1ab_2.conda#8d652ea2ee8eaee02ed8dc820bc794aa https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_0.conda#15dda3cdbf330abfe9f555d22f66db46 https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.14.2-h14ed4e7_0.conda#0f69b688f52ff6da70bccb7ff7001d1d @@ -146,7 +147,6 @@ https://conda.anaconda.org/conda-forge/linux-64/markupsafe-2.1.5-py39hd1e30aa_0. https://conda.anaconda.org/conda-forge/noarch/networkx-3.2-pyhd8ed1ab_0.conda#cec8cc498664cc00a070676aa89e69a7 https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.2-h488ebb8_0.conda#7f2e286780f072ed750df46dc2631138 https://conda.anaconda.org/conda-forge/noarch/packaging-24.0-pyhd8ed1ab_0.conda#248f521b64ce055e7feae3105e7abeb8 -https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.2.2-pyhd8ed1ab_0.conda#6f6cf28bf8e021933869bae3f84b8fc9 https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_0.conda#d3483c8fc2dc2cc3f5cf43e26d60cabf https://conda.anaconda.org/conda-forge/noarch/ply-3.11-pyhd8ed1ab_2.conda#18c6deb6f9602e32446398203c8f0e91 https://conda.anaconda.org/conda-forge/linux-64/psutil-5.9.8-py39hd1e30aa_0.conda#ec86403fde8793ac1c36f8afa3d15902 @@ -158,6 +158,7 @@ https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.1-py39hd1e30aa_1.cond https://conda.anaconda.org/conda-forge/linux-64/setuptools-59.8.0-py39hf3d152e_1.tar.bz2#4252d0c211566a9f65149ba7f6e87aa4 https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2#e5f25f8dbc060e9a8d912e432202afc2 https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-2.2.0-pyhd8ed1ab_0.tar.bz2#4d22a9315e78c6827f806065957d566e +https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.5-pyhd8ed1ab_1.conda#3f144b2c34f8cb5a9abd9ed23a39c561 https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_0.conda#da1d979339e2714c30a8e806a33ec087 https://conda.anaconda.org/conda-forge/linux-64/tbb-2021.12.0-h297d8ca_1.conda#3ff978d8994f591818a506640c6a7071 https://conda.anaconda.org/conda-forge/noarch/tenacity-8.3.0-pyhd8ed1ab_0.conda#216cfa8e32bcd1447646768351df6059 @@ -173,7 +174,9 @@ https://conda.anaconda.org/conda-forge/linux-64/xkeyboard-config-2.41-hd590300_0 https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.4-h0b41bf4_2.conda#82b6df12252e6f32402b96dacc656fec https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.11-hd590300_0.conda#ed67c36f215b310412b2af935bf3e530 https://conda.anaconda.org/conda-forge/noarch/zipp-3.17.0-pyhd8ed1ab_0.conda#2e4d6bc0b14e10f895fc6791a7d9b26a +https://conda.anaconda.org/conda-forge/noarch/accessible-pygments-0.0.4-pyhd8ed1ab_0.conda#46a2e6e3dfa718ce3492018d5a110dd6 https://conda.anaconda.org/conda-forge/noarch/babel-2.14.0-pyhd8ed1ab_0.conda#9669586875baeced8fc30c0826c3270e +https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.12.3-pyha770c72_0.conda#332493000404d8411859539a5a630865 https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.0-h3faef2a_0.conda#f907bb958910dc404647326ca80c263e https://conda.anaconda.org/conda-forge/linux-64/cxx-compiler-1.7.0-h00ab1b0_1.conda#28de2e073db9ca9b72858bee9fb6f571 https://conda.anaconda.org/conda-forge/linux-64/cytoolz-0.12.3-py39hd1e30aa_0.conda#dc0fb8e157c7caba4c98f1e1f9d2e5f4 @@ -196,6 +199,7 @@ https://conda.anaconda.org/conda-forge/noarch/pyproject-metadata-0.8.0-pyhd8ed1a https://conda.anaconda.org/conda-forge/noarch/pytest-7.4.4-pyhd8ed1ab_0.conda#a9d145de8c5f064b5fa68fb34725d9f4 https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0-pyhd8ed1ab_0.conda#2cf4264fffb9e6eff6031c5b6884d61c https://conda.anaconda.org/conda-forge/linux-64/sip-6.7.12-py39h3d6467e_0.conda#e667a3ab0df62c54e60e1843d2e6defb +https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.11.0-hd8ed1ab_0.conda#471e3988f8ca5e9eb3ce6be7eac3bcee https://conda.anaconda.org/conda-forge/noarch/urllib3-2.2.1-pyhd8ed1ab_0.conda#08807a87fa7af10754d46f63b368e016 https://conda.anaconda.org/conda-forge/linux-64/compilers-1.7.0-ha770c72_1.conda#d8d07866ac3b5b6937213c89a1874f08 https://conda.anaconda.org/conda-forge/linux-64/gstreamer-1.24.3-haf2f30d_0.conda#f3df87cc9ef0b5113bff55aefcbcafd5 @@ -212,7 +216,7 @@ https://conda.anaconda.org/conda-forge/noarch/dask-core-2024.5.1-pyhd8ed1ab_0.co https://conda.anaconda.org/conda-forge/linux-64/gst-plugins-base-1.24.3-h9ad1361_0.conda#8fb0e954c616bb0f9389efac4b4ed44b https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-22_linux64_mkl.conda#d6f942423116553f068b2f2d93ffea2e https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-22_linux64_mkl.conda#4edf2e7ce63920e4f539d12e32fb478e -https://conda.anaconda.org/conda-forge/noarch/pooch-1.8.1-pyhd8ed1ab_0.conda#d15917f33140f8d2ac9ca44db7ec8a25 +https://conda.anaconda.org/conda-forge/noarch/pooch-1.6.0-pyhd8ed1ab_0.tar.bz2#6429e1d1091c51f626b5dcfdd38bf429 https://conda.anaconda.org/conda-forge/linux-64/pulseaudio-client-17.0-hb77b528_0.conda#07f45f1be1c25345faddb8db0de8039b https://conda.anaconda.org/conda-forge/linux-64/liblapacke-3.9.0-22_linux64_mkl.conda#aa0a5a70e1c957d5911e76ac98e471e1 https://conda.anaconda.org/conda-forge/linux-64/numpy-1.19.5-py39hd249d9e_3.tar.bz2#0cf333996ebdeeba8d1c8c1c0ee9eff9 @@ -236,13 +240,18 @@ https://conda.anaconda.org/conda-forge/noarch/tifffile-2020.6.3-py_0.tar.bz2#1fb https://conda.anaconda.org/conda-forge/linux-64/scikit-image-0.17.2-py39hde0f152_4.tar.bz2#2a58a7e382317b03f023b2fddf40f8a1 https://conda.anaconda.org/conda-forge/noarch/seaborn-0.12.2-hd8ed1ab_0.conda#50847a47c07812f88581081c620f5160 https://conda.anaconda.org/conda-forge/noarch/numpydoc-1.2-pyhd8ed1ab_0.tar.bz2#025ad7ca2c7f65007ab6b6f5d93a56eb +https://conda.anaconda.org/conda-forge/noarch/pydata-sphinx-theme-0.15.2-pyhd8ed1ab_0.conda#ce99859070b0e17ccc63234ca58f3ed8 https://conda.anaconda.org/conda-forge/noarch/sphinx-copybutton-0.5.2-pyhd8ed1ab_0.conda#ac832cc43adc79118cf6e23f1f9b8995 -https://conda.anaconda.org/conda-forge/noarch/sphinx-gallery-0.15.0-pyhd8ed1ab_0.conda#1a49ca9515ef9a96edff2eea06143dc6 -https://conda.anaconda.org/conda-forge/noarch/sphinx-prompt-1.3.0-py_0.tar.bz2#9363002e2a134a287af4e32ff0f26cdc +https://conda.anaconda.org/conda-forge/noarch/sphinx-design-0.5.0-pyhd8ed1ab_0.conda#264b3c697fa9cdade87eb0abe4440d54 +https://conda.anaconda.org/conda-forge/noarch/sphinx-gallery-0.16.0-pyhd8ed1ab_0.conda#add28691ee89e875b190eda07929d5d4 +https://conda.anaconda.org/conda-forge/noarch/sphinx-prompt-1.4.0-pyhd8ed1ab_0.tar.bz2#88ee91e8679603f2a5bd036d52919cc2 +https://conda.anaconda.org/conda-forge/noarch/sphinx-remove-toctrees-1.0.0.post1-pyhd8ed1ab_0.conda#6dee8412218288a17f99f2cfffab334d https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-1.0.8-pyhd8ed1ab_0.conda#611a35a27914fac3aa37611a6fe40bb5 https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-devhelp-1.0.6-pyhd8ed1ab_0.conda#d7e4954df0d3aea2eacc7835ad12671d https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-htmlhelp-2.0.5-pyhd8ed1ab_0.conda#7e1e7437273682ada2ed5e9e9714b140 https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-qthelp-1.0.7-pyhd8ed1ab_0.conda#26acae54b06f178681bfb551760f5dd1 -https://conda.anaconda.org/conda-forge/noarch/sphinx-6.0.0-pyhd8ed1ab_2.conda#ac1d3b55da1669ee3a56973054fd7efb +https://conda.anaconda.org/conda-forge/noarch/sphinx-7.3.7-pyhd8ed1ab_0.conda#7b1465205e28d75d2c0e1a868ee00a67 https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-serializinghtml-1.1.10-pyhd8ed1ab_0.conda#e507335cb4ca9cff4c3d0fa9cdab255e -# pip sphinxext-opengraph @ https://files.pythonhosted.org/packages/50/ac/c105ed3e0a00b14b28c0aa630935af858fd8a32affeff19574b16e2c6ae8/sphinxext_opengraph-0.4.2-py3-none-any.whl#sha256=a51f2604f9a5b6c0d25d3a88e694d5c02e20812dc0e482adf96c8628f9109357 +# pip libsass @ https://files.pythonhosted.org/packages/fd/5a/eb5b62641df0459a3291fc206cf5bd669c0feed7814dded8edef4ade8512/libsass-0.23.0-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.whl#sha256=4a218406d605f325d234e4678bd57126a66a88841cb95bee2caeafdc6f138306 +# pip sphinxcontrib-sass @ https://files.pythonhosted.org/packages/2e/87/7c2eb08e3ca1d6baae32c0a5e005330fe1cec93a36aa085e714c3b3a3c7d/sphinxcontrib_sass-0.3.4-py2.py3-none-any.whl#sha256=a0c79a44ae8b8935c02dc340ebe40c9e002c839331201c899dc93708970c355a +# pip sphinxext-opengraph @ https://files.pythonhosted.org/packages/92/0a/970b80b4fa1feeb6deb6f2e22d4cb14e388b27b315a1afdb9db930ff91a4/sphinxext_opengraph-0.9.1-py3-none-any.whl#sha256=b3b230cc6a5b5189139df937f0d9c7b23c7c204493b22646273687969dcb760e diff --git a/build_tools/circle/list_versions.py b/build_tools/circle/list_versions.py index 345e08b4bece4..e1f8d54b84ec5 100755 --- a/build_tools/circle/list_versions.py +++ b/build_tools/circle/list_versions.py @@ -1,6 +1,11 @@ #!/usr/bin/env python3 -# List all available versions of the documentation +# Write the available versions page (--rst) and the version switcher JSON (--json). +# Version switcher see: +# https://pydata-sphinx-theme.readthedocs.io/en/stable/user_guide/version-dropdown.html +# https://pydata-sphinx-theme.readthedocs.io/en/stable/user_guide/announcements.html#announcement-banners + +import argparse import json import re import sys @@ -52,14 +57,19 @@ def get_file_size(version): return human_readable_data_quantity(path_details["size"], 1000) -print(":orphan:") -print() -heading = "Available documentation for Scikit-learn" -print(heading) -print("=" * len(heading)) -print() -print("Web-based documentation is available for versions listed below:") -print() +parser = argparse.ArgumentParser() +parser.add_argument("--rst", type=str, required=True) +parser.add_argument("--json", type=str, required=True) +args = parser.parse_args() + +heading = "Available documentation for scikit-learn" +json_content = [] +rst_content = [ + ":orphan:\n", + heading, + "=" * len(heading) + "\n", + "Web-based documentation is available for versions listed below:\n", +] ROOT_URL = ( "https://api.github.com/repos/scikit-learn/scikit-learn.github.io/contents/" # noqa @@ -93,8 +103,9 @@ def get_file_size(version): # Output in order: dev, stable, decreasing other version seen = set() -for name in NAMED_DIRS + sorted( - (k for k in dirs if k[:1].isdigit()), key=parse_version, reverse=True +for i, name in enumerate( + NAMED_DIRS + + sorted((k for k in dirs if k[:1].isdigit()), key=parse_version, reverse=True) ): version_num, file_size = dirs[name] if version_num in seen: @@ -102,17 +113,32 @@ def get_file_size(version): continue else: seen.add(version_num) - name_display = "" if name[:1].isdigit() else " (%s)" % name - path = "https://scikit-learn.org/%s/" % name - out = "* `Scikit-learn %s%s documentation <%s>`_" % ( - version_num, - name_display, - path, - ) + + full_name = f"{version_num}" if name[:1].isdigit() else f"{version_num} ({name})" + path = f"https://scikit-learn.org/{name}/" + + # Update JSON for the version switcher; only keep the 8 latest versions to avoid + # overloading the version switcher dropdown + if i < 8: + info = {"name": full_name, "version": version_num, "url": path} + if name == "stable": + info["preferred"] = True + json_content.append(info) + + # Printout for the historical version page + out = f"* `scikit-learn {full_name} documentation <{path}>`_" if file_size is not None: file_extension = get_file_extension(version_num) out += ( f" (`{file_extension.upper()} {file_size} <{path}/" f"_downloads/scikit-learn-docs.{file_extension}>`_)" ) - print(out) + rst_content.append(out) + +with open(args.rst, "w", encoding="utf-8") as f: + f.write("\n".join(rst_content) + "\n") +print(f"Written {args.rst}") + +with open(args.json, "w", encoding="utf-8") as f: + json.dump(json_content, f, indent=2) +print(f"Written {args.json}") diff --git a/build_tools/update_environments_and_lock_files.py b/build_tools/update_environments_and_lock_files.py index 86da119ec4547..bf086e21716e3 100644 --- a/build_tools/update_environments_and_lock_files.py +++ b/build_tools/update_environments_and_lock_files.py @@ -307,8 +307,14 @@ def remove_from(alist, to_remove): "plotly", "polars", "pooch", + "sphinx-remove-toctrees", + "sphinx-design", + "pydata-sphinx-theme", + ], + "pip_dependencies": [ + "sphinxext-opengraph", + "sphinxcontrib-sass", ], - "pip_dependencies": ["sphinxext-opengraph"], "package_constraints": { "python": "3.9", "numpy": "min", @@ -325,6 +331,11 @@ def remove_from(alist, to_remove): "sphinxext-opengraph": "min", "plotly": "min", "polars": "min", + "pooch": "min", + "sphinx-design": "min", + "sphinxcontrib-sass": "min", + "sphinx-remove-toctrees": "min", + "pydata-sphinx-theme": "min", }, }, { @@ -349,8 +360,15 @@ def remove_from(alist, to_remove): "polars", "pooch", "sphinxext-opengraph", + "sphinx-remove-toctrees", + "sphinx-design", + "pydata-sphinx-theme", + ], + "pip_dependencies": [ + "jupyterlite-sphinx", + "jupyterlite-pyodide-kernel", + "sphinxcontrib-sass", ], - "pip_dependencies": ["jupyterlite-sphinx", "jupyterlite-pyodide-kernel"], "package_constraints": { "python": "3.9", }, @@ -426,7 +444,7 @@ def execute_command(command_list): ) out, err = proc.communicate() - out, err = out.decode(), err.decode() + out, err = out.decode(errors="replace"), err.decode(errors="replace") if proc.returncode != 0: command_str = " ".join(command_list) diff --git a/doc/Makefile b/doc/Makefile index 44f02585f6205..f84d3c78b8051 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -47,9 +47,17 @@ help: clean: -rm -rf $(BUILDDIR)/* + @echo "Removed $(BUILDDIR)/*" -rm -rf auto_examples/ + @echo "Removed auto_examples/" -rm -rf generated/* + @echo "Removed generated/" -rm -rf modules/generated/ + @echo "Removed modules/generated/" + -rm -rf css/styles/ + @echo "Removed css/styles/" + -rm -rf api/*.rst + @echo "Removed api/*.rst" # Default to SPHINX_NUMJOBS=1 for full documentation build. Using # SPHINX_NUMJOBS!=1 may actually slow down the build, or cause weird issues in diff --git a/doc/about.rst b/doc/about.rst index 035bddb0ea4dc..47d57e4737318 100644 --- a/doc/about.rst +++ b/doc/about.rst @@ -13,8 +13,8 @@ this project as part of his thesis. In 2010 Fabian Pedregosa, Gael Varoquaux, Alexandre Gramfort and Vincent Michel of INRIA took leadership of the project and made the first public release, February the 1st 2010. Since then, several releases have appeared -following a ~ 3-month cycle, and a thriving international community has -been leading the development. +following an approximately 3-month cycle, and a thriving international +community has been leading the development. Governance ---------- @@ -28,7 +28,7 @@ out in the :ref:`governance document `. .. _authors: The people behind scikit-learn -------------------------------- +------------------------------ Scikit-learn is a community project, developed by a large group of people, all across the world. A few teams, listed below, have central @@ -44,14 +44,16 @@ consolidating scikit-learn's development and maintenance: .. include:: maintainers.rst -Please do not email the authors directly to ask for assistance or report issues. -Instead, please see `What's the best way to ask questions about scikit-learn -`_ -in the FAQ. +.. note:: + + Please do not email the authors directly to ask for assistance or report issues. + Instead, please see `What's the best way to ask questions about scikit-learn + `_ + in the FAQ. .. seealso:: - :ref:`How you can contribute to the project ` + How you can :ref:`contribute to the project `. Documentation Team .................. @@ -77,9 +79,8 @@ The following people help with :ref:`communication around scikit-learn .. include:: communication_team.rst - Emeritus Core Developers ------------------------- +........................ The following people have been active contributors in the past, but are no longer active in the project: @@ -87,7 +88,7 @@ longer active in the project: .. include:: maintainers_emeritus.rst Emeritus Communication Team ---------------------------- +........................... The following people have been active in the communication team in the past, but no longer have communication responsibilities: @@ -95,7 +96,7 @@ past, but no longer have communication responsibilities: .. include:: communication_team_emeritus.rst Emeritus Contributor Experience Team ------------------------------------- +.................................... The following people have been active in the contributor experience team in the past: @@ -157,488 +158,305 @@ High quality PNG and SVG logos are available in the `doc/logos/ source directory. .. image:: images/scikit-learn-logo-notext.png - :align: center + :align: center Funding ------- -Scikit-Learn is a community driven project, however institutional and private + +Scikit-learn is a community driven project, however institutional and private grants help to assure its sustainability. The project would like to thank the following funders. ................................... +.. div:: sk-text-image-grid-small -.. raw:: html - -
-
- -`:probabl. `_ funds Adrin Jalali, Arturo Amor, -François Goupil, Guillaume Lemaitre, Jérémie du Boisberranger, Olivier Grisel, and -Stefanie Senger. + .. div:: text-box -.. raw:: html - -
- -
- -.. image:: images/probabl.png - :width: 75pt - :align: center - :target: https://probabl.ai + `:probabl. `_ funds Adrin Jalali, Arturo Amor, François Goupil, + Guillaume Lemaitre, Jérémie du Boisberranger, Olivier Grisel, and Stefanie Senger. -.. raw:: html + .. div:: image-box -
-
+ .. image:: images/probabl.png + :target: https://probabl.ai .......... -.. raw:: html - -
-
- -The `Members `_ of -the `Scikit-Learn Consortium at Inria Foundation -`_ help at maintaining and -improving the project through their financial support. - -.. raw:: html - -
- .. |chanel| image:: images/chanel.png - :width: 55pt - :target: https://www.chanel.com + :target: https://www.chanel.com .. |axa| image:: images/axa.png - :width: 40pt - :target: https://www.axa.fr/ + :target: https://www.axa.fr/ .. |bnp| image:: images/bnp.png - :width: 120pt - :target: https://www.bnpparibascardif.com/ + :target: https://www.bnpparibascardif.com/ .. |dataiku| image:: images/dataiku.png - :width: 55pt - :target: https://www.dataiku.com/ + :target: https://www.dataiku.com/ .. |hf| image:: images/huggingface_logo-noborder.png - :width: 55pt - :target: https://huggingface.co + :target: https://huggingface.co .. |nvidia| image:: images/nvidia.png - :width: 55pt - :target: https://www.nvidia.com + :target: https://www.nvidia.com .. |inria| image:: images/inria-logo.jpg - :width: 75pt - :target: https://www.inria.fr - - -.. raw:: html - -
- -.. table:: - :class: sk-sponsor-table - - +----------+-----------+ - | |chanel| | - +----------+-----------+ - | | - +----------+-----------+ - | |axa| | |bnp| | - +----------+-----------+ - | | - +----------+-----------+ - | |nvidia| | |hf| | - +----------+-----------+ - | | - +----------+-----------+ - | |dataiku| | - +----------+-----------+ - | | - +----------+-----------+ - | |inria| | - +----------+-----------+ + :target: https://www.inria.fr .. raw:: html -
-
+ -.. raw:: html +.. div:: sk-text-image-grid-small - + .. div:: text-box -
+ The `Members `_ of + the `Scikit-learn Consortium at Inria Foundation + `_ help at maintaining and + improving the project through their financial support. -.. image:: images/nvidia.png - :width: 55pt - :align: center - :target: https://nvidia.com + .. div:: image-box -.. raw:: html + .. table:: + :class: image-subtable -
- + +----------+-----------+ + | |chanel| | + +----------+-----------+ + | |axa| | |bnp| | + +----------+-----------+ + | |nvidia| | |hf| | + +----------+-----------+ + | |dataiku| | + +----------+-----------+ + | |inria| | + +----------+-----------+ .......... -.. raw:: html - -
-
+.. div:: sk-text-image-grid-small -`Microsoft `_ funds Andreas Müller since 2020. + .. div:: text-box -.. raw:: html - -
+ `NVidia `_ funds Tim Head since 2022 + and is part of the scikit-learn consortium at Inria. -
+ .. div:: image-box -.. image:: images/microsoft.png - :width: 100pt - :align: center - :target: https://www.microsoft.com/ + .. image:: images/nvidia.png + :target: https://nvidia.com -.. raw:: html +.......... -
-
+.. div:: sk-text-image-grid-small -........... + .. div:: text-box -.. raw:: html + `Microsoft `_ funds Andreas Müller since 2020. -
-
+ .. div:: image-box -`Quansight Labs `_ funds Lucy Liu since 2022. + .. image:: images/microsoft.png + :target: https://microsoft.com -.. raw:: html +........... -
+.. div:: sk-text-image-grid-small -
+ .. div:: text-box -.. image:: images/quansight-labs.png - :width: 100pt - :align: center - :target: https://labs.quansight.org + `Quansight Labs `_ funds Lucy Liu since 2022. -.. raw:: html + .. div:: image-box -
-
+ .. image:: images/quansight-labs.png + :target: https://labs.quansight.org ........... -.. raw:: html - -
-
- -`Tidelift `_ supports the project via their service -agreement. +.. div:: sk-text-image-grid-small -.. raw:: html + .. div:: text-box -
+ `Tidelift `_ supports the project via their service + agreement. -
+ .. div:: image-box -.. image:: images/Tidelift-logo-on-light.svg - :width: 100pt - :align: center - :target: https://tidelift.com/ + .. image:: images/Tidelift-logo-on-light.svg + :target: https://tidelift.com/ -.. raw:: html +........... -
-
Past Sponsors ............. -.. raw:: html - -
-
- -`Quansight Labs `_ funded Meekail Zain in 2022 and 2023 and, -funded Thomas J. Fan from 2021 to 2023. - -.. raw:: html - -
+.. div:: sk-text-image-grid-small -
+ .. div:: text-box -.. image:: images/quansight-labs.png - :width: 100pt - :align: center - :target: https://labs.quansight.org + `Quansight Labs `_ funded Meekail Zain in 2022 and 2023, + and funded Thomas J. Fan from 2021 to 2023. -.. raw:: html + .. div:: image-box -
-
+ .. image:: images/quansight-labs.png + :target: https://labs.quansight.org ........... -.. raw:: html - -
-
+.. div:: sk-text-image-grid-small -`Columbia University `_ funded Andreas Müller -(2016-2020). + .. div:: text-box -.. raw:: html - -
- -
- -.. image:: images/columbia.png - :width: 50pt - :align: center - :target: https://www.columbia.edu/ + `Columbia University `_ funded Andreas Müller + (2016-2020). -.. raw:: html + .. div:: image-box -
-
+ .. image:: images/columbia.png + :target: https://columbia.edu ........ -.. raw:: html - -
-
+.. div:: sk-text-image-grid-small -`The University of Sydney `_ funded Joel Nothman -(2017-2021). + .. div:: text-box -.. raw:: html + `The University of Sydney `_ funded Joel Nothman + (2017-2021). -
+ .. div:: image-box -
- -.. image:: images/sydney-primary.jpeg - :width: 100pt - :align: center - :target: https://sydney.edu.au/ - -.. raw:: html - -
-
+ .. image:: images/sydney-primary.jpeg + :target: https://sydney.edu.au/ ........... -.. raw:: html - -
-
- -Andreas Müller received a grant to improve scikit-learn from the -`Alfred P. Sloan Foundation `_ . -This grant supported the position of Nicolas Hug and Thomas J. Fan. - -.. raw:: html - -
+.. div:: sk-text-image-grid-small -
+ .. div:: text-box -.. image:: images/sloan_banner.png - :width: 100pt - :align: center - :target: https://sloan.org/ + Andreas Müller received a grant to improve scikit-learn from the + `Alfred P. Sloan Foundation `_ . + This grant supported the position of Nicolas Hug and Thomas J. Fan. -.. raw:: html + .. div:: image-box -
-
+ .. image:: images/sloan_banner.png + :target: https://sloan.org/ ............. -.. raw:: html - -
-
- -`INRIA `_ actively supports this project. It has -provided funding for Fabian Pedregosa (2010-2012), Jaques Grobler -(2012-2013) and Olivier Grisel (2013-2017) to work on this project -full-time. It also hosts coding sprints and other events. - -.. raw:: html - -
+.. div:: sk-text-image-grid-small -
+ .. div:: text-box -.. image:: images/inria-logo.jpg - :width: 100pt - :align: center - :target: https://www.inria.fr + `INRIA `_ actively supports this project. It has + provided funding for Fabian Pedregosa (2010-2012), Jaques Grobler + (2012-2013) and Olivier Grisel (2013-2017) to work on this project + full-time. It also hosts coding sprints and other events. -.. raw:: html + .. div:: image-box -
-
+ .. image:: images/inria-logo.jpg + :target: https://www.inria.fr ..................... -.. raw:: html - -
-
+.. div:: sk-text-image-grid-small -`Paris-Saclay Center for Data Science -`_ -funded one year for a developer to work on the project full-time -(2014-2015), 50% of the time of Guillaume Lemaitre (2016-2017) and 50% of the -time of Joris van den Bossche (2017-2018). + .. div:: text-box -.. raw:: html - -
-
+ `Paris-Saclay Center for Data Science `_ + funded one year for a developer to work on the project full-time (2014-2015), 50% + of the time of Guillaume Lemaitre (2016-2017) and 50% of the time of Joris van den + Bossche (2017-2018). -.. image:: images/cds-logo.png - :width: 100pt - :align: center - :target: http://www.datascience-paris-saclay.fr/ - -.. raw:: html + .. div:: image-box -
-
+ .. image:: images/cds-logo.png + :target: http://www.datascience-paris-saclay.fr/ .......................... -.. raw:: html - -
-
- -`NYU Moore-Sloan Data Science Environment `_ -funded Andreas Mueller (2014-2016) to work on this project. The Moore-Sloan -Data Science Environment also funds several students to work on the project -part-time. - -.. raw:: html +.. div:: sk-text-image-grid-small -
-
+ .. div:: text-box -.. image:: images/nyu_short_color.png - :width: 100pt - :align: center - :target: https://cds.nyu.edu/mooresloan/ + `NYU Moore-Sloan Data Science Environment `_ + funded Andreas Mueller (2014-2016) to work on this project. The Moore-Sloan + Data Science Environment also funds several students to work on the project + part-time. -.. raw:: html + .. div:: image-box -
-
+ .. image:: images/nyu_short_color.png + :target: https://cds.nyu.edu/mooresloan/ ........................ -.. raw:: html +.. div:: sk-text-image-grid-small -
-
+ .. div:: text-box -`Télécom Paristech `_ funded Manoj Kumar -(2014), Tom Dupré la Tour (2015), Raghav RV (2015-2017), Thierry Guillemot -(2016-2017) and Albert Thomas (2017) to work on scikit-learn. + `Télécom Paristech `_ funded Manoj Kumar + (2014), Tom Dupré la Tour (2015), Raghav RV (2015-2017), Thierry Guillemot + (2016-2017) and Albert Thomas (2017) to work on scikit-learn. -.. raw:: html + .. div:: image-box -
-
- -.. image:: images/telecom.png - :width: 50pt - :align: center - :target: https://www.telecom-paristech.fr/ - -.. raw:: html - -
-
+ .. image:: images/telecom.png + :target: https://www.telecom-paristech.fr/ ..................... -.. raw:: html - -
-
- -`The Labex DigiCosme `_ funded Nicolas Goix -(2015-2016), Tom Dupré la Tour (2015-2016 and 2017-2018), Mathurin Massias -(2018-2019) to work part time on scikit-learn during their PhDs. It also -funded a scikit-learn coding sprint in 2015. - -.. raw:: html +.. div:: sk-text-image-grid-small -
-
+ .. div:: text-box -.. image:: images/digicosme.png - :width: 100pt - :align: center - :target: https://digicosme.lri.fr + `The Labex DigiCosme `_ funded Nicolas Goix + (2015-2016), Tom Dupré la Tour (2015-2016 and 2017-2018), Mathurin Massias + (2018-2019) to work part time on scikit-learn during their PhDs. It also + funded a scikit-learn coding sprint in 2015. -.. raw:: html + .. div:: image-box -
-
+ .. image:: images/digicosme.png + :target: https://digicosme.lri.fr ..................... -.. raw:: html - -
-
+.. div:: sk-text-image-grid-small -`The Chan-Zuckerberg Initiative `_ funded Nicolas -Hug to work full-time on scikit-learn in 2020. + .. div:: text-box -.. raw:: html - -
-
+ `The Chan-Zuckerberg Initiative `_ funded Nicolas + Hug to work full-time on scikit-learn in 2020. -.. image:: images/czi_logo.svg - :width: 100pt - :align: center - :target: https://chanzuckerberg.com - -.. raw:: html + .. div:: image-box -
-
+ .. image:: images/czi_logo.svg + :target: https://chanzuckerberg.com ...................... @@ -649,9 +467,9 @@ program. - 2007 - David Cournapeau - 2011 - `Vlad Niculae`_ -- 2012 - `Vlad Niculae`_, Immanuel Bayer. +- 2012 - `Vlad Niculae`_, Immanuel Bayer - 2013 - Kemal Eren, Nicolas Trésegnie -- 2014 - Hamzeh Alsalhi, Issam Laradji, Maheshakya Wijewardena, Manoj Kumar. +- 2014 - Hamzeh Alsalhi, Issam Laradji, Maheshakya Wijewardena, Manoj Kumar - 2015 - `Raghav RV `_, Wei Xue - 2016 - `Nelson Liu `_, `YenChen Lin `_ @@ -670,86 +488,112 @@ The following organizations funded the scikit-learn consortium at Inria in the past: .. |msn| image:: images/microsoft.png - :width: 100pt - :target: https://www.microsoft.com/ + :target: https://www.microsoft.com/ .. |bcg| image:: images/bcg.png - :width: 100pt - :target: https://www.bcg.com/beyond-consulting/bcg-gamma/default.aspx + :target: https://www.bcg.com/beyond-consulting/bcg-gamma/default.aspx .. |fujitsu| image:: images/fujitsu.png - :width: 100pt - :target: https://www.fujitsu.com/global/ + :target: https://www.fujitsu.com/global/ .. |aphp| image:: images/logo_APHP_text.png - :width: 150pt - :target: https://aphp.fr/ + :target: https://aphp.fr/ +.. raw:: html + + + +.. grid:: 2 2 4 4 + :class-row: image-subgrid + :gutter: 1 + + .. grid-item:: + :class: sd-text-center + :child-align: center + + |msn| + + .. grid-item:: + :class: sd-text-center + :child-align: center + + |bcg| + + .. grid-item:: + :class: sd-text-center + :child-align: center -|bcg| |msn| |fujitsu| |aphp| + |fujitsu| + + .. grid-item:: + :class: sd-text-center + :child-align: center + + |aphp| Sprints ------- -The International 2019 Paris sprint was kindly hosted by `AXA `_. -Also some participants could attend thanks to the support of the `Alfred P. -Sloan Foundation `_, the `Python Software -Foundation `_ (PSF) and the `DATAIA Institute -`_. - -..................... +- The International 2019 Paris sprint was kindly hosted by `AXA `_. + Also some participants could attend thanks to the support of the `Alfred P. + Sloan Foundation `_, the `Python Software + Foundation `_ (PSF) and the `DATAIA Institute + `_. -The 2013 International Paris Sprint was made possible thanks to the support of -`Télécom Paristech `_, `tinyclues -`_, the `French Python Association -`_ and the `Fonds de la Recherche Scientifique -`_. +- The 2013 International Paris Sprint was made possible thanks to the support of + `Télécom Paristech `_, `tinyclues + `_, the `French Python Association + `_ and the `Fonds de la Recherche Scientifique + `_. -.............. +- The 2011 International Granada sprint was made possible thanks to the support + of the `PSF `_ and `tinyclues + `_. -The 2011 International Granada sprint was made possible thanks to the support -of the `PSF `_ and `tinyclues -`_. Donating to the project -....................... +----------------------- If you are interested in donating to the project or to one of our code-sprints, please donate via the `NumFOCUS Donations Page `_. -.. raw :: html - - -
+.. raw:: html -All donations will be handled by `NumFOCUS -`_, a non-profit-organization which is -managed by a board of `Scipy community members -`_. NumFOCUS's mission is to foster -scientific computing software, in particular in Python. As a fiscal home -of scikit-learn, it ensures that money is available when needed to keep -the project funded and available while in compliance with tax regulations. +

+ + Help us, donate! + +

-The received donations for the scikit-learn project mostly will go towards -covering travel-expenses for code sprints, as well as towards the organization -budget of the project [#f1]_. +All donations will be handled by `NumFOCUS `_, a non-profit +organization which is managed by a board of `Scipy community members +`_. NumFOCUS's mission is to foster scientific +computing software, in particular in Python. As a fiscal home of scikit-learn, it +ensures that money is available when needed to keep the project funded and available +while in compliance with tax regulations. +The received donations for the scikit-learn project mostly will go towards covering +travel-expenses for code sprints, as well as towards the organization budget of the +project [#f1]_. .. rubric:: Notes .. [#f1] Regarding the organization budget, in particular, we might use some of - the donated funds to pay for other project expenses such as DNS, - hosting or continuous integration services. + the donated funds to pay for other project expenses such as DNS, + hosting or continuous integration services. + Infrastructure support ---------------------- -- We would also like to thank `Microsoft Azure - `_, `Cirrus Cl `_, - `CircleCl `_ for free CPU time on their Continuous - Integration servers, and `Anaconda Inc. `_ for the - storage they provide for our staging and nightly builds. +We would also like to thank `Microsoft Azure `_, +`Cirrus Cl `_, `CircleCl `_ for free CPU +time on their Continuous Integration servers, and `Anaconda Inc. `_ +for the storage they provide for our staging and nightly builds. diff --git a/doc/api/deprecated.rst.template b/doc/api/deprecated.rst.template new file mode 100644 index 0000000000000..a48f0180f76ed --- /dev/null +++ b/doc/api/deprecated.rst.template @@ -0,0 +1,24 @@ +:html_theme.sidebar_secondary.remove: + +.. _api_depr_ref: + +Recently Deprecated +=================== + +.. currentmodule:: sklearn + +{% for ver, objs in DEPRECATED_API_REFERENCE %} +.. _api_depr_ref-{{ ver|replace(".", "-") }}: + +.. rubric:: To be removed in {{ ver }} + +.. autosummary:: + :nosignatures: + :toctree: ../modules/generated/ + :template: base.rst + +{% for obj in objs %} + {{ obj }} +{%- endfor %} + +{% endfor %} diff --git a/doc/api/index.rst.template b/doc/api/index.rst.template new file mode 100644 index 0000000000000..a9f3209d350de --- /dev/null +++ b/doc/api/index.rst.template @@ -0,0 +1,77 @@ +:html_theme.sidebar_secondary.remove: + +.. _api_ref: + +============= +API Reference +============= + +This is the class and function reference of scikit-learn. Please refer to the +:ref:`full user guide ` for further details, as the raw specifications of +classes and functions may not be enough to give full guidelines on their uses. For +reference on concepts repeated across the API, see :ref:`glossary`. + +.. toctree:: + :maxdepth: 2 + :hidden: + +{% for module, _ in API_REFERENCE %} + {{ module }} +{%- endfor %} +{%- if DEPRECATED_API_REFERENCE %} + deprecated +{%- endif %} + +.. list-table:: + :header-rows: 1 + :class: apisearch-table + + * - Object + - Description + +{% for module, module_info in API_REFERENCE %} +{% for section in module_info["sections"] %} +{% for obj in section["autosummary"] %} +{% set parts = obj.rsplit(".", 1) %} +{% if parts|length > 1 %} +{% set full_module = module + "." + parts[0] %} +{% else %} +{% set full_module = module %} +{% endif %} + * - :obj:`~{{ module }}.{{ obj }}` + + - .. div:: sk-apisearch-desc + + .. currentmodule:: {{ full_module }} + + .. autoshortsummary:: {{ module }}.{{ obj }} + + .. div:: caption + + :mod:`{{ full_module }}` +{% endfor %} +{% endfor %} +{% endfor %} + +{% for ver, objs in DEPRECATED_API_REFERENCE %} +{% for obj in objs %} +{% set parts = obj.rsplit(".", 1) %} +{% if parts|length > 1 %} +{% set full_module = "sklearn." + parts[0] %} +{% else %} +{% set full_module = "sklearn" %} +{% endif %} + * - :obj:`~sklearn.{{ obj }}` + + - .. div:: sk-apisearch-desc + + .. currentmodule:: {{ full_module }} + + .. autoshortsummary:: sklearn.{{ obj }} + + .. div:: caption + + :mod:`{{ full_module }}` + :bdg-ref-danger-line:`Deprecated in version {{ ver }} ` +{% endfor %} +{% endfor %} diff --git a/doc/api/module.rst.template b/doc/api/module.rst.template new file mode 100644 index 0000000000000..1980f27aad158 --- /dev/null +++ b/doc/api/module.rst.template @@ -0,0 +1,46 @@ +:html_theme.sidebar_secondary.remove: + +{% if module == "sklearn" -%} +{%- set module_hook = "sklearn" -%} +{%- elif module.startswith("sklearn.") -%} +{%- set module_hook = module[8:] -%} +{%- else -%} +{%- set module_hook = None -%} +{%- endif -%} + +{% if module_hook %} +.. _{{ module_hook }}_ref: +{% endif %} + +{{ module }} +{{ "=" * module|length }} + +.. automodule:: {{ module }} + +{% if module_info["description"] %} +{{ module_info["description"] }} +{% endif %} + +{% for section in module_info["sections"] %} +{% if section["title"] and module_hook %} +.. _{{ module_hook }}_ref-{{ section["title"]|lower|replace(" ", "-") }}: +{% endif %} + +{% if section["title"] %} +{{ section["title"] }} +{{ "-" * section["title"]|length }} +{% endif %} + +{% if section["description"] %} +{{ section["description"] }} +{% endif %} + +.. autosummary:: + :nosignatures: + :toctree: ../modules/generated/ + :template: base.rst + +{% for obj in section["autosummary"] %} + {{ obj }} +{%- endfor %} +{% endfor %} diff --git a/doc/api_reference.py b/doc/api_reference.py new file mode 100644 index 0000000000000..c8a22ebc2d5b3 --- /dev/null +++ b/doc/api_reference.py @@ -0,0 +1,1336 @@ +"""Configuration for the API reference documentation.""" + + +def _get_guide(*refs, is_developer=False): + """Get the rst to refer to user/developer guide. + + `refs` is several references that can be used in the :ref:`...` directive. + """ + if len(refs) == 1: + ref_desc = f":ref:`{refs[0]}` section" + elif len(refs) == 2: + ref_desc = f":ref:`{refs[0]}` and :ref:`{refs[1]}` sections" + else: + ref_desc = ", ".join(f":ref:`{ref}`" for ref in refs[:-1]) + ref_desc += f", and :ref:`{refs[-1]}` sections" + + guide_name = "Developer" if is_developer else "User" + return f"**{guide_name} guide.** See the {ref_desc} for further details." + + +def _get_submodule(module_name, submodule_name): + """Get the submodule docstring and automatically add the hook. + + `module_name` is e.g. `sklearn.feature_extraction`, and `submodule_name` is e.g. + `image`, so we get the docstring and hook for `sklearn.feature_extraction.image` + submodule. `module_name` is used to reset the current module because autosummary + automatically changes the current module. + """ + lines = [ + f".. automodule:: {module_name}.{submodule_name}", + f".. currentmodule:: {module_name}", + ] + return "\n\n".join(lines) + + +""" +CONFIGURING API_REFERENCE +========================= + +API_REFERENCE maps each module name to a dictionary that consists of the following +components: + +short_summary (required) + The text to be printed on the index page; it has nothing to do the API reference + page of each module. +description (required, `None` if not needed) + The additional description for the module to be placed under the module + docstring, before the sections start. +sections (required) + A list of sections, each of which consists of: + - title (required, `None` if not needed): the section title, commonly it should + not be `None` except for the first section of a module, + - description (optional): the optional additional description for the section, + - autosummary (required): an autosummary block, assuming current module is the + current module name. + +Essentially, the rendered page would look like the following: + +|---------------------------------------------------------------------------------| +| {{ module_name }} | +| ================= | +| {{ module_docstring }} | +| {{ description }} | +| | +| {{ section_title_1 }} <-------------- Optional if one wants the first | +| --------------------- section to directly follow | +| {{ section_description_1 }} without a second-level heading. | +| {{ section_autosummary_1 }} | +| | +| {{ section_title_2 }} | +| --------------------- | +| {{ section_description_2 }} | +| {{ section_autosummary_2 }} | +| | +| More sections... | +|---------------------------------------------------------------------------------| + +Hooks will be automatically generated for each module and each section. For a module, +e.g., `sklearn.feature_extraction`, the hook would be `feature_extraction_ref`; for a +section, e.g., "From text" under `sklearn.feature_extraction`, the hook would be +`feature_extraction_ref-from-text`. However, note that a better way is to refer using +the :mod: directive, e.g., :mod:`sklearn.feature_extraction` for the module and +:mod:`sklearn.feature_extraction.text` for the section. Only in case that a section +is not a particular submodule does the hook become useful, e.g., the "Loaders" section +under `sklearn.datasets`. +""" + +API_REFERENCE = { + "sklearn": { + "short_summary": "Settings and information tools.", + "description": None, + "sections": [ + { + "title": None, + "autosummary": [ + "config_context", + "get_config", + "set_config", + "show_versions", + ], + }, + ], + }, + "sklearn.base": { + "short_summary": "Base classes and utility functions.", + "description": None, + "sections": [ + { + "title": None, + "autosummary": [ + "BaseEstimator", + "BiclusterMixin", + "ClassNamePrefixFeaturesOutMixin", + "ClassifierMixin", + "ClusterMixin", + "DensityMixin", + "MetaEstimatorMixin", + "OneToOneFeatureMixin", + "OutlierMixin", + "RegressorMixin", + "TransformerMixin", + "clone", + "is_classifier", + "is_regressor", + ], + } + ], + }, + "sklearn.calibration": { + "short_summary": "Probability calibration.", + "description": _get_guide("calibration"), + "sections": [ + { + "title": None, + "autosummary": ["CalibratedClassifierCV", "calibration_curve"], + }, + { + "title": "Visualization", + "autosummary": ["CalibrationDisplay"], + }, + ], + }, + "sklearn.cluster": { + "short_summary": "Clustering.", + "description": _get_guide("clustering", "biclustering"), + "sections": [ + { + "title": None, + "autosummary": [ + "AffinityPropagation", + "AgglomerativeClustering", + "Birch", + "BisectingKMeans", + "DBSCAN", + "FeatureAgglomeration", + "HDBSCAN", + "KMeans", + "MeanShift", + "MiniBatchKMeans", + "OPTICS", + "SpectralBiclustering", + "SpectralClustering", + "SpectralCoclustering", + "affinity_propagation", + "cluster_optics_dbscan", + "cluster_optics_xi", + "compute_optics_graph", + "dbscan", + "estimate_bandwidth", + "k_means", + "kmeans_plusplus", + "mean_shift", + "spectral_clustering", + "ward_tree", + ], + }, + ], + }, + "sklearn.compose": { + "short_summary": "Composite estimators.", + "description": _get_guide("combining_estimators"), + "sections": [ + { + "title": None, + "autosummary": [ + "ColumnTransformer", + "TransformedTargetRegressor", + "make_column_selector", + "make_column_transformer", + ], + }, + ], + }, + "sklearn.covariance": { + "short_summary": "Covariance estimation.", + "description": _get_guide("covariance"), + "sections": [ + { + "title": None, + "autosummary": [ + "EllipticEnvelope", + "EmpiricalCovariance", + "GraphicalLasso", + "GraphicalLassoCV", + "LedoitWolf", + "MinCovDet", + "OAS", + "ShrunkCovariance", + "empirical_covariance", + "graphical_lasso", + "ledoit_wolf", + "ledoit_wolf_shrinkage", + "oas", + "shrunk_covariance", + ], + }, + ], + }, + "sklearn.cross_decomposition": { + "short_summary": "Cross decomposition.", + "description": _get_guide("cross_decomposition"), + "sections": [ + { + "title": None, + "autosummary": ["CCA", "PLSCanonical", "PLSRegression", "PLSSVD"], + }, + ], + }, + "sklearn.datasets": { + "short_summary": "Datasets.", + "description": _get_guide("datasets"), + "sections": [ + { + "title": "Loaders", + "autosummary": [ + "clear_data_home", + "dump_svmlight_file", + "fetch_20newsgroups", + "fetch_20newsgroups_vectorized", + "fetch_california_housing", + "fetch_covtype", + "fetch_kddcup99", + "fetch_lfw_pairs", + "fetch_lfw_people", + "fetch_olivetti_faces", + "fetch_openml", + "fetch_rcv1", + "fetch_species_distributions", + "get_data_home", + "load_breast_cancer", + "load_diabetes", + "load_digits", + "load_files", + "load_iris", + "load_linnerud", + "load_sample_image", + "load_sample_images", + "load_svmlight_file", + "load_svmlight_files", + "load_wine", + ], + }, + { + "title": "Sample generators", + "autosummary": [ + "make_biclusters", + "make_blobs", + "make_checkerboard", + "make_circles", + "make_classification", + "make_friedman1", + "make_friedman2", + "make_friedman3", + "make_gaussian_quantiles", + "make_hastie_10_2", + "make_low_rank_matrix", + "make_moons", + "make_multilabel_classification", + "make_regression", + "make_s_curve", + "make_sparse_coded_signal", + "make_sparse_spd_matrix", + "make_sparse_uncorrelated", + "make_spd_matrix", + "make_swiss_roll", + ], + }, + ], + }, + "sklearn.decomposition": { + "short_summary": "Matrix decomposition.", + "description": _get_guide("decompositions"), + "sections": [ + { + "title": None, + "autosummary": [ + "DictionaryLearning", + "FactorAnalysis", + "FastICA", + "IncrementalPCA", + "KernelPCA", + "LatentDirichletAllocation", + "MiniBatchDictionaryLearning", + "MiniBatchNMF", + "MiniBatchSparsePCA", + "NMF", + "PCA", + "SparseCoder", + "SparsePCA", + "TruncatedSVD", + "dict_learning", + "dict_learning_online", + "fastica", + "non_negative_factorization", + "sparse_encode", + ], + }, + ], + }, + "sklearn.discriminant_analysis": { + "short_summary": "Discriminant analysis.", + "description": _get_guide("lda_qda"), + "sections": [ + { + "title": None, + "autosummary": [ + "LinearDiscriminantAnalysis", + "QuadraticDiscriminantAnalysis", + ], + }, + ], + }, + "sklearn.dummy": { + "short_summary": "Dummy estimators.", + "description": _get_guide("model_evaluation"), + "sections": [ + { + "title": None, + "autosummary": ["DummyClassifier", "DummyRegressor"], + }, + ], + }, + "sklearn.ensemble": { + "short_summary": "Ensemble methods.", + "description": _get_guide("ensemble"), + "sections": [ + { + "title": None, + "autosummary": [ + "AdaBoostClassifier", + "AdaBoostRegressor", + "BaggingClassifier", + "BaggingRegressor", + "ExtraTreesClassifier", + "ExtraTreesRegressor", + "GradientBoostingClassifier", + "GradientBoostingRegressor", + "HistGradientBoostingClassifier", + "HistGradientBoostingRegressor", + "IsolationForest", + "RandomForestClassifier", + "RandomForestRegressor", + "RandomTreesEmbedding", + "StackingClassifier", + "StackingRegressor", + "VotingClassifier", + "VotingRegressor", + ], + }, + ], + }, + "sklearn.exceptions": { + "short_summary": "Exceptions and warnings.", + "description": None, + "sections": [ + { + "title": None, + "autosummary": [ + "ConvergenceWarning", + "DataConversionWarning", + "DataDimensionalityWarning", + "EfficiencyWarning", + "FitFailedWarning", + "InconsistentVersionWarning", + "NotFittedError", + "UndefinedMetricWarning", + ], + }, + ], + }, + "sklearn.experimental": { + "short_summary": "Experimental tools.", + "description": None, + "sections": [ + { + "title": None, + "autosummary": ["enable_halving_search_cv", "enable_iterative_imputer"], + }, + ], + }, + "sklearn.feature_extraction": { + "short_summary": "Feature extraction.", + "description": _get_guide("feature_extraction"), + "sections": [ + { + "title": None, + "autosummary": ["DictVectorizer", "FeatureHasher"], + }, + { + "title": "From images", + "description": _get_submodule("sklearn.feature_extraction", "image"), + "autosummary": [ + "image.PatchExtractor", + "image.extract_patches_2d", + "image.grid_to_graph", + "image.img_to_graph", + "image.reconstruct_from_patches_2d", + ], + }, + { + "title": "From text", + "description": _get_submodule("sklearn.feature_extraction", "text"), + "autosummary": [ + "text.CountVectorizer", + "text.HashingVectorizer", + "text.TfidfTransformer", + "text.TfidfVectorizer", + ], + }, + ], + }, + "sklearn.feature_selection": { + "short_summary": "Feature selection.", + "description": _get_guide("feature_selection"), + "sections": [ + { + "title": None, + "autosummary": [ + "GenericUnivariateSelect", + "RFE", + "RFECV", + "SelectFdr", + "SelectFpr", + "SelectFromModel", + "SelectFwe", + "SelectKBest", + "SelectPercentile", + "SelectorMixin", + "SequentialFeatureSelector", + "VarianceThreshold", + "chi2", + "f_classif", + "f_regression", + "mutual_info_classif", + "mutual_info_regression", + "r_regression", + ], + }, + ], + }, + "sklearn.gaussian_process": { + "short_summary": "Gaussian processes.", + "description": _get_guide("gaussian_process"), + "sections": [ + { + "title": None, + "autosummary": [ + "GaussianProcessClassifier", + "GaussianProcessRegressor", + ], + }, + { + "title": "Kernels", + "description": _get_submodule("sklearn.gaussian_process", "kernels"), + "autosummary": [ + "kernels.CompoundKernel", + "kernels.ConstantKernel", + "kernels.DotProduct", + "kernels.ExpSineSquared", + "kernels.Exponentiation", + "kernels.Hyperparameter", + "kernels.Kernel", + "kernels.Matern", + "kernels.PairwiseKernel", + "kernels.Product", + "kernels.RBF", + "kernels.RationalQuadratic", + "kernels.Sum", + "kernels.WhiteKernel", + ], + }, + ], + }, + "sklearn.impute": { + "short_summary": "Imputation.", + "description": _get_guide("impute"), + "sections": [ + { + "title": None, + "autosummary": [ + "IterativeImputer", + "KNNImputer", + "MissingIndicator", + "SimpleImputer", + ], + }, + ], + }, + "sklearn.inspection": { + "short_summary": "Inspection.", + "description": _get_guide("inspection"), + "sections": [ + { + "title": None, + "autosummary": ["partial_dependence", "permutation_importance"], + }, + { + "title": "Plotting", + "autosummary": ["DecisionBoundaryDisplay", "PartialDependenceDisplay"], + }, + ], + }, + "sklearn.isotonic": { + "short_summary": "Isotonic regression.", + "description": _get_guide("isotonic"), + "sections": [ + { + "title": None, + "autosummary": [ + "IsotonicRegression", + "check_increasing", + "isotonic_regression", + ], + }, + ], + }, + "sklearn.kernel_approximation": { + "short_summary": "Isotonic regression.", + "description": _get_guide("kernel_approximation"), + "sections": [ + { + "title": None, + "autosummary": [ + "AdditiveChi2Sampler", + "Nystroem", + "PolynomialCountSketch", + "RBFSampler", + "SkewedChi2Sampler", + ], + }, + ], + }, + "sklearn.kernel_ridge": { + "short_summary": "Kernel ridge regression.", + "description": _get_guide("kernel_ridge"), + "sections": [ + { + "title": None, + "autosummary": ["KernelRidge"], + }, + ], + }, + "sklearn.linear_model": { + "short_summary": "Generalized linear models.", + "description": ( + _get_guide("linear_model") + + "\n\nThe following subsections are only rough guidelines: the same " + "estimator can fall into multiple categories, depending on its parameters." + ), + "sections": [ + { + "title": "Linear classifiers", + "autosummary": [ + "LogisticRegression", + "LogisticRegressionCV", + "PassiveAggressiveClassifier", + "Perceptron", + "RidgeClassifier", + "RidgeClassifierCV", + "SGDClassifier", + "SGDOneClassSVM", + ], + }, + { + "title": "Classical linear regressors", + "autosummary": ["LinearRegression", "Ridge", "RidgeCV", "SGDRegressor"], + }, + { + "title": "Regressors with variable selection", + "description": ( + "The following estimators have built-in variable selection fitting " + "procedures, but any estimator using a L1 or elastic-net penalty " + "also performs variable selection: typically " + ":class:`~linear_model.SGDRegressor` or " + ":class:`~sklearn.linear_model.SGDClassifier` with an appropriate " + "penalty." + ), + "autosummary": [ + "ElasticNet", + "ElasticNetCV", + "Lars", + "LarsCV", + "Lasso", + "LassoCV", + "LassoLars", + "LassoLarsCV", + "LassoLarsIC", + "OrthogonalMatchingPursuit", + "OrthogonalMatchingPursuitCV", + ], + }, + { + "title": "Bayesian regressors", + "autosummary": ["ARDRegression", "BayesianRidge"], + }, + { + "title": "Multi-task linear regressors with variable selection", + "description": ( + "These estimators fit multiple regression problems (or tasks)" + " jointly, while inducing sparse coefficients. While the inferred" + " coefficients may differ between the tasks, they are constrained" + " to agree on the features that are selected (non-zero" + " coefficients)." + ), + "autosummary": [ + "MultiTaskElasticNet", + "MultiTaskElasticNetCV", + "MultiTaskLasso", + "MultiTaskLassoCV", + ], + }, + { + "title": "Outlier-robust regressors", + "description": ( + "Any estimator using the Huber loss would also be robust to " + "outliers, e.g., :class:`~linear_model.SGDRegressor` with " + "``loss='huber'``." + ), + "autosummary": [ + "HuberRegressor", + "QuantileRegressor", + "RANSACRegressor", + "TheilSenRegressor", + ], + }, + { + "title": "Generalized linear models (GLM) for regression", + "description": ( + "These models allow for response variables to have error " + "distributions other than a normal distribution." + ), + "autosummary": [ + "GammaRegressor", + "PoissonRegressor", + "TweedieRegressor", + ], + }, + { + "title": "Miscellaneous", + "autosummary": [ + "PassiveAggressiveRegressor", + "enet_path", + "lars_path", + "lars_path_gram", + "lasso_path", + "orthogonal_mp", + "orthogonal_mp_gram", + "ridge_regression", + ], + }, + ], + }, + "sklearn.manifold": { + "short_summary": "Manifold learning.", + "description": _get_guide("manifold"), + "sections": [ + { + "title": None, + "autosummary": [ + "Isomap", + "LocallyLinearEmbedding", + "MDS", + "SpectralEmbedding", + "TSNE", + "locally_linear_embedding", + "smacof", + "spectral_embedding", + "trustworthiness", + ], + }, + ], + }, + "sklearn.metrics": { + "short_summary": "Metrics.", + "description": _get_guide("model_evaluation", "metrics"), + "sections": [ + { + "title": "Model selection interface", + "description": _get_guide("scoring_parameter"), + "autosummary": [ + "check_scoring", + "get_scorer", + "get_scorer_names", + "make_scorer", + ], + }, + { + "title": "Classification metrics", + "description": _get_guide("classification_metrics"), + "autosummary": [ + "accuracy_score", + "auc", + "average_precision_score", + "balanced_accuracy_score", + "brier_score_loss", + "class_likelihood_ratios", + "classification_report", + "cohen_kappa_score", + "confusion_matrix", + "d2_log_loss_score", + "dcg_score", + "det_curve", + "f1_score", + "fbeta_score", + "hamming_loss", + "hinge_loss", + "jaccard_score", + "log_loss", + "matthews_corrcoef", + "multilabel_confusion_matrix", + "ndcg_score", + "precision_recall_curve", + "precision_recall_fscore_support", + "precision_score", + "recall_score", + "roc_auc_score", + "roc_curve", + "top_k_accuracy_score", + "zero_one_loss", + ], + }, + { + "title": "Regression metrics", + "description": _get_guide("regression_metrics"), + "autosummary": [ + "d2_absolute_error_score", + "d2_pinball_score", + "d2_tweedie_score", + "explained_variance_score", + "max_error", + "mean_absolute_error", + "mean_absolute_percentage_error", + "mean_gamma_deviance", + "mean_pinball_loss", + "mean_poisson_deviance", + "mean_squared_error", + "mean_squared_log_error", + "mean_tweedie_deviance", + "median_absolute_error", + "r2_score", + "root_mean_squared_error", + "root_mean_squared_log_error", + ], + }, + { + "title": "Multilabel ranking metrics", + "description": _get_guide("multilabel_ranking_metrics"), + "autosummary": [ + "coverage_error", + "label_ranking_average_precision_score", + "label_ranking_loss", + ], + }, + { + "title": "Clustering metrics", + "description": ( + _get_submodule("sklearn.metrics", "cluster") + + "\n\n" + + _get_guide("clustering_evaluation") + ), + "autosummary": [ + "adjusted_mutual_info_score", + "adjusted_rand_score", + "calinski_harabasz_score", + "cluster.contingency_matrix", + "cluster.pair_confusion_matrix", + "completeness_score", + "davies_bouldin_score", + "fowlkes_mallows_score", + "homogeneity_completeness_v_measure", + "homogeneity_score", + "mutual_info_score", + "normalized_mutual_info_score", + "rand_score", + "silhouette_samples", + "silhouette_score", + "v_measure_score", + ], + }, + { + "title": "Biclustering metrics", + "description": _get_guide("biclustering_evaluation"), + "autosummary": ["consensus_score"], + }, + { + "title": "Distance metrics", + "autosummary": ["DistanceMetric"], + }, + { + "title": "Pairwise metrics", + "description": ( + _get_submodule("sklearn.metrics", "pairwise") + + "\n\n" + + _get_guide("metrics") + ), + "autosummary": [ + "pairwise.additive_chi2_kernel", + "pairwise.chi2_kernel", + "pairwise.cosine_distances", + "pairwise.cosine_similarity", + "pairwise.distance_metrics", + "pairwise.euclidean_distances", + "pairwise.haversine_distances", + "pairwise.kernel_metrics", + "pairwise.laplacian_kernel", + "pairwise.linear_kernel", + "pairwise.manhattan_distances", + "pairwise.nan_euclidean_distances", + "pairwise.paired_cosine_distances", + "pairwise.paired_distances", + "pairwise.paired_euclidean_distances", + "pairwise.paired_manhattan_distances", + "pairwise.pairwise_kernels", + "pairwise.polynomial_kernel", + "pairwise.rbf_kernel", + "pairwise.sigmoid_kernel", + "pairwise_distances", + "pairwise_distances_argmin", + "pairwise_distances_argmin_min", + "pairwise_distances_chunked", + ], + }, + { + "title": "Plotting", + "description": _get_guide("visualizations"), + "autosummary": [ + "ConfusionMatrixDisplay", + "DetCurveDisplay", + "PrecisionRecallDisplay", + "PredictionErrorDisplay", + "RocCurveDisplay", + ], + }, + ], + }, + "sklearn.mixture": { + "short_summary": "Gaussian mixture models.", + "description": _get_guide("mixture"), + "sections": [ + { + "title": None, + "autosummary": ["BayesianGaussianMixture", "GaussianMixture"], + }, + ], + }, + "sklearn.model_selection": { + "short_summary": "Model selection.", + "description": _get_guide("cross_validation", "grid_search", "learning_curve"), + "sections": [ + { + "title": "Splitters", + "autosummary": [ + "GroupKFold", + "GroupShuffleSplit", + "KFold", + "LeaveOneGroupOut", + "LeaveOneOut", + "LeavePGroupsOut", + "LeavePOut", + "PredefinedSplit", + "RepeatedKFold", + "RepeatedStratifiedKFold", + "ShuffleSplit", + "StratifiedGroupKFold", + "StratifiedKFold", + "StratifiedShuffleSplit", + "TimeSeriesSplit", + "check_cv", + "train_test_split", + ], + }, + { + "title": "Hyper-parameter optimizers", + "autosummary": [ + "GridSearchCV", + "HalvingGridSearchCV", + "HalvingRandomSearchCV", + "ParameterGrid", + "ParameterSampler", + "RandomizedSearchCV", + ], + }, + { + "title": "Post-fit model tuning", + "autosummary": [ + "FixedThresholdClassifier", + "TunedThresholdClassifierCV", + ], + }, + { + "title": "Model validation", + "autosummary": [ + "cross_val_predict", + "cross_val_score", + "cross_validate", + "learning_curve", + "permutation_test_score", + "validation_curve", + ], + }, + { + "title": "Visualization", + "autosummary": ["LearningCurveDisplay", "ValidationCurveDisplay"], + }, + ], + }, + "sklearn.multiclass": { + "short_summary": "Multiclass classification.", + "description": _get_guide("multiclass_classification"), + "sections": [ + { + "title": None, + "autosummary": [ + "OneVsOneClassifier", + "OneVsRestClassifier", + "OutputCodeClassifier", + ], + }, + ], + }, + "sklearn.multioutput": { + "short_summary": "Multioutput regression and classification.", + "description": _get_guide( + "multilabel_classification", + "multiclass_multioutput_classification", + "multioutput_regression", + ), + "sections": [ + { + "title": None, + "autosummary": [ + "ClassifierChain", + "MultiOutputClassifier", + "MultiOutputRegressor", + "RegressorChain", + ], + }, + ], + }, + "sklearn.naive_bayes": { + "short_summary": "Naive Bayes.", + "description": _get_guide("naive_bayes"), + "sections": [ + { + "title": None, + "autosummary": [ + "BernoulliNB", + "CategoricalNB", + "ComplementNB", + "GaussianNB", + "MultinomialNB", + ], + }, + ], + }, + "sklearn.neighbors": { + "short_summary": "Nearest neighbors.", + "description": _get_guide("neighbors"), + "sections": [ + { + "title": None, + "autosummary": [ + "BallTree", + "KDTree", + "KNeighborsClassifier", + "KNeighborsRegressor", + "KNeighborsTransformer", + "KernelDensity", + "LocalOutlierFactor", + "NearestCentroid", + "NearestNeighbors", + "NeighborhoodComponentsAnalysis", + "RadiusNeighborsClassifier", + "RadiusNeighborsRegressor", + "RadiusNeighborsTransformer", + "kneighbors_graph", + "radius_neighbors_graph", + "sort_graph_by_row_values", + ], + }, + ], + }, + "sklearn.neural_network": { + "short_summary": "Neural network models.", + "description": _get_guide( + "neural_networks_supervised", "neural_networks_unsupervised" + ), + "sections": [ + { + "title": None, + "autosummary": ["BernoulliRBM", "MLPClassifier", "MLPRegressor"], + }, + ], + }, + "sklearn.pipeline": { + "short_summary": "Pipeline.", + "description": _get_guide("combining_estimators"), + "sections": [ + { + "title": None, + "autosummary": [ + "FeatureUnion", + "Pipeline", + "make_pipeline", + "make_union", + ], + }, + ], + }, + "sklearn.preprocessing": { + "short_summary": "Preprocessing and normalization.", + "description": _get_guide("preprocessing"), + "sections": [ + { + "title": None, + "autosummary": [ + "Binarizer", + "FunctionTransformer", + "KBinsDiscretizer", + "KernelCenterer", + "LabelBinarizer", + "LabelEncoder", + "MaxAbsScaler", + "MinMaxScaler", + "MultiLabelBinarizer", + "Normalizer", + "OneHotEncoder", + "OrdinalEncoder", + "PolynomialFeatures", + "PowerTransformer", + "QuantileTransformer", + "RobustScaler", + "SplineTransformer", + "StandardScaler", + "TargetEncoder", + "add_dummy_feature", + "binarize", + "label_binarize", + "maxabs_scale", + "minmax_scale", + "normalize", + "power_transform", + "quantile_transform", + "robust_scale", + "scale", + ], + }, + ], + }, + "sklearn.random_projection": { + "short_summary": "Random projection.", + "description": _get_guide("random_projection"), + "sections": [ + { + "title": None, + "autosummary": [ + "GaussianRandomProjection", + "SparseRandomProjection", + "johnson_lindenstrauss_min_dim", + ], + }, + ], + }, + "sklearn.semi_supervised": { + "short_summary": "Semi-supervised learning.", + "description": _get_guide("semi_supervised"), + "sections": [ + { + "title": None, + "autosummary": [ + "LabelPropagation", + "LabelSpreading", + "SelfTrainingClassifier", + ], + }, + ], + }, + "sklearn.svm": { + "short_summary": "Support vector machines.", + "description": _get_guide("svm"), + "sections": [ + { + "title": None, + "autosummary": [ + "LinearSVC", + "LinearSVR", + "NuSVC", + "NuSVR", + "OneClassSVM", + "SVC", + "SVR", + "l1_min_c", + ], + }, + ], + }, + "sklearn.tree": { + "short_summary": "Decision trees.", + "description": _get_guide("tree"), + "sections": [ + { + "title": None, + "autosummary": [ + "DecisionTreeClassifier", + "DecisionTreeRegressor", + "ExtraTreeClassifier", + "ExtraTreeRegressor", + ], + }, + { + "title": "Exporting", + "autosummary": ["export_graphviz", "export_text"], + }, + { + "title": "Plotting", + "autosummary": ["plot_tree"], + }, + ], + }, + "sklearn.utils": { + "short_summary": "Utilities.", + "description": _get_guide("developers-utils", is_developer=True), + "sections": [ + { + "title": None, + "autosummary": [ + "Bunch", + "_safe_indexing", + "as_float_array", + "assert_all_finite", + "deprecated", + "estimator_html_repr", + "gen_batches", + "gen_even_slices", + "indexable", + "murmurhash3_32", + "resample", + "safe_mask", + "safe_sqr", + "shuffle", + ], + }, + { + "title": "Input and parameter validation", + "description": _get_submodule("sklearn.utils", "validation"), + "autosummary": [ + "check_X_y", + "check_array", + "check_consistent_length", + "check_random_state", + "check_scalar", + "validation.check_is_fitted", + "validation.check_memory", + "validation.check_symmetric", + "validation.column_or_1d", + "validation.has_fit_parameter", + ], + }, + { + "title": "Meta-estimators", + "description": _get_submodule("sklearn.utils", "metaestimators"), + "autosummary": ["metaestimators.available_if"], + }, + { + "title": "Weight handling based on class labels", + "description": _get_submodule("sklearn.utils", "class_weight"), + "autosummary": [ + "class_weight.compute_class_weight", + "class_weight.compute_sample_weight", + ], + }, + { + "title": "Dealing with multiclass target in classifiers", + "description": _get_submodule("sklearn.utils", "multiclass"), + "autosummary": [ + "multiclass.is_multilabel", + "multiclass.type_of_target", + "multiclass.unique_labels", + ], + }, + { + "title": "Optimal mathematical operations", + "description": _get_submodule("sklearn.utils", "extmath"), + "autosummary": [ + "extmath.density", + "extmath.fast_logdet", + "extmath.randomized_range_finder", + "extmath.randomized_svd", + "extmath.safe_sparse_dot", + "extmath.weighted_mode", + ], + }, + { + "title": "Working with sparse matrices and arrays", + "description": _get_submodule("sklearn.utils", "sparsefuncs"), + "autosummary": [ + "sparsefuncs.incr_mean_variance_axis", + "sparsefuncs.inplace_column_scale", + "sparsefuncs.inplace_csr_column_scale", + "sparsefuncs.inplace_row_scale", + "sparsefuncs.inplace_swap_column", + "sparsefuncs.inplace_swap_row", + "sparsefuncs.mean_variance_axis", + ], + }, + { + "title": None, + "description": _get_submodule("sklearn.utils", "sparsefuncs_fast"), + "autosummary": [ + "sparsefuncs_fast.inplace_csr_row_normalize_l1", + "sparsefuncs_fast.inplace_csr_row_normalize_l2", + ], + }, + { + "title": "Working with graphs", + "description": _get_submodule("sklearn.utils", "graph"), + "autosummary": ["graph.single_source_shortest_path_length"], + }, + { + "title": "Random sampling", + "description": _get_submodule("sklearn.utils", "random"), + "autosummary": ["random.sample_without_replacement"], + }, + { + "title": "Auxiliary functions that operate on arrays", + "description": _get_submodule("sklearn.utils", "arrayfuncs"), + "autosummary": ["arrayfuncs.min_pos"], + }, + { + "title": "Metadata routing", + "description": ( + _get_submodule("sklearn.utils", "metadata_routing") + + "\n\n" + + _get_guide("metadata_routing") + ), + "autosummary": [ + "metadata_routing.MetadataRequest", + "metadata_routing.MetadataRouter", + "metadata_routing.MethodMapping", + "metadata_routing.get_routing_for_object", + "metadata_routing.process_routing", + ], + }, + { + "title": "Discovering scikit-learn objects", + "description": _get_submodule("sklearn.utils", "discovery"), + "autosummary": [ + "discovery.all_displays", + ], + }, + { + "title": "API compatibility checkers", + "description": _get_submodule("sklearn.utils", "estimator_checks"), + "autosummary": [ + "estimator_checks.check_estimator", + "estimator_checks.parametrize_with_checks", + ], + }, + { + "title": "Parallel computing", + "description": _get_submodule("sklearn.utils", "parallel"), + "autosummary": [ + "parallel.Parallel", + "parallel.delayed", + ], + }, + ], + }, +} + + +""" +CONFIGURING DEPRECATED_API_REFERENCE +==================================== + +DEPRECATED_API_REFERENCE maps each deprecation target version to a corresponding +autosummary block. It will be placed at the bottom of the API index page under the +"Recently deprecated" section. Essentially, the rendered section would look like the +following: + +|------------------------------------------| +| To be removed in {{ version_1 }} | +| -------------------------------- | +| {{ autosummary_1 }} | +| | +| To be removed in {{ version_2 }} | +| -------------------------------- | +| {{ autosummary_2 }} | +| | +| More versions... | +|------------------------------------------| + +Note that the autosummary here assumes that the current module is `sklearn`, i.e., if +`sklearn.utils.Memory` is deprecated, one should put `utils.Memory` in the "entries" +slot of the autosummary block. + +Example: + +DEPRECATED_API_REFERENCE = { + "0.24": [ + "model_selection.fit_grid_point", + "utils.safe_indexing", + ], +} +""" + +DEPRECATED_API_REFERENCE = { + "1.6": [ + "utils.parallel_backend", + "utils.register_parallel_backend", + ], + "1.7": [ + "utils.discovery.all_estimators", + "utils.discovery.all_functions", + ], +} # type: ignore diff --git a/doc/common_pitfalls.rst b/doc/common_pitfalls.rst index 41eb16665a612..c16385943f9ad 100644 --- a/doc/common_pitfalls.rst +++ b/doc/common_pitfalls.rst @@ -1,9 +1,3 @@ -.. Places parent toc into the sidebar - -:parenttoc: True - -.. include:: includes/big_toc_css.rst - .. _common_pitfalls: ========================================= @@ -414,43 +408,40 @@ it will allow the estimator RNG to vary for each fold. illustration purpose: what matters is what we pass to the :class:`~sklearn.ensemble.RandomForestClassifier` estimator. -|details-start| -**Cloning** -|details-split| +.. dropdown:: Cloning -Another subtle side effect of passing `RandomState` instances is how -:func:`~sklearn.base.clone` will work:: + Another subtle side effect of passing `RandomState` instances is how + :func:`~sklearn.base.clone` will work:: - >>> from sklearn import clone - >>> from sklearn.ensemble import RandomForestClassifier - >>> import numpy as np + >>> from sklearn import clone + >>> from sklearn.ensemble import RandomForestClassifier + >>> import numpy as np + + >>> rng = np.random.RandomState(0) + >>> a = RandomForestClassifier(random_state=rng) + >>> b = clone(a) + + Since a `RandomState` instance was passed to `a`, `a` and `b` are not clones + in the strict sense, but rather clones in the statistical sense: `a` and `b` + will still be different models, even when calling `fit(X, y)` on the same + data. Moreover, `a` and `b` will influence each-other since they share the + same internal RNG: calling `a.fit` will consume `b`'s RNG, and calling + `b.fit` will consume `a`'s RNG, since they are the same. This bit is true for + any estimators that share a `random_state` parameter; it is not specific to + clones. + + If an integer were passed, `a` and `b` would be exact clones and they would not + influence each other. + + .. warning:: + Even though :func:`~sklearn.base.clone` is rarely used in user code, it is + called pervasively throughout scikit-learn codebase: in particular, most + meta-estimators that accept non-fitted estimators call + :func:`~sklearn.base.clone` internally + (:class:`~sklearn.model_selection.GridSearchCV`, + :class:`~sklearn.ensemble.StackingClassifier`, + :class:`~sklearn.calibration.CalibratedClassifierCV`, etc.). - >>> rng = np.random.RandomState(0) - >>> a = RandomForestClassifier(random_state=rng) - >>> b = clone(a) - -Since a `RandomState` instance was passed to `a`, `a` and `b` are not clones -in the strict sense, but rather clones in the statistical sense: `a` and `b` -will still be different models, even when calling `fit(X, y)` on the same -data. Moreover, `a` and `b` will influence each-other since they share the -same internal RNG: calling `a.fit` will consume `b`'s RNG, and calling -`b.fit` will consume `a`'s RNG, since they are the same. This bit is true for -any estimators that share a `random_state` parameter; it is not specific to -clones. - -If an integer were passed, `a` and `b` would be exact clones and they would not -influence each other. - -.. warning:: - Even though :func:`~sklearn.base.clone` is rarely used in user code, it is - called pervasively throughout scikit-learn codebase: in particular, most - meta-estimators that accept non-fitted estimators call - :func:`~sklearn.base.clone` internally - (:class:`~sklearn.model_selection.GridSearchCV`, - :class:`~sklearn.ensemble.StackingClassifier`, - :class:`~sklearn.calibration.CalibratedClassifierCV`, etc.). - -|details-end| CV splitters ............ diff --git a/doc/computing.rst b/doc/computing.rst index 6732b754918b0..9f166432006b2 100644 --- a/doc/computing.rst +++ b/doc/computing.rst @@ -1,13 +1,7 @@ -.. Places parent toc into the sidebar - -:parenttoc: True - ============================ Computing with scikit-learn ============================ -.. include:: includes/big_toc_css.rst - .. toctree:: :maxdepth: 2 diff --git a/doc/computing/computational_performance.rst b/doc/computing/computational_performance.rst index d6864689502c2..a7b6d3a37001e 100644 --- a/doc/computing/computational_performance.rst +++ b/doc/computing/computational_performance.rst @@ -1,7 +1,3 @@ -.. Places parent toc into the sidebar - -:parenttoc: True - .. _computational_performance: .. currentmodule:: sklearn diff --git a/doc/computing/parallelism.rst b/doc/computing/parallelism.rst index 53cef5603c5be..5c15cd9db440e 100644 --- a/doc/computing/parallelism.rst +++ b/doc/computing/parallelism.rst @@ -1,7 +1,3 @@ -.. Places parent toc into the sidebar - -:parenttoc: True - Parallelism, resource management, and configuration =================================================== diff --git a/doc/computing/scaling_strategies.rst b/doc/computing/scaling_strategies.rst index 143643131b0e8..286a1e79d0a8c 100644 --- a/doc/computing/scaling_strategies.rst +++ b/doc/computing/scaling_strategies.rst @@ -1,7 +1,3 @@ -.. Places parent toc into the sidebar - -:parenttoc: True - .. _scaling_strategies: Strategies to scale computationally: bigger data diff --git a/doc/conf.py b/doc/conf.py index 0587e98130118..f025c77dcce0c 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -15,7 +15,6 @@ import sys import warnings from datetime import datetime -from io import StringIO from pathlib import Path from sklearn.externals._packaging.version import parse @@ -25,8 +24,10 @@ # directory, add these directories to sys.path here. If the directory # is relative to the documentation root, use os.path.abspath to make it # absolute, like shown here. +sys.path.insert(0, os.path.abspath(".")) sys.path.insert(0, os.path.abspath("sphinxext")) +import jinja2 import sphinx_gallery from github_link import make_linkcode_resolve from sphinx_gallery.notebook import add_code_cell, add_markdown_cell @@ -56,14 +57,21 @@ "sphinx.ext.intersphinx", "sphinx.ext.imgconverter", "sphinx_gallery.gen_gallery", - "sphinx_issues", - "add_toctree_functions", "sphinx-prompt", "sphinx_copybutton", "sphinxext.opengraph", - "doi_role", - "allow_nan_estimators", "matplotlib.sphinxext.plot_directive", + "sphinxcontrib.sass", + "sphinx_remove_toctrees", + "sphinx_design", + # See sphinxext/ + "allow_nan_estimators", + "autoshortsummary", + "doi_role", + "dropdown_anchors", + "move_gallery_links", + "override_pst_pagetoc", + "sphinx_issues", ] # Specify how to identify the prompt when copying code snippets @@ -96,8 +104,12 @@ plot_html_show_formats = False plot_html_show_source_link = False -# this is needed for some reason... -# see https://github.com/numpy/numpydoc/issues/69 +# We do not need the table of class members because `sphinxext/override_pst_pagetoc.py` +# will show them in the secondary sidebar +numpydoc_show_class_members = False +numpydoc_show_inherited_class_members = False + +# We want in-page toc of class members instead of a separate page for each entry numpydoc_class_members_toctree = False @@ -111,8 +123,6 @@ extensions.append("sphinx.ext.mathjax") mathjax_path = "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js" -autodoc_default_options = {"members": True, "inherited-members": True} - # Add any paths that contain templates here, relative to this directory. templates_path = ["templates"] @@ -123,10 +133,10 @@ source_suffix = ".rst" # The encoding of source files. -# source_encoding = 'utf-8' +source_encoding = "utf-8" # The main toctree document. -root_doc = "contents" +root_doc = "index" # General information about the project. project = "scikit-learn" @@ -160,7 +170,12 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ["_build", "templates", "includes", "themes"] +exclude_patterns = [ + "_build", + "templates", + "includes", + "**/sg_execution_times.rst", +] # The reST default role (used for this markup: `text`) to use for all # documents. @@ -177,9 +192,6 @@ # output. They are ignored by default. # show_authors = False -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = "sphinx" - # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] @@ -188,21 +200,89 @@ # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. -html_theme = "scikit-learn-modern" +html_theme = "pydata_sphinx_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. html_theme_options = { - "legacy_google_analytics": True, - "analytics": True, - "mathjax_path": mathjax_path, - "link_to_live_contributing_page": not parsed_version.is_devrelease, + # -- General configuration ------------------------------------------------ + "sidebar_includehidden": True, + "use_edit_page_button": True, + "external_links": [], + "icon_links_label": "Icon Links", + "icon_links": [ + { + "name": "GitHub", + "url": "https://github.com/scikit-learn/scikit-learn", + "icon": "fa-brands fa-square-github", + "type": "fontawesome", + }, + ], + "analytics": { + "plausible_analytics_domain": "scikit-learn.org", + "plausible_analytics_url": "https://views.scientific-python.org/js/script.js", + }, + # If "prev-next" is included in article_footer_items, then setting show_prev_next + # to True would repeat prev and next links. See + # https://github.com/pydata/pydata-sphinx-theme/blob/b731dc230bc26a3d1d1bb039c56c977a9b3d25d8/src/pydata_sphinx_theme/theme/pydata_sphinx_theme/layout.html#L118-L129 + "show_prev_next": False, + "search_bar_text": "Search the docs ...", + "navigation_with_keys": False, + "collapse_navigation": False, + "navigation_depth": 2, + "show_nav_level": 1, + "show_toc_level": 1, + "navbar_align": "left", + "header_links_before_dropdown": 5, + "header_dropdown_text": "More", + # The switcher requires a JSON file with the list of documentation versions, which + # is generated by the script `build_tools/circle/list_versions.py` and placed under + # the `js/` static directory; it will then be copied to the `_static` directory in + # the built documentation + "switcher": { + "json_url": "https://scikit-learn.org/dev/_static/versions.json", + "version_match": release, + }, + # check_switcher may be set to False if docbuild pipeline fails. See + # https://pydata-sphinx-theme.readthedocs.io/en/stable/user_guide/version-dropdown.html#configure-switcher-json-url + "check_switcher": True, + "pygment_light_style": "tango", + "pygment_dark_style": "monokai", + "logo": { + "alt_text": "scikit-learn homepage", + "image_relative": "logos/scikit-learn-logo-small.png", + "image_light": "logos/scikit-learn-logo-small.png", + "image_dark": "logos/scikit-learn-logo-small.png", + }, + "surface_warnings": True, + # -- Template placement in theme layouts ---------------------------------- + "navbar_start": ["navbar-logo"], + # Note that the alignment of navbar_center is controlled by navbar_align + "navbar_center": ["navbar-nav"], + "navbar_end": ["theme-switcher", "navbar-icon-links", "version-switcher"], + # navbar_persistent is persistent right (even when on mobiles) + "navbar_persistent": ["search-button"], + "article_header_start": ["breadcrumbs"], + "article_header_end": [], + "article_footer_items": ["prev-next"], + "content_footer_items": [], + # Use html_sidebars that map page patterns to list of sidebar templates + "primary_sidebar_end": [], + "footer_start": ["copyright"], + "footer_center": [], + "footer_end": [], + # When specified as a dictionary, the keys should follow glob-style patterns, as in + # https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-exclude_patterns + # In particular, "**" specifies the default for all pages + # Use :html_theme.sidebar_secondary.remove: for file-wide removal + "secondary_sidebar_items": {"**": ["page-toc", "sourcelink"]}, + "show_version_warning_banner": True, + "announcement": None, } # Add any paths that contain custom themes here, relative to this directory. -html_theme_path = ["themes"] - +# html_theme_path = ["themes"] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". @@ -211,10 +291,6 @@ # A shorter title for the navigation bar. Default is the same as html_title. html_short_title = "scikit-learn" -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -html_logo = "logos/scikit-learn-logo-small.png" - # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. @@ -223,19 +299,77 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ["images"] +html_static_path = ["images", "css", "js"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # Custom sidebar templates, maps document names to template names. -# html_sidebars = {} +# Workaround for removing the left sidebar on pages without TOC +# A better solution would be to follow the merge of: +# https://github.com/pydata/pydata-sphinx-theme/pull/1682 +html_sidebars = { + "install": [], + "getting_started": [], + "glossary": [], + "faq": [], + "support": [], + "related_projects": [], + "roadmap": [], + "governance": [], + "about": [], +} # Additional templates that should be rendered to pages, maps page names to # template names. html_additional_pages = {"index": "index.html"} +# Additional files to copy +# html_extra_path = [] + +# Additional JS files +html_js_files = [ + "scripts/dropdown.js", + "scripts/version-switcher.js", +] + +# Compile scss files into css files using sphinxcontrib-sass +sass_src_dir, sass_out_dir = "scss", "css/styles" +sass_targets = { + f"{file.stem}.scss": f"{file.stem}.css" + for file in Path(sass_src_dir).glob("*.scss") +} + +# Additional CSS files, should be subset of the values of `sass_targets` +html_css_files = ["styles/colors.css", "styles/custom.css"] + + +def add_js_css_files(app, pagename, templatename, context, doctree): + """Load additional JS and CSS files only for certain pages. + + Note that `html_js_files` and `html_css_files` are included in all pages and + should be used for the ones that are used by multiple pages. All page-specific + JS and CSS files should be added here instead. + """ + if pagename == "api/index": + # External: jQuery and DataTables + app.add_js_file("https://code.jquery.com/jquery-3.7.0.js") + app.add_js_file("https://cdn.datatables.net/2.0.0/js/dataTables.min.js") + app.add_css_file( + "https://cdn.datatables.net/2.0.0/css/dataTables.dataTables.min.css" + ) + # Internal: API search intialization and styling + app.add_js_file("scripts/api-search.js") + app.add_css_file("styles/api-search.css") + elif pagename == "index": + app.add_css_file("styles/index.css") + elif pagename == "install": + app.add_css_file("styles/install.css") + elif pagename.startswith("modules/generated/"): + app.add_css_file("styles/api.css") + + # If false, no module index is generated. html_domain_indices = False @@ -285,6 +419,9 @@ # redirects dictionary maps from old links to new links redirects = { "documentation": "index", + "contents": "index", + "preface": "index", + "modules/classes": "api/index", "auto_examples/feature_selection/plot_permutation_test_for_classification": ( "auto_examples/model_selection/plot_permutation_tests_for_classification" ), @@ -316,32 +453,13 @@ for old_link in redirects: html_additional_pages[old_link] = "redirects.html" +# See https://github.com/scikit-learn/scikit-learn/pull/22550 +html_context["is_devrelease"] = parsed_version.is_devrelease + # Not showing the search summary makes the search page load faster. html_show_search_summary = True -# The "summary-anchor" IDs will be overwritten via JavaScript to be unique. -# See `doc/theme/scikit-learn-modern/static/js/details-permalink.js`. -rst_prolog = """ -.. |details-start| raw:: html - -
- - -.. |details-split| raw:: html - - Click for more details - - -
- -.. |details-end| raw:: html - -
-
- -""" - # -- Options for LaTeX output ------------------------------------------------ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). @@ -528,14 +646,16 @@ def reset_sklearn_config(gallery_conf, fname): sklearn.set_config(**default_global_config) +sg_examples_dir = "../examples" +sg_gallery_dir = "auto_examples" sphinx_gallery_conf = { "doc_module": "sklearn", "backreferences_dir": os.path.join("modules", "generated"), "show_memory": False, "reference_url": {"sklearn": None}, - "examples_dirs": ["../examples"], - "gallery_dirs": ["auto_examples"], - "subsection_order": SubSectionTitleOrder("../examples"), + "examples_dirs": [sg_examples_dir], + "gallery_dirs": [sg_gallery_dir], + "subsection_order": SubSectionTitleOrder(sg_examples_dir), "within_subsection_order": SKExampleTitleSortKey, "binder": { "org": "scikit-learn", @@ -549,7 +669,7 @@ def reset_sklearn_config(gallery_conf, fname): "inspect_global_variables": False, "remove_config_comments": True, "plot_gallery": "True", - "recommender": {"enable": True, "n_examples": 5, "min_df": 12}, + "recommender": {"enable": True, "n_examples": 4, "min_df": 12}, "reset_modules": ("matplotlib", "seaborn", reset_sklearn_config), } if with_jupyterlite: @@ -557,6 +677,26 @@ def reset_sklearn_config(gallery_conf, fname): "notebook_modification_function": notebook_modification_function } +# Secondary sidebar configuration for pages generated by sphinx-gallery + +# For the index page of the gallery and each nested section, we hide the secondary +# sidebar by specifying an empty list (no components), because there is no meaningful +# in-page toc for these pages, and they are generated so "sourcelink" is not useful +# either. + +# For each example page we keep default ["page-toc", "sourcelink"] specified by the +# "**" key. "page-toc" is wanted for these pages. "sourcelink" is also necessary since +# otherwise the secondary sidebar will degenerate when "page-toc" is empty, and the +# script `sphinxext/move_gallery_links.py` will fail (it assumes the existence of the +# secondary sidebar). The script will remove "sourcelink" in the end. + +html_theme_options["secondary_sidebar_items"][f"{sg_gallery_dir}/index"] = [] +for sub_sg_dir in (Path(".") / sg_examples_dir).iterdir(): + if sub_sg_dir.is_dir(): + html_theme_options["secondary_sidebar_items"][ + f"{sg_gallery_dir}/{sub_sg_dir.name}/index" + ] = [] + # The following dictionary contains the information used to create the # thumbnails for the front page of the scikit-learn home page. @@ -606,73 +746,6 @@ def filter_search_index(app, exception): f.write(searchindex_text) -def generate_min_dependency_table(app): - """Generate min dependency table for docs.""" - from sklearn._min_dependencies import dependent_packages - - # get length of header - package_header_len = max(len(package) for package in dependent_packages) + 4 - version_header_len = len("Minimum Version") + 4 - tags_header_len = max(len(tags) for _, tags in dependent_packages.values()) + 4 - - output = StringIO() - output.write( - " ".join( - ["=" * package_header_len, "=" * version_header_len, "=" * tags_header_len] - ) - ) - output.write("\n") - dependency_title = "Dependency" - version_title = "Minimum Version" - tags_title = "Purpose" - - output.write( - f"{dependency_title:<{package_header_len}} " - f"{version_title:<{version_header_len}} " - f"{tags_title}\n" - ) - - output.write( - " ".join( - ["=" * package_header_len, "=" * version_header_len, "=" * tags_header_len] - ) - ) - output.write("\n") - - for package, (version, tags) in dependent_packages.items(): - output.write( - f"{package:<{package_header_len}} {version:<{version_header_len}} {tags}\n" - ) - - output.write( - " ".join( - ["=" * package_header_len, "=" * version_header_len, "=" * tags_header_len] - ) - ) - output.write("\n") - output = output.getvalue() - - with (Path(".") / "min_dependency_table.rst").open("w") as f: - f.write(output) - - -def generate_min_dependency_substitutions(app): - """Generate min dependency substitutions for docs.""" - from sklearn._min_dependencies import dependent_packages - - output = StringIO() - - for package, (version, _) in dependent_packages.items(): - package = package.capitalize() - output.write(f".. |{package}MinVersion| replace:: {version}") - output.write("\n") - - output = output.getvalue() - - with (Path(".") / "min_dependency_substitutions.rst").open("w") as f: - f.write(output) - - # Config for sphinx_issues # we use the issues path for PRs since the issues URL will forward @@ -688,10 +761,11 @@ def setup(app): # do not run the examples when using linkcheck by using a small priority # (default priority is 500 and sphinx-gallery using builder-inited event too) app.connect("builder-inited", disable_plot_gallery_for_linkcheck, priority=50) - app.connect("builder-inited", generate_min_dependency_table) - app.connect("builder-inited", generate_min_dependency_substitutions) - # to hide/show the prompt in code examples: + # triggered just before the HTML for an individual page is created + app.connect("html-page-context", add_js_css_files) + + # to hide/show the prompt in code examples app.connect("build-finished", make_carousel_thumbs) app.connect("build-finished", filter_search_index) @@ -796,6 +870,10 @@ def setup(app): "consistently-create-same-random-numpy-array/5837352#comment6712034_5837352", ] +# Config for sphinx-remove-toctrees + +remove_from_toctrees = ["metadata_routing.rst"] + # Use a browser-like user agent to avoid some "403 Client Error: Forbidden for # url" errors. This is taken from the variable navigator.userAgent inside a # browser console. @@ -813,3 +891,78 @@ def setup(app): linkcheck_request_headers = { "https://github.com/": {"Authorization": f"token {github_token}"}, } + + +# -- Convert .rst.template files to .rst --------------------------------------- + +from api_reference import API_REFERENCE, DEPRECATED_API_REFERENCE + +from sklearn._min_dependencies import dependent_packages + +# If development build, link to local page in the top navbar; otherwise link to the +# development version; see https://github.com/scikit-learn/scikit-learn/pull/22550 +if parsed_version.is_devrelease: + development_link = "developers/index" +else: + development_link = "https://scikit-learn.org/dev/developers/index.html" + +# Define the templates and target files for conversion +# Each entry is in the format (template name, file name, kwargs for rendering) +rst_templates = [ + ("index", "index", {"development_link": development_link}), + ( + "min_dependency_table", + "min_dependency_table", + {"dependent_packages": dependent_packages}, + ), + ( + "min_dependency_substitutions", + "min_dependency_substitutions", + {"dependent_packages": dependent_packages}, + ), + ( + "api/index", + "api/index", + { + "API_REFERENCE": sorted(API_REFERENCE.items(), key=lambda x: x[0]), + "DEPRECATED_API_REFERENCE": sorted( + DEPRECATED_API_REFERENCE.items(), key=lambda x: x[0], reverse=True + ), + }, + ), +] + +# Convert each module API reference page +for module in API_REFERENCE: + rst_templates.append( + ( + "api/module", + f"api/{module}", + {"module": module, "module_info": API_REFERENCE[module]}, + ) + ) + +# Convert the deprecated API reference page (if there exists any) +if DEPRECATED_API_REFERENCE: + rst_templates.append( + ( + "api/deprecated", + "api/deprecated", + { + "DEPRECATED_API_REFERENCE": sorted( + DEPRECATED_API_REFERENCE.items(), key=lambda x: x[0], reverse=True + ) + }, + ) + ) + +for rst_template_name, rst_target_name, kwargs in rst_templates: + # Read the corresponding template file into jinja2 + with (Path(".") / f"{rst_template_name}.rst.template").open( + "r", encoding="utf-8" + ) as f: + t = jinja2.Template(f.read()) + + # Render the template and write to the target + with (Path(".") / f"{rst_target_name}.rst").open("w", encoding="utf-8") as f: + f.write(t.render(**kwargs)) diff --git a/doc/contents.rst b/doc/contents.rst deleted file mode 100644 index a28634621d558..0000000000000 --- a/doc/contents.rst +++ /dev/null @@ -1,24 +0,0 @@ -.. include:: includes/big_toc_css.rst -.. include:: tune_toc.rst - -.. Places global toc into the sidebar - -:globalsidebartoc: True - -================= -Table Of Contents -================= - -.. Define an order for the Table of Contents: - -.. toctree:: - :maxdepth: 2 - - preface - tutorial/index - getting_started - user_guide - glossary - auto_examples/index - modules/classes - developers/index diff --git a/doc/css/.gitkeep b/doc/css/.gitkeep new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/doc/data_transforms.rst b/doc/data_transforms.rst index 084214cb094f5..536539ec97007 100644 --- a/doc/data_transforms.rst +++ b/doc/data_transforms.rst @@ -1,9 +1,3 @@ -.. Places parent toc into the sidebar - -:parenttoc: True - -.. include:: includes/big_toc_css.rst - .. _data-transforms: Dataset transformations diff --git a/doc/datasets.rst b/doc/datasets.rst index b9484a02ce84c..ee767e5843256 100644 --- a/doc/datasets.rst +++ b/doc/datasets.rst @@ -1,9 +1,3 @@ -.. Places parent toc into the sidebar - -:parenttoc: True - -.. include:: includes/big_toc_css.rst - .. _datasets: ========================= diff --git a/doc/datasets/loading_other_datasets.rst b/doc/datasets/loading_other_datasets.rst index fdd7fd1666cce..004aa66c001e5 100644 --- a/doc/datasets/loading_other_datasets.rst +++ b/doc/datasets/loading_other_datasets.rst @@ -1,7 +1,3 @@ -.. Places parent toc into the sidebar - -:parenttoc: True - .. _loading_other_datasets: Loading other datasets @@ -37,9 +33,9 @@ and pipelines on 2D data. if you plan to use ``matplotlib.pyplpt.imshow``, don't forget to scale to the range 0 - 1 as done in the following example. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_cluster_plot_color_quantization.py` +* :ref:`sphx_glr_auto_examples_cluster_plot_color_quantization.py` .. _libsvm_loader: @@ -72,11 +68,10 @@ features:: ... "/path/to/test_dataset.txt", n_features=X_train.shape[1]) ... # doctest: +SKIP -.. topic:: Related links: - - _`Public datasets in svmlight / libsvm format`: https://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets +.. rubric:: Related links - _`Faster API-compatible implementation`: https://github.com/mblondel/svmlight-loader +- `Public datasets in svmlight / libsvm format`: https://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets +- `Faster API-compatible implementation`: https://github.com/mblondel/svmlight-loader .. For doctests: @@ -219,11 +214,11 @@ identifies the dataset:: '969' -.. topic:: References: +.. rubric:: References - * :arxiv:`Vanschoren, van Rijn, Bischl and Torgo. "OpenML: networked science in - machine learning" ACM SIGKDD Explorations Newsletter, 15(2), 49-60, 2014. - <1407.7722>` +* :arxiv:`Vanschoren, van Rijn, Bischl and Torgo. "OpenML: networked science in + machine learning" ACM SIGKDD Explorations Newsletter, 15(2), 49-60, 2014. + <1407.7722>` .. _openml_parser: diff --git a/doc/datasets/real_world.rst b/doc/datasets/real_world.rst index 78b09e6f722b0..f05d475b0db78 100644 --- a/doc/datasets/real_world.rst +++ b/doc/datasets/real_world.rst @@ -1,7 +1,3 @@ -.. Places parent toc into the sidebar - -:parenttoc: True - .. _real_world_datasets: Real world datasets diff --git a/doc/datasets/sample_generators.rst b/doc/datasets/sample_generators.rst index 7dc123f08424c..5b8264c2a22b5 100644 --- a/doc/datasets/sample_generators.rst +++ b/doc/datasets/sample_generators.rst @@ -1,7 +1,3 @@ -.. Places parent toc into the sidebar - -:parenttoc: True - .. _sample_generators: Generated datasets diff --git a/doc/datasets/toy_dataset.rst b/doc/datasets/toy_dataset.rst index 65fd20abd361d..d7edecddd3510 100644 --- a/doc/datasets/toy_dataset.rst +++ b/doc/datasets/toy_dataset.rst @@ -1,7 +1,3 @@ -.. Places parent toc into the sidebar - -:parenttoc: True - .. _toy_datasets: Toy datasets diff --git a/doc/developers/contributing.rst b/doc/developers/contributing.rst index 9f43d8ed52c38..402711dcd1bf3 100644 --- a/doc/developers/contributing.rst +++ b/doc/developers/contributing.rst @@ -70,10 +70,10 @@ link to it from your website, or simply star to say "I use it": .. raw:: html - Star - + Star + In case a contribution/issue involves changes to the API principles or changes to dependencies or supported versions, it must be backed by a @@ -82,31 +82,27 @@ or changes to dependencies or supported versions, it must be backed by a using the `SLEP template `_ and follows the decision-making process outlined in :ref:`governance`. -|details-start| -**Contributing to related projects** -|details-split| +.. dropdown:: Contributing to related projects - Scikit-learn thrives in an ecosystem of several related projects, which also - may have relevant issues to work on, including smaller projects such as: + Scikit-learn thrives in an ecosystem of several related projects, which also + may have relevant issues to work on, including smaller projects such as: - * `scikit-learn-contrib `__ - * `joblib `__ - * `sphinx-gallery `__ - * `numpydoc `__ - * `liac-arff `__ + * `scikit-learn-contrib `__ + * `joblib `__ + * `sphinx-gallery `__ + * `numpydoc `__ + * `liac-arff `__ - and larger projects: + and larger projects: - * `numpy `__ - * `scipy `__ - * `matplotlib `__ - * and so on. + * `numpy `__ + * `scipy `__ + * `matplotlib `__ + * and so on. - Look for issues marked "help wanted" or similar. - Helping these projects may help Scikit-learn too. - See also :ref:`related_projects`. - -|details-end| + Look for issues marked "help wanted" or similar. + Helping these projects may help Scikit-learn too. + See also :ref:`related_projects`. Submitting a bug report or a feature request ============================================ @@ -674,219 +670,200 @@ We are glad to accept any sort of documentation: useful information (e.g., the :ref:`contributing` guide) and live in `doc/ `_. -|details-start| -**Guidelines for writing docstrings** -|details-split| - -* When documenting the parameters and attributes, here is a list of some - well-formatted examples:: - - n_clusters : int, default=3 - The number of clusters detected by the algorithm. - - some_param : {'hello', 'goodbye'}, bool or int, default=True - The parameter description goes here, which can be either a string - literal (either `hello` or `goodbye`), a bool, or an int. The default - value is True. - array_parameter : {array-like, sparse matrix} of shape (n_samples, n_features) or (n_samples,) - This parameter accepts data in either of the mentioned forms, with one - of the mentioned shapes. The default value is - `np.ones(shape=(n_samples,))`. +.. dropdown:: Guidelines for writing docstrings - list_param : list of int + * When documenting the parameters and attributes, here is a list of some + well-formatted examples:: - typed_ndarray : ndarray of shape (n_samples,), dtype=np.int32 + n_clusters : int, default=3 + The number of clusters detected by the algorithm. - sample_weight : array-like of shape (n_samples,), default=None + some_param : {'hello', 'goodbye'}, bool or int, default=True + The parameter description goes here, which can be either a string + literal (either `hello` or `goodbye`), a bool, or an int. The default + value is True. - multioutput_array : ndarray of shape (n_samples, n_classes) or list of such arrays + array_parameter : {array-like, sparse matrix} of shape (n_samples, n_features) or (n_samples,) + This parameter accepts data in either of the mentioned forms, with one + of the mentioned shapes. The default value is + `np.ones(shape=(n_samples,))`. - In general have the following in mind: + list_param : list of int - * Use Python basic types. (``bool`` instead of ``boolean``) - * Use parenthesis for defining shapes: ``array-like of shape (n_samples,)`` - or ``array-like of shape (n_samples, n_features)`` - * For strings with multiple options, use brackets: ``input: {'log', - 'squared', 'multinomial'}`` - * 1D or 2D data can be a subset of ``{array-like, ndarray, sparse matrix, - dataframe}``. Note that ``array-like`` can also be a ``list``, while - ``ndarray`` is explicitly only a ``numpy.ndarray``. - * Specify ``dataframe`` when "frame-like" features are being used, such as - the column names. - * When specifying the data type of a list, use ``of`` as a delimiter: ``list - of int``. When the parameter supports arrays giving details about the - shape and/or data type and a list of such arrays, you can use one of - ``array-like of shape (n_samples,) or list of such arrays``. - * When specifying the dtype of an ndarray, use e.g. ``dtype=np.int32`` after - defining the shape: ``ndarray of shape (n_samples,), dtype=np.int32``. You - can specify multiple dtype as a set: ``array-like of shape (n_samples,), - dtype={np.float64, np.float32}``. If one wants to mention arbitrary - precision, use `integral` and `floating` rather than the Python dtype - `int` and `float`. When both `int` and `floating` are supported, there is - no need to specify the dtype. - * When the default is ``None``, ``None`` only needs to be specified at the - end with ``default=None``. Be sure to include in the docstring, what it - means for the parameter or attribute to be ``None``. + typed_ndarray : ndarray of shape (n_samples,), dtype=np.int32 -* Add "See Also" in docstrings for related classes/functions. + sample_weight : array-like of shape (n_samples,), default=None -* "See Also" in docstrings should be one line per reference, with a colon and an - explanation, for example:: + multioutput_array : ndarray of shape (n_samples, n_classes) or list of such arrays - See Also - -------- - SelectKBest : Select features based on the k highest scores. - SelectFpr : Select features based on a false positive rate test. + In general have the following in mind: -* Add one or two snippets of code in "Example" section to show how it can be used. + * Use Python basic types. (``bool`` instead of ``boolean``) + * Use parenthesis for defining shapes: ``array-like of shape (n_samples,)`` + or ``array-like of shape (n_samples, n_features)`` + * For strings with multiple options, use brackets: ``input: {'log', + 'squared', 'multinomial'}`` + * 1D or 2D data can be a subset of ``{array-like, ndarray, sparse matrix, + dataframe}``. Note that ``array-like`` can also be a ``list``, while + ``ndarray`` is explicitly only a ``numpy.ndarray``. + * Specify ``dataframe`` when "frame-like" features are being used, such as + the column names. + * When specifying the data type of a list, use ``of`` as a delimiter: ``list + of int``. When the parameter supports arrays giving details about the + shape and/or data type and a list of such arrays, you can use one of + ``array-like of shape (n_samples,) or list of such arrays``. + * When specifying the dtype of an ndarray, use e.g. ``dtype=np.int32`` after + defining the shape: ``ndarray of shape (n_samples,), dtype=np.int32``. You + can specify multiple dtype as a set: ``array-like of shape (n_samples,), + dtype={np.float64, np.float32}``. If one wants to mention arbitrary + precision, use `integral` and `floating` rather than the Python dtype + `int` and `float`. When both `int` and `floating` are supported, there is + no need to specify the dtype. + * When the default is ``None``, ``None`` only needs to be specified at the + end with ``default=None``. Be sure to include in the docstring, what it + means for the parameter or attribute to be ``None``. -|details-end| + * Add "See Also" in docstrings for related classes/functions. -|details-start| -**Guidelines for writing the user guide and other reStructuredText documents** -|details-split| + * "See Also" in docstrings should be one line per reference, with a colon and an + explanation, for example:: -It is important to keep a good compromise between mathematical and algorithmic -details, and give intuition to the reader on what the algorithm does. + See Also + -------- + SelectKBest : Select features based on the k highest scores. + SelectFpr : Select features based on a false positive rate test. -* Begin with a concise, hand-waving explanation of what the algorithm/code does on - the data. + * Add one or two snippets of code in "Example" section to show how it can be used. -* Highlight the usefulness of the feature and its recommended application. - Consider including the algorithm's complexity - (:math:`O\left(g\left(n\right)\right)`) if available, as "rules of thumb" can - be very machine-dependent. Only if those complexities are not available, then - rules of thumb may be provided instead. -* Incorporate a relevant figure (generated from an example) to provide intuitions. +.. dropdown:: Guidelines for writing the user guide and other reStructuredText documents -* Include one or two short code examples to demonstrate the feature's usage. + It is important to keep a good compromise between mathematical and algorithmic + details, and give intuition to the reader on what the algorithm does. -* Introduce any necessary mathematical equations, followed by references. By - deferring the mathematical aspects, the documentation becomes more accessible - to users primarily interested in understanding the feature's practical - implications rather than its underlying mechanics. + * Begin with a concise, hand-waving explanation of what the algorithm/code does on + the data. -* When editing reStructuredText (``.rst``) files, try to keep line length under - 88 characters when possible (exceptions include links and tables). + * Highlight the usefulness of the feature and its recommended application. + Consider including the algorithm's complexity + (:math:`O\left(g\left(n\right)\right)`) if available, as "rules of thumb" can + be very machine-dependent. Only if those complexities are not available, then + rules of thumb may be provided instead. -* In scikit-learn reStructuredText files both single and double backticks - surrounding text will render as inline literal (often used for code, e.g., - `list`). This is due to specific configurations we have set. Single - backticks should be used nowadays. + * Incorporate a relevant figure (generated from an example) to provide intuitions. -* Too much information makes it difficult for users to access the content they - are interested in. Use dropdowns to factorize it by using the following - syntax:: + * Include one or two short code examples to demonstrate the feature's usage. - |details-start| - **Dropdown title** - |details-split| + * Introduce any necessary mathematical equations, followed by references. By + deferring the mathematical aspects, the documentation becomes more accessible + to users primarily interested in understanding the feature's practical + implications rather than its underlying mechanics. - Dropdown content. + * When editing reStructuredText (``.rst``) files, try to keep line length under + 88 characters when possible (exceptions include links and tables). - |details-end| + * In scikit-learn reStructuredText files both single and double backticks + surrounding text will render as inline literal (often used for code, e.g., + `list`). This is due to specific configurations we have set. Single + backticks should be used nowadays. - The snippet above will result in the following dropdown: + * Too much information makes it difficult for users to access the content they + are interested in. Use dropdowns to factorize it by using the following syntax:: - |details-start| - **Dropdown title** - |details-split| + .. dropdown:: Dropdown title - Dropdown content. + Dropdown content. - |details-end| + The snippet above will result in the following dropdown: -* Information that can be hidden by default using dropdowns is: + .. dropdown:: Dropdown title - * low hierarchy sections such as `References`, `Properties`, etc. (see for - instance the subsections in :ref:`det_curve`); + Dropdown content. - * in-depth mathematical details; + * Information that can be hidden by default using dropdowns is: - * narrative that is use-case specific; + * low hierarchy sections such as `References`, `Properties`, etc. (see for + instance the subsections in :ref:`det_curve`); - * in general, narrative that may only interest users that want to go beyond - the pragmatics of a given tool. + * in-depth mathematical details; -* Do not use dropdowns for the low level section `Examples`, as it should stay - visible to all users. Make sure that the `Examples` section comes right after - the main discussion with the least possible folded section in-between. + * narrative that is use-case specific; -* Be aware that dropdowns break cross-references. If that makes sense, hide the - reference along with the text mentioning it. Else, do not use dropdown. + * in general, narrative that may only interest users that want to go beyond + the pragmatics of a given tool. -|details-end| + * Do not use dropdowns for the low level section `Examples`, as it should stay + visible to all users. Make sure that the `Examples` section comes right after + the main discussion with the least possible folded section in-between. + * Be aware that dropdowns break cross-references. If that makes sense, hide the + reference along with the text mentioning it. Else, do not use dropdown. -|details-start| -**Guidelines for writing references** -|details-split| -* When bibliographic references are available with `arxiv `_ - or `Digital Object Identifier `_ identification numbers, - use the sphinx directives `:arxiv:` or `:doi:`. For example, see references in - :ref:`Spectral Clustering Graphs `. +.. dropdown:: Guidelines for writing references -* For "References" in docstrings, see the Silhouette Coefficient - (:func:`sklearn.metrics.silhouette_score`). + * When bibliographic references are available with `arxiv `_ + or `Digital Object Identifier `_ identification numbers, + use the sphinx directives `:arxiv:` or `:doi:`. For example, see references in + :ref:`Spectral Clustering Graphs `. -* To cross-reference to other pages in the scikit-learn documentation use the - reStructuredText cross-referencing syntax: + * For "References" in docstrings, see the Silhouette Coefficient + (:func:`sklearn.metrics.silhouette_score`). - * Section - to link to an arbitrary section in the documentation, use - reference labels (see `Sphinx docs - `_). - For example: + * To cross-reference to other pages in the scikit-learn documentation use the + reStructuredText cross-referencing syntax: - .. code-block:: rst + * Section - to link to an arbitrary section in the documentation, use + reference labels (see `Sphinx docs + `_). + For example: - .. _my-section: + .. code-block:: rst - My section - ---------- + .. _my-section: - This is the text of the section. + My section + ---------- - To refer to itself use :ref:`my-section`. + This is the text of the section. - You should not modify existing sphinx reference labels as this would break - existing cross references and external links pointing to specific sections - in the scikit-learn documentation. + To refer to itself use :ref:`my-section`. - * Glossary - linking to a term in the :ref:`glossary`: + You should not modify existing sphinx reference labels as this would break + existing cross references and external links pointing to specific sections + in the scikit-learn documentation. - .. code-block:: rst + * Glossary - linking to a term in the :ref:`glossary`: - :term:`cross_validation` + .. code-block:: rst - * Function - to link to the documentation of a function, use the full import - path to the function: + :term:`cross_validation` - .. code-block:: rst + * Function - to link to the documentation of a function, use the full import + path to the function: - :func:`~sklearn.model_selection.cross_val_score` + .. code-block:: rst - However, if there is a `.. currentmodule::` directive above you in the document, - you will only need to use the path to the function succeeding the current - module specified. For example: + :func:`~sklearn.model_selection.cross_val_score` - .. code-block:: rst + However, if there is a `.. currentmodule::` directive above you in the document, + you will only need to use the path to the function succeeding the current + module specified. For example: - .. currentmodule:: sklearn.model_selection + .. code-block:: rst - :func:`cross_val_score` + .. currentmodule:: sklearn.model_selection - * Class - to link to documentation of a class, use the full import path to the - class, unless there is a 'currentmodule' directive in the document above - (see above): + :func:`cross_val_score` - .. code-block:: rst + * Class - to link to documentation of a class, use the full import path to the + class, unless there is a 'currentmodule' directive in the document above + (see above): - :class:`~sklearn.preprocessing.StandardScaler` + .. code-block:: rst -|details-end| + :class:`~sklearn.preprocessing.StandardScaler` You can edit the documentation using any text editor, and then generate the HTML output by following :ref:`building_documentation`. The resulting HTML files @@ -914,7 +891,9 @@ Building the documentation requires installing some additional packages: pip install sphinx sphinx-gallery numpydoc matplotlib Pillow pandas \ polars scikit-image packaging seaborn sphinx-prompt \ - sphinxext-opengraph sphinx-copybutton plotly pooch + sphinxext-opengraph sphinx-copybutton plotly pooch \ + pydata-sphinx-theme sphinxcontrib-sass sphinx-design \ + sphinx-remove-toctrees To build the documentation, you need to be in the ``doc`` folder: @@ -956,7 +935,8 @@ To build the PDF manual, run: make latexpdf -.. warning:: **Sphinx version** +.. admonition:: Sphinx version + :class: warning While we do our best to have the documentation build under as many versions of Sphinx as possible, the different versions tend to @@ -997,45 +977,37 @@ subpackages. For a more detailed `pytest` workflow, please refer to the We expect code coverage of new features to be at least around 90%. -|details-start| -**Writing matplotlib related tests** -|details-split| +.. dropdown:: Writing matplotlib related tests -Test fixtures ensure that a set of tests will be executing with the appropriate -initialization and cleanup. The scikit-learn test suite implements a fixture -which can be used with ``matplotlib``. + Test fixtures ensure that a set of tests will be executing with the appropriate + initialization and cleanup. The scikit-learn test suite implements a fixture + which can be used with ``matplotlib``. -``pyplot`` - The ``pyplot`` fixture should be used when a test function is dealing with - ``matplotlib``. ``matplotlib`` is a soft dependency and is not required. - This fixture is in charge of skipping the tests if ``matplotlib`` is not - installed. In addition, figures created during the tests will be - automatically closed once the test function has been executed. + ``pyplot`` + The ``pyplot`` fixture should be used when a test function is dealing with + ``matplotlib``. ``matplotlib`` is a soft dependency and is not required. + This fixture is in charge of skipping the tests if ``matplotlib`` is not + installed. In addition, figures created during the tests will be + automatically closed once the test function has been executed. -To use this fixture in a test function, one needs to pass it as an -argument:: + To use this fixture in a test function, one needs to pass it as an + argument:: - def test_requiring_mpl_fixture(pyplot): - # you can now safely use matplotlib + def test_requiring_mpl_fixture(pyplot): + # you can now safely use matplotlib -|details-end| +.. dropdown:: Workflow to improve test coverage -|details-start| -**Workflow to improve test coverage** -|details-split| + To test code coverage, you need to install the `coverage + `_ package in addition to pytest. -To test code coverage, you need to install the `coverage -`_ package in addition to pytest. + 1. Run 'make test-coverage'. The output lists for each file the line + numbers that are not tested. -1. Run 'make test-coverage'. The output lists for each file the line - numbers that are not tested. + 2. Find a low hanging fruit, looking at which lines are not tested, + write or adapt a test specifically for these lines. -2. Find a low hanging fruit, looking at which lines are not tested, - write or adapt a test specifically for these lines. - -3. Loop. - -|details-end| + 3. Loop. .. _monitoring_performances: @@ -1365,95 +1337,87 @@ up this process by providing your feedback. retraction. Regarding docs: typos, grammar issues and disambiguations are better addressed immediately. -|details-start| -**Important aspects to be covered in any code review** -|details-split| - -Here are a few important aspects that need to be covered in any code review, -from high-level questions to a more detailed check-list. +.. dropdown:: Important aspects to be covered in any code review -- Do we want this in the library? Is it likely to be used? Do you, as - a scikit-learn user, like the change and intend to use it? Is it in - the scope of scikit-learn? Will the cost of maintaining a new - feature be worth its benefits? + Here are a few important aspects that need to be covered in any code review, + from high-level questions to a more detailed check-list. -- Is the code consistent with the API of scikit-learn? Are public - functions/classes/parameters well named and intuitively designed? + - Do we want this in the library? Is it likely to be used? Do you, as + a scikit-learn user, like the change and intend to use it? Is it in + the scope of scikit-learn? Will the cost of maintaining a new + feature be worth its benefits? -- Are all public functions/classes and their parameters, return types, and - stored attributes named according to scikit-learn conventions and documented clearly? + - Is the code consistent with the API of scikit-learn? Are public + functions/classes/parameters well named and intuitively designed? -- Is any new functionality described in the user-guide and illustrated with examples? + - Are all public functions/classes and their parameters, return types, and + stored attributes named according to scikit-learn conventions and documented clearly? -- Is every public function/class tested? Are a reasonable set of - parameters, their values, value types, and combinations tested? Do - the tests validate that the code is correct, i.e. doing what the - documentation says it does? If the change is a bug-fix, is a - non-regression test included? Look at `this - `__ - to get started with testing in Python. + - Is any new functionality described in the user-guide and illustrated with examples? -- Do the tests pass in the continuous integration build? If - appropriate, help the contributor understand why tests failed. + - Is every public function/class tested? Are a reasonable set of + parameters, their values, value types, and combinations tested? Do + the tests validate that the code is correct, i.e. doing what the + documentation says it does? If the change is a bug-fix, is a + non-regression test included? Look at `this + `__ + to get started with testing in Python. -- Do the tests cover every line of code (see the coverage report in the build - log)? If not, are the lines missing coverage good exceptions? + - Do the tests pass in the continuous integration build? If + appropriate, help the contributor understand why tests failed. -- Is the code easy to read and low on redundancy? Should variable names be - improved for clarity or consistency? Should comments be added? Should comments - be removed as unhelpful or extraneous? + - Do the tests cover every line of code (see the coverage report in the build + log)? If not, are the lines missing coverage good exceptions? -- Could the code easily be rewritten to run much more efficiently for - relevant settings? + - Is the code easy to read and low on redundancy? Should variable names be + improved for clarity or consistency? Should comments be added? Should comments + be removed as unhelpful or extraneous? -- Is the code backwards compatible with previous versions? (or is a - deprecation cycle necessary?) + - Could the code easily be rewritten to run much more efficiently for + relevant settings? -- Will the new code add any dependencies on other libraries? (this is - unlikely to be accepted) + - Is the code backwards compatible with previous versions? (or is a + deprecation cycle necessary?) -- Does the documentation render properly (see the - :ref:`contribute_documentation` section for more details), and are the plots - instructive? + - Will the new code add any dependencies on other libraries? (this is + unlikely to be accepted) -:ref:`saved_replies` includes some frequent comments that reviewers may make. + - Does the documentation render properly (see the + :ref:`contribute_documentation` section for more details), and are the plots + instructive? -|details-end| + :ref:`saved_replies` includes some frequent comments that reviewers may make. .. _communication: -|details-start| -**Communication Guidelines** -|details-split| - -Reviewing open pull requests (PRs) helps move the project forward. It is a -great way to get familiar with the codebase and should motivate the -contributor to keep involved in the project. [1]_ - -- Every PR, good or bad, is an act of generosity. Opening with a positive - comment will help the author feel rewarded, and your subsequent remarks may - be heard more clearly. You may feel good also. -- Begin if possible with the large issues, so the author knows they've been - understood. Resist the temptation to immediately go line by line, or to open - with small pervasive issues. -- Do not let perfect be the enemy of the good. If you find yourself making - many small suggestions that don't fall into the :ref:`code_review`, consider - the following approaches: - - - refrain from submitting these; - - prefix them as "Nit" so that the contributor knows it's OK not to address; - - follow up in a subsequent PR, out of courtesy, you may want to let the - original contributor know. - -- Do not rush, take the time to make your comments clear and justify your - suggestions. -- You are the face of the project. Bad days occur to everyone, in that - occasion you deserve a break: try to take your time and stay offline. - -.. [1] Adapted from the numpy `communication guidelines - `_. - -|details-end| +.. dropdown:: Communication Guidelines + + Reviewing open pull requests (PRs) helps move the project forward. It is a + great way to get familiar with the codebase and should motivate the + contributor to keep involved in the project. [1]_ + + - Every PR, good or bad, is an act of generosity. Opening with a positive + comment will help the author feel rewarded, and your subsequent remarks may + be heard more clearly. You may feel good also. + - Begin if possible with the large issues, so the author knows they've been + understood. Resist the temptation to immediately go line by line, or to open + with small pervasive issues. + - Do not let perfect be the enemy of the good. If you find yourself making + many small suggestions that don't fall into the :ref:`code_review`, consider + the following approaches: + + - refrain from submitting these; + - prefix them as "Nit" so that the contributor knows it's OK not to address; + - follow up in a subsequent PR, out of courtesy, you may want to let the + original contributor know. + + - Do not rush, take the time to make your comments clear and justify your + suggestions. + - You are the face of the project. Bad days occur to everyone, in that + occasion you deserve a break: try to take your time and stay offline. + + .. [1] Adapted from the numpy `communication guidelines + `_. Reading the existing code base ============================== diff --git a/doc/developers/index.rst b/doc/developers/index.rst index c2cc35928cbf9..cca77b6a015c9 100644 --- a/doc/developers/index.rst +++ b/doc/developers/index.rst @@ -1,16 +1,9 @@ -.. Places parent toc into the sidebar - -:parenttoc: True - .. _developers_guide: ================= Developer's Guide ================= -.. include:: ../includes/big_toc_css.rst -.. include:: ../tune_toc.rst - .. toctree:: contributing diff --git a/doc/developers/maintainer.rst b/doc/developers/maintainer.rst index 70d132d2af604..ffc9b73156fa8 100644 --- a/doc/developers/maintainer.rst +++ b/doc/developers/maintainer.rst @@ -1,6 +1,5 @@ -Maintainer / core-developer information -======================================== - +Maintainer/Core-Developer Information +====================================== Releasing --------- diff --git a/doc/dispatching.rst b/doc/dispatching.rst index d42fdcc86f9e8..101e493ee96b7 100644 --- a/doc/dispatching.rst +++ b/doc/dispatching.rst @@ -1,9 +1,3 @@ -.. Places parent toc into the sidebar - -:parenttoc: True - -.. include:: includes/big_toc_css.rst - =========== Dispatching =========== diff --git a/doc/faq.rst b/doc/faq.rst index 8ddf0c4c238f6..81f03b49bc7c9 100644 --- a/doc/faq.rst +++ b/doc/faq.rst @@ -1,3 +1,32 @@ +.. raw:: html + + + .. _faq: ========================== @@ -9,8 +38,9 @@ Frequently Asked Questions Here we try to give some answers to questions that regularly pop up on the mailing list. .. contents:: Table of Contents - :local: - :depth: 2 + :local: + :depth: 2 + About the project ----------------- @@ -323,12 +353,14 @@ Using scikit-learn What's the best way to get help on scikit-learn usage? ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -**For general machine learning questions**, please use -`Cross Validated `_ with the ``[machine-learning]`` tag. -**For scikit-learn usage questions**, please use `Stack Overflow `_ -with the ``[scikit-learn]`` and ``[python]`` tags. You can alternatively use the `mailing list -`_. +* General machine learning questions: use `Cross Validated + `_ with the ``[machine-learning]`` tag. + +* scikit-learn usage questions: use `Stack Overflow + `_ with the + ``[scikit-learn]`` and ``[python]`` tags. You can alternatively use the `mailing list + `_. Please make sure to include a minimal reproduction code snippet (ideally shorter than 10 lines) that highlights your problem on a toy dataset (for instance from diff --git a/doc/images/ml_map.png b/doc/images/ml_map.png deleted file mode 100644 index 73ebd9c05fcc465d359c469abbdb34eafe5cb6bc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 761071 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�+rg_uL0Y&jwF=EXQ;hjbRG! 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ᕙPXp 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

uVJB&)Drb1PT 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 z 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

D2)R`+!JGE=(a>mXN(>az0vT)+=%ArT9d^ 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%0AXbp|5OZz^ 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�aMQxPz@nu6E` zpMc?gi@L57>1Kc4v>c!E`x@Cvrf5B`Iw{^1?Em7Aq&zPW`T&_I;yL?gssR8+R|T2g z;C+JQ+XmfEfwt6Vi{c-X`LAlnkB-S1lxA}dq!IG*+17U%+nN2KtoTkMQ?;8Bvw7L; z{t6ixQm z4Ga^~5*T~aw}S1*CC;?K>a}RWNN-W;*yw{bCp)ryw36F~MgJN&`!F&6FOm)?(|B*L zwv_9i?PqSTsdpqKDT(0eQ})p`ky;j+aEn4ez*e$~Qi2OIdofkke*iVzZ&ST={g6k^$n7%#RlqdGOj8K{NX81t&C@2a5dC)s$b#JiIJ3flcVWkMINg7D9xo8dza{6OB#_a|$g zlKGe`YWH#dgE}aB70T`HaoUs)YTq4tMYPonX*v zk4Wt^3&tIIe|C~TO&MJ|nVXq(|8B@QL>B9KPQ^f(2FovdUJbq2Kfja@k8Q)AB}qh)mp#n^FjcuM5)(uL7pZkB*ZS; zPTb$mo%<`DC=%HkA3u^qlEI6MSIWi26dfHC(|mj5ia^-#@bbRGrm;}EcMhT}bdxm|-<)JE)cmVupV%+Bn4>ZVAjWe8!e|2|1T|YP&HVXn`H8wZ{ zDfR&7q-v?oH#=TVPBRpg?jh)9eph(OSC+yWI-JHvRj;NG1AX(8O`n^U(7uQ&7!i>* z0%Ob{y@05JUlC=GEES4_x6gd7{${yjbQ3`g~zqQV8&Z98Z{yju6mnv;p5Q8|JdfEF`?gXO(iF;%kEDN98)DFM=+z#X& zi9udOk10}u?%n((Z|eZAZ^h4#kTv;WLux$YfA(vD2DWqbsFgF!zYmWpr;7&4Uf#Z+ z=15;kAgYy8M9V9H4T|?yphd+OZBmQ=VL8Wtti_mR@BBQv8dDkM8J}`;Ro#Cjl znr2`AvENT>o9wBZ?0H}Cn2=K37xF~VON(L?2W;GhlR(j4!u=8!0d?!(ctvgPB;^0_ ze{2&0p#tInkS1DS%W!aTu()Uhy0*5a3ot?BRLk*n06f0^J;HR=% z_1A)wKfFxt&^!cDuV+^BjE|jR+$^Ep5h?=HeR6mjt5jqH!JEiM8C1lHWvK?hZ90MU z!3_xhrm$LMdB2dP6vSb|*E_%8OqSA{18vp$Fy!-;x9v})ydjExyZ&C_3{0W)_zO`Y zT9SV1Uee({>Z1CI$FG?$o)pc&rMiz7=t0ULZe%c&De}jUAB7_JQ^N89yc78_?fCeZ z%?yH;^RIc~qOf)Hjjw~s9Y z)h{nj2$=xC*Tpv?3b_IY!V$mX6{zOoG50aTxj~x6^K;N*i!cRe+F&+1T_G`?{KZs; z$^@$r$Gqh)fDQNkwXNE!sqjF1^j#%xju+v|JCpL$0ZT`GXEB`9$yy9a-ce99W27ID)K zFF)C_%O+)#N0P|PsCIPpJ`QSW%FwCFRBM8;)ohwo$l+WU2zM-o+-+Jf5-N9LB*M%B z9ic4&Cg~^$y1iWGlM%Zo(g@H=TjX}CLVjCVJ$*zgFpigJYv-5>Vjp7SJQ~AO_t>2Q z28MjF=$49D$63dvnDE`X{8y&Gch=c7JbTT+DuvUobJ1hbe@YIAkw_)R`DoPSmNF5Z z2E^a~+hl%eTIVO9rUBX2$VE1#Ne7>KHyhd{nwlm*$=R45a$vV~vUWJEel#}l;WFyK zrp=Hc8fLi8>({|aNqm*T2L!@^g!@HD$E(C_(--q~^b}wnX69TM9a_;p=ESKnF~)IO z;fVd8eSJ1z3JL*kGys`_PTI4ws>ZRMMdOY6m|UHmU*KzU7ga%m!R(#Q6SlVmYLa17bbC>dfc+9-oH)UL{u;oHIeKIE}qUVH-;GKu0T1! z(63_P+*J1}=B+*lIi|IPOiCrzrzp) z3xd}{4+NJLh*cHlY`=$!A*iN<{pCXnNs#ovxC0>MYdxVRpInWPotUFhbKiX3w z2oxqm(2Q(8d@s4lzVHm)fGXIle54WL6R`=~kU%H4IlFADa?|ZA=Wef?8uJI>Z}Kc`FFewTC>ZCzC+u5yg+VS8M@AMM2Ess%!eg!!W7Qd~x+F zem*2-Q_;8BLcqpoW96mUT&jr83&sXBGmBD6GT~&oWBh(Qx5Ej<(3)$=moy{G(SN%; z!$twWz~f@tdzau2VaEH`1Ag0O1&jUH6JbSJzNcluq;m+=&cc>Io^l!A%MqK>?f%9O zJ9jtZM`J>pnI2h7jd^|oXjfr9xyYE^6TtI3fws0dTORoT4a-O)kMCa`d)_kt{c?(L z<=PYfF5MqLs!vWfKSgURDHSi5mhTHFs~D;d4eiVLpL=-HK^jg;*2G-EJd?FMYu_|}qZgB*Crp= zgOdrL$EA0fWu$DfCFZhG#5T1XkBg1J2_+P8Z0q=iFp|3>h?a|<{g11hMly#CQF1be2jMrC z*hpnjL)fm!6C*^5F8(Bs0g#x}syKE(h8-o-?xyj2F8@(gno?2Q6pE`q=EiXseyK(Bn1>l*K+ZqDh<5^iPprnu_klU;J)pm73y) zupg??MCT|T?T{NZj`f+*!Qua7T$4V~kbJ)i)j@o{xw$boGA|Mvfm%8^JSQQE)G}&G zaQ`FDAM_mvdA^lDJG<8~F_G}{!PZdEcGC+?`}lF}yY+BZq02gr05-c>0igzDYNu~L zt5FWvhRA-8{gz=8f-F;ipu%O&X!y+0mbL&{rsi}182xaRG1PvuL=u1YC~pC9yjpC_ z$4`A}$Ff=978~i^vOkr$T4Fn?{DLYARX2(%D&oLkM`y*Sq)fvG0^p2o#Qi!UW-oYaweWkq;k{1#8#7GOf>m(U+_|diCkh@G%;9-vDd|7Q{ee zXV?!U^To{9IoJdAV5fyDi*UJId@7wq1#!w%hi87obA&5&>7ovAK4MUT_&~ad90(k? zuK-XqSdBOq-ryM){?&O5UoAqZ3bR`B+>*BC(SM9sd|pjPIJU6XZmYH?%C*;{8K?N@ ze3+#)f@Q;d^|O92oibV!G!4!o#CC)RdzladCiZ=9=7JykF37X`=RH;i$zDdIzBM?? z4uEJC=*uZI7g4`3E&hG4V-U1KyCZOH_J%`$Em?c5ySuw@GJlQG8$0l)m@nQ@_?lv; zVqP){kC@@G^K%2a8w#7-K$$a6?>-HzCh_~@AeW6fJD&2Y{8aTLk?K#_4k9Vy%XW7a zFYHO4a20)Ou@WVz9V4Edxe-DssQJZ9HKKglmpVK5h1zq+~JZ`MTVQkn|}w5by4k2T#Yxx!K7R-R*drt9<%1B zQyh{1?@T~oV*^nG_$IIT0GYi*Yv{x17}dUBj)+H(qin|uWuI2+04StryZJiS#1vgQ zFnGoC&6~NrO=?YwmCp@cCmRo;T0^y)lE50x7pwtE-l^0H@Aj5o@34BBqd0?=c0eqr zvao7fV2V|p<`GfBbP=t}n}n&U!equl)JJ%(y<5L{!gT%JvdtoY>Ct#fbLFGBSfADa zY@10x0OrmrUb^qH+(K`e<)-w?Y-&(8CXiIGJJyvNn2&gWVVS`|SV&^`{e_xOfA;K40S>N0Fj+{9m#K=ZNxbo`a2gOFFwvyQ}U;er>g^5c68v@U{7NqF;Gw> z;Xs2MpHj6Dt1GQb!llTUK>w(v#eKE}M0~+QYKn8>1~jp%h8B;0-BwH)zo{j{OF|H6 zy1401?Jd_|C7WQ{>cl&Cv%Q}66^`L`IJ>ye?e6L-1~GuAI`q04Lg$H%!aOvup#%mR>HRaM(sI(13z`_f#H zMxc24mB-Ltyf&EQ{>5Y=+xeEo%2ZVA3;-^a21 zll?+xC3G!OuUkiOK)1ZNOqLlQNx0%qKrfkC-f$(EX1+GGPq9yO_dG2mW$FIo5FpyR z8s_t;M7jrHvtM$%p9+-+SG=hoym}2hOyYv|-N1^P;=zM{cw|rz1PdGW7JP^*H*Qwj zpRbRT$V700i6JNqBo-16L<#Z)>7jCQP?$=?ffEmm)Vc8$ckoZ3IHSKt17EgfXDK{sXg(D>*GRo zU%u^Xw7YI(1z8J#r?rkeaFN#s-Gtx3uvAeZa3z=p_(uck65d1^O$8@4?e!TYkcjQq zbegFzT7n(c0lzB^g#F3wnoJCn*uIm~-0>P&oRV8i+n&|a-kXcIW;I&QSH@Z8u*5@G zk!emUij65JIh;XEV15`%D=glxU-EnOJ*PT&NoIx^x;dm#(ea|lc_}Bse z$M<@Vok^FUi;Jzb#l^*5Ot)Z8Y<7?z-9J_HN@uwKh-m()z|)+r8)Pl-;V|Fbx3`q{ zN_@=N>|On5ujPra_l4A5`>mt2{ts7g9aUAheSrcJ(s1Z*JTypmw{%E%BVE$n(ka~t zNOwy~H_{DKhmdY~oBO->jW-_u%5do3-(G93Ip;SQ5%-1@nS$<7|LIaIM%LeoMZl3G zB)y?)PNIyPj!O4MW0D2na^{E}gMpRxbJ$Q6P_n>)@^9DB|2g!0xWDJ!H;uOj38ewW z_{-^n0~hK;%Rl}mAP~$k;a4pb3kIxI=iR4ZjY}D}e*l+Q|4 z_jjEuPqaBRODtjvr~@`$GIul@SZ{Ci5VQR}wl+vnC_3$P7{}Aaw5Ls3KvdfUTZuQt)loydKVN; zm0WERo1fH`avqx?p^c&`Rpmh>zOL8Dp1>Sgr+mxVRV%$P1i`ewvXlE?**oJQRMbTj zLR_qhtOhv>F2$uu*@qVb;Q;6Fm>dxY%Ip6QMYAbI{z_EoY$sQ}d@-<3CgY~>Jp6uM zT3@ics&$WkAW!1tfjH)-jgOFsF*jqg`h7BZEP5JjY1>+~5d@k-T`*)wc|3)@(v^E3 z7n&BC8u)C{LCXy#ClSaqd?qw7!X_kKMhejBm-^ooB5G9N+=s3iy-pI{SfB=0Z=41P(x52a6Bwfu~1rhs3hdd z!#yxOA04^jbAA^9*W?1R^nPHi)F_=&2J*(XDSz05hUt-E3Gh!Rk}HOg9VH;UxmX9| zJ}pCqLZL0yTTaUAp)YrG{54w1>hE6OirDR5fsRDOi@ZM^B%rP(-!aUvBP&K^DI3HM zPHWgJ3P^)5sR$8r9OktRFSu{v$j^_N986Be{!^||p%q0qMlgocLB*;G2hKnIsG_D; zh=hb>b5dKM(}`MKDlrqwWGk_$C-Vax`rCRC2!ODnD9HyhozTjZ{DSHMpi+WlktjsZ zc<-LoT(3X4J2>^6NAmJ0tXibLj4T^CV5h51__ocU%i;@$)EDC#fdB-WJlNl}|Dst^ zM+BeeOgr1qCxvf|)ba8nyWnYlcR!Y^pEl__9Mu^Vktb|$i|?4R27(aWKPTo^d#B|Z z&d$taAUtkB|92M{2&764%cEIsuyzcAtSqZlc-s*W5hb;^qYn&Ps>%-2;^N~#n^5HU zRJ65;C#N)q`S=7aUgmR8;vpkPH@z|0MRPUxjcyQn^MiImo@n1{@eSbyY83EBQ z;mDgKP~7lP=)@VnYWY1*%{F1P2%w}k{h!d@(m`OZp|nKS($TdD-rZsK601-e7^ErZ zHUV{^o4Enn4w|^&1;q`t5J-OAJLk>{+&Z*+2SR37n(6&1m0?OGh5X0GxlH;ot09J& zB5l{^Vq!@uFw|R%FYQOUsfi-m?iCT=UZJjLC-9!B??nJrmq>40{m3I9mKmD0R|{4- zm@9A=ykOP|SHuX<0C2m)zj zK6zxx?~$EntRUrC;Na`tGD!ncx=&Xr1A<4W;MyvVldN>^KPjzd=GONA>W@ZJ7!a#w zw(Gn9*H%Kj^88<)@lqGc)6>&&<>g0(oH94bX7uy~=xAwU0Qb4oN>d@_S4L`%U;0!~ zve2o{PUi=~uEHhQn3&UbPCHUKnqg?EmUTkPFwnbm<-SoOk@pF1@PMJz91hIJBZ^pm z+>HZgq0nifDl+vdE`xR&z z@&n{jBsAlH54^;I`_Lht1ri-TpJ(8-|3P|%8{m2l&^|~0ZnIk};q7NFY$x zvhMJVH=-6xDo!`7GK7u=T@C^9dwOcNLmxc&;c!oiYwEheHfzGz?OW6X3+4S;#1VNR zZHCj`P$vIS;K_GV3Q>}^viu~T4cr3xgXk$JM1gf23+e_26M70zaz^&XU$)8i5P!3; z(*S$1v2zgaK0&8Nt_N(}SH&i$;YfQEjYw|lzHLAj3&u%y)DvTlWuP0Hj0F{Rf@!tBEiLnL5hcDZ&;$);2WnN9t-kN zK?%LQgQwDG7tJoPaMNtphJ|A4Q4iaGCUZbhkpmPiXrhX8TW$}N;|za~wgl#Wa%`H$sPQMs1~5&@KqR_}CpaIM$% zQHdL5p3{D%DJDj;D~JS0-!j=mLvywYDy`q7V}ufg$`T=Y|4;0WVC5@4`YB~Ta^fZw zEIRD1x*P?vpWt1%dyfW;k@R?Km=xmMRThbGTysuLy$|@{Es(sFkhK}6j>t`|`EF`c zg3rYf-481=z#x2zB6npX1aRSb=Sdc55p2(EddLrd#`agUla_(QlMDSDDjphK&r=Gy zZvjK7dHMM(U|?X>ot&J?S65d{>gqny;h;fLLCd)+9ks4TXm~g-K*slRKU;dI=9rHD z{$o;H6R!+_(#l4-U1eGk#C#X&Zhj^j4@)(qbC0}!EwYhul){-_iDj)68WBW69YBAS ziuc+@U10lK(0=X1doCX1m4p9?0XR8Ym^0xvxgCaR{2hqi%=DnV0LyPxZ5Q=ai+%GtUOTyRZKl@95dhaI6 z^wFzC4JMP%gcc43C_bD0sQAtHgQ!g9DkVI+UlLCrobq~=bPHS;vsy_~5}-_J-HsZy zQgf?X=ku`B6)&_WE}M?!^?zDqQZ8V?OQAM~)a32|+sF76 z_`I&HOg%kY=@}V+HnObIwj7^i&ygL0z{1^$Z`TWhCV}Rw74R0wEuD}cyL)i3Y&i)n zq79_%jeFl5W5W92;a1~ zuRrP-3y6!7$9uH^#IoCNdQMJ_cgaxH0c?m?BBbemG2VGt#+Np4GGhU4tYwdm4}iKi zPI!npHsWOD)0w>UBkOi0WE!WV z@6Dy3x0u;_hDa*w$;eQEYfHOA;8Nkh6t2vN=xKjH17nl1A>#MinCd#o{UVVM z;}uISxOoyxC#yQ2HWVL1a(teQ3He;DJ}iV(RCgu9jr(Uk(A;}!Gn8Z9x&SV2k8DVw%az@rB-mAQkVyyspVwuE$?4l9;Zi_A$-aL5_lyS*2_yA8=>F1S zmG(S21R=nO`G9)j#a04H^=@fymyafvjS^=r_qEcbb6|9Y_dI@i4vxOW^gV7c9Z>lB zzOm8a0Ier^wT3K0;kdQ=HypJ+A`nxCDhQIsZ>9%0)cMv}$N|Wu-G)(QFzr@}ZyTWG zDvG#ojfx;%^8lqQQ6iPibnl;iR)3Il2f8n$MM1U3#|tT&cnr`f7YJy=ZiY?}j61QL zh%Q7JS<1=&v#dD8l&RKF18~-NE_Dlfq+Rj%=^mx_Erh=m#7-By~Zf1RCmULv_zinNx`sCS+$;4Oyx2r# z*;W*3LV*u}0apDzSJZx~vvLrE1!mTIo1OPgU6zz&VB@`kc0<2l;J0V?Vfv2vEKtP| zdwuK^l6zeqZxFB~_43#85-^ML7;I&iQ~V;fWdB8=;r3H4Q%{vEiygq=sllKgTeNuF z7ijZ}h1nM=LSg{nq|sASiUGRZZxJ8~$_8F;Q=R}#&T$aIw~X~l6pJ`(ipG;Lt{zbq z2sN`JGD0gt5H)H8%s$iLeY4C&Z@x)hO|0bqqda%hHuA8Nr*h)(qk({g=|{s?vB~aB zI{Av)Dc%gYO;3UVHVe^K*GrPyM*nlKznmOy`IXdJ$k!vy3i@V)OyRMLYCu7~SN%icx{0RNK>?&CFFO#@oo0AyfE1p2g+3_G)=1 z@M~ver2UmK8$;;5*&%G|6u-QoyD@h=BWD9F4D}Z=^1%3bpAc$8s8Nm1x0Mq5h3npF zOxl{YUxp={XqHkfPm^ymUlS&-v!{nhmu#BWeRDSI+cUoNv)^X1yAEHaCgj1+|0$BFzr@8ZAFy#p}u z5o>`T!ZWSR6Lei!8RhaEzwP4C$$g69R)>+`8`2hBb8DI`N~6M zs%(odm_4*_d*;k|Dw=JS(YDOqZO9A);+8aX?DdmHj5VV`U_~V*B}i~)+PGpQ$hy;&r!GPdt77;={Xi4z ziqm;j=+YWI|8K*~HfcL|H&~pj<1*KP?E_sh@9OahN^B1@Y84pMZ?#e?TzM~1E|N=G zZ1P4%c_2Kza9FzHwzlP|>FInj0s>4I3>VD004f!edR#nJ_nI_X;(WWE+O)(F_JR

#_d4hu(y;gvbY8^0qzl~-RL|_^JaTnzr&NRDJYPQuRSTVxoUQqk;;*;+ ztb6$1C2=*2io{PMOz+Gp`HQD8{W5%=eO>IJ&8p|_I$^tZC%)AB!YgRfOm$`#*EuOA z*W-DAVfd`RdQYyy=PGzw7JLkPr@f=6Mxc2x~T<+jrz`l=_L?U67lH zlCq~K)xcnBLTTwjQ+3b3e~$9j)@9aKRx+a94lgLH>%Bj}Jt#q1jGRkqZo-4UH}-z< zq4Biji0oxVDmX`-OsJSk zu1#Tk#do=FGcS#*8%#PH@9F806B7$c%*?DfaOD%XwEWOD8A=?`8o?Xc8p-R`TyShb zhc#jJy4a}~2Nu~8EF_QVY-3$*GSIKIJXOh45il~a1ZhGaRa0QQ=7QcGE zoPPuubV8{Ng*pTQ*-7H6ob(T2X>atR&rfL%QRB}W7ZbOuD#-zQHQ&L3w=`II26sx$sBxi=0V^-F{KGfA z8|U`tF4PlWZiJ?2cNGg@1FY)4gQ3m%U?&FHH(vO!_g{&UX+U&cx0kPV3F>Csu+->} zoOh)TUYRW?t!K}{8AZk{a+^;^tG)N?u;=2oP{1FDxgAwDUScN7%Bn zCKOy-SJzw6PwM!01m|sCVPayUiMxx-N?23y2QV$)xAGAet1mxP`Ws!?0zDTc5CeMI z`tt@tq1$*CzSKXa_G^W6F!OO*>ixXCLm;A}q)eWhQ!j35(E>gP z)~lNvdv6tYVPsHdBYS068Cg0_PgX%ufe@v|2d+@44D75w*ql>Ic`D!qKE zuP3>*dux!5J57pCt3l$pKTW}l<>2$!TOXO#vjG;r$UDZV2^#5JU;~){zu1C)2j4_) z92zufoQmdwKiN*{T+npTXTB8$u)9(1ksz7DmL%YmQpatj7ParR{<7_PUjH}m!6Pug z;CF}Y#BYX00VH_-;W;W~M$iVR&J| zLsti*t+iFv=L_r~^}Xq`1X^I(=ZGZyUOtGXiV_6RJ9c0}4AY z+R0xXB@wmb2Y_?K_8Z@}xecO-V}_2GTYq^W%{a=~iHu&uZ}ywXChWOSa6@nn@mY@iS^K|&g~QeqXJrCG)SKKFuJ zXp(EUR26TvKr8D26IWSL0UR@FHBBA-;t$~wCT^-f$!K%}1)#SJI{x^xk9Fo{OG8~E zUo!sT+h9E-moXwW5zaneMB;&|GU~a(Xzc!&S(=6P&&qXOpTk;E}RpG zmo(=V^FlP!d<6ejQ@$a+EJjE72L?hH7Zmg+r>9%pTwSf=cy2dIY%!>UKYqL@`}s34 zBNM^e@!wCdkqbrc67v+Ug{!ORSoO9E2@S1#o7PspojpyFppqH`l><`~=Jd<3?K=96yWf&)n4Nk9I?TfeCk1+sL zIPQAg7lvI7Xo6cFd|BpP-g9qYYMFJ(w|Z`V^-IpVpWm3d^nZ>kpb`1HR5M!uJCXTp z3nO#BINLGE4A{iB>O(C;0c^xyx0bmy#} z_ZNQIghwu^1y5xg+3>!C1dpcca&{vkxBC>;VQHpg-A)CD4#UG+!NK>q)25&w0M$3} z>-Sx8KdKR&Gkiyoe-rGG{Bi9m^K|*~01r5F9OzlbCnuvhJ%NiJ?`mnu)B!K-wV4~N`hU^WgF|N3*xDtb~(I zT#l89zQr>z`;bVii^82s<5Jc&(<%ltwo+U450nd)fFx$ z4*|2+$M^OXR(6ZDo^5wr*8-Vo82~nqk#Q1L2^H48DFqv|;@huvcwWvo+Ai4Zw>XLl z3kp`A5ob8IJ^#)>PH5CWF1yB#U!aqKvU9@a&$(6m&#CEniSxO!o~9L9_oM8Z*{hxO zJtLE#&|Hsw|6p=+TZD zQ83xd(Ns>;nn*^-m-d8p&&VgDv?1-<-)q8i_{79NscC8Hfmar#Sjz6|>Utn8Eq%?- z&h8ZvEiW%`qG)1I8dC7Hvn3>m18EklT&R5V*Q|H+Z1(X$#x>IFWHW6|Dxc;Zwi>%= zq=MxD&u}9c%b;Nfw8saFc%m2F5@5Po4ThGrDwRb1$wuL+mFprL4RY{acszT^Ft{)O z9R1jJZWlMbqvZQHmb^#0ow<>Yg6yhMCHCkF_9=kdQr?fk9k8#qRleU~N3c#I(1cZ11tn#%k*e3m!wdFP(gz<4?P8 zr$e9a#n3ohVk>=e^T@4r`dZ!Y_DYH4RR>$fywnE)l^^!y6N5u+{!yy!eQqVo3rq3%90m_$tK8Zd2zd*uMWx z^0$*SGl?&|!$|;-_$#(DARs_6;RAJ7*9$gDHaXJP-@h?)Gan^7J1q}(hod>zU4jP! z!{djCX%e_*miazuXjB3=chTWtZ9qGuD$3|g4+l(zxJ8n0^8f+@0uDA`YrusOwnc3; z$-u#%KN`nUooM zNeA=x-mQ3f|C)}&(}m+uNdc@#?R{gle;c zP_7tdhTV#*BGarLO=yky>K+MhNboYaiTWI6^7I@5GQ97ZY?e%(LPEYN=r?@X-Ixc_ zkosPB;nMsgJ-f+pG|1+4@X$%4J3)4BxJgxmTxi);$aqFZN2>vJKQ_dpHNUe{U-;uk z%6{JWuIX_IvIbs@TH4yv=%}dql~Hum)MhLG9_P20`QI9NY<@5moljfTwtq(^81s`$ z5n&@}KRYo;Y+%DDD>KBReJGdg?HBO~wHlpXQK66e1B$>#sn53B2OKiM=^+DAvy!m^ zhlfomftrE_amG4% zM7xGipkN^wKZXR6m*dEnScAT}@n zKok0nU9qQ^+aw+u6vv-l4|Ac6nq=`GfZsA^SJ}3Fos@ohf(SqS&B>}BpP@GE==R_F z$jh%3JBXdx$iL8t`8t25@_c`}f1Mtj9(^Yr1#<2pEegRavJv4E&u{zjm^EvAuKb0Q zB(6evfl_Cre?dt_Zl%MHff%CGk<{P{23Cw-A1y?*-u~@=IIM~QeVpFDKYJb9I_oHj9`m~inC5GzYML=1YL1m0iFh+=e4JJ;0IOkv~Wx14l0 zZ=&D5&E|UXifbn+uJ&!2$i!ngk-z{^ZP!C#j)jf)7uied;6MgMlDD=l=N7mnNk~lRFPD@!IZ4E{L{O;DA7X(bVR67`Z*z0B&nA=1iK@;iCl-oM#- z1t2lo2jRP6mc^$hU6!ZzWkM5v$;dciAO_|YU>~bYmwvTzabf-6T(#B7&m`AzDY(tH zakpY<=S5cZcgS?5)D|iNd~tJvK~ofQakS87@Uz+5I($d%tmv0|qu6?vpWN54Eq%#v z3n-rdj;2YDjEW|vp%LDloG9r1FiidBGiDX#Vf`({~Ip83JI&l*{VWpFhRwnwn9#q>uUqHD?a{5F(NM`~=D} zN@nKJ#3Xh)LBY7Xdif9|6;)MvU{$Nwdy$VYIzg_kS=e@>Q(Pth9DWb0rpEBdH*Vtf zz!ww8f+P!DO+rVEAzfavBDpLsDoSH+fg9O?_MR5Vd!p=lH-k7a8_b%szP_H?-!I_; zXwGHL&7Z$#X0qog4??buB0+UL3?8+GJp2Gh6ldRM& zjgs|uh(GjO$RbVpKpgD+(3TK{{imZ1JFFRy{>=}rLs%0`*vVmsZwS8HEwB8`Fue9R zXt%C=w?0|ua9__ky*;|&^wmcOExh-_&JP~3KLMoxt{~E9{nasU&G8ZV`_KNYOdAYN zvpJ$UKCX~@Z|G0H%RvvqK7Fkoq#|DbP~|uBaIgMX*+}k7yUausXZURU7?7PNr>>rC z(n+eJX7KR%IMC7e#2-P&fv1ZwqEY zOye)_Hla1cg8J6hR%&f6TWGZ@L6{^Wz$=G&=83Ys=WjcviJF=M;&@bDyFKoEV}NLg z;Yk*ehV%eO43cb-^Ybz>F&U*vJWBXPW>qy8Nu+mvXrfeXY<+TV*Im}zFBse=rb%F0 zS{34VAEnLAh$SWZb>fJqVo`^wI5>jD#Cqv?cs>byfDI1_FuJ%!Z|K4$1FpXGi@V3_ zSMHPd2CW5+kB|QbXr8`{i;vCYMk?E}C)~@AQrYXnXt70Z5o@cu}Y zj?KoUF{`~{@xO4mrIy=tN=Pv^;pTmLO=Iry&Y{hZxf#SaVWi5e<`HCxr`_5k`hxP` zvFhhf#<#I?YG$UOX>IW*uzH^I#}4Cn7S~-;(^bZZ1HS zu)7(6eMn7SKH3S;EWT%Fo3E#KIiAw2>CrgM}-=Ksp zstl5Y*JJxZf;1_n9XVG+eCzBg;Ge^@8oI-^+~ee#g>lW$)3+&RpPEMKEEV<^=0wCXE<;) z!}NVTdlm0Ic_w=$etz=s+jjE5^^dDa4B4IL?#_PM<`>TQ=LNMLdImt9KN~((e?{|T zHh6sC{&Du&N%o9l?#{$#748z5W&d5>@4G-}2R5X!NGlBv4$ed$*kOM3HhIoN5CoBS zWjuR)`a8^2g0vmjz9O4=h>Hhf3f3ejHYF{z*xOfO;}D3!wbISaXo+DJG8GsN4d7TX z)P*#mhHIwB&xBxqC=%#l^$yGmc2*qWZTn8%lL*@<(DF&CLCwjS&(qt}{(kxWN%ncO zP#3fLe-Bxhb{e<_84m=f^ML~(!BoJLBO@Pykc3R&?mQDIDJdhCz%TMO;L^mepmPaI z2?d21%!HwxogH}W>3;~e2FPRXjT<5?UJW8546qR5+2$J*g4nsEY$bGX1`eDK=S}xf z5m>RLVSMG-!ecJTl-n#4C{6~qVa*XMHB?Uq5{Rutqyu)^)GtGpT@ALBkyNt1U4O$q z_qmns_vIfEx%M*rvU)~Q1E%5RZ{JV`9!QEgI61fY<;Q9P%rWi$&PC~Mz=G3$48ePB zjJToJwGQ1$6s%iED%G|N#WECbq|ksdjb?jZDBPmd-#RbtqAUt0d%EzzHsIEUbRo+~3Ck+Kr~GKlf?MnIqW8oP1<))C}X166;_k^@b$VHVkcZ^c{2ExTs91 zoOGDW0@zy*7Z1Lyu}uAao-UsB+WSL;mc4C=(*MwRx;GTR>-3oqptrMr{8-@X?oQv> z*!a7kfHFZ5lZ3?X=Js|!Buent?5x@qc0W&YYH9}X^9?viNWYJk8*Q`TdyZ`$Q3>w- zU!FWM@8jOzw3P7ftI_-<9w#K9Y=B1^f)q_9_B5^?$JBE%@t7sx4s@+0)11=EZ8naq zaIZWw>q7B>Xr?Gd8a{q~X$M-HQCi2-lJN7op)XUnBjU1)EQ~4MvQJFYM?B?k*B2M6 zv(!LlLNc&JSKQmX-S6vHtuZ#cj_xL)ugy?5AhN{jY#A{X2+EpH$_7W*7f4(y@MYv- zRXRSo@b~kDV34cP#0v#r2X%h(FmB2xiKSJb%)bV;@?_H1uA>dAte`@!!?n_^9t-T+ zoiG+?T>QH|wDuFA7aC;vFVD|wkLTSu-7S7ggsR%RgZ^mltNDANj%S@>Owa2T8Ec!c zTlX5|7^QdiPIolqws({e>_I0|?IDCteb2&IxV^XqcK02+@)cfZWWfPYguf|EGS>kS zs@mGt5&}+ufEIM=c{E4&j{1Gp4nU&3n1?TYKH#ehxJ0C7 z_wDUA*D3aoSI1-4Gmx$rC)9SpOR!X{8uCWV4At8JuZuHE zD=poB)jfELnfwbnI=Xl&dr47II*_Hv)b3kJihu9{35Q0KU~XAU&NLXc!Lca~nU_(4 zb?hB;9Ot$wInXr?8|8#46JJxax zd0EfssQi1B#o;jFp@;i>?f9&~{r4y+4X|))3 zzGV8q>|?N%l^V^3t|DP`B6R@CN!6j94KkW`k%B$1^kH@yxczo{Irc_Bm1YW8A_^m7 z)WbCwu#)6{iN~d8#tc2$5xy!bC62&XI6CRRmUaK?^F+Ka`dI&_?r*bA`oCIo2}>i# z;&tEAY&YHdU9p=W+*iqg=Gl6+Xc|~%w)px|XQ*StioPp>l z7VAEmS4X!y({gjKC=qOfxYYq68&5W2K+soEN)amEY3YyhBy2-r8ux~?6vj6#qYa)+&eI3iU)h{C#tkmZeN=eAf(~ag< z;YwMo@?HSiQy*xHpK2SEY}%sR;HQJc8RUmNd+PeT5~#oE*#E3nqVDW(W}4C5{oW@V z!*$|V^lR?_%uv&S0<+Z6x79>6W^ek8K*tw<^ew&|5Q=X&dL||pGPidqf3UKo0Lq8e zkLuXHk~VAegy`>Y4h)z0;?m)yO7+ibA^-?>6<(k}KR>tl!n$+y{(n^u_{!3K-;Vjq zvU=p+jeTF_GEpiM&KLvkFnfIpP^`fXX3ED_aSrKSB(G;7jI|^K%Z8+A`AWjis1z(R zhnsK4DBqhUhv{l1iKSJ%>Jz|aQ9Ay-yW@9}iFLK{Vtr)i;JNR8K;-|(kU6;H>-%zl zR(=cp5eG)#zVR&jHe4O3YmYFUFEI{{t!M2*|C}h)tyX*mZae*z9sI9t4ZF8EH(a3S znJ4^%si!?d73bEL6RFR`XGV9A4o$Cs@JC82suVy^!#t^;J(OT`|N1q3min{~m*U$d zy7{sukE#x^{b5VNl&zwrNuY8G%AxYd@fqv8T6}R2~GeRZpS* zxkIgEueD%lvv1C)(j9uO?Ut?A`9#vxH;wd3hOMU~$r|)lCD5KRzGHPHsDo^i4Tn(qS=(Ql&C{Iy3pqwIny~qmbGnm!o}RBF%3Wd2n>DV2()5 zou^eX0$Ne830?iSV@T#SbQV<4y-HTSf94Ni1`(6`A2Cpx_p(=tu7-uYKiTnr*vLW4 zy>wJ(^L@p8mCt>>@POU$+S(Xqk}X}&nfEiEUAO{>11@5gA9>+^hag4+ycwn@x3sqF ztesU`n#kSQQCFYn#E|F|Q&Cykx9Nm~LtR!zMj7D0lcS=L-W5DQ0)&Fz46Z4!LhrpM z51>3h^z?}Oe%Z;gX4r9Vu}T$CgDiv=H{uc_qOwztQv=+n^Fx{>-$>Z?U|s)90`S-i zElhO(gN5Dl=H|22BnnUEebH`GKl1?@}#AcN_7sm^@Il@>~C*DXn)m}yOA?3 za$c`X$l%u3H(>sk%%~W@p`PJoBk}*Fmbc%07f&CE@7`Y<7aM!T&(D9Y;#Ob6Gz!v? z-%a^LVHR04LqVi1X4l77W1{wRt;`jXxof#`Su2g&@sthTHnE?Qq+&y-9aSY>=|{bF zYaj_^U|>Mb*S9mjqC$Qw|M1Y7b2&GKt1**q*V%9x$gjGt2*s|Lnd{QYWD~siBdIVF%@2 zgM)x)sKBN2qMG;%zrR_(q|i+_jash`o~g>-9?9gnu%KWrmB@LJZx0QG$(qJm%G%62 z*f9OlVf%bq>1b9ip*^8t~CEFXW(Iw79Y9MM}la$%jcQVqeH zrJUNBq;zVpxUzCQLxb;>$l~JqTBEnGFJ*Vvj7C`4P(RbEvj4;}Js1X-yy3iN!du4Y z_v)%%(ltMFA{oGYi#c!iBVGWyP5KYzkxy}@GX*F%KbZ>FCu}nC?Mm*K40N=K|8C>H zJ&4tJ(U-w<8~WmsaMlIc^X|U>g@WnVw??T{`2S_Q-Y9m`{_Nvrn3HSBf}MN4=>nVA z(gJrQ;i}tbCS9fyvhr40()&IeBD4mmi0a&e-Bg$4c_)>%rP!~Yb_{5^wC=Q$F#GuX zI=Dtuy45oYOxPQZ?SJ&w1Y_zjqP1RufB2hfU-B1z4`UDX1`&`nZw9n*X^5M9YYoF? zpd%`C*LB*W6_u)%e0MYd$n}JFVym#p0Szg6PT<3QuBPTpRp-f<;J#J;M91VC$^|Ql zOAY6;m4be3HTxR?@COA4rvSV#4L`pw9Sh56VEjP26G%I8G|PZ(?lVR^vOEJAv?2;| z4-ak~sqd<4YUA^6rhzpz8`MaZpSJcP3V^Bs*p#JdLJ!re8H#dZ>G?QkB=#-6tnNRK zV!!n(c86z0;^WHVi$un#Z|_S^p8TY3aCRdAa_;~49K#m zXx`nU8^UQ=D6X4KjrKRYN!CkNULH%91(@>S8&nAa&vPzh6)oJD3t>YIBl7p}-^l

?!LZ3FPh)dsuo@_IDbo3+$5{4Zl~7_-fWz%d+M1O3{?jB zdqIT2!@T_Ee6n3*W%_4copy$D+oX4RcGfhr`?D|O{D!B3zP>)v4Cvk~@_$hK&FM?Q zI5o0%ylBp->q!1UrgB0;Lh{ht#{9R&{yGQgSbCHt(a?`26#F*Xgvq_>17J)7#Ixn7 zovK5SUW=PSykk6Npi9-mUkR}MQ*5#_e{9ES}>?Uu}aO&4x^t(%&ncPTy0QmU`-&sCSmwG_It zd8|``nq~M?jZW5T)*vV|P2w?`DaPe563HzZ*V2d1Hziv~R~Pl&yLS)3iFsaK&3c(B zmd~Q?VZHqYtS3vj3f`q|sAns%f{6qu7bC+DDyyj6+@Ar4t>j+~xm-ka-nWLY^kYiJ zj{vX}C(wr%+l-hJWL;liTcz_87 z;1I`G0n1EQZZ7w~YqtMyFM$ShyXXdR9q_6NoL$#O61jgV}LQH@f z-(~Doi8DwtL!*_xC=t6T5nm&lfUkwQkG;=T2mBu^JKdB__f>tfW8wQN5+wk=wM|za z@Te`XtXu;mKmi7W>9hGLsx<%neVoEXG2ko{c7LVHeKb39Z|#%13a5&PJV`^_Pq z29!Cm{8P|AxSpHINn$zr`pHsjkRs2?8h}3@(XsHf<&asSL%2#^;0*Q)v^j1C$s}zy z4zsQx&toim0=u#ae&#O7NlPP0(3k?W865>h4>2~jnUkBFuaP;DH=sbrSI6OnhQh1r zeQ5z};p~-CO z*D27`J(TfX3yX@VT}(|2rytrGn`e#w$Eg8@ly``UXNp!7*IGqA6Ug7m<#UtUSXd}^ zu(oE~)${;MyKzkovmACv;Ux)BEfsp;krq>DpKx(9Qb3Wo@3Ra2wp!yB&H zdJv3T$d9sho#R%;HVzI7NsS!W$_2u1bt2*bJ;6@JGen!caGxpdyx_8l_nU$>DCezLz zb{2OM#}2W3fBhP9jTi~2Wct8W-8xw>QDnRZ=LA`nbi)oV0*hN=NVQ>iY@-#A=S2}T z<~m>YM0#$ngIFLfFrjXrp1w>CVc(Fu^$o5>iv_&{(|-&BZBx;4(xle0V@U?VJq$t0 zFC?NQz2$kY-8G}Bqt6p8GIFf&_#;`e$?}Tjss10X-a0DkuInF0LJ1L=p`?UiD3R{2 z0fug*k&s4dX#|HDdgzn}C8ebWhElp4X%LWBLh$`^UH9|5@Aa}=vljlroU_mF{n>k) zz^#GG3+`K(kifEffT@749*VIYjEUv{X7id=P;ff>@Wq}$&Al~l^OFBH^hd9L5z5a4 zV`M!Q#k`=ufBpIa^y(A(By)V#T56+MG4&7{sP#7Cn4K?C$+dG_iS0&lA9JgK-y|^7 z;DOw1n@OQAkGhR3$5Em;Z7QJ6=R0W{v-g!CfD!&!TFU!^^M!i<(vLLu=anWT35W*b zZ(`Gj6=bmm9u#%o-)4b~_#E3)c_g%bLj6yCU$M08sK2&*_inANf$Tv^vy;0DjT)dC z4iOFVqGoU6?75CTZQi_j!$eOnjf{-cW-119R9MI1g~ zbu<*orlv@7**KZlj0#B(tYr~E00((SMn+bwQ=b`{`RveH9 zboM|g*<~^WOB;XK&q}AZvvI3SVgM9AdY>gIErh_2t<@JhRG{qcW_+&uwsO1WoId;7A1iHTE9phl+FL%?4P_+S*U@6S&19Oz0S zUBmHRE{F)9jG9ezVR%sp?a@>)%7Sf?Xqu;2ne(s|Ute%KPv%H+ZC~iO-Z@be3NG zZ9MKhEe=V}5KOEaSX6NvXqTQW|6C7%&XA1foxjnMFc$-SzJGARcg`QTaRSp35&rUQ zaCCNm#9u}KmL#S-??_&cFw$>mEnon|Y1gOaio#S;?0NKt)LCBa5u0kR%N{&!wC+KC z7edb7GA9!J2E@3(89^9=IB#2Moc$uAY}cY{c0SW1(#EBf@6zkGIAIP40WB_&;UGRG zOm|FotOV>s{f@t$AJpD-`b;)_e;XZgDgoq<4}bg^pAqDzZuO)%biH}pU+)2|nDJ$+ zvmPxbjCJaN0qdQWWdhYlRVue^sf4ttsgF zJkooA-T8>Rfc0pO2_C9eqaY$k8 z?JHWsU(@57=Cp*rXC`)}v0?}X@Rc?u;fb?aj|{C(blKHn8u9G0Kho7EX6KJaHasPU zliT?1cA4Z5D~;OVRR4KS9=7u%)IuxCv{@VilGZ2hGvEfWHyDltjzrospFNi>O|6!M zcJO;_6%~xch}rX*7e^J6K_h#IF6a0@ z6tT;%-W7&6H_{;MYg*jk~4PP7lDZ+%CHr`@(;&OxyXE}gR zKpV`1jD_EmNMGW9#6JbEVQooHe+@KS2hb4^ru?QR!$R0IE-rUvpylA-K9)PasCAb& zvbDyW3o8JSS;VHxuSNaiNKP8?!1qOi5y+b3`Yz`obOv{x{Q!W&wXb+vrj$Obn(?_z z*!6*rE9O1!hMrnDl1L`C74cb!d-n;mrm;g{+kjvLQ;)JVvz z98@0dducel2jiH`rN|Yl+7;o=yU=kNMA$`He%Zq>lbG5AO?CmX-EGcI4Zm3L5 zP21{gYTWjK^-xy?6r4M@wzlU$q52Bwg$OV<&Xg%B6TsG2UwK{=r7}c#@>E2Pp$Dsx zsVL08db#dEmGU2k%0^35^TpS%UqMK6AZE^giHjy6ogQruiCTSl>cR+?6PK9LmW_WDVoh8aLQe?g zhf1qqXc^C-!=f4ll<>45ON?r8dRNF076f-(Tv}Z};UcL20sQ(cYseYV;u9pj_cIWs z3r%4dy#%%!rqMdO7onx9`vGqIbS~s{B*#^_d{CprCajRNa*`HUHYF~ z(gF`X#4Cb%h;xW{$kBcd!r?b7D5!6=6C0^eQON0;ni$yuX72$duYdZ5w6zoNB%jpy>HwMM3=Kfd{>Q*w55bK_V8bFN@0KbZ`u zYGc&fSbYuz5OKlUOp_XxM?ega_?P#%AT zd688c_@j;x_O}|8GCzS?bbTz_`pc48onVnb2|P@^(x2|K74Ywl=8x$>MqJvO>jG{S#zhB` zad4C{D1liUYF!qSmPUCOcHa5O++o0s<9v*$Y6&0+q})5*9;t;^KX4>&c{L_<3p8Vw zJ_}xIoS2wss6nXP!k-=T%W^iah`81Lpt?+nE;|s8-w7cXHnNa)9;oJ2bxPr_w%^NC zo1ffIC$*S?*vh$~nRiL%$*Jx1Kx3`&zDBDhea^A5T&TbS6EKiMNmW%>o;e+wmkhKi za7%G((9gVBcKJpK6pF5~a&q>@A(H0jcY)fB-0A7*ecZ>>j!;yXMH{nzSXdY$ZER_& z;zbGnM)cK{FN58S-yT5gcRYXjj4_0P5lE3le*i`<%zmSq^&zzA2HIKHc6XJCg#bg2 zhe!3y1+aAs-}!KJw1@}qCKkYN9s{!rY;0^E+I1pEly8TDqOd%k4ft7OzrUOYdFaSL znd-Y#*EbN*SJKF7-u?`KyT%~O(iHGo+Vk={HG#@}KO3u-ze?W_=Yr`5MN2uEs97z{ zOz)J`L_&T{04$q9tOVZcF{#`BM8eWcF@xXIHUra|Fw2bxh3Xf6Lb3e}`NeZ0eDb-J zxcau$o6wuXhzGMupm`U9BWP9t_7rL`k2R2b%?TK?=NeB?5E4pC*2t>aJRlj*Y2Olt{;rU@pp~V{odsH@{TeARNkPiE_nhqJ|43W@5ZWyZiKed&C*4`=Zn;h}viEbej<@iL=yF9DQ?Oz`BD z1QbnNr_wp>iVJ}sO7D^Gc_OO;B{n-gZV8jAudddDGXe~(u+Oh(eCJFs#Ovznrj1(S zK=ANC0fi7*R+(?ft*zn=DZq-62vTBPW?&&jF@Qj-J-?cIIENi-{C4^o5O~N+0=&p? zWh(}fqt8e8_GVZCHa;FiM)tBHu+eC=3n%x2nw;DQkC3n)7}APs$i&JDuX_XZ_Yki| z=H%qWKd-}#I`_+{14RKi_NZrrSc$i9eWFF;&V%a_0?+mIn5NmMd8b7s30z{3Bc!*3 zFYmZ(OOSk>gI9HrqfGApw{!iT8r{4!irSc7f@+2Ctd8C3bjGbcmcLa#_D4^irHZv% z8QT3JFtYPkaQS$!=TU#ym$FyT!Mh@UBUT$YJyAHQn5LRK5xrH`qwr0+&9Z6EivH$v zu&T-*9Q0k_2sk#5hL&wrbwJ5o_O4M3$TUw20qvW>B$|h_=QKIyw&D$H@(XGU?mMqg z#^-t`Ws(}J0=6)}NBeJ6?ZHYz?u}rUc`jU@dpKql(hLh;cJlaaI>PVnST;YRlmv2= z<^8Jlerw8X3IXY&OQ`sds)V&W9-4@$mh+FoKS zBu{e7q+!Br%lQMB%0p%#rNfzM8u|4p*8}c&x-TM%3@@!px$_#n*k?AXrMp1Y z?2J}~K`b=&O|DSBMXt>HLDxw@)%eko%k$oHgtfsU=p>Zw9d zKRXa{${GZhLnf5zOQcnI4-WKzadt3EJZMr_*nNig(xSmGm~(|6KEQFAiB;&6q#?oL znD*E;xEsU~4DmA7Wsh;65K9<9ypO)j%KQk%z&6CSBtbL6mC{(!gwhn#o~PNyf4pdp zw*YdEX%!Ymz=U8As%Q#Ov`}x5WBcTlo{^n*#@619u=#JMWcPaWIt%q}%7cKqxCg?_ zu|(CD_D6>t@mzx{dialR&LG(#eTcj24`tY#m7%P0x#=NJMNU@M0onn zGi>n>CR6#R1&=~sAH|n%7vz`pM77*wS{*pGrH4fDXNEML4jxBV<%Y|L= z9w?NQtx$FK;mPgEv3A&(>O|vV9j8+qeCO6@k)^Nd*%yoq+T1f^*gg15`}engTAUpp zYlVb_pnrO#Lr^(6Re)4d0)_{OMR~&Rz{ev)L&)l$CzICqQkal8Oe%0gJR~M-MuB%M zL^N`D+Eovt;;XhW7AYrR!qQl(LTca|1}Rh5RvkhZJO|X3^EEXa-CSe)SS0w@2?EBh zB@ANA>b#|#UtehaHb<{uH}7D#8KJv(9pzvKtqR-Q$merfX{s>E@3s)JaaPHioi$+M z;7~Iyf&1<5?=L1mI~|km=$2OT))Cj_upl>97X(fCZnVDE69rEP?=k`w(BTIzNYQ#8 zcYY+7n@dNba5C6~q%I#d0I^eK1k0CqaN&2#F(ai1JxGwG4|00m^&s{gnwSQR2N9Np;)qjNgVTjWX5K13U;IJ_-~PtWyT|q@p2{UKzBlAF%-!xLTNU?_@Ju`ne@80b+`SI!BY!}t zSZG;3__P=jkHK><1XG)#kTj2sVar2fZ0<*iUBi`9rz+V6=8$e+a4{2^hSTs>fDP2! zX)5u@v5P;Ot{qOK%E3zh)PS7L#HMP&OV2OTTY7YSciaTPqn-nB!)mXWFE`2a{g1;qt0TwXlyhJS}et=U>PR{ZG=nsz+6gCVci?r>TbJp+_$v0+-E5*UD6t~Lc zwO~=}|3$-FFTZf2u<%pC`6ZS`Ckkc}r|&x&LG-JC{R1z$nK&;QLW3l!skm z`Kn#Do$#B7^@DM96W;sJ`Xp`y>(D@ONy_0`T2|I0dhp|i_JkRI$tuxSEEI1LoC5tv zIxSwA>o_jV$?!{OJdifV`_mC|@o9Mrj=$~R4=CzS16gdldo|mU7ITVXW>9xoyBjfZ7wJd!%mc1BzTp}G>9l-v{pU}s(I?u+kF_S4 zrr&&~);v@B_VxK6{0Bpv0%a{+#7_Dj%5`AZ1f)T6)0N}Z^VA-)Z?a81zisHZKwTlzp4c+Vl@pLSe4}X*6ybf$+;o5`&u_}y z5yrIVJ9B8T_6-&90SrKHfulwn=A~7#5)wf2KwS&i2mO#zBbq(I&w8E!#{7Rz^v^@d zBRNKs!F_F^MIQlb(d^sLTqkIH1f23RsS5mD6|4Ye^^{uFnWAtXA z8`gxxAMK;Kov*IZS5}33bB-CRtD)mDgbj@8(3)T_6lMUi zOfM|;3kp$VM|vV0u?mx_Dp4tt6j&8N7*7pL~Y09RhTph=xV;{5NJog@6MX1$hz~Py^6kNr<06qKgB8M|uj_-{cR`paR8JrBjQg zr%}Y2TAtaDY|qj(E)=P?oaC*8?)rV`=qdrccdgD~;4%MKxb3JeNxpr)yNW#{ayH^Ia$mG8C})5p$v(432?3$tq8 z+c0xaZuLy{o}Czqo)CD0EE!rd`~SKj*b4V7>=f3u3=zfj_G%s#j+-F z@b9B>V*o1;xk`{!l|Vi*V%&Wn%augJ*2N<~5E^l)aDYLicQK54cQ!gOxGfH~R-31Jo3~20)obD*d!SYwV&da~3xx z`T3#vjoNAPcwkm?AW(j)#I~!fSv;WE*Cr zs|7eY)%W3jP2D0%cp&ZgFHOB7GYxa`p))N-@K4a=7fda=5wX@3&cG`;N$=e+RO#2H zfGfkf`UeK|x=u3G6oFM_1uzOM+K+AWeI7!)umeKTN@IqZ`Il}p9Epkt3CWIYD?g`m znx-lY4&Ae3*8}Uz)?EDJJEgRC-f zcJ*P&ubtl&r)CaOb)a)P8OMz!)Niu5+7PSn2DH_O;68o%NCZ*o3EE~2tzd6iArY>c zF6~HCXeXU!VCNt9N{v=1@fgCQ`tM6~i@oOzURws%@X1>PrOZ}hX3fv?48%AxeioQ5 zrDJp$JZJXy_R6wu8K5*nM>md)I-2n%QE4~yQ`%v=YMQ7)z9zp*s&0Teva1M!-MdeV%p=t zIX+ZX3Eef_deIopi6|SWXAjgn#TtKKeC>;E6ua@hj)p)lFrXW;|6PrJ04&A z>_l(C9?a)pkEgqg{Gh)KKjLmMJQWS&8Ur7S>(deR^&A(e-V5$hcn>7V=sjjtz>WtZ zl8it$R+V0&+br$#NkLl z0tk--+pilQ8X{A|gR4fUDC>ocKUIC-)V5VnL_nSzUi!s>J~h8KrGSlN3v7|jO!Op3 zKt%Q{2@q^TN}q)4XMyW9zKC=+b%}HXVt{T!i%j8zgtq~#Li8bEsMS&47uI+O&9xe- z@d?_`pQpAL70C}~17KIJ&wZ_t1J8JT$+P)eXrMgyVjtRNI6u;Efdf~W=g1#8KF0@L zJWK@Yxc*V6v8QpapEOWkyj@jj{vZ8;fMD%s+ViuomoFw{*Ug>a@dl1n$8(-CYdW&f zcxQoln!IF2YfE$nWp&CKhJ}nW35Wibl@#1|Tv8_0O-fM($2>Q)M+4QqZ(pPP2S;nO z7Ix8CIDSvCQHq^AMM|xV$-FSRi)Gj0cH5qFU57v8!s5pl_bLL?c$T^~Z0tu7G{mC& zMMwdQHCKjrxkV9-7P1W+26TVYdul|{2g3hyNg6HN{Cwm3=S1${rAfg4`l3e~8yX_B zr{|OK(37^-)~sx7Y_gOQ(HdHRbl6pY`e1D_Zc=c>6VhdwZr^v4c@|_8ehGuc4onDX zMvwqyV9Vbk^A5RRA~$ebpU)X#twjh*U@Fo4pBMnegiL(UFK5BeM^{Hk-kLSrpqpc*Qn3nID8?^H4{?CKg ziGM(kAAf9^9mNIV5AzKR4U5g^<#OQn%4J^EB&P_slK)o_wxlCLDaI+AR4=Rd{K!df zP${|iAVEA(BM#n}kyTqon4AdYM+4;;-Q_DPr3>OA8~=n9{0`}@NZVHg#20TaMK2C( zY^PaQFi&{MGS=FS`t0lft2H_5er8h<~~DwA+WR-enb%shM4u&d1-z>eNq> zv)YNqcKKu9W>z9-w^>UadT9VEP^CZNoH(30SU-0R(^JYhKl@_odDuNdqGE+x zl8|Th4Kzj;n0VCi`L~*w)iCG2v0Dzntmx3^9(DakJ$k96#kVYJEh_GdWTcf)h_#QsC|;t>N>AeB$_JG(W8BUQG9BLj4}_O zgtgGJM5GY6Zaku;Ktz^ThxO+4Ef&hs;ZuX2_UTTmAiM{^?qiZ}7lQpO8iC74iom*uz}IwWLt% zOv32!0#dx_Cslg?tI1^5P~4`TRu}<31r|*gF@f<_YCHBvNoiHd5F$vkasR#p= z*qxY53=ZpnFxBLd^2Tl;e)zgy6k+QmAF;+&p=DVFmph(#qv-cH!4q9g(@*;H5gK`p z#)Fz>ss6#ON37%7;o;$-^z@0$KzIq;!`q*%EG-4ugN{m9%v3Q)xJ!Y$Wl?=ZKX)_f-*w*WU%;Y?xxh)mG;03g0E}#BQ~1 zRpRRgE}P^?hUi#>Wt#W#tq|z)AnBRDcO5^>J1V)11KR@`_MZza7x7C z5)VkdHUyZ}@oek0tYrcyL)+=>>rc{M4o=aVZYG7v@&uJ-#HO8W#hd_i3@GwBGb(;R z#mZqZ7;)n$a8@!jOuu{6iH4rB;`Z#Zs>nCqP!uK!hrVe-hKYSzZH+MmU0I#7Hm zMs0BXdwOH1JNdiG?^mR z(0KpoI`{OP!dGfvvB!DiGc34NrRinH?*qkArhMA)a~g*UCUUV3D~203EH3*0Xg!ut zyru-4BD}V{qNYyfd!O|7M?ji1s3y%Gk?86MhMFk=36sCWr{C?jve`CrS_-9{ZRc$J4IgH$h=?d9aweRtdah8Y4PR!Xn0?xSCHLa0o?}vA0X*gqGU2Oj4VE-u z++UyX@1-~I>SrM?9EjKY3DVN_|ejeIlJ;?kuW{;v_PL%-UZZtseI)l_>69^I7#iJeA05CTB^bCwYGb zx_v)Y6%`nOn}KonL!SiSqK(Tk`MY;fO2{Xl5B*jik6Sf2`Vmhhd>QBuiNRU}LoI{R z35zy0j@}PknSa5{C*hHd+FcPIS}<|{4;Lc^EzCPXLZPqNf19G2;%(Qw*0&*ZwB8IH{fBwvjEiq zr+`PRtg=M8=S@G{A9djda9m}a)#Eq{ga|`~tqJ=`IzQhYS+Ikl@toSFPc=(l;l!$Q z8@BuVh%4h4t0HDr?D+Fm2&nGc!*chm-LAIgs?MlaTe5N_EL1v|>3D#i7Z7j7^+fmoj>XV)TWba{t~ zfkY*hJxW67Ma_qX?dJFXeI-^3Tl0;bNX45%J!)`b2^==#R3{jW=*1v7&CImNZ?=@f zLMGoBe;qqE1x+xjLP^S}cG4plLs;{)DaSZS_^>UyKj@mO%Vq2pN~$}E^Hlns)FJ7Q zaI3aIcxV4Y)73Z|h^1fK5UcVFtiriXW#1+2h{n!(Adj=HbzH-oIF!oQX88(IFTzV^ z{TYEXefPf)UX@#%w1cGa=drV#NeiV3l00JZz<+n^{EF7+2^!qFffobj*BUSK+XBvPODdj}Qv$oOSP;m)}L6r=B@O)rLk-7(7)fc?tCa3wjfj^g%ySP`j zf~~jA5zJ?PGTroqp&ek*BhZx&EfmTb~57a#WnsF~&??n_XoA;Lg z&f%QjloGh?PuB<=4N9d+kcwIPDJ9`h@po`EblLoV>$*gy@y6}V5a#d8)Mb|tdWb~Q;qFQQ%JDzK=s=i|U+>g3_`cH4j6@Pa-Z zH0_6yi)w!y2lZH!@t9IzF_dbM+hhY_8Kv#wiOO62%8Yp_lT%#tT32CH8X_)XDXUC_ z;*5sJFevE%+3w=RV;rYUS7=@cttyzIovU|)X-%P#^cfKv84*38ycb%wxV&LZT1E~hd+q4Q z-s{<_c9d1eABD=(HR{XZ4nHCfvmJ>^A4yxpsmY4gPpQ-1&m-HhZaIg1gFfxQUu=g8 zU5Wkmm6*AlDGSI7YCFj|T~p!8#E@+}`O40W@IIeN;qWpuwvy`K5N2{&Ay8$giE~&aw0w?vjsq@DNX$P7mZuxx@0hNULv! zs(fvX5ls8ETrCi~6=%Zi)2>M5+;!vCDh0q{Y;-GoEM%fBc2!3B1WUR zV{v$OzBhEwmRNg7_kqOO!w~!;#xnsoxS|xrBIoBEGe%s7BuNEsCI~Jdci378$-Wns zFkyhNNogv7I7(y-hji;_{e-1Xk_oS}fv>jz5wUoFD%|2mJMqE^@-D~MfPhuzja^8W zsAD_6^00h&A78XhrbO7Tju=7O35|T)ODG~DqKCis0N8tC`|}Bc*!Mu@-Tm~wvA&5u zLjA3~FO>>--x*0m$avvR5U@Upjroep}S}*(`;!pkz`XVAZvn;kv z0E&YG9Ic}jmhSz@OVy9>o`p|m_81%J#yBGx)JclmyfNxW;+YocK$(kG_quK-N`_IsA&0lQ7?szxE5O$t~|K&wCX8<#X*DS*3^Em9g@{00=CO0 z1Z1jF^J(qINc@wYC7Ggy7+*y<8U#+XLY}Av9iba9dNDCDzW9isD>yUmL`a&K={@&q z=k3k)`aOB>09h;?Q^&lsw>r-sexNVKwVTzJ z6Hld3NcX9juEuRN(w$6oM{1RoZRsCD!BFbXeQ_S6o{8Iqmn}?E-ip@;2K-vVJ*SYj zvhQ#NfSp{xGC!VBbstSCp)6&W1^N(50f(d}GZ$ymoV>V_IO_7DvF(Bpc7w!%5g`K< z*bHEGE^!eEz~yDGUBNnfuHco%{}8(H?-S?PcIVWg{ba#}(Q{(aOL{t`hjtc^uD8!# zK?tt+Q8iuaNCE0jSm=vLShKX{8QWTf3#F?l4>}#U54^g~!%M1$U{S-?VIIE^mdNi! zqjx-zCy{Yg`zzWuzuVQewxIg1J06d+$Me|LN+X$Ks{#ipR<<$;fCFrMeuXuk(+J?P zbg_%^E5l=0L*gM3Y$3V%f+5#9GkBHMu?%;LI4^JHP}c&cm&lk#7wrC6RkuDx?r(W< zWe1*y%NU$BZ83j>+~Y@AhJa&&{xV`6FMBc<_=>5B{N?EOMUR-1#6opb=WJ07pVS)DQ|FQ9POF&N~)=PXal5kQHh0 z3|uD*$@@0#^Ag>~hkuuq5r_|D-s{HUMOLZ-qf&iA;FeBVVt1PXBqrg#MuacfxI-Eicdru_9MYiN4HrS|({dI_*P-i4{_H1r&NM-$~ zL0(sNh%W)NJfJsN^p}@e9WOQ|$OAohb`w6Od}yaSNXR4X$1t2+ks{tDCUMD&6_PB2+o3_iqom`a4ONkY}#NA)OVRP*%9 z4J?W!SYv3kadIXWaDI}%FR4QzDx=bFY^>(^3uDrwiOuU#UXd_WXDp=0 zn5@H}+%TIgT0I4_5?q!LyL@f#F#(eB=-u}QeHO(kXR1a$t{x^7=`$_&t6@qhPMPBZ zH)95g_y!gV%gN6|2Wrv!gYSBOCj&_IkH*wB}5vMx-tmJ#i zSHv*vJ-4&VB4bn{XY>bqn$F*M^v1wIfTn61ZilAWaC|YUbO=1Ax_j}mtq;Q|Y$_^C$Q3v%5H56Ci{q}ZH|8CWJ`pO!$6%-dKH>{W0YZF8EEEiD4v`AjMg{Y zFeu+q(}VWwmQuRSiJ}LNqK2zi>&D5@eGH7Z1QNycAsD59+dpz<;a0UMQD|cS#XgoL z*2#)J5(puNT(FoDqne+V6z{oSxc_r{j6Ae`bz?_-otqU9ef)dw$Y3P&sxj%ZX=t$X z26b$>%ZPNXe)soWhDElHWQK~^>&w)93Uw~bBx;6ys?g+RnrCF2OQ@)w> z`fx%vZ5))GsNNFG`Mh4m@W%lU%J$)Z&I$N}P2eK^xxvX~>VMeX#y41ObE*rRJd2Si?;7tPnq(P(W|0 z-l2szyy{;*Mu2S7%>z9)8QbYTWhPT6cC!o3e?18i8|rLT=NtWaKa*fSQnj7kH=-*1 zQeGlq#CPM+Fr-l-C42Esj62m+shwSn*l4(l7Dylnl z0YNOi%qqIAhc65G24QiRK0`x@xJfb^V-=7N(<83W=KLY6)I@hDaH*7yoans1a~?1N z_PLf+TbnI?!c?r3WSx%n``k(_4a-|1f=K_>4nJ~Teknf>jh^KPX1(_IJo?p#od+Xi zL}p-v1R|-1!zYNCV>djLM1tB)uOzPw(@#^y9OzVbu1ga|Jt~wwg;|}L);N4QP+}L> zf9S|jMzAn&D5XbFsht?nv-1PGy`9U~4Ds@9FI3YDK*^LTFJIKYSU%?6g%PLS~S z)T{Q`IJN!J-@e*_j9FX86_U{ai5doW#X2Q1y)|4NrvEtr0))pheY+T?k(ibxt|m+L z{hZ#6XoF(Ak(kSKv-jfXzy$Q{Z3nm4_Z-fI4|NU&4eWxVGdv}anD_5xZ$z{X-yL zFZjn85Vq-%6WK8loMa4)|4D)9bWcEi6^MT7Jbx7pxO}Rts8>r0Q)Pnh11=KBb35gG zgKlPm^!CWI+0_>Y_G*Kao`-6VfUQz9B2k(&nwc0R_o zM$22fDUB922bGUsx0LnG{yed0XmN^I=luF6Yuu#Ibg`-nqt8@y)-{#TbCHbE>CM!K z$c5O3rFyW_n)M<}v;oemsfKL>h^#`w3|gfbw1^MZuH}jy4-+nB_q;Ca%?sS16Uu z#2NFasR9m~Y$=~l#_#iP@1}Tq(e^d+N`Z)&?%#ZNJ#mcd13g9>Kw3i>CG{W;Ay%>{ zx_G9cHJ@Jdfott1^IfA9IH_9f&OqiJJb)5vyD{j{(y7$@`D6inOym^lEk@Yi_+IISM9bGH-oh-600Jw zrPf7jZx4Q~r+=|Lr(9SNp8EAw-{>~4f+Qf;zv92dBUYXas~539=mkt%3pVqA!?#A>Bomx47kr2g79%a56uuuB= z_V$Ql{DTFgcxV2s+mkg39?K*d(EXD6�zlcgF%^Ih?BeJ8rQupwu!xf{erKg?OSA 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 diff --git a/doc/images/ml_map.svg b/doc/images/ml_map.svg new file mode 100644 index 0000000000000..629b4c91172fb --- /dev/null +++ b/doc/images/ml_map.svg @@ -0,0 +1,4 @@ + + + +
    START
    START
    >50
    samples
    >50...
    get
    more
    data
    get...
    NO
    NO
    predicting a
    category
    predicting...
    YES
    YES
    do you have
    labeled
    data
    do you hav...
    YES
    YES
    predicting a
    quantity
    predicting...
    NO
    NO
    just
    looking
    just...
    NO
    NO
    predicting
    structure
    predicting...
    NO
    NO
    tough
    luck
    tough...
    <100K
    samples
    <100K...
    YES
    YES
    SGD
    Classifier
    SGD...
    NO
    NO
    Linear
    SVC
    Linear...
    YES
    YES
    text
    data
    text...
    😭
    😭
    Kernel
    Approximation
    Kernel...
    😭
    😭
    KNeighbors
    Classifier
    KNeighbors...
    NO
    NO
    SVC
    SVC
    Ensemble
    Classifiers
    Ensemble...
    😭
    😭
    Naive
    Bayes
    Naive...
    YES
    YES
    classification
    classification
    number of
    categories
    known
    number of...
    NO
    NO
    <10K
    samples
    <10K...
    <10K
    samples
    <10K...
    NO
    NO
    NO
    NO
    YES
    YES
    MeanShift
    MeanShift
    VBGMM
    VBGMM
    YES
    YES
    MiniBatch
    KMeans
    MiniBatch...
    NO
    NO
    clustering
    clustering
    KMeans
    KMeans
    YES
    YES
    Spectral
    Clustering
    Spectral...
    GMM
    GMM
    😭
    😭
    <100K
    samples
    <100K...
    YES
    YES
    few features
    should be
    important
    few features...
    YES
    YES
    SGD
    Regressor
    SGD...
    NO
    NO
    Lasso
    Lasso
    ElasticNet
    ElasticNet
    YES
    YES
    RidgeRegression
    RidgeRegression
    SVR(kernel="linear")
    SVR(kernel="linea...
    NO
    NO
    SVR(kernel="rbf")
    SVR(kernel="rbf...
    Ensemble
    Regressors
    Ensemble...
    😭
    😭
    regression
    regression
    Ramdomized
    PCA
    Ramdomized...
    YES
    YES
    <10K
    samples
    <10K...
    😭
    😭
    Kernel
    Approximation
    Kernel...
    NO
    NO
    IsoMap
    IsoMap
    Spectral
    Embedding
    Spectral...
    YES
    YES
    LLE
    LLE
    😭
    😭
    dimensionality
    reduction
    dimensionality...
    scikit-learn
    algorithm cheat sheet
    scikit-learn...
    Text is not SVG - cannot display
    diff --git a/doc/includes/big_toc_css.rst b/doc/includes/big_toc_css.rst deleted file mode 100644 index a8ba83e99c5b8..0000000000000 --- a/doc/includes/big_toc_css.rst +++ /dev/null @@ -1,40 +0,0 @@ -.. - File to ..include in a document with a big table of content, to give - it 'style' - -.. raw:: html - - - - - diff --git a/doc/includes/bigger_toc_css.rst b/doc/includes/bigger_toc_css.rst deleted file mode 100644 index d866bd145d883..0000000000000 --- a/doc/includes/bigger_toc_css.rst +++ /dev/null @@ -1,60 +0,0 @@ -.. - File to ..include in a document with a very big table of content, to - give it 'style' - -.. raw:: html - - - - - diff --git a/doc/index.rst.template b/doc/index.rst.template new file mode 100644 index 0000000000000..df058f5fb6185 --- /dev/null +++ b/doc/index.rst.template @@ -0,0 +1,25 @@ +.. title:: Index + +.. Define the overall structure, that affects the prev-next buttons and the order + of the sections in the top navbar. + +.. toctree:: + :hidden: + :maxdepth: 2 + + Install + user_guide + API + auto_examples/index + Community + getting_started + Tutorials + whats_new + Glossary + Development <{{ development_link }}> + FAQ + support + related_projects + roadmap + Governance + about diff --git a/doc/inspection.rst b/doc/inspection.rst index 57c1cfc3275e8..95d121ec10d7d 100644 --- a/doc/inspection.rst +++ b/doc/inspection.rst @@ -1,9 +1,3 @@ -.. Places parent toc into the sidebar - -:parenttoc: True - -.. include:: includes/big_toc_css.rst - .. _inspection: Inspection @@ -21,9 +15,9 @@ predictions from a model and what affects them. This can be used to evaluate assumptions and biases of a model, design a better model, or to diagnose issues with model performance. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_inspection_plot_linear_model_coefficient_interpretation.py` +* :ref:`sphx_glr_auto_examples_inspection_plot_linear_model_coefficient_interpretation.py` .. toctree:: diff --git a/doc/install.rst b/doc/install.rst index 89851171f4588..be924b012ce65 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -6,21 +6,21 @@ Installing scikit-learn There are different ways to install scikit-learn: - * :ref:`Install the latest official release `. This - is the best approach for most users. It will provide a stable version - and pre-built packages are available for most platforms. +* :ref:`Install the latest official release `. This + is the best approach for most users. It will provide a stable version + and pre-built packages are available for most platforms. - * Install the version of scikit-learn provided by your - :ref:`operating system or Python distribution `. - This is a quick option for those who have operating systems or Python - distributions that distribute scikit-learn. - It might not provide the latest release version. +* Install the version of scikit-learn provided by your + :ref:`operating system or Python distribution `. + This is a quick option for those who have operating systems or Python + distributions that distribute scikit-learn. + It might not provide the latest release version. - * :ref:`Building the package from source - `. This is best for users who want the - latest-and-greatest features and aren't afraid of running - brand-new code. This is also needed for users who wish to contribute to the - project. +* :ref:`Building the package from source + `. This is best for users who want the + latest-and-greatest features and aren't afraid of running + brand-new code. This is also needed for users who wish to contribute to the + project. .. _install_official_release: @@ -28,117 +28,132 @@ There are different ways to install scikit-learn: Installing the latest release ============================= -.. This quickstart installation is a hack of the awesome - https://spacy.io/usage/#quickstart page. - See the original javascript implementation - https://github.com/ines/quickstart - - -.. raw:: html - -
    - Operating System - - - - - -
    - Packager - - - -
    - - - - -.. raw:: html - -
    - Install the 64bit version of Python 3, for instance from https://www.python.org.Install Python 3 using homebrew (brew install python) or by manually installing the package from https://www.python.org.Install python3 and python3-pip using the package manager of the Linux Distribution.Install conda using the Anaconda or miniconda - installers or the miniforge installers - (no administrator permission required for any of those). -
    - -Then run: - -.. raw:: html - -
    -
    pip3 install -U scikit-learn
    - -
    pip install -U scikit-learn
    - -
    pip install -U scikit-learn
    - -
    python3 -m venv sklearn-venv
    -  source sklearn-venv/bin/activate
    -  pip3 install -U scikit-learn
    - -
    python -m venv sklearn-venv
    -  sklearn-venv\Scripts\activate
    -  pip install -U scikit-learn
    - -
    python -m venv sklearn-venv
    -  source sklearn-venv/bin/activate
    -  pip install -U scikit-learn
    - -
    conda create -n sklearn-env -c conda-forge scikit-learn
    -  conda activate sklearn-env
    -
    - -In order to check your installation you can use - -.. raw:: html - -
    -
    python3 -m pip show scikit-learn  # to see which version and where scikit-learn is installed
    -  python3 -m pip freeze  # to see all packages installed in the active virtualenv
    -  python3 -c "import sklearn; sklearn.show_versions()"
    - -
    python -m pip show scikit-learn  # to see which version and where scikit-learn is installed
    -  python -m pip freeze  # to see all packages installed in the active virtualenv
    -  python -c "import sklearn; sklearn.show_versions()"
    - -
    python -m pip show scikit-learn  # to see which version and where scikit-learn is installed
    -  python -m pip freeze  # to see all packages installed in the active virtualenv
    -  python -c "import sklearn; sklearn.show_versions()"
    - -
    python -m pip show scikit-learn  # to see which version and where scikit-learn is installed
    -  python -m pip freeze  # to see all packages installed in the active virtualenv
    -  python -c "import sklearn; sklearn.show_versions()"
    - -
    conda list scikit-learn  # to see which scikit-learn version is installed
    -  conda list  # to see all packages installed in the active conda environment
    -  python -c "import sklearn; sklearn.show_versions()"
    -
    - -Note that in order to avoid potential conflicts with other packages it is -strongly recommended to use a `virtual environment (venv) -`_ or a `conda environment -`_. - -Using such an isolated environment makes it possible to install a specific -version of scikit-learn with pip or conda and its dependencies independently of -any previously installed Python packages. In particular under Linux is it -discouraged to install pip packages alongside the packages managed by the +.. `scss/install.scss` overrides some default sphinx-design styling for the tabs + +.. div:: install-instructions + + .. tab-set:: + + .. tab-item:: pip + :class-label: tab-6 + :sync: packager-pip + + .. tab-set:: + + .. tab-item:: Windows + :class-label: tab-4 + :sync: os-windows + + Install the 64-bit version of Python 3, for instance from the + `official website `__. + + Now create a `virtual environment (venv) + `_ and install scikit-learn. + Note that the virtual environment is optional but strongly recommended, in + order to avoid potential conflicts with other packages. + + .. prompt:: powershell + + python -m venv sklearn-env + sklearn-env\Scripts\activate # activate + pip install -U scikit-learn + + In order to check your installation, you can use: + + .. prompt:: powershell + + python -m pip show scikit-learn # show scikit-learn version and location + python -m pip freeze # show all installed packages in the environment + python -c "import sklearn; sklearn.show_versions()" + + .. tab-item:: macOS + :class-label: tab-4 + :sync: os-macos + + Install Python 3 using `homebrew `_ (`brew install python`) + or by manually installing the package from the `official website + `__. + + Now create a `virtual environment (venv) + `_ and install scikit-learn. + Note that the virtual environment is optional but strongly recommended, in + order to avoid potential conflicts with other packges. + + .. prompt:: bash + + python -m venv sklearn-env + source sklearn-env/bin/activate # activate + pip install -U scikit-learn + + In order to check your installation, you can use: + + .. prompt:: bash + + python -m pip show scikit-learn # show scikit-learn version and location + python -m pip freeze # show all installed packages in the environment + python -c "import sklearn; sklearn.show_versions()" + + .. tab-item:: Linux + :class-label: tab-4 + :sync: os-linux + + Python 3 is usually installed by default on most Linux distributions. To + check if you have it installed, try: + + .. prompt:: bash + + python3 --version + pip3 --version + + If you don't have Python 3 installed, please install `python3` and + `python3-pip` from your distribution's package manager. + + Now create a `virtual environment (venv) + `_ and install scikit-learn. + Note that the virtual environment is optional but strongly recommended, in + order to avoid potential conflicts with other packages. + + .. prompt:: bash + + python3 -m venv sklearn-env + source sklearn-env/bin/activate # activate + pip3 install -U scikit-learn + + In order to check your installation, you can use: + + .. prompt:: bash + + python3 -m pip show scikit-learn # show scikit-learn version and location + python3 -m pip freeze # show all installed packages in the environment + python3 -c "import sklearn; sklearn.show_versions()" + + .. tab-item:: conda + :class-label: tab-6 + :sync: packager-conda + + Install conda using the `Anaconda or miniconda installers + `__ + or the `miniforge installers + `__ (no administrator + permission required for any of those). Then run: + + .. prompt:: bash + + conda create -n sklearn-env -c conda-forge scikit-learn + conda activate sklearn-env + + In order to check your installation, you can use: + + .. prompt:: bash + + conda list scikit-learn # show scikit-learn version and location + conda list # show all installed packages in the environment + python -c "import sklearn; sklearn.show_versions()" + +Using an isolated environment such as pip venv or conda makes it possible to +install a specific version of scikit-learn with pip or conda and its dependencies +independently of any previously installed Python packages. In particular under Linux +it is discouraged to install pip packages alongside the packages managed by the package manager of the distribution (apt, dnf, pacman...). Note that you should always remember to activate the environment of your choice @@ -150,11 +165,10 @@ and NumPy and SciPy are not recompiled from source, which can happen when using particular configurations of operating system and hardware (such as Linux on a Raspberry Pi). - -Scikit-learn plotting capabilities (i.e., functions start with "plot\_" -and classes end with "Display") require Matplotlib. The examples require +Scikit-learn plotting capabilities (i.e., functions starting with `plot\_` +and classes ending with `Display`) require Matplotlib. The examples require Matplotlib and some examples require scikit-image, pandas, or seaborn. The -minimum version of Scikit-learn dependencies are listed below along with its +minimum version of scikit-learn dependencies are listed below along with its purpose. .. include:: min_dependency_table.rst @@ -164,12 +178,11 @@ purpose. Scikit-learn 0.20 was the last version to support Python 2.7 and Python 3.4. Scikit-learn 0.21 supported Python 3.5-3.7. Scikit-learn 0.22 supported Python 3.5-3.8. - Scikit-learn 0.23 - 0.24 require Python 3.6 or newer. + Scikit-learn 0.23-0.24 required Python 3.6 or newer. Scikit-learn 1.0 supported Python 3.7-3.10. Scikit-learn 1.1, 1.2 and 1.3 support Python 3.8-3.12 Scikit-learn 1.4 requires Python 3.9 or newer. - .. _install_by_distribution: Third party distributions of scikit-learn @@ -193,7 +206,7 @@ Alpine Linux's package is provided through the `official repositories ``py3-scikit-learn`` for Python. It can be installed by typing the following command: -.. prompt:: bash $ +.. prompt:: bash sudo apk add py3-scikit-learn @@ -206,7 +219,7 @@ Arch Linux's package is provided through the `official repositories ``python-scikit-learn`` for Python. It can be installed by typing the following command: -.. prompt:: bash $ +.. prompt:: bash sudo pacman -S python-scikit-learn @@ -221,7 +234,7 @@ Note that scikit-learn requires Python 3, hence the need to use the `python3-` suffixed package names. Packages can be installed using ``apt-get``: -.. prompt:: bash $ +.. prompt:: bash sudo apt-get install python3-sklearn python3-sklearn-lib python3-sklearn-doc @@ -233,7 +246,7 @@ The Fedora package is called ``python3-scikit-learn`` for the python 3 version, the only one available in Fedora. It can be installed using ``dnf``: -.. prompt:: bash $ +.. prompt:: bash sudo dnf install python3-scikit-learn @@ -241,10 +254,8 @@ It can be installed using ``dnf``: NetBSD ------ -scikit-learn is available via `pkgsrc-wip -`_: - - https://pkgsrc.se/math/py-scikit-learn +scikit-learn is available via `pkgsrc-wip `_: +https://pkgsrc.se/math/py-scikit-learn MacPorts for Mac OSX @@ -255,7 +266,7 @@ where ``XY`` denotes the Python version. It can be installed by typing the following command: -.. prompt:: bash $ +.. prompt:: bash sudo port install py39-scikit-learn @@ -277,7 +288,7 @@ Intel Extension for Scikit-learn Intel maintains an optimized x86_64 package, available in PyPI (via `pip`), and in the `main`, `conda-forge` and `intel` conda channels: -.. prompt:: bash $ +.. prompt:: bash conda install scikit-learn-intelex @@ -303,7 +314,7 @@ with `scikit-learn-intelex`, please report the issue on their WinPython for Windows ------------------------ +--------------------- The `WinPython `_ project distributes scikit-learn as an additional plugin. @@ -312,6 +323,10 @@ scikit-learn as an additional plugin. Troubleshooting =============== +If you encounter unexpected failures when installing scikit-learn, you may submit +an issue to the `issue tracker `_. +Before that, please also make sure to check the following common issues. + .. _windows_longpath: Error caused by file path length limit on Windows @@ -341,6 +356,6 @@ using the ``regedit`` tool: #. Reinstall scikit-learn (ignoring the previous broken installation): -.. prompt:: bash $ + .. prompt:: powershell - pip install --exists-action=i scikit-learn + pip install --exists-action=i scikit-learn diff --git a/doc/js/scripts/api-search.js b/doc/js/scripts/api-search.js new file mode 100644 index 0000000000000..2148e0c429aaa --- /dev/null +++ b/doc/js/scripts/api-search.js @@ -0,0 +1,12 @@ +/** + * This script is for initializing the search table on the API index page. See + * DataTables documentation for more information: https://datatables.net/ + */ + +document.addEventListener("DOMContentLoaded", function () { + new DataTable("table.apisearch-table", { + order: [], // Keep original order + lengthMenu: [10, 25, 50, 100, { label: "All", value: -1 }], + pageLength: -1, // Show all entries by default + }); +}); diff --git a/doc/js/scripts/dropdown.js b/doc/js/scripts/dropdown.js new file mode 100644 index 0000000000000..ec2e6d9419a28 --- /dev/null +++ b/doc/js/scripts/dropdown.js @@ -0,0 +1,61 @@ +/** + * This script is used to add the functionality of collapsing/expanding all dropdowns + * on the page to the sphinx-design dropdowns. This is because some browsers cannot + * search into collapsed
    (such as Firefox). + * + * The reason why the buttons are added to the page with JS (dynamic) instead of with + * sphinx (static) is that the button will not work without JS activated, so we do not + * want them to show up in that case. + */ + +function addToggleAllButtons() { + // Get all sphinx-design dropdowns + const allDropdowns = document.querySelectorAll("details.sd-dropdown"); + + function collapseAll() { + // Function to collapse all dropdowns on the page + console.log("[SK] Collapsing all dropdowns..."); + allDropdowns.forEach((dropdown) => { + dropdown.removeAttribute("open"); + }); + } + + function expandAll() { + // Function to expand all dropdowns on the page + console.log("[SK] Expanding all dropdowns..."); + allDropdowns.forEach((dropdown) => { + dropdown.setAttribute("open", ""); + }); + } + + const buttonConfigs = new Map([ + ["up", { desc: "Collapse", action: collapseAll }], + ["down", { desc: "Expand", action: expandAll }], + ]); + + allDropdowns.forEach((dropdown) => { + // Get the summary element of the dropdown, where we will place the buttons + const summaryTitle = dropdown.querySelector("summary.sd-summary-title"); + for (const [direction, config] of buttonConfigs) { + // Button with icon inside + var newButton = document.createElement("button"); + var newIcon = document.createElement("i"); + newIcon.classList.add("fa-solid", `fa-angles-${direction}`); + newButton.appendChild(newIcon); + // Class for styling; `sd-summary-up/down` is implemented by sphinx-design; + // `sk-toggle-all` is implemented by us + newButton.classList.add(`sd-summary-${direction}`, `sk-toggle-all`); + // Bootstrap tooltip configurations + newButton.setAttribute("data-bs-toggle", "tooltip"); + newButton.setAttribute("data-bs-placement", "top"); + newButton.setAttribute("data-bs-offset", "0,10"); + newButton.setAttribute("data-bs-title", `${config.desc} all dropdowns`); + // Assign the collapse/expand action to the button + newButton.onclick = config.action; + // Append the button to the summary element + summaryTitle.appendChild(newButton); + } + }); +} + +document.addEventListener("DOMContentLoaded", addToggleAllButtons); diff --git a/doc/js/scripts/vendor/svg-pan-zoom.min.js b/doc/js/scripts/vendor/svg-pan-zoom.min.js new file mode 100644 index 0000000000000..bde44a689bfe1 --- /dev/null +++ b/doc/js/scripts/vendor/svg-pan-zoom.min.js @@ -0,0 +1,31 @@ +/** + * svg-pan-zoom v3.6.2 + * + * https://github.com/bumbu/svg-pan-zoom + * + * Copyright 2009-2010 Andrea Leofreddi + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ +!function s(r,a,l){function u(e,t){if(!a[e]){if(!r[e]){var o="function"==typeof require&&require;if(!t&&o)return o(e,!0);if(h)return h(e,!0);var n=new Error("Cannot find module '"+e+"'");throw n.code="MODULE_NOT_FOUND",n}var i=a[e]={exports:{}};r[e][0].call(i.exports,function(t){return u(r[e][1][t]||t)},i,i.exports,s,r,a,l)}return a[e].exports}for(var h="function"==typeof require&&require,t=0;tthis.options.maxZoom*n.zoom&&(t=this.options.maxZoom*n.zoom/this.getZoom());var i=this.viewport.getCTM(),s=e.matrixTransform(i.inverse()),r=this.svg.createSVGMatrix().translate(s.x,s.y).scale(t).translate(-s.x,-s.y),a=i.multiply(r);a.a!==i.a&&this.viewport.setCTM(a)},i.prototype.zoom=function(t,e){this.zoomAtPoint(t,a.getSvgCenterPoint(this.svg,this.width,this.height),e)},i.prototype.publicZoom=function(t,e){e&&(t=this.computeFromRelativeZoom(t)),this.zoom(t,e)},i.prototype.publicZoomAtPoint=function(t,e,o){if(o&&(t=this.computeFromRelativeZoom(t)),"SVGPoint"!==r.getType(e)){if(!("x"in e&&"y"in e))throw new Error("Given point is invalid");e=a.createSVGPoint(this.svg,e.x,e.y)}this.zoomAtPoint(t,e,o)},i.prototype.getZoom=function(){return this.viewport.getZoom()},i.prototype.getRelativeZoom=function(){return this.viewport.getRelativeZoom()},i.prototype.computeFromRelativeZoom=function(t){return t*this.viewport.getOriginalState().zoom},i.prototype.resetZoom=function(){var t=this.viewport.getOriginalState();this.zoom(t.zoom,!0)},i.prototype.resetPan=function(){this.pan(this.viewport.getOriginalState())},i.prototype.reset=function(){this.resetZoom(),this.resetPan()},i.prototype.handleDblClick=function(t){var e;if((this.options.preventMouseEventsDefault&&(t.preventDefault?t.preventDefault():t.returnValue=!1),this.options.controlIconsEnabled)&&-1<(t.target.getAttribute("class")||"").indexOf("svg-pan-zoom-control"))return!1;e=t.shiftKey?1/(2*(1+this.options.zoomScaleSensitivity)):2*(1+this.options.zoomScaleSensitivity);var o=a.getEventPoint(t,this.svg).matrixTransform(this.svg.getScreenCTM().inverse());this.zoomAtPoint(e,o)},i.prototype.handleMouseDown=function(t,e){this.options.preventMouseEventsDefault&&(t.preventDefault?t.preventDefault():t.returnValue=!1),r.mouseAndTouchNormalize(t,this.svg),this.options.dblClickZoomEnabled&&r.isDblClick(t,e)?this.handleDblClick(t):(this.state="pan",this.firstEventCTM=this.viewport.getCTM(),this.stateOrigin=a.getEventPoint(t,this.svg).matrixTransform(this.firstEventCTM.inverse()))},i.prototype.handleMouseMove=function(t){if(this.options.preventMouseEventsDefault&&(t.preventDefault?t.preventDefault():t.returnValue=!1),"pan"===this.state&&this.options.panEnabled){var e=a.getEventPoint(t,this.svg).matrixTransform(this.firstEventCTM.inverse()),o=this.firstEventCTM.translate(e.x-this.stateOrigin.x,e.y-this.stateOrigin.y);this.viewport.setCTM(o)}},i.prototype.handleMouseUp=function(t){this.options.preventMouseEventsDefault&&(t.preventDefault?t.preventDefault():t.returnValue=!1),"pan"===this.state&&(this.state="none")},i.prototype.fit=function(){var t=this.viewport.getViewBox(),e=Math.min(this.width/t.width,this.height/t.height);this.zoom(e,!0)},i.prototype.contain=function(){var t=this.viewport.getViewBox(),e=Math.max(this.width/t.width,this.height/t.height);this.zoom(e,!0)},i.prototype.center=function(){var t=this.viewport.getViewBox(),e=.5*(this.width-(t.width+2*t.x)*this.getZoom()),o=.5*(this.height-(t.height+2*t.y)*this.getZoom());this.getPublicInstance().pan({x:e,y:o})},i.prototype.updateBBox=function(){this.viewport.simpleViewBoxCache()},i.prototype.pan=function(t){var e=this.viewport.getCTM();e.e=t.x,e.f=t.y,this.viewport.setCTM(e)},i.prototype.panBy=function(t){var e=this.viewport.getCTM();e.e+=t.x,e.f+=t.y,this.viewport.setCTM(e)},i.prototype.getPan=function(){var t=this.viewport.getState();return{x:t.x,y:t.y}},i.prototype.resize=function(){var t=a.getBoundingClientRectNormalized(this.svg);this.width=t.width,this.height=t.height;var e=this.viewport;e.options.width=this.width,e.options.height=this.height,e.processCTM(),this.options.controlIconsEnabled&&(this.getPublicInstance().disableControlIcons(),this.getPublicInstance().enableControlIcons())},i.prototype.destroy=function(){var e=this;for(var t in this.beforeZoom=null,this.onZoom=null,this.beforePan=null,this.onPan=null,(this.onUpdatedCTM=null)!=this.options.customEventsHandler&&this.options.customEventsHandler.destroy({svgElement:this.svg,eventsListenerElement:this.options.eventsListenerElement,instance:this.getPublicInstance()}),this.eventListeners)(this.options.eventsListenerElement||this.svg).removeEventListener(t,this.eventListeners[t],!this.options.preventMouseEventsDefault&&h);this.disableMouseWheelZoom(),this.getPublicInstance().disableControlIcons(),this.reset(),c=c.filter(function(t){return t.svg!==e.svg}),delete this.options,delete this.viewport,delete this.publicInstance,delete this.pi,this.getPublicInstance=function(){return null}},i.prototype.getPublicInstance=function(){var o=this;return this.publicInstance||(this.publicInstance=this.pi={enablePan:function(){return o.options.panEnabled=!0,o.pi},disablePan:function(){return o.options.panEnabled=!1,o.pi},isPanEnabled:function(){return!!o.options.panEnabled},pan:function(t){return o.pan(t),o.pi},panBy:function(t){return o.panBy(t),o.pi},getPan:function(){return o.getPan()},setBeforePan:function(t){return o.options.beforePan=null===t?null:r.proxy(t,o.publicInstance),o.pi},setOnPan:function(t){return o.options.onPan=null===t?null:r.proxy(t,o.publicInstance),o.pi},enableZoom:function(){return o.options.zoomEnabled=!0,o.pi},disableZoom:function(){return o.options.zoomEnabled=!1,o.pi},isZoomEnabled:function(){return!!o.options.zoomEnabled},enableControlIcons:function(){return o.options.controlIconsEnabled||(o.options.controlIconsEnabled=!0,s.enable(o)),o.pi},disableControlIcons:function(){return o.options.controlIconsEnabled&&(o.options.controlIconsEnabled=!1,s.disable(o)),o.pi},isControlIconsEnabled:function(){return!!o.options.controlIconsEnabled},enableDblClickZoom:function(){return o.options.dblClickZoomEnabled=!0,o.pi},disableDblClickZoom:function(){return o.options.dblClickZoomEnabled=!1,o.pi},isDblClickZoomEnabled:function(){return!!o.options.dblClickZoomEnabled},enableMouseWheelZoom:function(){return o.enableMouseWheelZoom(),o.pi},disableMouseWheelZoom:function(){return o.disableMouseWheelZoom(),o.pi},isMouseWheelZoomEnabled:function(){return!!o.options.mouseWheelZoomEnabled},setZoomScaleSensitivity:function(t){return o.options.zoomScaleSensitivity=t,o.pi},setMinZoom:function(t){return o.options.minZoom=t,o.pi},setMaxZoom:function(t){return o.options.maxZoom=t,o.pi},setBeforeZoom:function(t){return o.options.beforeZoom=null===t?null:r.proxy(t,o.publicInstance),o.pi},setOnZoom:function(t){return o.options.onZoom=null===t?null:r.proxy(t,o.publicInstance),o.pi},zoom:function(t){return o.publicZoom(t,!0),o.pi},zoomBy:function(t){return o.publicZoom(t,!1),o.pi},zoomAtPoint:function(t,e){return o.publicZoomAtPoint(t,e,!0),o.pi},zoomAtPointBy:function(t,e){return o.publicZoomAtPoint(t,e,!1),o.pi},zoomIn:function(){return this.zoomBy(1+o.options.zoomScaleSensitivity),o.pi},zoomOut:function(){return this.zoomBy(1/(1+o.options.zoomScaleSensitivity)),o.pi},getZoom:function(){return o.getRelativeZoom()},setOnUpdatedCTM:function(t){return o.options.onUpdatedCTM=null===t?null:r.proxy(t,o.publicInstance),o.pi},resetZoom:function(){return o.resetZoom(),o.pi},resetPan:function(){return o.resetPan(),o.pi},reset:function(){return o.reset(),o.pi},fit:function(){return o.fit(),o.pi},contain:function(){return o.contain(),o.pi},center:function(){return o.center(),o.pi},updateBBox:function(){return o.updateBBox(),o.pi},resize:function(){return o.resize(),o.pi},getSizes:function(){return{width:o.width,height:o.height,realZoom:o.getZoom(),viewBox:o.viewport.getViewBox()}},destroy:function(){return o.destroy(),o.pi}}),this.publicInstance};var c=[];e.exports=function(t,e){var o=r.getSvg(t);if(null===o)return null;for(var n=c.length-1;0<=n;n--)if(c[n].svg===o)return c[n].instance.getPublicInstance();return c.push({svg:o,instance:new i(o,e)}),c[c.length-1].instance.getPublicInstance()}},{"./control-icons":1,"./shadow-viewport":2,"./svg-utilities":5,"./uniwheel":6,"./utilities":7}],5:[function(t,e,o){var l=t("./utilities"),s="unknown";document.documentMode&&(s="ie"),e.exports={svgNS:"http://www.w3.org/2000/svg",xmlNS:"http://www.w3.org/XML/1998/namespace",xmlnsNS:"http://www.w3.org/2000/xmlns/",xlinkNS:"http://www.w3.org/1999/xlink",evNS:"http://www.w3.org/2001/xml-events",getBoundingClientRectNormalized:function(t){if(t.clientWidth&&t.clientHeight)return{width:t.clientWidth,height:t.clientHeight};if(t.getBoundingClientRect())return t.getBoundingClientRect();throw new Error("Cannot get BoundingClientRect for SVG.")},getOrCreateViewport:function(t,e){var o=null;if(!(o=l.isElement(e)?e:t.querySelector(e))){var n=Array.prototype.slice.call(t.childNodes||t.children).filter(function(t){return"defs"!==t.nodeName&&"#text"!==t.nodeName});1===n.length&&"g"===n[0].nodeName&&null===n[0].getAttribute("transform")&&(o=n[0])}if(!o){var i="viewport-"+(new Date).toISOString().replace(/\D/g,"");(o=document.createElementNS(this.svgNS,"g")).setAttribute("id",i);var s=t.childNodes||t.children;if(s&&0`__:: +.. dropdown:: Using ONNX - from skl2onnx import to_onnx - onx = to_onnx(clf, X[:1].astype(numpy.float32), target_opset=12) - with open("filename.onnx", "wb") as f: - f.write(onx.SerializeToString()) + To convert the model to `ONNX` format, you need to give the converter some + information about the input as well, about which you can read more `here + `__:: -You can load the model in Python and use the `ONNX` runtime to get -predictions:: + from skl2onnx import to_onnx + onx = to_onnx(clf, X[:1].astype(numpy.float32), target_opset=12) + with open("filename.onnx", "wb") as f: + f.write(onx.SerializeToString()) - from onnxruntime import InferenceSession - with open("filename.onnx", "rb") as f: - onx = f.read() - sess = InferenceSession(onx, providers=["CPUExecutionProvider"]) - pred_ort = sess.run(None, {"X": X_test.astype(numpy.float32)})[0] + You can load the model in Python and use the `ONNX` runtime to get + predictions:: - -|details-end| + from onnxruntime import InferenceSession + with open("filename.onnx", "rb") as f: + onx = f.read() + sess = InferenceSession(onx, providers=["CPUExecutionProvider"]) + pred_ort = sess.run(None, {"X": X_test.astype(numpy.float32)})[0] .. _skops_persistence: @@ -154,33 +145,30 @@ Therefore it provides a more secure format than :mod:`pickle`, :mod:`joblib`, and `cloudpickle`_. -|details-start| -**Using skops** -|details-split| +.. dropdown:: Using skops -The API is very similar to :mod:`pickle`, and you can persist your models as -explained in the `documentation -`__ using -:func:`skops.io.dump` and :func:`skops.io.dumps`:: + The API is very similar to :mod:`pickle`, and you can persist your models as + explained in the `documentation + `__ using + :func:`skops.io.dump` and :func:`skops.io.dumps`:: - import skops.io as sio - obj = sio.dump(clf, "filename.skops") + import skops.io as sio + obj = sio.dump(clf, "filename.skops") -And you can load them back using :func:`skops.io.load` and -:func:`skops.io.loads`. However, you need to specify the types which are -trusted by you. You can get existing unknown types in a dumped object / file -using :func:`skops.io.get_untrusted_types`, and after checking its contents, -pass it to the load function:: + And you can load them back using :func:`skops.io.load` and + :func:`skops.io.loads`. However, you need to specify the types which are + trusted by you. You can get existing unknown types in a dumped object / file + using :func:`skops.io.get_untrusted_types`, and after checking its contents, + pass it to the load function:: - unknown_types = sio.get_untrusted_types(file="filename.skops") - # investigate the contents of unknown_types, and only load if you trust - # everything you see. - clf = sio.load("filename.skops", trusted=unknown_types) + unknown_types = sio.get_untrusted_types(file="filename.skops") + # investigate the contents of unknown_types, and only load if you trust + # everything you see. + clf = sio.load("filename.skops", trusted=unknown_types) -Please report issues and feature requests related to this format on the `skops -issue tracker `__. + Please report issues and feature requests related to this format on the `skops + issue tracker `__. -|details-end| .. _pickle_persistence: @@ -201,31 +189,27 @@ come with slight variations: :class:`~sklearn.preprocessing.FunctionTransformer` and using a custom function to transform the data. -|details-start| -**Using** ``pickle``, ``joblib``, **or** ``cloudpickle`` -|details-split| - -Depending on your use-case, you can choose one of these three methods to -persist and load your scikit-learn model, and they all follow the same API:: +.. dropdown:: Using `pickle`, `joblib`, or `cloudpickle` - # Here you can replace pickle with joblib or cloudpickle - from pickle import dump - with open("filename.pkl", "wb") as f: - dump(clf, f, protocol=5) + Depending on your use-case, you can choose one of these three methods to + persist and load your scikit-learn model, and they all follow the same API:: -Using `protocol=5` is recommended to reduce memory usage and make it faster to -store and load any large NumPy array stored as a fitted attribute in the model. -You can alternatively pass `protocol=pickle.HIGHEST_PROTOCOL` which is -equivalent to `protocol=5` in Python 3.8 and later (at the time of writing). + # Here you can replace pickle with joblib or cloudpickle + from pickle import dump + with open("filename.pkl", "wb") as f: + dump(clf, f, protocol=5) -And later when needed, you can load the same object from the persisted file:: + Using `protocol=5` is recommended to reduce memory usage and make it faster to + store and load any large NumPy array stored as a fitted attribute in the model. + You can alternatively pass `protocol=pickle.HIGHEST_PROTOCOL` which is + equivalent to `protocol=5` in Python 3.8 and later (at the time of writing). - # Here you can replace pickle with joblib or cloudpickle - from pickle import load - with open("filename.pkl", "rb") as f: - clf = load(f) + And later when needed, you can load the same object from the persisted file:: -|details-end| + # Here you can replace pickle with joblib or cloudpickle + from pickle import load + with open("filename.pkl", "rb") as f: + clf = load(f) .. _persistence_limitations: @@ -296,25 +280,21 @@ recipe (e.g. a Python script) and training set information, and metadata about all the dependencies to be able to automatically reconstruct the same training environment for the updated software. -|details-start| -**InconsistentVersionWarning** -|details-split| - -When an estimator is loaded with a scikit-learn version that is inconsistent -with the version the estimator was pickled with, a -:class:`~sklearn.exceptions.InconsistentVersionWarning` is raised. This warning -can be caught to obtain the original version the estimator was pickled with:: +.. dropdown:: InconsistentVersionWarning - from sklearn.exceptions import InconsistentVersionWarning - warnings.simplefilter("error", InconsistentVersionWarning) + When an estimator is loaded with a scikit-learn version that is inconsistent + with the version the estimator was pickled with, a + :class:`~sklearn.exceptions.InconsistentVersionWarning` is raised. This warning + can be caught to obtain the original version the estimator was pickled with:: - try: - with open("model_from_prevision_version.pickle", "rb") as f: - est = pickle.load(f) - except InconsistentVersionWarning as w: - print(w.original_sklearn_version) + from sklearn.exceptions import InconsistentVersionWarning + warnings.simplefilter("error", InconsistentVersionWarning) -|details-end| + try: + with open("model_from_prevision_version.pickle", "rb") as f: + est = pickle.load(f) + except InconsistentVersionWarning as w: + print(w.original_sklearn_version) Serving the model artifact diff --git a/doc/model_selection.rst b/doc/model_selection.rst index 522544aefc820..b78c9ff4c3aa8 100644 --- a/doc/model_selection.rst +++ b/doc/model_selection.rst @@ -1,9 +1,3 @@ -.. Places parent toc into the sidebar - -:parenttoc: True - -.. include:: includes/big_toc_css.rst - .. _model_selection: Model selection and evaluation diff --git a/doc/modules/array_api.rst b/doc/modules/array_api.rst index 310df6b12a6ec..9ad4f2f640924 100644 --- a/doc/modules/array_api.rst +++ b/doc/modules/array_api.rst @@ -1,7 +1,3 @@ -.. Places parent toc into the sidebar - -:parenttoc: True - .. _array_api: ================================ diff --git a/doc/modules/biclustering.rst b/doc/modules/biclustering.rst index 2189e85e0f0ef..503a535c408f0 100644 --- a/doc/modules/biclustering.rst +++ b/doc/modules/biclustering.rst @@ -147,21 +147,21 @@ Then the rows of :math:`Z` are clustered using :ref:`k-means and the remaining ``n_columns`` labels provide the column partitioning. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_bicluster_plot_spectral_coclustering.py`: A simple example - showing how to generate a data matrix with biclusters and apply - this method to it. +* :ref:`sphx_glr_auto_examples_bicluster_plot_spectral_coclustering.py`: A simple example + showing how to generate a data matrix with biclusters and apply + this method to it. - * :ref:`sphx_glr_auto_examples_bicluster_plot_bicluster_newsgroups.py`: An example of finding - biclusters in the twenty newsgroup dataset. +* :ref:`sphx_glr_auto_examples_bicluster_plot_bicluster_newsgroups.py`: An example of finding + biclusters in the twenty newsgroup dataset. -.. topic:: References: +.. rubric:: References - * Dhillon, Inderjit S, 2001. :doi:`Co-clustering documents and words using - bipartite spectral graph partitioning - <10.1145/502512.502550>` +* Dhillon, Inderjit S, 2001. :doi:`Co-clustering documents and words using + bipartite spectral graph partitioning + <10.1145/502512.502550>` .. _spectral_biclustering: @@ -234,17 +234,17 @@ Similarly, projecting the columns to :math:`A^{\top} * U` and clustering this :math:`n \times q` matrix yields the column labels. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_bicluster_plot_spectral_biclustering.py`: a simple example - showing how to generate a checkerboard matrix and bicluster it. +* :ref:`sphx_glr_auto_examples_bicluster_plot_spectral_biclustering.py`: a simple example + showing how to generate a checkerboard matrix and bicluster it. -.. topic:: References: +.. rubric:: References - * Kluger, Yuval, et. al., 2003. :doi:`Spectral biclustering of microarray - data: coclustering genes and conditions - <10.1101/gr.648603>` +* Kluger, Yuval, et. al., 2003. :doi:`Spectral biclustering of microarray + data: coclustering genes and conditions + <10.1101/gr.648603>` .. _biclustering_evaluation: @@ -298,8 +298,8 @@ are totally dissimilar. The maximum score, 1, occurs when both sets are identical. -.. topic:: References: +.. rubric:: References - * Hochreiter, Bodenhofer, et. al., 2010. `FABIA: factor analysis - for bicluster acquisition - `__. +* Hochreiter, Bodenhofer, et. al., 2010. `FABIA: factor analysis + for bicluster acquisition + `__. diff --git a/doc/modules/calibration.rst b/doc/modules/calibration.rst index c0a6edb837b2f..a2bfa152d2b26 100644 --- a/doc/modules/calibration.rst +++ b/doc/modules/calibration.rst @@ -262,51 +262,51 @@ probabilities, the calibrated probabilities for each class are predicted separately. As those probabilities do not necessarily sum to one, a postprocessing is performed to normalize them. -.. topic:: Examples: - - * :ref:`sphx_glr_auto_examples_calibration_plot_calibration_curve.py` - * :ref:`sphx_glr_auto_examples_calibration_plot_calibration_multiclass.py` - * :ref:`sphx_glr_auto_examples_calibration_plot_calibration.py` - * :ref:`sphx_glr_auto_examples_calibration_plot_compare_calibration.py` - -.. topic:: References: - - .. [1] Allan H. Murphy (1973). - :doi:`"A New Vector Partition of the Probability Score" - <10.1175/1520-0450(1973)012%3C0595:ANVPOT%3E2.0.CO;2>` - Journal of Applied Meteorology and Climatology - - .. [2] `On the combination of forecast probabilities for - consecutive precipitation periods. - `_ - Wea. Forecasting, 5, 640–650., Wilks, D. S., 1990a - - .. [3] `Predicting Good Probabilities with Supervised Learning - `_, - A. Niculescu-Mizil & R. Caruana, ICML 2005 - - - .. [4] `Probabilistic Outputs for Support Vector Machines and Comparisons - to Regularized Likelihood Methods. - `_ - J. Platt, (1999) - - .. [5] `Transforming Classifier Scores into Accurate Multiclass - Probability Estimates. - `_ - B. Zadrozny & C. Elkan, (KDD 2002) - - .. [6] `Predicting accurate probabilities with a ranking loss. - `_ - Menon AK, Jiang XJ, Vembu S, Elkan C, Ohno-Machado L. - Proc Int Conf Mach Learn. 2012;2012:703-710 - - .. [7] `Beyond sigmoids: How to obtain well-calibrated probabilities from - binary classifiers with beta calibration - `_ - Kull, M., Silva Filho, T. M., & Flach, P. (2017). - - .. [8] Mario V. Wüthrich, Michael Merz (2023). - :doi:`"Statistical Foundations of Actuarial Learning and its Applications" - <10.1007/978-3-031-12409-9>` - Springer Actuarial +.. rubric:: Examples + +* :ref:`sphx_glr_auto_examples_calibration_plot_calibration_curve.py` +* :ref:`sphx_glr_auto_examples_calibration_plot_calibration_multiclass.py` +* :ref:`sphx_glr_auto_examples_calibration_plot_calibration.py` +* :ref:`sphx_glr_auto_examples_calibration_plot_compare_calibration.py` + +.. rubric:: References + +.. [1] Allan H. Murphy (1973). + :doi:`"A New Vector Partition of the Probability Score" + <10.1175/1520-0450(1973)012%3C0595:ANVPOT%3E2.0.CO;2>` + Journal of Applied Meteorology and Climatology + +.. [2] `On the combination of forecast probabilities for + consecutive precipitation periods. + `_ + Wea. Forecasting, 5, 640–650., Wilks, D. S., 1990a + +.. [3] `Predicting Good Probabilities with Supervised Learning + `_, + A. Niculescu-Mizil & R. Caruana, ICML 2005 + + +.. [4] `Probabilistic Outputs for Support Vector Machines and Comparisons + to Regularized Likelihood Methods. + `_ + J. Platt, (1999) + +.. [5] `Transforming Classifier Scores into Accurate Multiclass + Probability Estimates. + `_ + B. Zadrozny & C. Elkan, (KDD 2002) + +.. [6] `Predicting accurate probabilities with a ranking loss. + `_ + Menon AK, Jiang XJ, Vembu S, Elkan C, Ohno-Machado L. + Proc Int Conf Mach Learn. 2012;2012:703-710 + +.. [7] `Beyond sigmoids: How to obtain well-calibrated probabilities from + binary classifiers with beta calibration + `_ + Kull, M., Silva Filho, T. M., & Flach, P. (2017). + +.. [8] Mario V. Wüthrich, Michael Merz (2023). + :doi:`"Statistical Foundations of Actuarial Learning and its Applications" + <10.1007/978-3-031-12409-9>` + Springer Actuarial diff --git a/doc/modules/classes.rst b/doc/modules/classes.rst deleted file mode 100644 index 1da5b337ad7a4..0000000000000 --- a/doc/modules/classes.rst +++ /dev/null @@ -1,1916 +0,0 @@ -.. _api_ref: - -============= -API Reference -============= - -This is the class and function reference of scikit-learn. Please refer to -the :ref:`full user guide ` for further details, as the class and -function raw specifications may not be enough to give full guidelines on their -uses. -For reference on concepts repeated across the API, see :ref:`glossary`. - -:mod:`sklearn`: Settings and information tools -============================================== - -.. automodule:: sklearn - :no-members: - :no-inherited-members: - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - config_context - get_config - set_config - show_versions - -:mod:`sklearn.base`: Base classes and utility functions -======================================================= - -.. automodule:: sklearn.base - :no-members: - :no-inherited-members: - -Base classes ------------- -.. currentmodule:: sklearn - -.. autosummary:: - :nosignatures: - :toctree: generated/ - :template: class.rst - - base.BaseEstimator - base.BiclusterMixin - base.ClassifierMixin - base.ClusterMixin - base.DensityMixin - base.RegressorMixin - base.TransformerMixin - base.MetaEstimatorMixin - base.OneToOneFeatureMixin - base.OutlierMixin - base.ClassNamePrefixFeaturesOutMixin - feature_selection.SelectorMixin - -Functions ---------- -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - base.clone - base.is_classifier - base.is_regressor - -.. _calibration_ref: - -:mod:`sklearn.calibration`: Probability Calibration -=================================================== - -.. automodule:: sklearn.calibration - :no-members: - :no-inherited-members: - -**User guide:** See the :ref:`calibration` section for further details. - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: class.rst - - calibration.CalibratedClassifierCV - - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - calibration.calibration_curve - -.. _cluster_ref: - -:mod:`sklearn.cluster`: Clustering -================================== - -.. automodule:: sklearn.cluster - :no-members: - :no-inherited-members: - -**User guide:** See the :ref:`clustering` and :ref:`biclustering` sections for -further details. - -Classes -------- -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: class.rst - - cluster.AffinityPropagation - cluster.AgglomerativeClustering - cluster.Birch - cluster.DBSCAN - cluster.HDBSCAN - cluster.FeatureAgglomeration - cluster.KMeans - cluster.BisectingKMeans - cluster.MiniBatchKMeans - cluster.MeanShift - cluster.OPTICS - cluster.SpectralClustering - cluster.SpectralBiclustering - cluster.SpectralCoclustering - -Functions ---------- -.. autosummary:: - :toctree: generated/ - :template: function.rst - - cluster.affinity_propagation - cluster.cluster_optics_dbscan - cluster.cluster_optics_xi - cluster.compute_optics_graph - cluster.dbscan - cluster.estimate_bandwidth - cluster.k_means - cluster.kmeans_plusplus - cluster.mean_shift - cluster.spectral_clustering - cluster.ward_tree - -.. _compose_ref: - -:mod:`sklearn.compose`: Composite Estimators -============================================ - -.. automodule:: sklearn.compose - :no-members: - :no-inherited-members: - -**User guide:** See the :ref:`combining_estimators` section for further -details. - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: class.rst - - compose.ColumnTransformer - compose.TransformedTargetRegressor - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - compose.make_column_transformer - compose.make_column_selector - -.. _covariance_ref: - -:mod:`sklearn.covariance`: Covariance Estimators -================================================ - -.. automodule:: sklearn.covariance - :no-members: - :no-inherited-members: - -**User guide:** See the :ref:`covariance` section for further details. - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: class.rst - - covariance.EmpiricalCovariance - covariance.EllipticEnvelope - covariance.GraphicalLasso - covariance.GraphicalLassoCV - covariance.LedoitWolf - covariance.MinCovDet - covariance.OAS - covariance.ShrunkCovariance - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - covariance.empirical_covariance - covariance.graphical_lasso - covariance.ledoit_wolf - covariance.ledoit_wolf_shrinkage - covariance.oas - covariance.shrunk_covariance - -.. _cross_decomposition_ref: - -:mod:`sklearn.cross_decomposition`: Cross decomposition -======================================================= - -.. automodule:: sklearn.cross_decomposition - :no-members: - :no-inherited-members: - -**User guide:** See the :ref:`cross_decomposition` section for further details. - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: class.rst - - cross_decomposition.CCA - cross_decomposition.PLSCanonical - cross_decomposition.PLSRegression - cross_decomposition.PLSSVD - -.. _datasets_ref: - -:mod:`sklearn.datasets`: Datasets -================================= - -.. automodule:: sklearn.datasets - :no-members: - :no-inherited-members: - -**User guide:** See the :ref:`datasets` section for further details. - -Loaders -------- - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - datasets.clear_data_home - datasets.dump_svmlight_file - datasets.fetch_20newsgroups - datasets.fetch_20newsgroups_vectorized - datasets.fetch_california_housing - datasets.fetch_covtype - datasets.fetch_kddcup99 - datasets.fetch_lfw_pairs - datasets.fetch_lfw_people - datasets.fetch_olivetti_faces - datasets.fetch_openml - datasets.fetch_rcv1 - datasets.fetch_species_distributions - datasets.get_data_home - datasets.load_breast_cancer - datasets.load_diabetes - datasets.load_digits - datasets.load_files - datasets.load_iris - datasets.load_linnerud - datasets.load_sample_image - datasets.load_sample_images - datasets.load_svmlight_file - datasets.load_svmlight_files - datasets.load_wine - -Samples generator ------------------ - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - datasets.make_biclusters - datasets.make_blobs - datasets.make_checkerboard - datasets.make_circles - datasets.make_classification - datasets.make_friedman1 - datasets.make_friedman2 - datasets.make_friedman3 - datasets.make_gaussian_quantiles - datasets.make_hastie_10_2 - datasets.make_low_rank_matrix - datasets.make_moons - datasets.make_multilabel_classification - datasets.make_regression - datasets.make_s_curve - datasets.make_sparse_coded_signal - datasets.make_sparse_spd_matrix - datasets.make_sparse_uncorrelated - datasets.make_spd_matrix - datasets.make_swiss_roll - - -.. _decomposition_ref: - -:mod:`sklearn.decomposition`: Matrix Decomposition -================================================== - -.. automodule:: sklearn.decomposition - :no-members: - :no-inherited-members: - -**User guide:** See the :ref:`decompositions` section for further details. - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: class.rst - - decomposition.DictionaryLearning - decomposition.FactorAnalysis - decomposition.FastICA - decomposition.IncrementalPCA - decomposition.KernelPCA - decomposition.LatentDirichletAllocation - decomposition.MiniBatchDictionaryLearning - decomposition.MiniBatchSparsePCA - decomposition.NMF - decomposition.MiniBatchNMF - decomposition.PCA - decomposition.SparsePCA - decomposition.SparseCoder - decomposition.TruncatedSVD - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - decomposition.dict_learning - decomposition.dict_learning_online - decomposition.fastica - decomposition.non_negative_factorization - decomposition.sparse_encode - -.. _lda_ref: - -:mod:`sklearn.discriminant_analysis`: Discriminant Analysis -=========================================================== - -.. automodule:: sklearn.discriminant_analysis - :no-members: - :no-inherited-members: - -**User guide:** See the :ref:`lda_qda` section for further details. - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: class.rst - - discriminant_analysis.LinearDiscriminantAnalysis - discriminant_analysis.QuadraticDiscriminantAnalysis - -.. _dummy_ref: - -:mod:`sklearn.dummy`: Dummy estimators -====================================== - -.. automodule:: sklearn.dummy - :no-members: - :no-inherited-members: - -**User guide:** See the :ref:`model_evaluation` section for further details. - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: class.rst - - dummy.DummyClassifier - dummy.DummyRegressor - -.. autosummary:: - :toctree: generated/ - :template: function.rst - -.. _ensemble_ref: - -:mod:`sklearn.ensemble`: Ensemble Methods -========================================= - -.. automodule:: sklearn.ensemble - :no-members: - :no-inherited-members: - -**User guide:** See the :ref:`ensemble` section for further details. - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: class.rst - - ensemble.AdaBoostClassifier - ensemble.AdaBoostRegressor - ensemble.BaggingClassifier - ensemble.BaggingRegressor - ensemble.ExtraTreesClassifier - ensemble.ExtraTreesRegressor - ensemble.GradientBoostingClassifier - ensemble.GradientBoostingRegressor - ensemble.IsolationForest - ensemble.RandomForestClassifier - ensemble.RandomForestRegressor - ensemble.RandomTreesEmbedding - ensemble.StackingClassifier - ensemble.StackingRegressor - ensemble.VotingClassifier - ensemble.VotingRegressor - ensemble.HistGradientBoostingRegressor - ensemble.HistGradientBoostingClassifier - - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - -.. _exceptions_ref: - -:mod:`sklearn.exceptions`: Exceptions and warnings -================================================== - -.. automodule:: sklearn.exceptions - :no-members: - :no-inherited-members: - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: class.rst - - exceptions.ConvergenceWarning - exceptions.DataConversionWarning - exceptions.DataDimensionalityWarning - exceptions.EfficiencyWarning - exceptions.FitFailedWarning - exceptions.InconsistentVersionWarning - exceptions.NotFittedError - exceptions.UndefinedMetricWarning - - -:mod:`sklearn.experimental`: Experimental -========================================= - -.. automodule:: sklearn.experimental - :no-members: - :no-inherited-members: - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - - experimental.enable_iterative_imputer - experimental.enable_halving_search_cv - - -.. _feature_extraction_ref: - -:mod:`sklearn.feature_extraction`: Feature Extraction -===================================================== - -.. automodule:: sklearn.feature_extraction - :no-members: - :no-inherited-members: - -**User guide:** See the :ref:`feature_extraction` section for further details. - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: class.rst - - feature_extraction.DictVectorizer - feature_extraction.FeatureHasher - -From images ------------ - -.. automodule:: sklearn.feature_extraction.image - :no-members: - :no-inherited-members: - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - feature_extraction.image.extract_patches_2d - feature_extraction.image.grid_to_graph - feature_extraction.image.img_to_graph - feature_extraction.image.reconstruct_from_patches_2d - - :template: class.rst - - feature_extraction.image.PatchExtractor - -.. _text_feature_extraction_ref: - -From text ---------- - -.. automodule:: sklearn.feature_extraction.text - :no-members: - :no-inherited-members: - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: class.rst - - feature_extraction.text.CountVectorizer - feature_extraction.text.HashingVectorizer - feature_extraction.text.TfidfTransformer - feature_extraction.text.TfidfVectorizer - - -.. _feature_selection_ref: - -:mod:`sklearn.feature_selection`: Feature Selection -=================================================== - -.. automodule:: sklearn.feature_selection - :no-members: - :no-inherited-members: - -**User guide:** See the :ref:`feature_selection` section for further details. - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: class.rst - - feature_selection.GenericUnivariateSelect - feature_selection.SelectPercentile - feature_selection.SelectKBest - feature_selection.SelectFpr - feature_selection.SelectFdr - feature_selection.SelectFromModel - feature_selection.SelectFwe - feature_selection.SequentialFeatureSelector - feature_selection.RFE - feature_selection.RFECV - feature_selection.VarianceThreshold - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - feature_selection.chi2 - feature_selection.f_classif - feature_selection.f_regression - feature_selection.r_regression - feature_selection.mutual_info_classif - feature_selection.mutual_info_regression - - -.. _gaussian_process_ref: - -:mod:`sklearn.gaussian_process`: Gaussian Processes -=================================================== - -.. automodule:: sklearn.gaussian_process - :no-members: - :no-inherited-members: - -**User guide:** See the :ref:`gaussian_process` section for further details. - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: class.rst - - gaussian_process.GaussianProcessClassifier - gaussian_process.GaussianProcessRegressor - -Kernels -------- - -.. automodule:: sklearn.gaussian_process.kernels - :no-members: - :no-inherited-members: - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: class_with_call.rst - - gaussian_process.kernels.CompoundKernel - gaussian_process.kernels.ConstantKernel - gaussian_process.kernels.DotProduct - gaussian_process.kernels.ExpSineSquared - gaussian_process.kernels.Exponentiation - gaussian_process.kernels.Hyperparameter - gaussian_process.kernels.Kernel - gaussian_process.kernels.Matern - gaussian_process.kernels.PairwiseKernel - gaussian_process.kernels.Product - gaussian_process.kernels.RBF - gaussian_process.kernels.RationalQuadratic - gaussian_process.kernels.Sum - gaussian_process.kernels.WhiteKernel - - -.. _impute_ref: - -:mod:`sklearn.impute`: Impute -============================= - -.. automodule:: sklearn.impute - :no-members: - :no-inherited-members: - -**User guide:** See the :ref:`Impute` section for further details. - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: class.rst - - impute.SimpleImputer - impute.IterativeImputer - impute.MissingIndicator - impute.KNNImputer - - -.. _inspection_ref: - -:mod:`sklearn.inspection`: Inspection -===================================== - -.. automodule:: sklearn.inspection - :no-members: - :no-inherited-members: - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - inspection.partial_dependence - inspection.permutation_importance - -Plotting --------- - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: display_only_from_estimator.rst - - inspection.DecisionBoundaryDisplay - inspection.PartialDependenceDisplay - -.. _isotonic_ref: - -:mod:`sklearn.isotonic`: Isotonic regression -============================================ - -.. automodule:: sklearn.isotonic - :no-members: - :no-inherited-members: - -**User guide:** See the :ref:`isotonic` section for further details. - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: class.rst - - isotonic.IsotonicRegression - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - isotonic.check_increasing - isotonic.isotonic_regression - - -.. _kernel_approximation_ref: - -:mod:`sklearn.kernel_approximation`: Kernel Approximation -========================================================= - -.. automodule:: sklearn.kernel_approximation - :no-members: - :no-inherited-members: - -**User guide:** See the :ref:`kernel_approximation` section for further details. - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: class.rst - - kernel_approximation.AdditiveChi2Sampler - kernel_approximation.Nystroem - kernel_approximation.PolynomialCountSketch - kernel_approximation.RBFSampler - kernel_approximation.SkewedChi2Sampler - -.. _kernel_ridge_ref: - -:mod:`sklearn.kernel_ridge`: Kernel Ridge Regression -==================================================== - -.. automodule:: sklearn.kernel_ridge - :no-members: - :no-inherited-members: - -**User guide:** See the :ref:`kernel_ridge` section for further details. - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: class.rst - - kernel_ridge.KernelRidge - -.. _linear_model_ref: - -:mod:`sklearn.linear_model`: Linear Models -========================================== - -.. automodule:: sklearn.linear_model - :no-members: - :no-inherited-members: - -**User guide:** See the :ref:`linear_model` section for further details. - -The following subsections are only rough guidelines: the same estimator can -fall into multiple categories, depending on its parameters. - -.. currentmodule:: sklearn - -Linear classifiers ------------------- -.. autosummary:: - :toctree: generated/ - :template: class.rst - - linear_model.LogisticRegression - linear_model.LogisticRegressionCV - linear_model.PassiveAggressiveClassifier - linear_model.Perceptron - linear_model.RidgeClassifier - linear_model.RidgeClassifierCV - linear_model.SGDClassifier - linear_model.SGDOneClassSVM - -Classical linear regressors ---------------------------- - -.. autosummary:: - :toctree: generated/ - :template: class.rst - - linear_model.LinearRegression - linear_model.Ridge - linear_model.RidgeCV - linear_model.SGDRegressor - -Regressors with variable selection ----------------------------------- - -The following estimators have built-in variable selection fitting -procedures, but any estimator using a L1 or elastic-net penalty also -performs variable selection: typically :class:`~linear_model.SGDRegressor` -or :class:`~sklearn.linear_model.SGDClassifier` with an appropriate penalty. - -.. autosummary:: - :toctree: generated/ - :template: class.rst - - linear_model.ElasticNet - linear_model.ElasticNetCV - linear_model.Lars - linear_model.LarsCV - linear_model.Lasso - linear_model.LassoCV - linear_model.LassoLars - linear_model.LassoLarsCV - linear_model.LassoLarsIC - linear_model.OrthogonalMatchingPursuit - linear_model.OrthogonalMatchingPursuitCV - -Bayesian regressors -------------------- - -.. autosummary:: - :toctree: generated/ - :template: class.rst - - linear_model.ARDRegression - linear_model.BayesianRidge - -Multi-task linear regressors with variable selection ----------------------------------------------------- - -These estimators fit multiple regression problems (or tasks) jointly, while -inducing sparse coefficients. While the inferred coefficients may differ -between the tasks, they are constrained to agree on the features that are -selected (non-zero coefficients). - -.. autosummary:: - :toctree: generated/ - :template: class.rst - - linear_model.MultiTaskElasticNet - linear_model.MultiTaskElasticNetCV - linear_model.MultiTaskLasso - linear_model.MultiTaskLassoCV - -Outlier-robust regressors -------------------------- - -Any estimator using the Huber loss would also be robust to outliers, e.g. -:class:`~linear_model.SGDRegressor` with ``loss='huber'``. - -.. autosummary:: - :toctree: generated/ - :template: class.rst - - linear_model.HuberRegressor - linear_model.QuantileRegressor - linear_model.RANSACRegressor - linear_model.TheilSenRegressor - -Generalized linear models (GLM) for regression ----------------------------------------------- - -These models allow for response variables to have error distributions other -than a normal distribution: - -.. autosummary:: - :toctree: generated/ - :template: class.rst - - linear_model.PoissonRegressor - linear_model.TweedieRegressor - linear_model.GammaRegressor - - -Miscellaneous -------------- - -.. autosummary:: - :toctree: generated/ - :template: classes.rst - - linear_model.PassiveAggressiveRegressor - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - linear_model.enet_path - linear_model.lars_path - linear_model.lars_path_gram - linear_model.lasso_path - linear_model.orthogonal_mp - linear_model.orthogonal_mp_gram - linear_model.ridge_regression - - -.. _manifold_ref: - -:mod:`sklearn.manifold`: Manifold Learning -========================================== - -.. automodule:: sklearn.manifold - :no-members: - :no-inherited-members: - -**User guide:** See the :ref:`manifold` section for further details. - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated - :template: class.rst - - manifold.Isomap - manifold.LocallyLinearEmbedding - manifold.MDS - manifold.SpectralEmbedding - manifold.TSNE - -.. autosummary:: - :toctree: generated - :template: function.rst - - manifold.locally_linear_embedding - manifold.smacof - manifold.spectral_embedding - manifold.trustworthiness - - -.. _metrics_ref: - -:mod:`sklearn.metrics`: Metrics -=============================== - -See the :ref:`model_evaluation` section and the :ref:`metrics` section of the -user guide for further details. - -.. automodule:: sklearn.metrics - :no-members: - :no-inherited-members: - -.. currentmodule:: sklearn - -Model Selection Interface -------------------------- -See the :ref:`scoring_parameter` section of the user guide for further -details. - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - metrics.check_scoring - metrics.get_scorer - metrics.get_scorer_names - metrics.make_scorer - -Classification metrics ----------------------- - -See the :ref:`classification_metrics` section of the user guide for further -details. - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - metrics.accuracy_score - metrics.auc - metrics.average_precision_score - metrics.balanced_accuracy_score - metrics.brier_score_loss - metrics.class_likelihood_ratios - metrics.classification_report - metrics.cohen_kappa_score - metrics.confusion_matrix - metrics.d2_log_loss_score - metrics.dcg_score - metrics.det_curve - metrics.f1_score - metrics.fbeta_score - metrics.hamming_loss - metrics.hinge_loss - metrics.jaccard_score - metrics.log_loss - metrics.matthews_corrcoef - metrics.multilabel_confusion_matrix - metrics.ndcg_score - metrics.precision_recall_curve - metrics.precision_recall_fscore_support - metrics.precision_score - metrics.recall_score - metrics.roc_auc_score - metrics.roc_curve - metrics.top_k_accuracy_score - metrics.zero_one_loss - -Regression metrics ------------------- - -See the :ref:`regression_metrics` section of the user guide for further -details. - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - metrics.explained_variance_score - metrics.max_error - metrics.mean_absolute_error - metrics.mean_squared_error - metrics.mean_squared_log_error - metrics.median_absolute_error - metrics.mean_absolute_percentage_error - metrics.r2_score - metrics.root_mean_squared_log_error - metrics.root_mean_squared_error - metrics.mean_poisson_deviance - metrics.mean_gamma_deviance - metrics.mean_tweedie_deviance - metrics.d2_tweedie_score - metrics.mean_pinball_loss - metrics.d2_pinball_score - metrics.d2_absolute_error_score - -Multilabel ranking metrics --------------------------- -See the :ref:`multilabel_ranking_metrics` section of the user guide for further -details. - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - metrics.coverage_error - metrics.label_ranking_average_precision_score - metrics.label_ranking_loss - - -Clustering metrics ------------------- - -See the :ref:`clustering_evaluation` section of the user guide for further -details. - -.. automodule:: sklearn.metrics.cluster - :no-members: - :no-inherited-members: - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - metrics.adjusted_mutual_info_score - metrics.adjusted_rand_score - metrics.calinski_harabasz_score - metrics.davies_bouldin_score - metrics.completeness_score - metrics.cluster.contingency_matrix - metrics.cluster.pair_confusion_matrix - metrics.fowlkes_mallows_score - metrics.homogeneity_completeness_v_measure - metrics.homogeneity_score - metrics.mutual_info_score - metrics.normalized_mutual_info_score - metrics.rand_score - metrics.silhouette_score - metrics.silhouette_samples - metrics.v_measure_score - -Biclustering metrics --------------------- - -See the :ref:`biclustering_evaluation` section of the user guide for -further details. - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - metrics.consensus_score - -Distance metrics ----------------- - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: class.rst - - metrics.DistanceMetric - -Pairwise metrics ----------------- - -See the :ref:`metrics` section of the user guide for further details. - -.. automodule:: sklearn.metrics.pairwise - :no-members: - :no-inherited-members: - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - metrics.pairwise.additive_chi2_kernel - metrics.pairwise.chi2_kernel - metrics.pairwise.cosine_similarity - metrics.pairwise.cosine_distances - metrics.pairwise.distance_metrics - metrics.pairwise.euclidean_distances - metrics.pairwise.haversine_distances - metrics.pairwise.kernel_metrics - metrics.pairwise.laplacian_kernel - metrics.pairwise.linear_kernel - metrics.pairwise.manhattan_distances - metrics.pairwise.nan_euclidean_distances - metrics.pairwise.pairwise_kernels - metrics.pairwise.polynomial_kernel - metrics.pairwise.rbf_kernel - metrics.pairwise.sigmoid_kernel - metrics.pairwise.paired_euclidean_distances - metrics.pairwise.paired_manhattan_distances - metrics.pairwise.paired_cosine_distances - metrics.pairwise.paired_distances - metrics.pairwise_distances - metrics.pairwise_distances_argmin - metrics.pairwise_distances_argmin_min - metrics.pairwise_distances_chunked - - -Plotting --------- - -See the :ref:`visualizations` section of the user guide for further details. - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: display_all_class_methods.rst - - metrics.ConfusionMatrixDisplay - metrics.DetCurveDisplay - metrics.PrecisionRecallDisplay - metrics.PredictionErrorDisplay - metrics.RocCurveDisplay - calibration.CalibrationDisplay - -.. _mixture_ref: - -:mod:`sklearn.mixture`: Gaussian Mixture Models -=============================================== - -.. automodule:: sklearn.mixture - :no-members: - :no-inherited-members: - -**User guide:** See the :ref:`mixture` section for further details. - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: class.rst - - mixture.BayesianGaussianMixture - mixture.GaussianMixture - -.. _modelselection_ref: - -:mod:`sklearn.model_selection`: Model Selection -=============================================== - -.. automodule:: sklearn.model_selection - :no-members: - :no-inherited-members: - -**User guide:** See the :ref:`cross_validation`, :ref:`grid_search` and -:ref:`learning_curve` sections for further details. - -Splitter Classes ----------------- - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: class.rst - - model_selection.GroupKFold - model_selection.GroupShuffleSplit - model_selection.KFold - model_selection.LeaveOneGroupOut - model_selection.LeavePGroupsOut - model_selection.LeaveOneOut - model_selection.LeavePOut - model_selection.PredefinedSplit - model_selection.RepeatedKFold - model_selection.RepeatedStratifiedKFold - model_selection.ShuffleSplit - model_selection.StratifiedKFold - model_selection.StratifiedShuffleSplit - model_selection.StratifiedGroupKFold - model_selection.TimeSeriesSplit - -Splitter Functions ------------------- - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - model_selection.check_cv - model_selection.train_test_split - -.. _hyper_parameter_optimizers: - -Hyper-parameter optimizers --------------------------- - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: class.rst - - model_selection.GridSearchCV - model_selection.HalvingGridSearchCV - model_selection.ParameterGrid - model_selection.ParameterSampler - model_selection.RandomizedSearchCV - model_selection.HalvingRandomSearchCV - -Post-fit model tuning ---------------------- - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: class.rst - - model_selection.FixedThresholdClassifier - model_selection.TunedThresholdClassifierCV - -Model validation ----------------- - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - model_selection.cross_validate - model_selection.cross_val_predict - model_selection.cross_val_score - model_selection.learning_curve - model_selection.permutation_test_score - model_selection.validation_curve - -Visualization -------------- - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: display_only_from_estimator.rst - - model_selection.LearningCurveDisplay - model_selection.ValidationCurveDisplay - -.. _multiclass_ref: - -:mod:`sklearn.multiclass`: Multiclass classification -==================================================== - -.. automodule:: sklearn.multiclass - :no-members: - :no-inherited-members: - -**User guide:** See the :ref:`multiclass_classification` section for further details. - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: class.rst - - multiclass.OneVsRestClassifier - multiclass.OneVsOneClassifier - multiclass.OutputCodeClassifier - -.. _multioutput_ref: - -:mod:`sklearn.multioutput`: Multioutput regression and classification -===================================================================== - -.. automodule:: sklearn.multioutput - :no-members: - :no-inherited-members: - -**User guide:** See the :ref:`multilabel_classification`, -:ref:`multiclass_multioutput_classification`, and -:ref:`multioutput_regression` sections for further details. - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated - :template: class.rst - - multioutput.ClassifierChain - multioutput.MultiOutputRegressor - multioutput.MultiOutputClassifier - multioutput.RegressorChain - -.. _naive_bayes_ref: - -:mod:`sklearn.naive_bayes`: Naive Bayes -======================================= - -.. automodule:: sklearn.naive_bayes - :no-members: - :no-inherited-members: - -**User guide:** See the :ref:`naive_bayes` section for further details. - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: class.rst - - naive_bayes.BernoulliNB - naive_bayes.CategoricalNB - naive_bayes.ComplementNB - naive_bayes.GaussianNB - naive_bayes.MultinomialNB - - -.. _neighbors_ref: - -:mod:`sklearn.neighbors`: Nearest Neighbors -=========================================== - -.. automodule:: sklearn.neighbors - :no-members: - :no-inherited-members: - -**User guide:** See the :ref:`neighbors` section for further details. - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: class.rst - - neighbors.BallTree - neighbors.KDTree - neighbors.KernelDensity - neighbors.KNeighborsClassifier - neighbors.KNeighborsRegressor - neighbors.KNeighborsTransformer - neighbors.LocalOutlierFactor - neighbors.RadiusNeighborsClassifier - neighbors.RadiusNeighborsRegressor - neighbors.RadiusNeighborsTransformer - neighbors.NearestCentroid - neighbors.NearestNeighbors - neighbors.NeighborhoodComponentsAnalysis - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - neighbors.kneighbors_graph - neighbors.radius_neighbors_graph - neighbors.sort_graph_by_row_values - -.. _neural_network_ref: - -:mod:`sklearn.neural_network`: Neural network models -==================================================== - -.. automodule:: sklearn.neural_network - :no-members: - :no-inherited-members: - -**User guide:** See the :ref:`neural_networks_supervised` and :ref:`neural_networks_unsupervised` sections for further details. - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: class.rst - - neural_network.BernoulliRBM - neural_network.MLPClassifier - neural_network.MLPRegressor - -.. _pipeline_ref: - -:mod:`sklearn.pipeline`: Pipeline -================================= - -.. automodule:: sklearn.pipeline - :no-members: - :no-inherited-members: - -**User guide:** See the :ref:`combining_estimators` section for further -details. - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: class.rst - - pipeline.FeatureUnion - pipeline.Pipeline - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - pipeline.make_pipeline - pipeline.make_union - -.. _preprocessing_ref: - -:mod:`sklearn.preprocessing`: Preprocessing and Normalization -============================================================= - -.. automodule:: sklearn.preprocessing - :no-members: - :no-inherited-members: - -**User guide:** See the :ref:`preprocessing` section for further details. - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: class.rst - - preprocessing.Binarizer - preprocessing.FunctionTransformer - preprocessing.KBinsDiscretizer - preprocessing.KernelCenterer - preprocessing.LabelBinarizer - preprocessing.LabelEncoder - preprocessing.MultiLabelBinarizer - preprocessing.MaxAbsScaler - preprocessing.MinMaxScaler - preprocessing.Normalizer - preprocessing.OneHotEncoder - preprocessing.OrdinalEncoder - preprocessing.PolynomialFeatures - preprocessing.PowerTransformer - preprocessing.QuantileTransformer - preprocessing.RobustScaler - preprocessing.SplineTransformer - preprocessing.StandardScaler - preprocessing.TargetEncoder - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - preprocessing.add_dummy_feature - preprocessing.binarize - preprocessing.label_binarize - preprocessing.maxabs_scale - preprocessing.minmax_scale - preprocessing.normalize - preprocessing.quantile_transform - preprocessing.robust_scale - preprocessing.scale - preprocessing.power_transform - - -.. _random_projection_ref: - -:mod:`sklearn.random_projection`: Random projection -=================================================== - -.. automodule:: sklearn.random_projection - :no-members: - :no-inherited-members: - -**User guide:** See the :ref:`random_projection` section for further details. - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: class.rst - - random_projection.GaussianRandomProjection - random_projection.SparseRandomProjection - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - random_projection.johnson_lindenstrauss_min_dim - - -.. _semi_supervised_ref: - -:mod:`sklearn.semi_supervised`: Semi-Supervised Learning -======================================================== - -.. automodule:: sklearn.semi_supervised - :no-members: - :no-inherited-members: - -**User guide:** See the :ref:`semi_supervised` section for further details. - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: class.rst - - semi_supervised.LabelPropagation - semi_supervised.LabelSpreading - semi_supervised.SelfTrainingClassifier - - -.. _svm_ref: - -:mod:`sklearn.svm`: Support Vector Machines -=========================================== - -.. automodule:: sklearn.svm - :no-members: - :no-inherited-members: - -**User guide:** See the :ref:`svm` section for further details. - -Estimators ----------- - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: class.rst - - svm.LinearSVC - svm.LinearSVR - svm.NuSVC - svm.NuSVR - svm.OneClassSVM - svm.SVC - svm.SVR - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - svm.l1_min_c - -.. _tree_ref: - -:mod:`sklearn.tree`: Decision Trees -=================================== - -.. automodule:: sklearn.tree - :no-members: - :no-inherited-members: - -**User guide:** See the :ref:`tree` section for further details. - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: class.rst - - tree.DecisionTreeClassifier - tree.DecisionTreeRegressor - tree.ExtraTreeClassifier - tree.ExtraTreeRegressor - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - tree.export_graphviz - tree.export_text - -Plotting --------- - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - tree.plot_tree - -.. _utils_ref: - -:mod:`sklearn.utils`: Utilities -=============================== - -.. automodule:: sklearn.utils - :no-members: - :no-inherited-members: - -**Developer guide:** See the :ref:`developers-utils` page for further details. - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: class.rst - - utils.Bunch - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - utils.as_float_array - utils.assert_all_finite - utils.deprecated - utils.estimator_html_repr - utils.gen_batches - utils.gen_even_slices - utils.indexable - utils.murmurhash3_32 - utils.resample - utils._safe_indexing - utils.safe_mask - utils.safe_sqr - utils.shuffle - -Input and parameter validation ------------------------------- - -.. automodule:: sklearn.utils.validation - :no-members: - :no-inherited-members: - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - utils.check_X_y - utils.check_array - utils.check_scalar - utils.check_consistent_length - utils.check_random_state - utils.validation.check_is_fitted - utils.validation.check_memory - utils.validation.check_symmetric - utils.validation.column_or_1d - utils.validation.has_fit_parameter - -Utilities used in meta-estimators ---------------------------------- - -.. automodule:: sklearn.utils.metaestimators - :no-members: - :no-inherited-members: - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - utils.metaestimators.available_if - -Utilities to handle weights based on class labels -------------------------------------------------- - -.. automodule:: sklearn.utils.class_weight - :no-members: - :no-inherited-members: - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - utils.class_weight.compute_class_weight - utils.class_weight.compute_sample_weight - -Utilities to deal with multiclass target in classifiers -------------------------------------------------------- - -.. automodule:: sklearn.utils.multiclass - :no-members: - :no-inherited-members: - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - utils.multiclass.type_of_target - utils.multiclass.is_multilabel - utils.multiclass.unique_labels - -Utilities for optimal mathematical operations ---------------------------------------------- - -.. automodule:: sklearn.utils.extmath - :no-members: - :no-inherited-members: - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - utils.extmath.safe_sparse_dot - utils.extmath.randomized_range_finder - utils.extmath.randomized_svd - utils.extmath.fast_logdet - utils.extmath.density - utils.extmath.weighted_mode - -Utilities to work with sparse matrices and arrays -------------------------------------------------- - -.. automodule:: sklearn.utils.sparsefuncs - :no-members: - :no-inherited-members: - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - utils.sparsefuncs.incr_mean_variance_axis - utils.sparsefuncs.inplace_column_scale - utils.sparsefuncs.inplace_row_scale - utils.sparsefuncs.inplace_swap_row - utils.sparsefuncs.inplace_swap_column - utils.sparsefuncs.mean_variance_axis - utils.sparsefuncs.inplace_csr_column_scale - -.. automodule:: sklearn.utils.sparsefuncs_fast - :no-members: - :no-inherited-members: - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - utils.sparsefuncs_fast.inplace_csr_row_normalize_l1 - utils.sparsefuncs_fast.inplace_csr_row_normalize_l2 - -Utilities to work with graphs ------------------------------ - -.. automodule:: sklearn.utils.graph - :no-members: - :no-inherited-members: - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - utils.graph.single_source_shortest_path_length - -Utilities for random sampling ------------------------------ - -.. automodule:: sklearn.utils.random - :no-members: - :no-inherited-members: - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - utils.random.sample_without_replacement - - -Utilities to operate on arrays ------------------------------- - -.. automodule:: sklearn.utils.arrayfuncs - :no-members: - :no-inherited-members: - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - utils.arrayfuncs.min_pos - -Metadata routing ----------------- - -.. automodule:: sklearn.utils.metadata_routing - :no-members: - :no-inherited-members: - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - utils.metadata_routing.get_routing_for_object - utils.metadata_routing.process_routing - -.. autosummary:: - :toctree: generated/ - :template: class.rst - - utils.metadata_routing.MetadataRouter - utils.metadata_routing.MetadataRequest - utils.metadata_routing.MethodMapping - -Scikit-learn object discovery ------------------------------ - -.. automodule:: sklearn.utils.discovery - :no-members: - :no-inherited-members: - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - utils.discovery.all_estimators - utils.discovery.all_displays - utils.discovery.all_functions - -Scikit-learn compatibility checker ----------------------------------- - -.. automodule:: sklearn.utils.estimator_checks - :no-members: - :no-inherited-members: - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - utils.estimator_checks.check_estimator - utils.estimator_checks.parametrize_with_checks - -Utilities for parallel computing --------------------------------- - -.. automodule:: sklearn.utils.parallel - :no-members: - :no-inherited-members: - -.. currentmodule:: sklearn - -.. autosummary:: - :toctree: generated/ - :template: function.rst - - utils.parallel.delayed - utils.parallel_backend - utils.register_parallel_backend - -.. autosummary:: - :toctree: generated/ - :template: class.rst - - utils.parallel.Parallel - - -Recently deprecated -=================== diff --git a/doc/modules/clustering.rst b/doc/modules/clustering.rst index ed27b369171e5..2de39d0317bf5 100644 --- a/doc/modules/clustering.rst +++ b/doc/modules/clustering.rst @@ -241,13 +241,13 @@ K-means can be used for vector quantization. This is achieved using the performing vector quantization on an image refer to :ref:`sphx_glr_auto_examples_cluster_plot_color_quantization.py`. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_cluster_plot_cluster_iris.py`: Example usage of - :class:`KMeans` using the iris dataset +* :ref:`sphx_glr_auto_examples_cluster_plot_cluster_iris.py`: Example usage of + :class:`KMeans` using the iris dataset - * :ref:`sphx_glr_auto_examples_text_plot_document_clustering.py`: Document clustering - using :class:`KMeans` and :class:`MiniBatchKMeans` based on sparse data +* :ref:`sphx_glr_auto_examples_text_plot_document_clustering.py`: Document clustering + using :class:`KMeans` and :class:`MiniBatchKMeans` based on sparse data Low-level parallelism --------------------- @@ -257,24 +257,20 @@ chunks of data (256 samples) are processed in parallel, which in addition yields a low memory footprint. For more details on how to control the number of threads, please refer to our :ref:`parallelism` notes. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_cluster_plot_kmeans_assumptions.py`: Demonstrating - when k-means performs intuitively and when it does not - * :ref:`sphx_glr_auto_examples_cluster_plot_kmeans_digits.py`: Clustering - handwritten digits +* :ref:`sphx_glr_auto_examples_cluster_plot_kmeans_assumptions.py`: Demonstrating when + k-means performs intuitively and when it does not +* :ref:`sphx_glr_auto_examples_cluster_plot_kmeans_digits.py`: Clustering handwritten digits +.. dropdown:: References -|details-start| -**References** -|details-split| + * `"k-means++: The advantages of careful seeding" + `_ + Arthur, David, and Sergei Vassilvitskii, + *Proceedings of the eighteenth annual ACM-SIAM symposium on Discrete + algorithms*, Society for Industrial and Applied Mathematics (2007) -* `"k-means++: The advantages of careful seeding" - `_ Arthur, David, and - Sergei Vassilvitskii, *Proceedings of the eighteenth annual ACM-SIAM symposium - on Discrete algorithms*, Society for Industrial and Applied Mathematics (2007) - -|details-end| .. _mini_batch_kmeans: @@ -310,24 +306,22 @@ small, as shown in the example and cited reference. :scale: 100 -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_cluster_plot_mini_batch_kmeans.py`: Comparison of - :class:`KMeans` and :class:`MiniBatchKMeans` +* :ref:`sphx_glr_auto_examples_cluster_plot_mini_batch_kmeans.py`: Comparison of + :class:`KMeans` and :class:`MiniBatchKMeans` - * :ref:`sphx_glr_auto_examples_text_plot_document_clustering.py`: Document clustering - using :class:`KMeans` and :class:`MiniBatchKMeans` based on sparse data +* :ref:`sphx_glr_auto_examples_text_plot_document_clustering.py`: Document clustering + using :class:`KMeans` and :class:`MiniBatchKMeans` based on sparse data -|details-start| -**References** -|details-split| +* :ref:`sphx_glr_auto_examples_cluster_plot_dict_face_patches.py` -* `"Web Scale K-Means clustering" - `_ - D. Sculley, *Proceedings of the 19th international conference on World - wide web* (2010) +.. dropdown:: References -|details-end| + * `"Web Scale K-Means clustering" + `_ + D. Sculley, *Proceedings of the 19th international conference on World + wide web* (2010) .. _affinity_propagation: @@ -364,55 +358,50 @@ convergence. Further, the memory complexity is of the order sparse similarity matrix is used. This makes Affinity Propagation most appropriate for small to medium sized datasets. -|details-start| -**Algorithm description** -|details-split| - -The messages sent between points belong to one of two categories. The first is -the responsibility :math:`r(i, k)`, which is the accumulated evidence that -sample :math:`k` should be the exemplar for sample :math:`i`. The second is the -availability :math:`a(i, k)` which is the accumulated evidence that sample -:math:`i` should choose sample :math:`k` to be its exemplar, and considers the -values for all other samples that :math:`k` should be an exemplar. In this way, -exemplars are chosen by samples if they are (1) similar enough to many samples -and (2) chosen by many samples to be representative of themselves. +.. dropdown:: Algorithm description -More formally, the responsibility of a sample :math:`k` to be the exemplar of -sample :math:`i` is given by: + The messages sent between points belong to one of two categories. The first is + the responsibility :math:`r(i, k)`, which is the accumulated evidence that + sample :math:`k` should be the exemplar for sample :math:`i`. The second is the + availability :math:`a(i, k)` which is the accumulated evidence that sample + :math:`i` should choose sample :math:`k` to be its exemplar, and considers the + values for all other samples that :math:`k` should be an exemplar. In this way, + exemplars are chosen by samples if they are (1) similar enough to many samples + and (2) chosen by many samples to be representative of themselves. -.. math:: + More formally, the responsibility of a sample :math:`k` to be the exemplar of + sample :math:`i` is given by: - r(i, k) \leftarrow s(i, k) - max [ a(i, k') + s(i, k') \forall k' \neq k ] + .. math:: -Where :math:`s(i, k)` is the similarity between samples :math:`i` and :math:`k`. -The availability of sample :math:`k` to be the exemplar of sample :math:`i` is -given by: - -.. math:: + r(i, k) \leftarrow s(i, k) - max [ a(i, k') + s(i, k') \forall k' \neq k ] - a(i, k) \leftarrow min [0, r(k, k) + \sum_{i'~s.t.~i' \notin \{i, k\}}{r(i', - k)}] + Where :math:`s(i, k)` is the similarity between samples :math:`i` and :math:`k`. + The availability of sample :math:`k` to be the exemplar of sample :math:`i` is + given by: -To begin with, all values for :math:`r` and :math:`a` are set to zero, and the -calculation of each iterates until convergence. As discussed above, in order to -avoid numerical oscillations when updating the messages, the damping factor -:math:`\lambda` is introduced to iteration process: + .. math:: -.. math:: r_{t+1}(i, k) = \lambda\cdot r_{t}(i, k) + (1-\lambda)\cdot r_{t+1}(i, k) -.. math:: a_{t+1}(i, k) = \lambda\cdot a_{t}(i, k) + (1-\lambda)\cdot a_{t+1}(i, k) + a(i, k) \leftarrow min [0, r(k, k) + \sum_{i'~s.t.~i' \notin \{i, k\}}{r(i', + k)}] -where :math:`t` indicates the iteration times. + To begin with, all values for :math:`r` and :math:`a` are set to zero, and the + calculation of each iterates until convergence. As discussed above, in order to + avoid numerical oscillations when updating the messages, the damping factor + :math:`\lambda` is introduced to iteration process: -|details-end| + .. math:: r_{t+1}(i, k) = \lambda\cdot r_{t}(i, k) + (1-\lambda)\cdot r_{t+1}(i, k) + .. math:: a_{t+1}(i, k) = \lambda\cdot a_{t}(i, k) + (1-\lambda)\cdot a_{t+1}(i, k) + where :math:`t` indicates the iteration times. -.. topic:: Examples: - * :ref:`sphx_glr_auto_examples_cluster_plot_affinity_propagation.py`: Affinity - Propagation on a synthetic 2D datasets with 3 classes. +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_applications_plot_stock_market.py` Affinity - Propagation on Financial time series to find groups of companies +* :ref:`sphx_glr_auto_examples_cluster_plot_affinity_propagation.py`: Affinity + Propagation on a synthetic 2D datasets with 3 classes +* :ref:`sphx_glr_auto_examples_applications_plot_stock_market.py` Affinity Propagation + on financial time series to find groups of companies .. _mean_shift: @@ -425,43 +414,40 @@ 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. -|details-start| -**Mathematical details** -|details-split| +.. dropdown:: Mathematical details -The position of centroid candidates is iteratively adjusted using a technique -called hill climbing, which finds local maxima of the estimated probability -density. Given a candidate centroid :math:`x` for iteration :math:`t`, the -candidate is updated according to the following equation: + The position of centroid candidates is iteratively adjusted using a technique + called hill climbing, which finds local maxima of the estimated probability + density. Given a candidate centroid :math:`x` for iteration :math:`t`, the + candidate is updated according to the following equation: -.. math:: + .. math:: - x^{t+1} = x^t + m(x^t) + x^{t+1} = x^t + m(x^t) -Where :math:`m` is the *mean shift* vector that is computed for each centroid -that points towards a region of the maximum increase in the density of points. -To compute :math:`m` we define :math:`N(x)` as the neighborhood of samples -within a given distance around :math:`x`. Then :math:`m` is computed using the -following equation, effectively updating a centroid to be the mean of the -samples within its neighborhood: + Where :math:`m` is the *mean shift* vector that is computed for each centroid + that points towards a region of the maximum increase in the density of points. + To compute :math:`m` we define :math:`N(x)` as the neighborhood of samples + within a given distance around :math:`x`. Then :math:`m` is computed using the + following equation, effectively updating a centroid to be the mean of the + samples within its neighborhood: -.. math:: + .. math:: - m(x) = \frac{1}{|N(x)|} \sum_{x_j \in N(x)}x_j - x + m(x) = \frac{1}{|N(x)|} \sum_{x_j \in N(x)}x_j - x -In general, the equation for :math:`m` depends on a kernel used for density -estimation. The generic formula is: + In general, the equation for :math:`m` depends on a kernel used for density + estimation. The generic formula is: -.. math:: + .. math:: - m(x) = \frac{\sum_{x_j \in N(x)}K(x_j - x)x_j}{\sum_{x_j \in N(x)}K(x_j - - x)} - x + m(x) = \frac{\sum_{x_j \in N(x)}K(x_j - x)x_j}{\sum_{x_j \in N(x)}K(x_j - + x)} - x -In our implementation, :math:`K(x)` is equal to 1 if :math:`x` is small enough -and is equal to 0 otherwise. Effectively :math:`K(y - x)` indicates whether -:math:`y` is in the neighborhood of :math:`x`. + In our implementation, :math:`K(x)` is equal to 1 if :math:`x` is small enough + and is equal to 0 otherwise. Effectively :math:`K(y - x)` indicates whether + :math:`y` is in the neighborhood of :math:`x`. -|details-end| The algorithm automatically sets the number of clusters, instead of relying on a parameter ``bandwidth``, which dictates the size of the region to search through. @@ -483,21 +469,17 @@ given sample. :scale: 50 -.. topic:: Examples: - - * :ref:`sphx_glr_auto_examples_cluster_plot_mean_shift.py`: Mean Shift - clustering on a synthetic 2D datasets with 3 classes. +.. rubric:: Examples +* :ref:`sphx_glr_auto_examples_cluster_plot_mean_shift.py`: Mean Shift clustering + on a synthetic 2D datasets with 3 classes. -|details-start| -**References** -|details-split| +.. dropdown:: References -* :doi:`"Mean shift: A robust approach toward feature space analysis" - <10.1109/34.1000236>` D. Comaniciu and P. Meer, *IEEE Transactions on Pattern - Analysis and Machine Intelligence* (2002) + * :doi:`"Mean shift: A robust approach toward feature space analysis" + <10.1109/34.1000236>` D. Comaniciu and P. Meer, *IEEE Transactions on Pattern + Analysis and Machine Intelligence* (2002) -|details-end| .. _spectral_clustering: @@ -547,13 +529,13 @@ computed using a function of a gradient of the image. See the examples for such an application. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_cluster_plot_segmentation_toy.py`: Segmenting - objects from a noisy background using spectral clustering. +* :ref:`sphx_glr_auto_examples_cluster_plot_segmentation_toy.py`: Segmenting objects + from a noisy background using spectral clustering. +* :ref:`sphx_glr_auto_examples_cluster_plot_coin_segmentation.py`: Spectral clustering + to split the image of coins in regions. - * :ref:`sphx_glr_auto_examples_cluster_plot_coin_segmentation.py`: Spectral - clustering to split the image of coins in regions. .. |coin_kmeans| image:: ../auto_examples/cluster/images/sphx_glr_plot_coin_segmentation_001.png :target: ../auto_examples/cluster/plot_coin_segmentation.html @@ -588,18 +570,15 @@ below. |coin_kmeans| |coin_discretize| |coin_cluster_qr| ================================ ================================ ================================ -|details-start| -**References** -|details-split| +.. dropdown:: References -* `"Multiclass spectral clustering" - `_ - Stella X. Yu, Jianbo Shi, 2003 + * `"Multiclass spectral clustering" + `_ + Stella X. Yu, Jianbo Shi, 2003 -* :doi:`"Simple, direct, and efficient multi-way spectral clustering"<10.1093/imaiai/iay008>` - Anil Damle, Victor Minden, Lexing Ying, 2019 + * :doi:`"Simple, direct, and efficient multi-way spectral clustering"<10.1093/imaiai/iay008>` + Anil Damle, Victor Minden, Lexing Ying, 2019 -|details-end| .. _spectral_clustering_graph: @@ -615,28 +594,25 @@ graph, and SpectralClustering is initialized with `affinity='precomputed'`:: ... assign_labels='discretize') >>> sc.fit_predict(adjacency_matrix) # doctest: +SKIP -|details-start| -**References** -|details-split| +.. dropdown:: References -* :doi:`"A Tutorial on Spectral Clustering" <10.1007/s11222-007-9033-z>` Ulrike - von Luxburg, 2007 + * :doi:`"A Tutorial on Spectral Clustering" <10.1007/s11222-007-9033-z>` Ulrike + von Luxburg, 2007 -* :doi:`"Normalized cuts and image segmentation" <10.1109/34.868688>` Jianbo - Shi, Jitendra Malik, 2000 + * :doi:`"Normalized cuts and image segmentation" <10.1109/34.868688>` Jianbo + Shi, Jitendra Malik, 2000 -* `"A Random Walks View of Spectral Segmentation" - `_ - Marina Meila, Jianbo Shi, 2001 + * `"A Random Walks View of Spectral Segmentation" + `_ + Marina Meila, Jianbo Shi, 2001 -* `"On Spectral Clustering: Analysis and an algorithm" - `_ - Andrew Y. Ng, Michael I. Jordan, Yair Weiss, 2001 + * `"On Spectral Clustering: Analysis and an algorithm" + `_ + Andrew Y. Ng, Michael I. Jordan, Yair Weiss, 2001 -* :arxiv:`"Preconditioned Spectral Clustering for Stochastic Block Partition - Streaming Graph Challenge" <1708.07481>` David Zhuzhunashvili, Andrew Knyazev + * :arxiv:`"Preconditioned Spectral Clustering for Stochastic Block Partition + Streaming Graph Challenge" <1708.07481>` David Zhuzhunashvili, Andrew Knyazev -|details-end| .. _hierarchical_clustering: @@ -697,10 +673,10 @@ while not robust to noisy data, can be computed very efficiently and can therefore be useful to provide hierarchical clustering of larger datasets. Single linkage can also perform well on non-globular data. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_cluster_plot_digits_linkage.py`: exploration of - the different linkage strategies in a real dataset. +* :ref:`sphx_glr_auto_examples_cluster_plot_digits_linkage.py`: exploration of the + different linkage strategies in a real dataset. * :ref:`sphx_glr_auto_examples_cluster_plot_linkage_comparison.py`: exploration of the different linkage strategies in toy datasets. @@ -717,9 +693,9 @@ of the data, though more so in the case of small sample sizes. :target: ../auto_examples/cluster/plot_agglomerative_dendrogram.html :scale: 42 -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_cluster_plot_agglomerative_dendrogram.py` +* :ref:`sphx_glr_auto_examples_cluster_plot_agglomerative_dendrogram.py` Adding connectivity constraints @@ -788,20 +764,20 @@ enable only merging of neighboring pixels on an image, as in the :target: ../auto_examples/cluster/plot_agglomerative_clustering.html :scale: 38 -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_cluster_plot_coin_ward_segmentation.py`: Ward - clustering to split the image of coins in regions. +* :ref:`sphx_glr_auto_examples_cluster_plot_coin_ward_segmentation.py`: Ward + clustering to split the image of coins in regions. - * :ref:`sphx_glr_auto_examples_cluster_plot_ward_structured_vs_unstructured.py`: Example - of Ward algorithm on a swiss-roll, comparison of structured approaches - versus unstructured approaches. +* :ref:`sphx_glr_auto_examples_cluster_plot_ward_structured_vs_unstructured.py`: Example + of Ward algorithm on a swiss-roll, comparison of structured approaches + versus unstructured approaches. - * :ref:`sphx_glr_auto_examples_cluster_plot_feature_agglomeration_vs_univariate_selection.py`: Example - of dimensionality reduction with feature agglomeration based on Ward - hierarchical clustering. +* :ref:`sphx_glr_auto_examples_cluster_plot_feature_agglomeration_vs_univariate_selection.py`: Example + of dimensionality reduction with feature agglomeration based on Ward + hierarchical clustering. - * :ref:`sphx_glr_auto_examples_cluster_plot_agglomerative_clustering.py` +* :ref:`sphx_glr_auto_examples_cluster_plot_agglomerative_clustering.py` Varying the metric @@ -835,9 +811,9 @@ each class. :target: ../auto_examples/cluster/plot_agglomerative_clustering_metrics.html :scale: 32 -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_cluster_plot_agglomerative_clustering_metrics.py` +* :ref:`sphx_glr_auto_examples_cluster_plot_agglomerative_clustering_metrics.py` Bisecting K-Means @@ -881,26 +857,23 @@ Difference between Bisecting K-Means and regular K-Means can be seen on example While the regular K-Means algorithm tends to create non-related clusters, clusters from Bisecting K-Means are well ordered and create quite a visible hierarchy. -|details-start| -**References** -|details-split| - -* `"A Comparison of Document Clustering Techniques" - `_ Michael - Steinbach, George Karypis and Vipin Kumar, Department of Computer Science and - Egineering, University of Minnesota (June 2000) -* `"Performance Analysis of K-Means and Bisecting K-Means Algorithms in Weblog - Data" - `_ - K.Abirami and Dr.P.Mayilvahanan, International Journal of Emerging - Technologies in Engineering Research (IJETER) Volume 4, Issue 8, (August 2016) -* `"Bisecting K-means Algorithm Based on K-valued Self-determining and - Clustering Center Optimization" - `_ Jian Di, Xinyue Gou School - of Control and Computer Engineering,North China Electric Power University, - Baoding, Hebei, China (August 2017) - -|details-end| +.. dropdown:: References + + * `"A Comparison of Document Clustering Techniques" + `_ Michael + Steinbach, George Karypis and Vipin Kumar, Department of Computer Science and + Egineering, University of Minnesota (June 2000) + * `"Performance Analysis of K-Means and Bisecting K-Means Algorithms in Weblog + Data" + `_ + K.Abirami and Dr.P.Mayilvahanan, International Journal of Emerging + Technologies in Engineering Research (IJETER) Volume 4, Issue 8, (August 2016) + * `"Bisecting K-means Algorithm Based on K-valued Self-determining and + Clustering Center Optimization" + `_ Jian Di, Xinyue Gou School + of Control and Computer Engineering,North China Electric Power University, + Baoding, Hebei, China (August 2017) + .. _dbscan: @@ -954,79 +927,68 @@ samples that are still part of a cluster. Moreover, the outliers are indicated by black points below. .. |dbscan_results| image:: ../auto_examples/cluster/images/sphx_glr_plot_dbscan_002.png - :target: ../auto_examples/cluster/plot_dbscan.html - :scale: 50 + :target: ../auto_examples/cluster/plot_dbscan.html + :scale: 50 .. centered:: |dbscan_results| -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_cluster_plot_dbscan.py` +* :ref:`sphx_glr_auto_examples_cluster_plot_dbscan.py` -|details-start| -**Implementation** -|details-split| +.. dropdown:: Implementation -The DBSCAN algorithm is deterministic, always generating the same clusters when -given the same data in the same order. However, the results can differ when -data is provided in a different order. First, even though the core samples will -always be assigned to the same clusters, the labels of those clusters will -depend on the order in which those samples are encountered in the data. Second -and more importantly, the clusters to which non-core samples are assigned can -differ depending on the data order. This would happen when a non-core sample -has a distance lower than ``eps`` to two core samples in different clusters. By -the triangular inequality, those two core samples must be more distant than -``eps`` from each other, or they would be in the same cluster. The non-core -sample is assigned to whichever cluster is generated first in a pass through the -data, and so the results will depend on the data ordering. + The DBSCAN algorithm is deterministic, always generating the same clusters when + given the same data in the same order. However, the results can differ when + data is provided in a different order. First, even though the core samples will + always be assigned to the same clusters, the labels of those clusters will + depend on the order in which those samples are encountered in the data. Second + and more importantly, the clusters to which non-core samples are assigned can + differ depending on the data order. This would happen when a non-core sample + has a distance lower than ``eps`` to two core samples in different clusters. By + the triangular inequality, those two core samples must be more distant than + ``eps`` from each other, or they would be in the same cluster. The non-core + sample is assigned to whichever cluster is generated first in a pass through the + data, and so the results will depend on the data ordering. -The current implementation uses ball trees and kd-trees to determine the -neighborhood of points, which avoids calculating the full distance matrix (as -was done in scikit-learn versions before 0.14). The possibility to use custom -metrics is retained; for details, see :class:`NearestNeighbors`. + The current implementation uses ball trees and kd-trees to determine the + neighborhood of points, which avoids calculating the full distance matrix (as + was done in scikit-learn versions before 0.14). The possibility to use custom + metrics is retained; for details, see :class:`NearestNeighbors`. -|details-end| +.. dropdown:: Memory consumption for large sample sizes -|details-start| -**Memory consumption for large sample sizes** -|details-split| + This implementation is by default not memory efficient because it constructs a + full pairwise similarity matrix in the case where kd-trees or ball-trees cannot + be used (e.g., with sparse matrices). This matrix will consume :math:`n^2` + floats. A couple of mechanisms for getting around this are: -This implementation is by default not memory efficient because it constructs a -full pairwise similarity matrix in the case where kd-trees or ball-trees cannot -be used (e.g., with sparse matrices). This matrix will consume :math:`n^2` -floats. A couple of mechanisms for getting around this are: + - Use :ref:`OPTICS ` clustering in conjunction with the `extract_dbscan` + method. OPTICS clustering also calculates the full pairwise matrix, but only + keeps one row in memory at a time (memory complexity n). -- Use :ref:`OPTICS ` clustering in conjunction with the `extract_dbscan` - method. OPTICS clustering also calculates the full pairwise matrix, but only - keeps one row in memory at a time (memory complexity n). + - A sparse radius neighborhood graph (where missing entries are presumed to be + out of eps) can be precomputed in a memory-efficient way and dbscan can be run + over this with ``metric='precomputed'``. See + :meth:`sklearn.neighbors.NearestNeighbors.radius_neighbors_graph`. -- A sparse radius neighborhood graph (where missing entries are presumed to be - out of eps) can be precomputed in a memory-efficient way and dbscan can be run - over this with ``metric='precomputed'``. See - :meth:`sklearn.neighbors.NearestNeighbors.radius_neighbors_graph`. + - The dataset can be compressed, either by removing exact duplicates if these + occur in your data, or by using BIRCH. Then you only have a relatively small + number of representatives for a large number of points. You can then provide a + ``sample_weight`` when fitting DBSCAN. -- The dataset can be compressed, either by removing exact duplicates if these - occur in your data, or by using BIRCH. Then you only have a relatively small - number of representatives for a large number of points. You can then provide a - ``sample_weight`` when fitting DBSCAN. - -|details-end| - -|details-start| -**References** -|details-split| +.. dropdown:: References * `A Density-Based Algorithm for Discovering Clusters in Large Spatial Databases with Noise `_ Ester, M., H. P. Kriegel, J. Sander, and X. Xu, In Proceedings of the 2nd International Conference on Knowledge Discovery and Data Mining, Portland, OR, - AAAI Press, pp. 226–231. 1996 + AAAI Press, pp. 226-231. 1996 * :doi:`DBSCAN revisited, revisited: why and how you should (still) use DBSCAN. <10.1145/3068335>` Schubert, E., Sander, J., Ester, M., Kriegel, H. P., & Xu, X. (2017). In ACM Transactions on Database Systems (TODS), 42(3), 19. -|details-end| .. _hdbscan: @@ -1046,9 +1008,9 @@ scales by building an alternative representation of the clustering problem. This implementation is adapted from the original implementation of HDBSCAN, `scikit-learn-contrib/hdbscan `_ based on [LJ2017]_. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_cluster_plot_hdbscan.py` +* :ref:`sphx_glr_auto_examples_cluster_plot_hdbscan.py` Mutual Reachability Graph ------------------------- @@ -1109,11 +1071,11 @@ it relies solely on the choice of `min_samples`, which tends to be a more robust hyperparameter. .. |hdbscan_ground_truth| image:: ../auto_examples/cluster/images/sphx_glr_plot_hdbscan_005.png - :target: ../auto_examples/cluster/plot_hdbscan.html - :scale: 75 + :target: ../auto_examples/cluster/plot_hdbscan.html + :scale: 75 .. |hdbscan_results| image:: ../auto_examples/cluster/images/sphx_glr_plot_hdbscan_007.png - :target: ../auto_examples/cluster/plot_hdbscan.html - :scale: 75 + :target: ../auto_examples/cluster/plot_hdbscan.html + :scale: 75 .. centered:: |hdbscan_ground_truth| .. centered:: |hdbscan_results| @@ -1124,19 +1086,19 @@ than `minimum_cluster_size` many samples are considered noise. In practice, one can set `minimum_cluster_size = min_samples` to couple the parameters and simplify the hyperparameter space. -.. topic:: References: +.. rubric:: References - .. [CM2013] Campello, R.J.G.B., Moulavi, D., Sander, J. (2013). Density-Based - Clustering Based on Hierarchical Density Estimates. In: Pei, J., Tseng, V.S., - Cao, L., Motoda, H., Xu, G. (eds) Advances in Knowledge Discovery and Data - Mining. PAKDD 2013. Lecture Notes in Computer Science(), vol 7819. Springer, - Berlin, Heidelberg. :doi:`Density-Based Clustering Based on Hierarchical - Density Estimates <10.1007/978-3-642-37456-2_14>` +.. [CM2013] Campello, R.J.G.B., Moulavi, D., Sander, J. (2013). Density-Based + Clustering Based on Hierarchical Density Estimates. In: Pei, J., Tseng, V.S., + Cao, L., Motoda, H., Xu, G. (eds) Advances in Knowledge Discovery and Data + Mining. PAKDD 2013. Lecture Notes in Computer Science(), vol 7819. Springer, + Berlin, Heidelberg. :doi:`Density-Based Clustering Based on Hierarchical + Density Estimates <10.1007/978-3-642-37456-2_14>` - .. [LJ2017] L. McInnes and J. Healy, (2017). Accelerated Hierarchical Density - Based Clustering. In: IEEE International Conference on Data Mining Workshops - (ICDMW), 2017, pp. 33-42. :doi:`Accelerated Hierarchical Density Based - Clustering <10.1109/ICDMW.2017.12>` +.. [LJ2017] L. McInnes and J. Healy, (2017). Accelerated Hierarchical Density + Based Clustering. In: IEEE International Conference on Data Mining Workshops + (ICDMW), 2017, pp. 33-42. :doi:`Accelerated Hierarchical Density Based + Clustering <10.1109/ICDMW.2017.12>` .. _optics: @@ -1182,58 +1144,48 @@ the linear segment clusters of the reachability plot. Note that the blue and red clusters are adjacent in the reachability plot, and can be hierarchically represented as children of a larger parent cluster. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_cluster_plot_optics.py` +* :ref:`sphx_glr_auto_examples_cluster_plot_optics.py` -|details-start| -**Comparison with DBSCAN** -|details-split| +.. dropdown:: Comparison with DBSCAN -The results from OPTICS ``cluster_optics_dbscan`` method and DBSCAN are very -similar, but not always identical; specifically, labeling of periphery and noise -points. This is in part because the first samples of each dense area processed -by OPTICS have a large reachability value while being close to other points in -their area, and will thus sometimes be marked as noise rather than periphery. -This affects adjacent points when they are considered as candidates for being -marked as either periphery or noise. + The results from OPTICS ``cluster_optics_dbscan`` method and DBSCAN are very + similar, but not always identical; specifically, labeling of periphery and noise + points. This is in part because the first samples of each dense area processed + by OPTICS have a large reachability value while being close to other points in + their area, and will thus sometimes be marked as noise rather than periphery. + This affects adjacent points when they are considered as candidates for being + marked as either periphery or noise. -Note that for any single value of ``eps``, DBSCAN will tend to have a shorter -run time than OPTICS; however, for repeated runs at varying ``eps`` values, a -single run of OPTICS may require less cumulative runtime than DBSCAN. It is also -important to note that OPTICS' output is close to DBSCAN's only if ``eps`` and -``max_eps`` are close. + Note that for any single value of ``eps``, DBSCAN will tend to have a shorter + run time than OPTICS; however, for repeated runs at varying ``eps`` values, a + single run of OPTICS may require less cumulative runtime than DBSCAN. It is also + important to note that OPTICS' output is close to DBSCAN's only if ``eps`` and + ``max_eps`` are close. -|details-end| +.. dropdown:: Computational Complexity -|details-start| -**Computational Complexity** -|details-split| + Spatial indexing trees are used to avoid calculating the full distance matrix, + and allow for efficient memory usage on large sets of samples. Different + distance metrics can be supplied via the ``metric`` keyword. -Spatial indexing trees are used to avoid calculating the full distance matrix, -and allow for efficient memory usage on large sets of samples. Different -distance metrics can be supplied via the ``metric`` keyword. + For large datasets, similar (but not identical) results can be obtained via + :class:`HDBSCAN`. The HDBSCAN implementation is multithreaded, and has better + algorithmic runtime complexity than OPTICS, at the cost of worse memory scaling. + For extremely large datasets that exhaust system memory using HDBSCAN, OPTICS + will maintain :math:`n` (as opposed to :math:`n^2`) memory scaling; however, + tuning of the ``max_eps`` parameter will likely need to be used to give a + solution in a reasonable amount of wall time. -For large datasets, similar (but not identical) results can be obtained via -:class:`HDBSCAN`. The HDBSCAN implementation is multithreaded, and has better -algorithmic runtime complexity than OPTICS, at the cost of worse memory scaling. -For extremely large datasets that exhaust system memory using HDBSCAN, OPTICS -will maintain :math:`n` (as opposed to :math:`n^2`) memory scaling; however, -tuning of the ``max_eps`` parameter will likely need to be used to give a -solution in a reasonable amount of wall time. -|details-end| +.. dropdown:: References -|details-start| -**References** -|details-split| + * "OPTICS: ordering points to identify the clustering structure." Ankerst, + Mihael, Markus M. Breunig, Hans-Peter Kriegel, and Jörg Sander. In ACM Sigmod + Record, vol. 28, no. 2, pp. 49-60. ACM, 1999. -* "OPTICS: ordering points to identify the clustering structure." Ankerst, - Mihael, Markus M. Breunig, Hans-Peter Kriegel, and Jörg Sander. In ACM Sigmod - Record, vol. 28, no. 2, pp. 49-60. ACM, 1999. - -|details-end| .. _birch: @@ -1269,75 +1221,60 @@ If ``n_clusters`` is set to None, the subclusters from the leaves are directly read off, otherwise a global clustering step labels these subclusters into global clusters (labels) and the samples are mapped to the global label of the nearest subcluster. -|details-start| -**Algorithm description** -|details-split| - -- A new sample is inserted into the root of the CF Tree which is a CF Node. It - is then merged with the subcluster of the root, that has the smallest radius - after merging, constrained by the threshold and branching factor conditions. - If the subcluster has any child node, then this is done repeatedly till it - reaches a leaf. After finding the nearest subcluster in the leaf, the - properties of this subcluster and the parent subclusters are recursively - updated. - -- If the radius of the subcluster obtained by merging the new sample and the - nearest subcluster is greater than the square of the threshold and if the - number of subclusters is greater than the branching factor, then a space is - temporarily allocated to this new sample. The two farthest subclusters are - taken and the subclusters are divided into two groups on the basis of the - distance between these subclusters. - -- If this split node has a parent subcluster and there is room for a new - subcluster, then the parent is split into two. If there is no room, then this - node is again split into two and the process is continued recursively, till it - reaches the root. - -|details-end| - -|details-start| -**BIRCH or MiniBatchKMeans?** -|details-split| - -- BIRCH does not scale very well to high dimensional data. As a rule of thumb if - ``n_features`` is greater than twenty, it is generally better to use MiniBatchKMeans. -- If the number of instances of data needs to be reduced, or if one wants a - large number of subclusters either as a preprocessing step or otherwise, - BIRCH is more useful than MiniBatchKMeans. - -.. image:: ../auto_examples/cluster/images/sphx_glr_plot_birch_vs_minibatchkmeans_001.png +.. dropdown:: Algorithm description + + - A new sample is inserted into the root of the CF Tree which is a CF Node. It + is then merged with the subcluster of the root, that has the smallest radius + after merging, constrained by the threshold and branching factor conditions. + If the subcluster has any child node, then this is done repeatedly till it + reaches a leaf. After finding the nearest subcluster in the leaf, the + properties of this subcluster and the parent subclusters are recursively + updated. + + - If the radius of the subcluster obtained by merging the new sample and the + nearest subcluster is greater than the square of the threshold and if the + number of subclusters is greater than the branching factor, then a space is + temporarily allocated to this new sample. The two farthest subclusters are + taken and the subclusters are divided into two groups on the basis of the + distance between these subclusters. + + - If this split node has a parent subcluster and there is room for a new + subcluster, then the parent is split into two. If there is no room, then this + node is again split into two and the process is continued recursively, till it + reaches the root. + +.. dropdown:: BIRCH or MiniBatchKMeans? + + - BIRCH does not scale very well to high dimensional data. As a rule of thumb if + ``n_features`` is greater than twenty, it is generally better to use MiniBatchKMeans. + - If the number of instances of data needs to be reduced, or if one wants a + large number of subclusters either as a preprocessing step or otherwise, + BIRCH is more useful than MiniBatchKMeans. + + .. image:: ../auto_examples/cluster/images/sphx_glr_plot_birch_vs_minibatchkmeans_001.png :target: ../auto_examples/cluster/plot_birch_vs_minibatchkmeans.html -|details-end| - -|details-start| -**How to use partial_fit?** -|details-split| +.. dropdown:: How to use partial_fit? -To avoid the computation of global clustering, for every call of ``partial_fit`` -the user is advised + To avoid the computation of global clustering, for every call of ``partial_fit`` + the user is advised: -1. To set ``n_clusters=None`` initially -2. Train all data by multiple calls to partial_fit. -3. Set ``n_clusters`` to a required value using - ``brc.set_params(n_clusters=n_clusters)``. -4. Call ``partial_fit`` finally with no arguments, i.e. ``brc.partial_fit()`` - which performs the global clustering. + 1. To set ``n_clusters=None`` initially. + 2. Train all data by multiple calls to partial_fit. + 3. Set ``n_clusters`` to a required value using + ``brc.set_params(n_clusters=n_clusters)``. + 4. Call ``partial_fit`` finally with no arguments, i.e. ``brc.partial_fit()`` + which performs the global clustering. -|details-end| +.. dropdown:: References -|details-start| -**References** -|details-split| + * 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 -* 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 -* Roberto Perdisci JBirch - Java implementation of BIRCH clustering algorithm - https://code.google.com/archive/p/jbirch - -|details-end| .. _clustering_evaluation: @@ -1460,64 +1397,53 @@ will not necessarily be close to zero.:: ground truth clustering resulting in a high proportion of pair labels that agree, which leads subsequently to a high score. -.. topic:: Examples: - - * :ref:`sphx_glr_auto_examples_cluster_plot_adjusted_for_chance_measures.py`: - Analysis of the impact of the dataset size on the value of clustering measures - for random assignments. - - -|details-start| -**Mathematical formulation** -|details-split| +.. rubric:: Examples -If C is a ground truth class assignment and K the clustering, let us define -:math:`a` and :math:`b` as: +* :ref:`sphx_glr_auto_examples_cluster_plot_adjusted_for_chance_measures.py`: + Analysis of the impact of the dataset size on the value of + clustering measures for random assignments. -- :math:`a`, the number of pairs of elements that are in the same set in C and - in the same set in K +.. dropdown:: Mathematical formulation -- :math:`b`, the number of pairs of elements that are in different sets in C and - in different sets in K + If C is a ground truth class assignment and K the clustering, let us define + :math:`a` and :math:`b` as: -The unadjusted Rand index is then given by: + - :math:`a`, the number of pairs of elements that are in the same set in C and + in the same set in K -.. math:: \text{RI} = \frac{a + b}{C_2^{n_{samples}}} + - :math:`b`, the number of pairs of elements that are in different sets in C and + in different sets in K -where :math:`C_2^{n_{samples}}` is the total number of possible pairs in the -dataset. It does not matter if the calculation is performed on ordered pairs or -unordered pairs as long as the calculation is performed consistently. + The unadjusted Rand index is then given by: -However, the Rand index does not guarantee that random label assignments will -get a value close to zero (esp. if the number of clusters is in the same order -of magnitude as the number of samples). + .. math:: \text{RI} = \frac{a + b}{C_2^{n_{samples}}} -To counter this effect we can discount the expected RI :math:`E[\text{RI}]` of -random labelings by defining the adjusted Rand index as follows: + where :math:`C_2^{n_{samples}}` is the total number of possible pairs in the + dataset. It does not matter if the calculation is performed on ordered pairs or + unordered pairs as long as the calculation is performed consistently. -.. math:: \text{ARI} = \frac{\text{RI} - E[\text{RI}]}{\max(\text{RI}) - E[\text{RI}]} + However, the Rand index does not guarantee that random label assignments will + get a value close to zero (esp. if the number of clusters is in the same order + of magnitude as the number of samples). -|details-end| + To counter this effect we can discount the expected RI :math:`E[\text{RI}]` of + random labelings by defining the adjusted Rand index as follows: -|details-start| -**References** -|details-split| + .. math:: \text{ARI} = \frac{\text{RI} - E[\text{RI}]}{\max(\text{RI}) - E[\text{RI}]} -* `Comparing Partitions - `_ L. Hubert and P. - Arabie, Journal of Classification 1985 +.. dropdown:: References -* `Properties of the Hubert-Arabie adjusted Rand index - `_ D. Steinley, Psychological - Methods 2004 + * `Comparing Partitions + `_ L. Hubert and P. + Arabie, Journal of Classification 1985 -* `Wikipedia entry for the Rand index - `_ + * `Properties of the Hubert-Arabie adjusted Rand index + `_ D. Steinley, Psychological + Methods 2004 -* `Wikipedia entry for the adjusted Rand index - `_ + * `Wikipedia entry for the Rand index + `_ -|details-end| .. _mutual_info_score: @@ -1598,80 +1524,77 @@ Bad (e.g. independent labelings) have non-positive scores:: - NMI and MI are not adjusted against chance. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_cluster_plot_adjusted_for_chance_measures.py`: Analysis - of the impact of the dataset size on the value of clustering measures for - random assignments. This example also includes the Adjusted Rand Index. +* :ref:`sphx_glr_auto_examples_cluster_plot_adjusted_for_chance_measures.py`: Analysis + of the impact of the dataset size on the value of clustering measures for random + assignments. This example also includes the Adjusted Rand Index. +.. dropdown:: Mathematical formulation -|details-start| -**Mathematical formulation** -|details-split| + Assume two label assignments (of the same N objects), :math:`U` and :math:`V`. + Their entropy is the amount of uncertainty for a partition set, defined by: -Assume two label assignments (of the same N objects), :math:`U` and :math:`V`. -Their entropy is the amount of uncertainty for a partition set, defined by: + .. math:: H(U) = - \sum_{i=1}^{|U|}P(i)\log(P(i)) -.. math:: H(U) = - \sum_{i=1}^{|U|}P(i)\log(P(i)) + where :math:`P(i) = |U_i| / N` is the probability that an object picked at + random from :math:`U` falls into class :math:`U_i`. Likewise for :math:`V`: -where :math:`P(i) = |U_i| / N` is the probability that an object picked at -random from :math:`U` falls into class :math:`U_i`. Likewise for :math:`V`: + .. math:: H(V) = - \sum_{j=1}^{|V|}P'(j)\log(P'(j)) -.. math:: H(V) = - \sum_{j=1}^{|V|}P'(j)\log(P'(j)) + With :math:`P'(j) = |V_j| / N`. The mutual information (MI) between :math:`U` + and :math:`V` is calculated by: -With :math:`P'(j) = |V_j| / N`. The mutual information (MI) between :math:`U` -and :math:`V` is calculated by: + .. math:: \text{MI}(U, V) = \sum_{i=1}^{|U|}\sum_{j=1}^{|V|}P(i, j)\log\left(\frac{P(i,j)}{P(i)P'(j)}\right) -.. math:: \text{MI}(U, V) = \sum_{i=1}^{|U|}\sum_{j=1}^{|V|}P(i, j)\log\left(\frac{P(i,j)}{P(i)P'(j)}\right) + where :math:`P(i, j) = |U_i \cap V_j| / N` is the probability that an object + picked at random falls into both classes :math:`U_i` and :math:`V_j`. -where :math:`P(i, j) = |U_i \cap V_j| / N` is the probability that an object -picked at random falls into both classes :math:`U_i` and :math:`V_j`. + It also can be expressed in set cardinality formulation: -It also can be expressed in set cardinality formulation: + .. math:: \text{MI}(U, V) = \sum_{i=1}^{|U|} \sum_{j=1}^{|V|} \frac{|U_i \cap V_j|}{N}\log\left(\frac{N|U_i \cap V_j|}{|U_i||V_j|}\right) -.. math:: \text{MI}(U, V) = \sum_{i=1}^{|U|} \sum_{j=1}^{|V|} \frac{|U_i \cap V_j|}{N}\log\left(\frac{N|U_i \cap V_j|}{|U_i||V_j|}\right) + The normalized mutual information is defined as -The normalized mutual information is defined as + .. math:: \text{NMI}(U, V) = \frac{\text{MI}(U, V)}{\text{mean}(H(U), H(V))} -.. math:: \text{NMI}(U, V) = \frac{\text{MI}(U, V)}{\text{mean}(H(U), H(V))} + This value of the mutual information and also the normalized variant is not + adjusted for chance and will tend to increase as the number of different labels + (clusters) increases, regardless of the actual amount of "mutual information" + between the label assignments. -This value of the mutual information and also the normalized variant is not -adjusted for chance and will tend to increase as the number of different labels -(clusters) increases, regardless of the actual amount of "mutual information" -between the label assignments. + The expected value for the mutual information can be calculated using the + following equation [VEB2009]_. In this equation, :math:`a_i = |U_i|` (the number + of elements in :math:`U_i`) and :math:`b_j = |V_j|` (the number of elements in + :math:`V_j`). -The expected value for the mutual information can be calculated using the -following equation [VEB2009]_. In this equation, :math:`a_i = |U_i|` (the number -of elements in :math:`U_i`) and :math:`b_j = |V_j|` (the number of elements in -:math:`V_j`). + .. math:: E[\text{MI}(U,V)]=\sum_{i=1}^{|U|} \sum_{j=1}^{|V|} \sum_{n_{ij}=(a_i+b_j-N)^+ + }^{\min(a_i, b_j)} \frac{n_{ij}}{N}\log \left( \frac{ N.n_{ij}}{a_i b_j}\right) + \frac{a_i!b_j!(N-a_i)!(N-b_j)!}{N!n_{ij}!(a_i-n_{ij})!(b_j-n_{ij})! + (N-a_i-b_j+n_{ij})!} -.. math:: E[\text{MI}(U,V)]=\sum_{i=1}^{|U|} \sum_{j=1}^{|V|} \sum_{n_{ij}=(a_i+b_j-N)^+ - }^{\min(a_i, b_j)} \frac{n_{ij}}{N}\log \left( \frac{ N.n_{ij}}{a_i b_j}\right) - \frac{a_i!b_j!(N-a_i)!(N-b_j)!}{N!n_{ij}!(a_i-n_{ij})!(b_j-n_{ij})! - (N-a_i-b_j+n_{ij})!} + Using the expected value, the adjusted mutual information can then be calculated + using a similar form to that of the adjusted Rand index: -Using the expected value, the adjusted mutual information can then be calculated -using a similar form to that of the adjusted Rand index: + .. math:: \text{AMI} = \frac{\text{MI} - E[\text{MI}]}{\text{mean}(H(U), H(V)) - E[\text{MI}]} -.. math:: \text{AMI} = \frac{\text{MI} - E[\text{MI}]}{\text{mean}(H(U), H(V)) - E[\text{MI}]} + For normalized mutual information and adjusted mutual information, the + normalizing value is typically some *generalized* mean of the entropies of each + clustering. Various generalized means exist, and no firm rules exist for + preferring one over the others. The decision is largely a field-by-field basis; + for instance, in community detection, the arithmetic mean is most common. Each + normalizing method provides "qualitatively similar behaviours" [YAT2016]_. In + our implementation, this is controlled by the ``average_method`` parameter. -For normalized mutual information and adjusted mutual information, the -normalizing value is typically some *generalized* mean of the entropies of each -clustering. Various generalized means exist, and no firm rules exist for -preferring one over the others. The decision is largely a field-by-field basis; -for instance, in community detection, the arithmetic mean is most common. Each -normalizing method provides "qualitatively similar behaviours" [YAT2016]_. In -our implementation, this is controlled by the ``average_method`` parameter. + Vinh et al. (2010) named variants of NMI and AMI by their averaging method + [VEB2010]_. Their 'sqrt' and 'sum' averages are the geometric and arithmetic + means; we use these more broadly common names. -Vinh et al. (2010) named variants of NMI and AMI by their averaging method -[VEB2010]_. Their 'sqrt' and 'sum' averages are the geometric and arithmetic -means; we use these more broadly common names. + .. rubric:: References -.. topic:: References: - - * Strehl, Alexander, and Joydeep Ghosh (2002). "Cluster ensembles – a + * Strehl, Alexander, and Joydeep Ghosh (2002). "Cluster ensembles - a knowledge reuse framework for combining multiple partitions". Journal of - Machine Learning Research 3: 583–617. `doi:10.1162/153244303321897735 + Machine Learning Research 3: 583-617. `doi:10.1162/153244303321897735 `_. * `Wikipedia entry for the (normalized) Mutual Information @@ -1696,7 +1619,6 @@ means; we use these more broadly common names. Reports 6: 30750. `doi:10.1038/srep30750 `_. -|details-end| .. _homogeneity_completeness: @@ -1814,57 +1736,53 @@ homogeneous but not complete:: almost never available in practice or requires manual assignment by human annotators (as in the supervised learning setting). -.. topic:: Examples: - - * :ref:`sphx_glr_auto_examples_cluster_plot_adjusted_for_chance_measures.py`: Analysis - of the impact of the dataset size on the value of clustering measures for - random assignments. +.. rubric:: Examples +* :ref:`sphx_glr_auto_examples_cluster_plot_adjusted_for_chance_measures.py`: Analysis + of the impact of the dataset size on the value of clustering measures for + random assignments. -|details-start| -**Mathematical formulation** -|details-split| +.. dropdown:: Mathematical formulation -Homogeneity and completeness scores are formally given by: + Homogeneity and completeness scores are formally given by: -.. math:: h = 1 - \frac{H(C|K)}{H(C)} + .. math:: h = 1 - \frac{H(C|K)}{H(C)} -.. math:: c = 1 - \frac{H(K|C)}{H(K)} + .. math:: c = 1 - \frac{H(K|C)}{H(K)} -where :math:`H(C|K)` is the **conditional entropy of the classes given the -cluster assignments** and is given by: + where :math:`H(C|K)` is the **conditional entropy of the classes given the + cluster assignments** and is given by: -.. math:: H(C|K) = - \sum_{c=1}^{|C|} \sum_{k=1}^{|K|} \frac{n_{c,k}}{n} - \cdot \log\left(\frac{n_{c,k}}{n_k}\right) + .. math:: H(C|K) = - \sum_{c=1}^{|C|} \sum_{k=1}^{|K|} \frac{n_{c,k}}{n} + \cdot \log\left(\frac{n_{c,k}}{n_k}\right) -and :math:`H(C)` is the **entropy of the classes** and is given by: + and :math:`H(C)` is the **entropy of the classes** and is given by: -.. math:: H(C) = - \sum_{c=1}^{|C|} \frac{n_c}{n} \cdot \log\left(\frac{n_c}{n}\right) + .. math:: H(C) = - \sum_{c=1}^{|C|} \frac{n_c}{n} \cdot \log\left(\frac{n_c}{n}\right) -with :math:`n` the total number of samples, :math:`n_c` and :math:`n_k` the -number of samples respectively belonging to class :math:`c` and cluster -:math:`k`, and finally :math:`n_{c,k}` the number of samples from class -:math:`c` assigned to cluster :math:`k`. + with :math:`n` the total number of samples, :math:`n_c` and :math:`n_k` the + number of samples respectively belonging to class :math:`c` and cluster + :math:`k`, and finally :math:`n_{c,k}` the number of samples from class + :math:`c` assigned to cluster :math:`k`. -The **conditional entropy of clusters given class** :math:`H(K|C)` and the -**entropy of clusters** :math:`H(K)` are defined in a symmetric manner. + The **conditional entropy of clusters given class** :math:`H(K|C)` and the + **entropy of clusters** :math:`H(K)` are defined in a symmetric manner. -Rosenberg and Hirschberg further define **V-measure** as the **harmonic mean of -homogeneity and completeness**: + Rosenberg and Hirschberg further define **V-measure** as the **harmonic mean of + homogeneity and completeness**: -.. math:: v = 2 \cdot \frac{h \cdot c}{h + c} + .. math:: v = 2 \cdot \frac{h \cdot c}{h + c} -|details-end| +.. rubric:: References -.. topic:: References: +* `V-Measure: A conditional entropy-based external cluster evaluation measure + `_ Andrew Rosenberg and Julia + Hirschberg, 2007 - * `V-Measure: A conditional entropy-based external cluster evaluation measure - `_ Andrew Rosenberg and Julia - Hirschberg, 2007 +.. [B2011] `Identification and Characterization of Events in Social Media + `_, Hila + Becker, PhD Thesis. - .. [B2011] `Identification and Characterization of Events in Social Media - `_, Hila - Becker, PhD Thesis. .. _fowlkes_mallows_scores: @@ -1941,19 +1859,15 @@ Bad (e.g. independent labelings) have zero scores:: manual assignment by human annotators (as in the supervised learning setting). -|details-start| -**References** -|details-split| +.. dropdown:: References -* 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 + * 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 -* `Wikipedia entry for the Fowlkes-Mallows Index - `_ + * `Wikipedia entry for the Fowlkes-Mallows Index + `_ -|details-end| .. _silhouette_coefficient: @@ -1997,7 +1911,6 @@ cluster analysis. >>> metrics.silhouette_score(X, labels, metric='euclidean') 0.55... - .. topic:: Advantages: - The score is bounded between -1 for incorrect clustering and +1 for highly @@ -2012,23 +1925,18 @@ cluster analysis. other concepts of clusters, such as density based clusters like those obtained through DBSCAN. -.. topic:: Examples: - - * :ref:`sphx_glr_auto_examples_cluster_plot_kmeans_silhouette_analysis.py` : In - this example the silhouette analysis is used to choose an optimal value for - n_clusters. +.. rubric:: Examples +* :ref:`sphx_glr_auto_examples_cluster_plot_kmeans_silhouette_analysis.py` : In + this example the silhouette analysis is used to choose an optimal value for + n_clusters. -|details-start| -**References** -|details-split| +.. dropdown:: References -* Peter J. Rousseeuw (1987). :doi:`"Silhouettes: a Graphical Aid to the - Interpretation and Validation of Cluster - Analysis"<10.1016/0377-0427(87)90125-7>` . Computational and Applied - Mathematics 20: 53–65. + * Peter J. Rousseeuw (1987). :doi:`"Silhouettes: a Graphical Aid to the + Interpretation and Validation of Cluster Analysis"<10.1016/0377-0427(87)90125-7>`. + Computational and Applied Mathematics 20: 53-65. -|details-end| .. _calinski_harabasz_index: @@ -2074,42 +1982,35 @@ cluster analysis: other concepts of clusters, such as density based clusters like those obtained through DBSCAN. -|details-start| -**Mathematical formulation** -|details-split| +.. dropdown:: Mathematical formulation -For a set of data :math:`E` of size :math:`n_E` which has been clustered into -:math:`k` clusters, the Calinski-Harabasz score :math:`s` is defined as the -ratio of the between-clusters dispersion mean and the within-cluster -dispersion: + For a set of data :math:`E` of size :math:`n_E` which has been clustered into + :math:`k` clusters, the Calinski-Harabasz score :math:`s` is defined as the + ratio of the between-clusters dispersion mean and the within-cluster + dispersion: -.. math:: - s = \frac{\mathrm{tr}(B_k)}{\mathrm{tr}(W_k)} \times \frac{n_E - k}{k - 1} - -where :math:`\mathrm{tr}(B_k)` is trace of the between group dispersion matrix -and :math:`\mathrm{tr}(W_k)` is the trace of the within-cluster dispersion -matrix defined by: + .. math:: + s = \frac{\mathrm{tr}(B_k)}{\mathrm{tr}(W_k)} \times \frac{n_E - k}{k - 1} -.. math:: W_k = \sum_{q=1}^k \sum_{x \in C_q} (x - c_q) (x - c_q)^T + where :math:`\mathrm{tr}(B_k)` is trace of the between group dispersion matrix + and :math:`\mathrm{tr}(W_k)` is the trace of the within-cluster dispersion + matrix defined by: -.. math:: B_k = \sum_{q=1}^k n_q (c_q - c_E) (c_q - c_E)^T + .. math:: W_k = \sum_{q=1}^k \sum_{x \in C_q} (x - c_q) (x - c_q)^T -with :math:`C_q` the set of points in cluster :math:`q`, :math:`c_q` the -center of cluster :math:`q`, :math:`c_E` the center of :math:`E`, and -:math:`n_q` the number of points in cluster :math:`q`. + .. math:: B_k = \sum_{q=1}^k n_q (c_q - c_E) (c_q - c_E)^T -|details-end| + with :math:`C_q` the set of points in cluster :math:`q`, :math:`c_q` the + center of cluster :math:`q`, :math:`c_E` the center of :math:`E`, and + :math:`n_q` the number of points in cluster :math:`q`. -|details-start| -**References** -|details-split| +.. dropdown:: References -* Caliński, T., & Harabasz, J. (1974). `"A Dendrite Method for Cluster Analysis" - `_. - :doi:`Communications in Statistics-theory and Methods 3: 1-27 - <10.1080/03610927408827101>`. + * Caliński, T., & Harabasz, J. (1974). `"A Dendrite Method for Cluster Analysis" + `_. + :doi:`Communications in Statistics-theory and Methods 3: 1-27 + <10.1080/03610927408827101>`. -|details-end| .. _davies-bouldin_index: @@ -2156,49 +2057,41 @@ cluster analysis as follows: - The usage of centroid distance limits the distance metric to Euclidean space. +.. dropdown:: Mathematical formulation -|details-start| -**Mathematical formulation** -|details-split| - -The index is defined as the average similarity between each cluster :math:`C_i` -for :math:`i=1, ..., k` and its most similar one :math:`C_j`. In the context of -this index, similarity is defined as a measure :math:`R_{ij}` that trades off: - -- :math:`s_i`, the average distance between each point of cluster :math:`i` and - the centroid of that cluster -- also know as cluster diameter. -- :math:`d_{ij}`, the distance between cluster centroids :math:`i` and - :math:`j`. + The index is defined as the average similarity between each cluster :math:`C_i` + for :math:`i=1, ..., k` and its most similar one :math:`C_j`. In the context of + this index, similarity is defined as a measure :math:`R_{ij}` that trades off: -A simple choice to construct :math:`R_{ij}` so that it is nonnegative and -symmetric is: + - :math:`s_i`, the average distance between each point of cluster :math:`i` and + the centroid of that cluster -- also know as cluster diameter. + - :math:`d_{ij}`, the distance between cluster centroids :math:`i` and + :math:`j`. -.. math:: - R_{ij} = \frac{s_i + s_j}{d_{ij}} + A simple choice to construct :math:`R_{ij}` so that it is nonnegative and + symmetric is: -Then the Davies-Bouldin index is defined as: + .. math:: + R_{ij} = \frac{s_i + s_j}{d_{ij}} -.. math:: - DB = \frac{1}{k} \sum_{i=1}^k \max_{i \neq j} R_{ij} + Then the Davies-Bouldin index is defined as: -|details-end| + .. math:: + DB = \frac{1}{k} \sum_{i=1}^k \max_{i \neq j} R_{ij} -|details-start| -**References** -|details-split| +.. dropdown:: References -* Davies, David L.; Bouldin, Donald W. (1979). :doi:`"A Cluster Separation - Measure" <10.1109/TPAMI.1979.4766909>` IEEE Transactions on Pattern Analysis - and Machine Intelligence. PAMI-1 (2): 224-227. + * Davies, David L.; Bouldin, Donald W. (1979). :doi:`"A Cluster Separation + Measure" <10.1109/TPAMI.1979.4766909>` IEEE Transactions on Pattern Analysis + and Machine Intelligence. PAMI-1 (2): 224-227. -* Halkidi, Maria; Batistakis, Yannis; Vazirgiannis, Michalis (2001). :doi:`"On - Clustering Validation Techniques" <10.1023/A:1012801612483>` Journal of - Intelligent Information Systems, 17(2-3), 107-145. + * Halkidi, Maria; Batistakis, Yannis; Vazirgiannis, Michalis (2001). :doi:`"On + Clustering Validation Techniques" <10.1023/A:1012801612483>` Journal of + Intelligent Information Systems, 17(2-3), 107-145. -* `Wikipedia entry for Davies-Bouldin index - `_. + * `Wikipedia entry for Davies-Bouldin index + `_. -|details-end| .. _contingency_matrix: @@ -2248,15 +2141,11 @@ of classes. - It doesn't give a single metric to use as an objective for clustering optimisation. +.. dropdown:: References -|details-start| -**References** -|details-split| + * `Wikipedia entry for contingency matrix + `_ -* `Wikipedia entry for contingency matrix - `_ - -|details-end| .. _pair_confusion_matrix: @@ -2334,11 +2223,7 @@ diagonal entries:: array([[ 0, 0], [12, 0]]) -|details-start| -**References** -|details-split| - - * :doi:`"Comparing Partitions" <10.1007/BF01908075>` L. Hubert and P. Arabie, - Journal of Classification 1985 +.. dropdown:: References -|details-end| + * :doi:`"Comparing Partitions" <10.1007/BF01908075>` L. Hubert and P. Arabie, + Journal of Classification 1985 diff --git a/doc/modules/compose.rst b/doc/modules/compose.rst index 28931cf52f283..655ea551e0375 100644 --- a/doc/modules/compose.rst +++ b/doc/modules/compose.rst @@ -79,20 +79,16 @@ is an estimator object:: >>> pipe Pipeline(steps=[('reduce_dim', PCA()), ('clf', SVC())]) -|details-start| -**Shorthand version using :func:`make_pipeline`** -|details-split| +.. dropdown:: Shorthand version using :func:`make_pipeline` -The utility function :func:`make_pipeline` is a shorthand -for constructing pipelines; -it takes a variable number of estimators and returns a pipeline, -filling in the names automatically:: + The utility function :func:`make_pipeline` is a shorthand + for constructing pipelines; + it takes a variable number of estimators and returns a pipeline, + filling in the names automatically:: - >>> from sklearn.pipeline import make_pipeline - >>> make_pipeline(PCA(), SVC()) - Pipeline(steps=[('pca', PCA()), ('svc', SVC())]) - -|details-end| + >>> from sklearn.pipeline import make_pipeline + >>> make_pipeline(PCA(), SVC()) + Pipeline(steps=[('pca', PCA()), ('svc', SVC())]) Access pipeline steps ..................... @@ -108,27 +104,23 @@ permitted). This is convenient for performing only some of the transformations >>> pipe[-1:] Pipeline(steps=[('clf', SVC())]) -|details-start| -**Accessing a step by name or position** -|details-split| - -A specific step can also be accessed by index or name by indexing (with ``[idx]``) the -pipeline:: +.. dropdown:: Accessing a step by name or position - >>> pipe.steps[0] - ('reduce_dim', PCA()) - >>> pipe[0] - PCA() - >>> pipe['reduce_dim'] - PCA() + A specific step can also be accessed by index or name by indexing (with ``[idx]``) the + pipeline:: -`Pipeline`'s `named_steps` attribute allows accessing steps by name with tab -completion in interactive environments:: + >>> pipe.steps[0] + ('reduce_dim', PCA()) + >>> pipe[0] + PCA() + >>> pipe['reduce_dim'] + PCA() - >>> pipe.named_steps.reduce_dim is pipe['reduce_dim'] - True + `Pipeline`'s `named_steps` attribute allows accessing steps by name with tab + completion in interactive environments:: -|details-end| + >>> pipe.named_steps.reduce_dim is pipe['reduce_dim'] + True Tracking feature names in a pipeline .................................... @@ -149,17 +141,13 @@ pipeline slicing to get the feature names going into each step:: >>> pipe[:-1].get_feature_names_out() array(['x2', 'x3'], ...) -|details-start| -**Customize feature names** -|details-split| - -You can also provide custom feature names for the input data using -``get_feature_names_out``:: +.. dropdown:: Customize feature names - >>> pipe[:-1].get_feature_names_out(iris.feature_names) - array(['petal length (cm)', 'petal width (cm)'], ...) + You can also provide custom feature names for the input data using + ``get_feature_names_out``:: -|details-end| + >>> pipe[:-1].get_feature_names_out(iris.feature_names) + array(['petal length (cm)', 'petal width (cm)'], ...) .. _pipeline_nested_parameters: @@ -175,40 +163,37 @@ syntax:: >>> pipe.set_params(clf__C=10) Pipeline(steps=[('reduce_dim', PCA()), ('clf', SVC(C=10))]) -|details-start| -**When does it matter?** -|details-split| +.. dropdown:: When does it matter? -This is particularly important for doing grid searches:: + This is particularly important for doing grid searches:: - >>> from sklearn.model_selection import GridSearchCV - >>> param_grid = dict(reduce_dim__n_components=[2, 5, 10], - ... clf__C=[0.1, 10, 100]) - >>> grid_search = GridSearchCV(pipe, param_grid=param_grid) + >>> from sklearn.model_selection import GridSearchCV + >>> param_grid = dict(reduce_dim__n_components=[2, 5, 10], + ... clf__C=[0.1, 10, 100]) + >>> grid_search = GridSearchCV(pipe, param_grid=param_grid) -Individual steps may also be replaced as parameters, and non-final steps may be -ignored by setting them to ``'passthrough'``:: + Individual steps may also be replaced as parameters, and non-final steps may be + ignored by setting them to ``'passthrough'``:: - >>> param_grid = dict(reduce_dim=['passthrough', PCA(5), PCA(10)], - ... clf=[SVC(), LogisticRegression()], - ... clf__C=[0.1, 10, 100]) - >>> grid_search = GridSearchCV(pipe, param_grid=param_grid) + >>> param_grid = dict(reduce_dim=['passthrough', PCA(5), PCA(10)], + ... clf=[SVC(), LogisticRegression()], + ... clf__C=[0.1, 10, 100]) + >>> grid_search = GridSearchCV(pipe, param_grid=param_grid) -.. topic:: See Also: + .. seealso:: - * :ref:`composite_grid_search` + * :ref:`composite_grid_search` -|details-end| -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_feature_selection_plot_feature_selection_pipeline.py` - * :ref:`sphx_glr_auto_examples_model_selection_plot_grid_search_text_feature_extraction.py` - * :ref:`sphx_glr_auto_examples_compose_plot_digits_pipe.py` - * :ref:`sphx_glr_auto_examples_miscellaneous_plot_kernel_approximation.py` - * :ref:`sphx_glr_auto_examples_svm_plot_svm_anova.py` - * :ref:`sphx_glr_auto_examples_compose_plot_compare_reduction.py` - * :ref:`sphx_glr_auto_examples_miscellaneous_plot_pipeline_display.py` +* :ref:`sphx_glr_auto_examples_feature_selection_plot_feature_selection_pipeline.py` +* :ref:`sphx_glr_auto_examples_model_selection_plot_grid_search_text_feature_extraction.py` +* :ref:`sphx_glr_auto_examples_compose_plot_digits_pipe.py` +* :ref:`sphx_glr_auto_examples_miscellaneous_plot_kernel_approximation.py` +* :ref:`sphx_glr_auto_examples_svm_plot_svm_anova.py` +* :ref:`sphx_glr_auto_examples_compose_plot_compare_reduction.py` +* :ref:`sphx_glr_auto_examples_miscellaneous_plot_pipeline_display.py` .. _pipeline_cache: @@ -245,53 +230,49 @@ object:: >>> # Clear the cache directory when you don't need it anymore >>> rmtree(cachedir) -|details-start| -**Warning: Side effect of caching transformers** -|details-split| - -Using a :class:`Pipeline` without cache enabled, it is possible to -inspect the original instance such as:: - - >>> from sklearn.datasets import load_digits - >>> X_digits, y_digits = load_digits(return_X_y=True) - >>> pca1 = PCA(n_components=10) - >>> svm1 = SVC() - >>> pipe = Pipeline([('reduce_dim', pca1), ('clf', svm1)]) - >>> pipe.fit(X_digits, y_digits) - Pipeline(steps=[('reduce_dim', PCA(n_components=10)), ('clf', SVC())]) - >>> # The pca instance can be inspected directly - >>> pca1.components_.shape - (10, 64) - - -Enabling caching triggers a clone of the transformers before fitting. -Therefore, the transformer instance given to the pipeline cannot be -inspected directly. -In following example, accessing the :class:`~sklearn.decomposition.PCA` -instance ``pca2`` will raise an ``AttributeError`` since ``pca2`` will be an -unfitted transformer. -Instead, use the attribute ``named_steps`` to inspect estimators within -the pipeline:: - - >>> cachedir = mkdtemp() - >>> pca2 = PCA(n_components=10) - >>> svm2 = SVC() - >>> cached_pipe = Pipeline([('reduce_dim', pca2), ('clf', svm2)], - ... memory=cachedir) - >>> cached_pipe.fit(X_digits, y_digits) - Pipeline(memory=..., - steps=[('reduce_dim', PCA(n_components=10)), ('clf', SVC())]) - >>> cached_pipe.named_steps['reduce_dim'].components_.shape - (10, 64) - >>> # Remove the cache directory - >>> rmtree(cachedir) - - -|details-end| - -.. topic:: Examples: - - * :ref:`sphx_glr_auto_examples_compose_plot_compare_reduction.py` +.. dropdown:: Side effect of caching transformers + :color: warning + + Using a :class:`Pipeline` without cache enabled, it is possible to + inspect the original instance such as:: + + >>> from sklearn.datasets import load_digits + >>> X_digits, y_digits = load_digits(return_X_y=True) + >>> pca1 = PCA(n_components=10) + >>> svm1 = SVC() + >>> pipe = Pipeline([('reduce_dim', pca1), ('clf', svm1)]) + >>> pipe.fit(X_digits, y_digits) + Pipeline(steps=[('reduce_dim', PCA(n_components=10)), ('clf', SVC())]) + >>> # The pca instance can be inspected directly + >>> pca1.components_.shape + (10, 64) + + Enabling caching triggers a clone of the transformers before fitting. + Therefore, the transformer instance given to the pipeline cannot be + inspected directly. + In following example, accessing the :class:`~sklearn.decomposition.PCA` + instance ``pca2`` will raise an ``AttributeError`` since ``pca2`` will be an + unfitted transformer. + Instead, use the attribute ``named_steps`` to inspect estimators within + the pipeline:: + + >>> cachedir = mkdtemp() + >>> pca2 = PCA(n_components=10) + >>> svm2 = SVC() + >>> cached_pipe = Pipeline([('reduce_dim', pca2), ('clf', svm2)], + ... memory=cachedir) + >>> cached_pipe.fit(X_digits, y_digits) + Pipeline(memory=..., + steps=[('reduce_dim', PCA(n_components=10)), ('clf', SVC())]) + >>> cached_pipe.named_steps['reduce_dim'].components_.shape + (10, 64) + >>> # Remove the cache directory + >>> rmtree(cachedir) + + +.. rubric:: Examples + +* :ref:`sphx_glr_auto_examples_compose_plot_compare_reduction.py` .. _transformed_target_regressor: @@ -364,9 +345,9 @@ each other. However, it is possible to bypass this checking by setting pair of functions ``func`` and ``inverse_func``. However, setting both options will raise an error. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_compose_plot_transformed_target.py` +* :ref:`sphx_glr_auto_examples_compose_plot_transformed_target.py` .. _feature_union: @@ -428,9 +409,9 @@ and ignored by setting to ``'drop'``:: FeatureUnion(transformer_list=[('linear_pca', PCA()), ('kernel_pca', 'drop')]) -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_compose_plot_feature_union.py` +* :ref:`sphx_glr_auto_examples_compose_plot_feature_union.py` .. _column_transformer: @@ -623,7 +604,7 @@ As an alternative, the HTML can be written to a file using >>> with open('my_estimator.html', 'w') as f: # doctest: +SKIP ... f.write(estimator_html_repr(clf)) -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_compose_plot_column_transformer.py` - * :ref:`sphx_glr_auto_examples_compose_plot_column_transformer_mixed_types.py` +* :ref:`sphx_glr_auto_examples_compose_plot_column_transformer.py` +* :ref:`sphx_glr_auto_examples_compose_plot_column_transformer_mixed_types.py` diff --git a/doc/modules/covariance.rst b/doc/modules/covariance.rst index 50927f9a677f6..847e489c87333 100644 --- a/doc/modules/covariance.rst +++ b/doc/modules/covariance.rst @@ -40,11 +40,10 @@ on whether the data are centered, so one may want to use the same mean vector as the training set. If not, both should be centered by the user, and ``assume_centered=True`` should be used. -.. topic:: Examples: +.. rubric:: Examples - * See :ref:`sphx_glr_auto_examples_covariance_plot_covariance_estimation.py` for - an example on how to fit an :class:`EmpiricalCovariance` object - to data. +* See :ref:`sphx_glr_auto_examples_covariance_plot_covariance_estimation.py` for + an example on how to fit an :class:`EmpiricalCovariance` object to data. .. _shrunk_covariance: @@ -84,11 +83,10 @@ Tr}\hat{\Sigma}}{p}\rm Id`. Choosing the amount of shrinkage, :math:`\alpha` amounts to setting a bias/variance trade-off, and is discussed below. -.. topic:: Examples: +.. rubric:: Examples - * See :ref:`sphx_glr_auto_examples_covariance_plot_covariance_estimation.py` for - an example on how to fit a :class:`ShrunkCovariance` object - to data. +* See :ref:`sphx_glr_auto_examples_covariance_plot_covariance_estimation.py` for + an example on how to fit a :class:`ShrunkCovariance` object to data. Ledoit-Wolf shrinkage @@ -121,18 +119,18 @@ fitting a :class:`LedoitWolf` object to the same sample. Since the population covariance is already a multiple of the identity matrix, the Ledoit-Wolf solution is indeed a reasonable estimate. -.. topic:: Examples: +.. rubric:: Examples - * See :ref:`sphx_glr_auto_examples_covariance_plot_covariance_estimation.py` for - an example on how to fit a :class:`LedoitWolf` object to data and - for visualizing the performances of the Ledoit-Wolf estimator in - terms of likelihood. +* See :ref:`sphx_glr_auto_examples_covariance_plot_covariance_estimation.py` for + an example on how to fit a :class:`LedoitWolf` object to data and + for visualizing the performances of the Ledoit-Wolf estimator in + terms of likelihood. -.. topic:: References: +.. rubric:: References - .. [1] O. Ledoit and M. Wolf, "A Well-Conditioned Estimator for Large-Dimensional - Covariance Matrices", Journal of Multivariate Analysis, Volume 88, Issue 2, - February 2004, pages 365-411. +.. [1] O. Ledoit and M. Wolf, "A Well-Conditioned Estimator for Large-Dimensional + Covariance Matrices", Journal of Multivariate Analysis, Volume 88, Issue 2, + February 2004, pages 365-411. .. _oracle_approximating_shrinkage: @@ -158,22 +156,21 @@ object to the same sample. Bias-variance trade-off when setting the shrinkage: comparing the choices of Ledoit-Wolf and OAS estimators -.. topic:: References: +.. rubric:: References - .. [2] :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>` +.. [2] :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>` -.. topic:: Examples: +.. rubric:: Examples - * See :ref:`sphx_glr_auto_examples_covariance_plot_covariance_estimation.py` for - an example on how to fit an :class:`OAS` object - to data. +* See :ref:`sphx_glr_auto_examples_covariance_plot_covariance_estimation.py` for + an example on how to fit an :class:`OAS` object to data. - * See :ref:`sphx_glr_auto_examples_covariance_plot_lw_vs_oas.py` to visualize the - Mean Squared Error difference between a :class:`LedoitWolf` and - an :class:`OAS` estimator of the covariance. +* See :ref:`sphx_glr_auto_examples_covariance_plot_lw_vs_oas.py` to visualize the + Mean Squared Error difference between a :class:`LedoitWolf` and + an :class:`OAS` estimator of the covariance. .. figure:: ../auto_examples/covariance/images/sphx_glr_plot_lw_vs_oas_001.png @@ -254,20 +251,20 @@ problem is the GLasso algorithm, from the Friedman 2008 Biostatistics paper. It is the same algorithm as in the R ``glasso`` package. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_covariance_plot_sparse_cov.py`: example on synthetic - data showing some recovery of a structure, and comparing to other - covariance estimators. +* :ref:`sphx_glr_auto_examples_covariance_plot_sparse_cov.py`: example on synthetic + data showing some recovery of a structure, and comparing to other + covariance estimators. - * :ref:`sphx_glr_auto_examples_applications_plot_stock_market.py`: example on real - stock market data, finding which symbols are most linked. +* :ref:`sphx_glr_auto_examples_applications_plot_stock_market.py`: example on real + stock market data, finding which symbols are most linked. -.. topic:: References: +.. rubric:: References - * Friedman et al, `"Sparse inverse covariance estimation with the - graphical lasso" `_, - Biostatistics 9, pp 432, 2008 +* Friedman et al, `"Sparse inverse covariance estimation with the + graphical lasso" `_, + Biostatistics 9, pp 432, 2008 .. _robust_covariance: @@ -313,24 +310,24 @@ the same time. Raw estimates can be accessed as ``raw_location_`` and ``raw_covariance_`` attributes of a :class:`MinCovDet` robust covariance estimator object. -.. topic:: References: +.. rubric:: References - .. [3] P. J. Rousseeuw. Least median of squares regression. - J. Am Stat Ass, 79:871, 1984. - .. [4] A Fast Algorithm for the Minimum Covariance Determinant Estimator, - 1999, American Statistical Association and the American Society - for Quality, TECHNOMETRICS. +.. [3] P. J. Rousseeuw. Least median of squares regression. + J. Am Stat Ass, 79:871, 1984. +.. [4] A Fast Algorithm for the Minimum Covariance Determinant Estimator, + 1999, American Statistical Association and the American Society + for Quality, TECHNOMETRICS. -.. topic:: Examples: +.. rubric:: Examples - * See :ref:`sphx_glr_auto_examples_covariance_plot_robust_vs_empirical_covariance.py` for - an example on how to fit a :class:`MinCovDet` object to data and see how - the estimate remains accurate despite the presence of outliers. +* See :ref:`sphx_glr_auto_examples_covariance_plot_robust_vs_empirical_covariance.py` for + an example on how to fit a :class:`MinCovDet` object to data and see how + the estimate remains accurate despite the presence of outliers. - * See :ref:`sphx_glr_auto_examples_covariance_plot_mahalanobis_distances.py` to - visualize the difference between :class:`EmpiricalCovariance` and - :class:`MinCovDet` covariance estimators in terms of Mahalanobis distance - (so we get a better estimate of the precision matrix too). +* See :ref:`sphx_glr_auto_examples_covariance_plot_mahalanobis_distances.py` to + visualize the difference between :class:`EmpiricalCovariance` and + :class:`MinCovDet` covariance estimators in terms of Mahalanobis distance + (so we get a better estimate of the precision matrix too). .. |robust_vs_emp| image:: ../auto_examples/covariance/images/sphx_glr_plot_robust_vs_empirical_covariance_001.png :target: ../auto_examples/covariance/plot_robust_vs_empirical_covariance.html diff --git a/doc/modules/cross_decomposition.rst b/doc/modules/cross_decomposition.rst index 8f8d217f87144..2d630de699c7a 100644 --- a/doc/modules/cross_decomposition.rst +++ b/doc/modules/cross_decomposition.rst @@ -92,42 +92,35 @@ Step *a)* may be performed in two ways: either by computing the whole SVD of values, or by directly computing the singular vectors using the power method (cf section 11.3 in [1]_), which corresponds to the `'nipals'` option of the `algorithm` parameter. -|details-start| -**Transforming data** -|details-split| +.. dropdown:: Transforming data -To transform :math:`X` into :math:`\bar{X}`, we need to find a projection -matrix :math:`P` such that :math:`\bar{X} = XP`. We know that for the -training data, :math:`\Xi = XP`, and :math:`X = \Xi \Gamma^T`. Setting -:math:`P = U(\Gamma^T U)^{-1}` where :math:`U` is the matrix with the -:math:`u_k` in the columns, we have :math:`XP = X U(\Gamma^T U)^{-1} = \Xi -(\Gamma^T U) (\Gamma^T U)^{-1} = \Xi` as desired. The rotation matrix -:math:`P` can be accessed from the `x_rotations_` attribute. + To transform :math:`X` into :math:`\bar{X}`, we need to find a projection + matrix :math:`P` such that :math:`\bar{X} = XP`. We know that for the + training data, :math:`\Xi = XP`, and :math:`X = \Xi \Gamma^T`. Setting + :math:`P = U(\Gamma^T U)^{-1}` where :math:`U` is the matrix with the + :math:`u_k` in the columns, we have :math:`XP = X U(\Gamma^T U)^{-1} = \Xi + (\Gamma^T U) (\Gamma^T U)^{-1} = \Xi` as desired. The rotation matrix + :math:`P` can be accessed from the `x_rotations_` attribute. -Similarly, :math:`Y` can be transformed using the rotation matrix -:math:`V(\Delta^T V)^{-1}`, accessed via the `y_rotations_` attribute. -|details-end| + Similarly, :math:`Y` can be transformed using the rotation matrix + :math:`V(\Delta^T V)^{-1}`, accessed via the `y_rotations_` attribute. -|details-start| -**Predicting the targets Y** -|details-split| +.. dropdown:: Predicting the targets `Y` -To predict the targets of some data :math:`X`, we are looking for a -coefficient matrix :math:`\beta \in R^{d \times t}` such that :math:`Y = -X\beta`. + To predict the targets of some data :math:`X`, we are looking for a + coefficient matrix :math:`\beta \in R^{d \times t}` such that :math:`Y = + X\beta`. -The idea is to try to predict the transformed targets :math:`\Omega` as a -function of the transformed samples :math:`\Xi`, by computing :math:`\alpha -\in \mathbb{R}` such that :math:`\Omega = \alpha \Xi`. + The idea is to try to predict the transformed targets :math:`\Omega` as a + function of the transformed samples :math:`\Xi`, by computing :math:`\alpha + \in \mathbb{R}` such that :math:`\Omega = \alpha \Xi`. -Then, we have :math:`Y = \Omega \Delta^T = \alpha \Xi \Delta^T`, and since -:math:`\Xi` is the transformed training data we have that :math:`Y = X \alpha -P \Delta^T`, and as a result the coefficient matrix :math:`\beta = \alpha P -\Delta^T`. + Then, we have :math:`Y = \Omega \Delta^T = \alpha \Xi \Delta^T`, and since + :math:`\Xi` is the transformed training data we have that :math:`Y = X \alpha + P \Delta^T`, and as a result the coefficient matrix :math:`\beta = \alpha P + \Delta^T`. -:math:`\beta` can be accessed through the `coef_` attribute. - -|details-end| + :math:`\beta` can be accessed through the `coef_` attribute. PLSSVD ------ @@ -184,18 +177,13 @@ Since :class:`CCA` involves the inversion of :math:`X_k^TX_k` and :math:`Y_k^TY_k`, this estimator can be unstable if the number of features or targets is greater than the number of samples. -|details-start| -**Reference** -|details-split| - - .. [1] `A survey of Partial Least Squares (PLS) methods, with emphasis on - the two-block case - `_ - JA Wegelin +.. rubric:: References -|details-end| +.. [1] `A survey of Partial Least Squares (PLS) methods, with emphasis on the two-block + case `_, + JA Wegelin -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_cross_decomposition_plot_compare_cross_decomposition.py` - * :ref:`sphx_glr_auto_examples_cross_decomposition_plot_pcr_vs_pls.py` +* :ref:`sphx_glr_auto_examples_cross_decomposition_plot_compare_cross_decomposition.py` +* :ref:`sphx_glr_auto_examples_cross_decomposition_plot_pcr_vs_pls.py` diff --git a/doc/modules/cross_validation.rst b/doc/modules/cross_validation.rst index 34f14fe6846a2..defcd91a6008a 100644 --- a/doc/modules/cross_validation.rst +++ b/doc/modules/cross_validation.rst @@ -170,36 +170,33 @@ indices, for example:: >>> cross_val_score(clf, X, y, cv=custom_cv) array([1. , 0.973...]) -|details-start| -**Data transformation with held out data** -|details-split| +.. dropdown:: Data transformation with held-out data - Just as it is important to test a predictor on data held-out from - training, preprocessing (such as standardization, feature selection, etc.) - and similar :ref:`data transformations ` similarly should - be learnt from a training set and applied to held-out data for prediction:: + Just as it is important to test a predictor on data held-out from + training, preprocessing (such as standardization, feature selection, etc.) + and similar :ref:`data transformations ` similarly should + be learnt from a training set and applied to held-out data for prediction:: - >>> from sklearn import preprocessing - >>> X_train, X_test, y_train, y_test = train_test_split( - ... X, y, test_size=0.4, random_state=0) - >>> scaler = preprocessing.StandardScaler().fit(X_train) - >>> X_train_transformed = scaler.transform(X_train) - >>> clf = svm.SVC(C=1).fit(X_train_transformed, y_train) - >>> X_test_transformed = scaler.transform(X_test) - >>> clf.score(X_test_transformed, y_test) - 0.9333... + >>> from sklearn import preprocessing + >>> X_train, X_test, y_train, y_test = train_test_split( + ... X, y, test_size=0.4, random_state=0) + >>> scaler = preprocessing.StandardScaler().fit(X_train) + >>> X_train_transformed = scaler.transform(X_train) + >>> clf = svm.SVC(C=1).fit(X_train_transformed, y_train) + >>> X_test_transformed = scaler.transform(X_test) + >>> clf.score(X_test_transformed, y_test) + 0.9333... - A :class:`Pipeline ` makes it easier to compose - estimators, providing this behavior under cross-validation:: + A :class:`Pipeline ` makes it easier to compose + estimators, providing this behavior under cross-validation:: - >>> from sklearn.pipeline import make_pipeline - >>> clf = make_pipeline(preprocessing.StandardScaler(), svm.SVC(C=1)) - >>> cross_val_score(clf, X, y, cv=cv) - array([0.977..., 0.933..., 0.955..., 0.933..., 0.977...]) + >>> from sklearn.pipeline import make_pipeline + >>> clf = make_pipeline(preprocessing.StandardScaler(), svm.SVC(C=1)) + >>> cross_val_score(clf, X, y, cv=cv) + array([0.977..., 0.933..., 0.955..., 0.933..., 0.977...]) - See :ref:`combining_estimators`. + See :ref:`combining_estimators`. -|details-end| .. _multimetric_cross_validation: @@ -294,14 +291,14 @@ The function :func:`cross_val_predict` is appropriate for: The available cross validation iterators are introduced in the following section. -.. topic:: Examples +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_model_selection_plot_roc_crossval.py`, - * :ref:`sphx_glr_auto_examples_feature_selection_plot_rfe_with_cross_validation.py`, - * :ref:`sphx_glr_auto_examples_model_selection_plot_grid_search_digits.py`, - * :ref:`sphx_glr_auto_examples_model_selection_plot_grid_search_text_feature_extraction.py`, - * :ref:`sphx_glr_auto_examples_model_selection_plot_cv_predict.py`, - * :ref:`sphx_glr_auto_examples_model_selection_plot_nested_cross_validation_iris.py`. +* :ref:`sphx_glr_auto_examples_model_selection_plot_roc_crossval.py`, +* :ref:`sphx_glr_auto_examples_feature_selection_plot_rfe_with_cross_validation.py`, +* :ref:`sphx_glr_auto_examples_model_selection_plot_grid_search_digits.py`, +* :ref:`sphx_glr_auto_examples_model_selection_plot_grid_search_text_feature_extraction.py`, +* :ref:`sphx_glr_auto_examples_model_selection_plot_cv_predict.py`, +* :ref:`sphx_glr_auto_examples_model_selection_plot_nested_cross_validation_iris.py`. Cross validation iterators ========================== @@ -442,23 +439,19 @@ then 5- or 10- fold cross validation can overestimate the generalization error. As a general rule, most authors, and empirical evidence, suggest that 5- or 10- fold cross validation should be preferred to LOO. -|details-start| -**References** -|details-split| +.. dropdown:: References - * ``_; - * T. Hastie, R. Tibshirani, J. Friedman, `The Elements of Statistical Learning - `_, Springer 2009 - * L. Breiman, P. Spector `Submodel selection and evaluation in regression: The X-random case - `_, International Statistical Review 1992; - * R. Kohavi, `A Study of Cross-Validation and Bootstrap for Accuracy Estimation and Model Selection - `_, Intl. Jnt. Conf. AI - * R. Bharat Rao, G. Fung, R. Rosales, `On the Dangers of Cross-Validation. An Experimental Evaluation - `_, SIAM 2008; - * G. James, D. Witten, T. Hastie, R Tibshirani, `An Introduction to - Statistical Learning `_, Springer 2013. - -|details-end| + * ``_; + * T. Hastie, R. Tibshirani, J. Friedman, `The Elements of Statistical Learning + `_, Springer 2009 + * L. Breiman, P. Spector `Submodel selection and evaluation in regression: The X-random case + `_, International Statistical Review 1992; + * R. Kohavi, `A Study of Cross-Validation and Bootstrap for Accuracy Estimation and Model Selection + `_, Intl. Jnt. Conf. AI + * R. Bharat Rao, G. Fung, R. Rosales, `On the Dangers of Cross-Validation. An Experimental Evaluation + `_, SIAM 2008; + * G. James, D. Witten, T. Hastie, R Tibshirani, `An Introduction to + Statistical Learning `_, Springer 2013. .. _leave_p_out: @@ -700,30 +693,27 @@ Example:: [ 0 1 4 5 6 7 8 9 11 12 13 14] [ 2 3 10 15 16 17] [ 1 2 3 8 9 10 12 13 14 15 16 17] [ 0 4 5 6 7 11] -|details-start| -**Implementation notes** -|details-split| +.. dropdown:: Implementation notes -- With the current implementation full shuffle is not possible in most - scenarios. When shuffle=True, the following happens: + - With the current implementation full shuffle is not possible in most + scenarios. When shuffle=True, the following happens: - 1. All groups are shuffled. - 2. Groups are sorted by standard deviation of classes using stable sort. - 3. Sorted groups are iterated over and assigned to folds. + 1. All groups are shuffled. + 2. Groups are sorted by standard deviation of classes using stable sort. + 3. Sorted groups are iterated over and assigned to folds. - That means that only groups with the same standard deviation of class - distribution will be shuffled, which might be useful when each group has only - a single class. -- The algorithm greedily assigns each group to one of n_splits test sets, - choosing the test set that minimises the variance in class distribution - across test sets. Group assignment proceeds from groups with highest to - lowest variance in class frequency, i.e. large groups peaked on one or few - classes are assigned first. -- This split is suboptimal in a sense that it might produce imbalanced splits - even if perfect stratification is possible. If you have relatively close - distribution of classes in each group, using :class:`GroupKFold` is better. + That means that only groups with the same standard deviation of class + distribution will be shuffled, which might be useful when each group has only + a single class. + - The algorithm greedily assigns each group to one of n_splits test sets, + choosing the test set that minimises the variance in class distribution + across test sets. Group assignment proceeds from groups with highest to + lowest variance in class frequency, i.e. large groups peaked on one or few + classes are assigned first. + - This split is suboptimal in a sense that it might produce imbalanced splits + even if perfect stratification is possible. If you have relatively close + distribution of classes in each group, using :class:`GroupKFold` is better. -|details-end| Here is a visualization of cross-validation behavior for uneven groups: @@ -999,16 +989,12 @@ using brute force and internally fits ``(n_permutations + 1) * n_cv`` models. It is therefore only tractable with small datasets for which fitting an individual model is very fast. -.. topic:: Examples - - * :ref:`sphx_glr_auto_examples_model_selection_plot_permutation_tests_for_classification.py` +.. rubric:: Examples -|details-start| -**References** -|details-split| +* :ref:`sphx_glr_auto_examples_model_selection_plot_permutation_tests_for_classification.py` - * Ojala and Garriga. `Permutation Tests for Studying Classifier Performance - `_. - J. Mach. Learn. Res. 2010. +.. dropdown:: References -|details-end| + * Ojala and Garriga. `Permutation Tests for Studying Classifier Performance + `_. + J. Mach. Learn. Res. 2010. diff --git a/doc/modules/decomposition.rst b/doc/modules/decomposition.rst index e34818a322c7d..926a4482f1428 100644 --- a/doc/modules/decomposition.rst +++ b/doc/modules/decomposition.rst @@ -51,11 +51,11 @@ data based on the amount of variance it explains. As such it implements a :scale: 75% -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_decomposition_plot_pca_iris.py` - * :ref:`sphx_glr_auto_examples_decomposition_plot_pca_vs_lda.py` - * :ref:`sphx_glr_auto_examples_decomposition_plot_pca_vs_fa_model_selection.py` +* :ref:`sphx_glr_auto_examples_decomposition_plot_pca_iris.py` +* :ref:`sphx_glr_auto_examples_decomposition_plot_pca_vs_lda.py` +* :ref:`sphx_glr_auto_examples_decomposition_plot_pca_vs_fa_model_selection.py` .. _IncrementalPCA: @@ -97,9 +97,9 @@ input data for each feature before applying the SVD. :scale: 75% -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_decomposition_plot_incremental_pca.py` +* :ref:`sphx_glr_auto_examples_decomposition_plot_incremental_pca.py` .. _RandomizedPCA: @@ -160,20 +160,20 @@ Note: the implementation of ``inverse_transform`` in :class:`PCA` with ``transform`` even when ``whiten=False`` (default). -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_applications_plot_face_recognition.py` - * :ref:`sphx_glr_auto_examples_decomposition_plot_faces_decomposition.py` +* :ref:`sphx_glr_auto_examples_applications_plot_face_recognition.py` +* :ref:`sphx_glr_auto_examples_decomposition_plot_faces_decomposition.py` -.. topic:: References: +.. rubric:: References - * Algorithm 4.3 in - :arxiv:`"Finding structure with randomness: Stochastic algorithms for - constructing approximate matrix decompositions" <0909.4061>` - Halko, et al., 2009 +* Algorithm 4.3 in + :arxiv:`"Finding structure with randomness: Stochastic algorithms for + constructing approximate matrix decompositions" <0909.4061>` + Halko, et al., 2009 - * :arxiv:`"An implementation of a randomized algorithm for principal component - analysis" <1412.3510>` A. Szlam et al. 2014 +* :arxiv:`"An implementation of a randomized algorithm for principal component + analysis" <1412.3510>` A. Szlam et al. 2014 .. _SparsePCA: @@ -248,18 +248,18 @@ factorization, while larger values shrink many coefficients to zero. the algorithm is online along the features direction, not the samples direction. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_decomposition_plot_faces_decomposition.py` +* :ref:`sphx_glr_auto_examples_decomposition_plot_faces_decomposition.py` -.. topic:: References: +.. rubric:: References - .. [Mrl09] `"Online Dictionary Learning for Sparse Coding" - `_ - J. Mairal, F. Bach, J. Ponce, G. Sapiro, 2009 - .. [Jen09] `"Structured Sparse Principal Component Analysis" - `_ - R. Jenatton, G. Obozinski, F. Bach, 2009 +.. [Mrl09] `"Online Dictionary Learning for Sparse Coding" + `_ + J. Mairal, F. Bach, J. Ponce, G. Sapiro, 2009 +.. [Jen09] `"Structured Sparse Principal Component Analysis" + `_ + R. Jenatton, G. Obozinski, F. Bach, 2009 .. _kernel_PCA: @@ -288,24 +288,23 @@ prediction (kernel dependency estimation). :class:`KernelPCA` supports both :meth:`KernelPCA.inverse_transform` is an approximation. See the example linked below for more details. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_decomposition_plot_kernel_pca.py` - * :ref:`sphx_glr_auto_examples_applications_plot_digits_denoising.py` +* :ref:`sphx_glr_auto_examples_decomposition_plot_kernel_pca.py` +* :ref:`sphx_glr_auto_examples_applications_plot_digits_denoising.py` +.. rubric:: References -.. topic:: References: +.. [Scholkopf1997] Schölkopf, Bernhard, Alexander Smola, and Klaus-Robert Müller. + `"Kernel principal component analysis." + `_ + International conference on artificial neural networks. + Springer, Berlin, Heidelberg, 1997. - .. [Scholkopf1997] Schölkopf, Bernhard, Alexander Smola, and Klaus-Robert Müller. - `"Kernel principal component analysis." - `_ - International conference on artificial neural networks. - Springer, Berlin, Heidelberg, 1997. - - .. [Bakir2003] Bakır, Gökhan H., Jason Weston, and Bernhard Schölkopf. - `"Learning to find pre-images." - `_ - Advances in neural information processing systems 16 (2003): 449-456. +.. [Bakir2003] Bakır, Gökhan H., Jason Weston, and Bernhard Schölkopf. + `"Learning to find pre-images." + `_ + Advances in neural information processing systems 16 (2003): 449-456. .. _kPCA_Solvers: @@ -323,36 +322,33 @@ is much smaller than its size. This is a situation where approximate eigensolvers can provide speedup with very low precision loss. -|details-start| -**Eigensolvers** -|details-split| - -The optional parameter ``eigen_solver='randomized'`` can be used to -*significantly* reduce the computation time when the number of requested -``n_components`` is small compared with the number of samples. It relies on -randomized decomposition methods to find an approximate solution in a shorter -time. +.. dropdown:: Eigensolvers -The time complexity of the randomized :class:`KernelPCA` is -:math:`O(n_{\mathrm{samples}}^2 \cdot n_{\mathrm{components}})` -instead of :math:`O(n_{\mathrm{samples}}^3)` for the exact method -implemented with ``eigen_solver='dense'``. + The optional parameter ``eigen_solver='randomized'`` can be used to + *significantly* reduce the computation time when the number of requested + ``n_components`` is small compared with the number of samples. It relies on + randomized decomposition methods to find an approximate solution in a shorter + time. -The memory footprint of randomized :class:`KernelPCA` is also proportional to -:math:`2 \cdot n_{\mathrm{samples}} \cdot n_{\mathrm{components}}` instead of -:math:`n_{\mathrm{samples}}^2` for the exact method. + The time complexity of the randomized :class:`KernelPCA` is + :math:`O(n_{\mathrm{samples}}^2 \cdot n_{\mathrm{components}})` + instead of :math:`O(n_{\mathrm{samples}}^3)` for the exact method + implemented with ``eigen_solver='dense'``. -Note: this technique is the same as in :ref:`RandomizedPCA`. + The memory footprint of randomized :class:`KernelPCA` is also proportional to + :math:`2 \cdot n_{\mathrm{samples}} \cdot n_{\mathrm{components}}` instead of + :math:`n_{\mathrm{samples}}^2` for the exact method. -In addition to the above two solvers, ``eigen_solver='arpack'`` can be used as -an alternate way to get an approximate decomposition. In practice, this method -only provides reasonable execution times when the number of components to find -is extremely small. It is enabled by default when the desired number of -components is less than 10 (strict) and the number of samples is more than 200 -(strict). See :class:`KernelPCA` for details. + Note: this technique is the same as in :ref:`RandomizedPCA`. + In addition to the above two solvers, ``eigen_solver='arpack'`` can be used as + an alternate way to get an approximate decomposition. In practice, this method + only provides reasonable execution times when the number of components to find + is extremely small. It is enabled by default when the desired number of + components is less than 10 (strict) and the number of samples is more than 200 + (strict). See :class:`KernelPCA` for details. -.. topic:: References: + .. rubric:: References * *dense* solver: `scipy.linalg.eigh documentation @@ -374,8 +370,6 @@ components is less than 10 (strict) and the number of samples is more than 200 `_ R. B. Lehoucq, D. C. Sorensen, and C. Yang, (1998) -|details-end| - .. _LSA: @@ -392,72 +386,67 @@ When the columnwise (per-feature) means of :math:`X` are subtracted from the feature values, truncated SVD on the resulting matrix is equivalent to PCA. -|details-start| -**About truncated SVD and latent semantic analysis (LSA)** -|details-split| - -When truncated SVD is applied to term-document matrices -(as returned by :class:`~sklearn.feature_extraction.text.CountVectorizer` or -:class:`~sklearn.feature_extraction.text.TfidfVectorizer`), -this transformation is known as -`latent semantic analysis `_ -(LSA), because it transforms such matrices -to a "semantic" space of low dimensionality. -In particular, LSA is known to combat the effects of synonymy and polysemy -(both of which roughly mean there are multiple meanings per word), -which cause term-document matrices to be overly sparse -and exhibit poor similarity under measures such as cosine similarity. +.. dropdown:: About truncated SVD and latent semantic analysis (LSA) -.. note:: - LSA is also known as latent semantic indexing, LSI, - though strictly that refers to its use in persistent indexes - for information retrieval purposes. + When truncated SVD is applied to term-document matrices + (as returned by :class:`~sklearn.feature_extraction.text.CountVectorizer` or + :class:`~sklearn.feature_extraction.text.TfidfVectorizer`), + this transformation is known as + `latent semantic analysis `_ + (LSA), because it transforms such matrices + to a "semantic" space of low dimensionality. + In particular, LSA is known to combat the effects of synonymy and polysemy + (both of which roughly mean there are multiple meanings per word), + which cause term-document matrices to be overly sparse + and exhibit poor similarity under measures such as cosine similarity. -Mathematically, truncated SVD applied to training samples :math:`X` -produces a low-rank approximation :math:`X`: + .. note:: + LSA is also known as latent semantic indexing, LSI, + though strictly that refers to its use in persistent indexes + for information retrieval purposes. -.. math:: - X \approx X_k = U_k \Sigma_k V_k^\top + Mathematically, truncated SVD applied to training samples :math:`X` + produces a low-rank approximation :math:`X`: -After this operation, :math:`U_k \Sigma_k` -is the transformed training set with :math:`k` features -(called ``n_components`` in the API). + .. math:: + X \approx X_k = U_k \Sigma_k V_k^\top -To also transform a test set :math:`X`, we multiply it with :math:`V_k`: + After this operation, :math:`U_k \Sigma_k` + is the transformed training set with :math:`k` features + (called ``n_components`` in the API). -.. math:: - X' = X V_k - -.. note:: - Most treatments of LSA in the natural language processing (NLP) - and information retrieval (IR) literature - swap the axes of the matrix :math:`X` so that it has shape - ``n_features`` × ``n_samples``. - We present LSA in a different way that matches the scikit-learn API better, - but the singular values found are the same. + To also transform a test set :math:`X`, we multiply it with :math:`V_k`: + .. math:: + X' = X V_k -While the :class:`TruncatedSVD` transformer -works with any feature matrix, -using it on tf–idf matrices is recommended over raw frequency counts -in an LSA/document processing setting. -In particular, sublinear scaling and inverse document frequency -should be turned on (``sublinear_tf=True, use_idf=True``) -to bring the feature values closer to a Gaussian distribution, -compensating for LSA's erroneous assumptions about textual data. + .. note:: + Most treatments of LSA in the natural language processing (NLP) + and information retrieval (IR) literature + swap the axes of the matrix :math:`X` so that it has shape + ``(n_features, n_samples)``. + We present LSA in a different way that matches the scikit-learn API better, + but the singular values found are the same. -|details-end| + While the :class:`TruncatedSVD` transformer + works with any feature matrix, + using it on tf-idf matrices is recommended over raw frequency counts + in an LSA/document processing setting. + In particular, sublinear scaling and inverse document frequency + should be turned on (``sublinear_tf=True, use_idf=True``) + to bring the feature values closer to a Gaussian distribution, + compensating for LSA's erroneous assumptions about textual data. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_text_plot_document_clustering.py` +* :ref:`sphx_glr_auto_examples_text_plot_document_clustering.py` -.. topic:: References: +.. rubric:: References - * Christopher D. Manning, Prabhakar Raghavan and Hinrich Schütze (2008), - *Introduction to Information Retrieval*, Cambridge University Press, - chapter 18: `Matrix decompositions & latent semantic indexing - `_ +* Christopher D. Manning, Prabhakar Raghavan and Hinrich Schütze (2008), + *Introduction to Information Retrieval*, Cambridge University Press, + chapter 18: `Matrix decompositions & latent semantic indexing + `_ @@ -511,9 +500,9 @@ the split code is filled with the negative part of the code vector, only with a positive sign. Therefore, the split_code is non-negative. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_decomposition_plot_sparse_coding.py` +* :ref:`sphx_glr_auto_examples_decomposition_plot_sparse_coding.py` Generic dictionary learning @@ -593,16 +582,16 @@ extracted from part of the image of a raccoon face looks like. :scale: 50% -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_decomposition_plot_image_denoising.py` +* :ref:`sphx_glr_auto_examples_decomposition_plot_image_denoising.py` -.. topic:: References: +.. rubric:: References - * `"Online dictionary learning for sparse coding" - `_ - J. Mairal, F. Bach, J. Ponce, G. Sapiro, 2009 +* `"Online dictionary learning for sparse coding" + `_ + J. Mairal, F. Bach, J. Ponce, G. Sapiro, 2009 .. _MiniBatchDictionaryLearning: @@ -733,10 +722,10 @@ Varimax rotation maximizes the sum of the variances of the squared loadings, i.e., it tends to produce sparser factors, which are influenced by only a few features each (the "simple structure"). See e.g., the first example below. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_decomposition_plot_varimax_fa.py` - * :ref:`sphx_glr_auto_examples_decomposition_plot_pca_vs_fa_model_selection.py` +* :ref:`sphx_glr_auto_examples_decomposition_plot_varimax_fa.py` +* :ref:`sphx_glr_auto_examples_decomposition_plot_pca_vs_fa_model_selection.py` .. _ICA: @@ -775,11 +764,11 @@ components with some sparsity: .. centered:: |pca_img4| |ica_img4| -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_decomposition_plot_ica_blind_source_separation.py` - * :ref:`sphx_glr_auto_examples_decomposition_plot_ica_vs_pca.py` - * :ref:`sphx_glr_auto_examples_decomposition_plot_faces_decomposition.py` +* :ref:`sphx_glr_auto_examples_decomposition_plot_ica_blind_source_separation.py` +* :ref:`sphx_glr_auto_examples_decomposition_plot_ica_vs_pca.py` +* :ref:`sphx_glr_auto_examples_decomposition_plot_faces_decomposition.py` .. _NMF: @@ -902,24 +891,20 @@ Note that this definition is not valid if :math:`\beta \in (0; 1)`, yet it can be continuously extended to the definitions of :math:`d_{KL}` and :math:`d_{IS}` respectively. -|details-start| -**NMF implemented solvers** -|details-split| - -:class:`NMF` implements two solvers, using Coordinate Descent ('cd') [5]_, and -Multiplicative Update ('mu') [6]_. The 'mu' solver can optimize every -beta-divergence, including of course the Frobenius norm (:math:`\beta=2`), the -(generalized) Kullback-Leibler divergence (:math:`\beta=1`) and the -Itakura-Saito divergence (:math:`\beta=0`). Note that for -:math:`\beta \in (1; 2)`, the 'mu' solver is significantly faster than for other -values of :math:`\beta`. Note also that with a negative (or 0, i.e. -'itakura-saito') :math:`\beta`, the input matrix cannot contain zero values. +.. dropdown:: NMF implemented solvers -The 'cd' solver can only optimize the Frobenius norm. Due to the -underlying non-convexity of NMF, the different solvers may converge to -different minima, even when optimizing the same distance function. + :class:`NMF` implements two solvers, using Coordinate Descent ('cd') [5]_, and + Multiplicative Update ('mu') [6]_. The 'mu' solver can optimize every + beta-divergence, including of course the Frobenius norm (:math:`\beta=2`), the + (generalized) Kullback-Leibler divergence (:math:`\beta=1`) and the + Itakura-Saito divergence (:math:`\beta=0`). Note that for + :math:`\beta \in (1; 2)`, the 'mu' solver is significantly faster than for other + values of :math:`\beta`. Note also that with a negative (or 0, i.e. + 'itakura-saito') :math:`\beta`, the input matrix cannot contain zero values. -|details-end| + The 'cd' solver can only optimize the Frobenius norm. Due to the + underlying non-convexity of NMF, the different solvers may converge to + different minima, even when optimizing the same distance function. NMF is best used with the ``fit_transform`` method, which returns the matrix W. The matrix H is stored into the fitted model in the ``components_`` attribute; @@ -937,10 +922,10 @@ stored components:: -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_decomposition_plot_faces_decomposition.py` - * :ref:`sphx_glr_auto_examples_applications_plot_topics_extraction_with_nmf_lda.py` +* :ref:`sphx_glr_auto_examples_decomposition_plot_faces_decomposition.py` +* :ref:`sphx_glr_auto_examples_applications_plot_topics_extraction_with_nmf_lda.py` .. _MiniBatchNMF: @@ -965,33 +950,33 @@ The estimator also implements ``partial_fit``, which updates ``H`` by iterating only once over a mini-batch. This can be used for online learning when the data is not readily available from the start, or when the data does not fit into memory. -.. topic:: References: +.. rubric:: References - .. [1] `"Learning the parts of objects by non-negative matrix factorization" - `_ - D. Lee, S. Seung, 1999 +.. [1] `"Learning the parts of objects by non-negative matrix factorization" + `_ + D. Lee, S. Seung, 1999 - .. [2] `"Non-negative Matrix Factorization with Sparseness Constraints" - `_ - P. Hoyer, 2004 +.. [2] `"Non-negative Matrix Factorization with Sparseness Constraints" + `_ + P. Hoyer, 2004 - .. [4] `"SVD based initialization: A head start for nonnegative - matrix factorization" - `_ - C. Boutsidis, E. Gallopoulos, 2008 +.. [4] `"SVD based initialization: A head start for nonnegative + matrix factorization" + `_ + C. Boutsidis, E. Gallopoulos, 2008 - .. [5] `"Fast local algorithms for large scale nonnegative matrix and tensor - factorizations." - `_ - A. Cichocki, A. Phan, 2009 +.. [5] `"Fast local algorithms for large scale nonnegative matrix and tensor + factorizations." + `_ + A. Cichocki, A. Phan, 2009 - .. [6] :arxiv:`"Algorithms for nonnegative matrix factorization with - the beta-divergence" <1010.1763>` - C. Fevotte, J. Idier, 2011 +.. [6] :arxiv:`"Algorithms for nonnegative matrix factorization with + the beta-divergence" <1010.1763>` + C. Fevotte, J. Idier, 2011 - .. [7] :arxiv:`"Online algorithms for nonnegative matrix factorization with the - Itakura-Saito divergence" <1106.4198>` - A. Lefevre, F. Bach, C. Fevotte, 2011 +.. [7] :arxiv:`"Online algorithms for nonnegative matrix factorization with the + Itakura-Saito divergence" <1106.4198>` + A. Lefevre, F. Bach, C. Fevotte, 2011 .. _LatentDirichletAllocation: @@ -1023,51 +1008,48 @@ of topics in the corpus and the distribution of words in the documents. The goal of LDA is to use the observed words to infer the hidden topic structure. -|details-start| -**Details on modeling text corpora** -|details-split| +.. dropdown:: Details on modeling text corpora -When modeling text corpora, the model assumes the following generative process -for a corpus with :math:`D` documents and :math:`K` topics, with :math:`K` -corresponding to `n_components` in the API: + When modeling text corpora, the model assumes the following generative process + for a corpus with :math:`D` documents and :math:`K` topics, with :math:`K` + corresponding to `n_components` in the API: -1. For each topic :math:`k \in K`, draw :math:`\beta_k \sim - \mathrm{Dirichlet}(\eta)`. This provides a distribution over the words, - i.e. the probability of a word appearing in topic :math:`k`. - :math:`\eta` corresponds to `topic_word_prior`. + 1. For each topic :math:`k \in K`, draw :math:`\beta_k \sim + \mathrm{Dirichlet}(\eta)`. This provides a distribution over the words, + i.e. the probability of a word appearing in topic :math:`k`. + :math:`\eta` corresponds to `topic_word_prior`. -2. For each document :math:`d \in D`, draw the topic proportions - :math:`\theta_d \sim \mathrm{Dirichlet}(\alpha)`. :math:`\alpha` - corresponds to `doc_topic_prior`. + 2. For each document :math:`d \in D`, draw the topic proportions + :math:`\theta_d \sim \mathrm{Dirichlet}(\alpha)`. :math:`\alpha` + corresponds to `doc_topic_prior`. -3. For each word :math:`i` in document :math:`d`: + 3. For each word :math:`i` in document :math:`d`: - a. Draw the topic assignment :math:`z_{di} \sim \mathrm{Multinomial} - (\theta_d)` - b. Draw the observed word :math:`w_{ij} \sim \mathrm{Multinomial} - (\beta_{z_{di}})` + a. Draw the topic assignment :math:`z_{di} \sim \mathrm{Multinomial} + (\theta_d)` + b. Draw the observed word :math:`w_{ij} \sim \mathrm{Multinomial} + (\beta_{z_{di}})` -For parameter estimation, the posterior distribution is: + For parameter estimation, the posterior distribution is: -.. math:: - p(z, \theta, \beta |w, \alpha, \eta) = - \frac{p(z, \theta, \beta|\alpha, \eta)}{p(w|\alpha, \eta)} + .. math:: + p(z, \theta, \beta |w, \alpha, \eta) = + \frac{p(z, \theta, \beta|\alpha, \eta)}{p(w|\alpha, \eta)} -Since the posterior is intractable, variational Bayesian method -uses a simpler distribution :math:`q(z,\theta,\beta | \lambda, \phi, \gamma)` -to approximate it, and those variational parameters :math:`\lambda`, -:math:`\phi`, :math:`\gamma` are optimized to maximize the Evidence -Lower Bound (ELBO): + Since the posterior is intractable, variational Bayesian method + uses a simpler distribution :math:`q(z,\theta,\beta | \lambda, \phi, \gamma)` + to approximate it, and those variational parameters :math:`\lambda`, + :math:`\phi`, :math:`\gamma` are optimized to maximize the Evidence + Lower Bound (ELBO): -.. math:: - \log\: P(w | \alpha, \eta) \geq L(w,\phi,\gamma,\lambda) \overset{\triangle}{=} - E_{q}[\log\:p(w,z,\theta,\beta|\alpha,\eta)] - E_{q}[\log\:q(z, \theta, \beta)] + .. math:: + \log\: P(w | \alpha, \eta) \geq L(w,\phi,\gamma,\lambda) \overset{\triangle}{=} + E_{q}[\log\:p(w,z,\theta,\beta|\alpha,\eta)] - E_{q}[\log\:q(z, \theta, \beta)] -Maximizing ELBO is equivalent to minimizing the Kullback-Leibler(KL) divergence -between :math:`q(z,\theta,\beta)` and the true posterior -:math:`p(z, \theta, \beta |w, \alpha, \eta)`. + Maximizing ELBO is equivalent to minimizing the Kullback-Leibler(KL) divergence + between :math:`q(z,\theta,\beta)` and the true posterior + :math:`p(z, \theta, \beta |w, \alpha, \eta)`. -|details-end| :class:`LatentDirichletAllocation` implements the online variational Bayes algorithm and supports both online and batch update methods. @@ -1089,27 +1071,27 @@ can be calculated from ``transform`` method. :class:`LatentDirichletAllocation` also implements ``partial_fit`` method. This is used when data can be fetched sequentially. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_applications_plot_topics_extraction_with_nmf_lda.py` +* :ref:`sphx_glr_auto_examples_applications_plot_topics_extraction_with_nmf_lda.py` -.. topic:: References: +.. rubric:: References - * `"Latent Dirichlet Allocation" - `_ - D. Blei, A. Ng, M. Jordan, 2003 +* `"Latent Dirichlet Allocation" + `_ + D. Blei, A. Ng, M. Jordan, 2003 - * `"Online Learning for Latent Dirichlet Allocation” - `_ - M. Hoffman, D. Blei, F. Bach, 2010 +* `"Online Learning for Latent Dirichlet Allocation” + `_ + M. Hoffman, D. Blei, F. Bach, 2010 - * `"Stochastic Variational Inference" - `_ - M. Hoffman, D. Blei, C. Wang, J. Paisley, 2013 +* `"Stochastic Variational Inference" + `_ + M. Hoffman, D. Blei, C. Wang, J. Paisley, 2013 - * `"The varimax criterion for analytic rotation in factor analysis" - `_ - H. F. Kaiser, 1958 +* `"The varimax criterion for analytic rotation in factor analysis" + `_ + H. F. Kaiser, 1958 See also :ref:`nca_dim_reduction` for dimensionality reduction with Neighborhood Components Analysis. diff --git a/doc/modules/density.rst b/doc/modules/density.rst index 5a9b456010aa3..39264f226185d 100644 --- a/doc/modules/density.rst +++ b/doc/modules/density.rst @@ -113,37 +113,34 @@ forms, which are shown in the following figure: .. centered:: |kde_kernels| -|details-start| -**kernels' mathematical expressions** -|details-split| +.. dropdown:: Kernels' mathematical expressions -The form of these kernels is as follows: + The form of these kernels is as follows: -* Gaussian kernel (``kernel = 'gaussian'``) + * Gaussian kernel (``kernel = 'gaussian'``) - :math:`K(x; h) \propto \exp(- \frac{x^2}{2h^2} )` + :math:`K(x; h) \propto \exp(- \frac{x^2}{2h^2} )` -* Tophat kernel (``kernel = 'tophat'``) + * Tophat kernel (``kernel = 'tophat'``) - :math:`K(x; h) \propto 1` if :math:`x < h` + :math:`K(x; h) \propto 1` if :math:`x < h` -* Epanechnikov kernel (``kernel = 'epanechnikov'``) + * Epanechnikov kernel (``kernel = 'epanechnikov'``) - :math:`K(x; h) \propto 1 - \frac{x^2}{h^2}` + :math:`K(x; h) \propto 1 - \frac{x^2}{h^2}` -* Exponential kernel (``kernel = 'exponential'``) + * Exponential kernel (``kernel = 'exponential'``) - :math:`K(x; h) \propto \exp(-x/h)` + :math:`K(x; h) \propto \exp(-x/h)` -* Linear kernel (``kernel = 'linear'``) + * Linear kernel (``kernel = 'linear'``) - :math:`K(x; h) \propto 1 - x/h` if :math:`x < h` + :math:`K(x; h) \propto 1 - x/h` if :math:`x < h` -* Cosine kernel (``kernel = 'cosine'``) + * Cosine kernel (``kernel = 'cosine'``) - :math:`K(x; h) \propto \cos(\frac{\pi x}{2h})` if :math:`x < h` + :math:`K(x; h) \propto \cos(\frac{\pi x}{2h})` if :math:`x < h` -|details-end| The kernel density estimator can be used with any of the valid distance metrics (see :class:`~sklearn.metrics.DistanceMetric` for a list of @@ -177,14 +174,14 @@ on a PCA projection of the data: The "new" data consists of linear combinations of the input data, with weights probabilistically drawn given the KDE model. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_neighbors_plot_kde_1d.py`: computation of simple kernel - density estimates in one dimension. +* :ref:`sphx_glr_auto_examples_neighbors_plot_kde_1d.py`: computation of simple kernel + density estimates in one dimension. - * :ref:`sphx_glr_auto_examples_neighbors_plot_digits_kde_sampling.py`: an example of using - Kernel Density estimation to learn a generative model of the hand-written - digits data, and drawing new samples from this model. +* :ref:`sphx_glr_auto_examples_neighbors_plot_digits_kde_sampling.py`: an example of using + Kernel Density estimation to learn a generative model of the hand-written + digits data, and drawing new samples from this model. - * :ref:`sphx_glr_auto_examples_neighbors_plot_species_kde.py`: an example of Kernel Density - estimation using the Haversine distance metric to visualize geospatial data +* :ref:`sphx_glr_auto_examples_neighbors_plot_species_kde.py`: an example of Kernel Density + estimation using the Haversine distance metric to visualize geospatial data diff --git a/doc/modules/ensemble.rst b/doc/modules/ensemble.rst index 40e3894a836fc..08c831431d197 100644 --- a/doc/modules/ensemble.rst +++ b/doc/modules/ensemble.rst @@ -18,10 +18,6 @@ trees, in averaging methods such as :ref:`Bagging methods `, :ref:`model stacking `, or :ref:`Voting `, or in boosting, as :ref:`AdaBoost `. -.. contents:: - :local: - :depth: 1 - .. _gradient_boosting: Gradient-boosted trees @@ -78,10 +74,10 @@ estimators is slightly different, and some of the features from :class:`GradientBoostingClassifier` and :class:`GradientBoostingRegressor` are not yet supported, for instance some loss functions. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_inspection_plot_partial_dependence.py` - * :ref:`sphx_glr_auto_examples_ensemble_plot_forest_hist_grad_boosting_comparison.py` +* :ref:`sphx_glr_auto_examples_inspection_plot_partial_dependence.py` +* :ref:`sphx_glr_auto_examples_ensemble_plot_forest_hist_grad_boosting_comparison.py` Usage ^^^^^ @@ -126,43 +122,40 @@ in [XGBoost]_): \mathcal{L}(\phi) = \sum_i l(\hat{y}_i, y_i) + \frac12 \sum_k \lambda ||w_k||^2 -|details-start| -**Details on l2 regularization**: -|details-split| - -It is important to notice that the loss term :math:`l(\hat{y}_i, y_i)` describes -only half of the actual loss function except for the pinball loss and absolute -error. - -The index :math:`k` refers to the k-th tree in the ensemble of trees. In the -case of regression and binary classification, gradient boosting models grow one -tree per iteration, then :math:`k` runs up to `max_iter`. In the case of -multiclass classification problems, the maximal value of the index :math:`k` is -`n_classes` :math:`\times` `max_iter`. - -If :math:`T_k` denotes the number of leaves in the k-th tree, then :math:`w_k` -is a vector of length :math:`T_k`, which contains the leaf values of the form `w -= -sum_gradient / (sum_hessian + l2_regularization)` (see equation (5) in -[XGBoost]_). - -The leaf values :math:`w_k` are derived by dividing the sum of the gradients of -the loss function by the combined sum of hessians. Adding the regularization to -the denominator penalizes the leaves with small hessians (flat regions), -resulting in smaller updates. Those :math:`w_k` values contribute then to the -model's prediction for a given input that ends up in the corresponding leaf. The -final prediction is the sum of the base prediction and the contributions from -each tree. The result of that sum is then transformed by the inverse link -function depending on the choice of the loss function (see -:ref:`gradient_boosting_formulation`). - -Notice that the original paper [XGBoost]_ introduces a term :math:`\gamma\sum_k -T_k` that penalizes the number of leaves (making it a smooth version of -`max_leaf_nodes`) not presented here as it is not implemented in scikit-learn; -whereas :math:`\lambda` penalizes the magnitude of the individual tree -predictions before being rescaled by the learning rate, see -:ref:`gradient_boosting_shrinkage`. - -|details-end| +.. dropdown:: Details on l2 regularization + + It is important to notice that the loss term :math:`l(\hat{y}_i, y_i)` describes + only half of the actual loss function except for the pinball loss and absolute + error. + + The index :math:`k` refers to the k-th tree in the ensemble of trees. In the + case of regression and binary classification, gradient boosting models grow one + tree per iteration, then :math:`k` runs up to `max_iter`. In the case of + multiclass classification problems, the maximal value of the index :math:`k` is + `n_classes` :math:`\times` `max_iter`. + + If :math:`T_k` denotes the number of leaves in the k-th tree, then :math:`w_k` + is a vector of length :math:`T_k`, which contains the leaf values of the form `w + = -sum_gradient / (sum_hessian + l2_regularization)` (see equation (5) in + [XGBoost]_). + + The leaf values :math:`w_k` are derived by dividing the sum of the gradients of + the loss function by the combined sum of hessians. Adding the regularization to + the denominator penalizes the leaves with small hessians (flat regions), + resulting in smaller updates. Those :math:`w_k` values contribute then to the + model's prediction for a given input that ends up in the corresponding leaf. The + final prediction is the sum of the base prediction and the contributions from + each tree. The result of that sum is then transformed by the inverse link + function depending on the choice of the loss function (see + :ref:`gradient_boosting_formulation`). + + Notice that the original paper [XGBoost]_ introduces a term :math:`\gamma\sum_k + T_k` that penalizes the number of leaves (making it a smooth version of + `max_leaf_nodes`) not presented here as it is not implemented in scikit-learn; + whereas :math:`\lambda` penalizes the magnitude of the individual tree + predictions before being rescaled by the learning rate, see + :ref:`gradient_boosting_shrinkage`. + Note that **early-stopping is enabled by default if the number of samples is larger than 10,000**. The early-stopping behaviour is controlled via the @@ -213,9 +206,9 @@ 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. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_ensemble_plot_hgbt_regression.py` +* :ref:`sphx_glr_auto_examples_ensemble_plot_hgbt_regression.py` .. _sw_hgbdt: @@ -302,30 +295,25 @@ the most samples (just like for continuous features). When predicting, categories that were not seen during fit time will be treated as missing values. -|details-start| -**Split finding with categorical features**: -|details-split| +.. dropdown:: Split finding with categorical features -The canonical way of considering -categorical splits in a tree is to consider -all of the :math:`2^{K - 1} - 1` partitions, where :math:`K` is the number of -categories. This can quickly become prohibitive when :math:`K` is large. -Fortunately, since gradient boosting trees are always regression trees (even -for classification problems), there exist a faster strategy that can yield -equivalent splits. First, the categories of a feature are sorted according to -the variance of the target, for each category `k`. Once the categories are -sorted, one can consider *continuous partitions*, i.e. treat the categories -as if they were ordered continuous values (see Fisher [Fisher1958]_ for a -formal proof). As a result, only :math:`K - 1` splits need to be considered -instead of :math:`2^{K - 1} - 1`. The initial sorting is a -:math:`\mathcal{O}(K \log(K))` operation, leading to a total complexity of -:math:`\mathcal{O}(K \log(K) + K)`, instead of :math:`\mathcal{O}(2^K)`. + The canonical way of considering categorical splits in a tree is to consider + all of the :math:`2^{K - 1} - 1` partitions, where :math:`K` is the number of + categories. This can quickly become prohibitive when :math:`K` is large. + Fortunately, since gradient boosting trees are always regression trees (even + for classification problems), there exist a faster strategy that can yield + equivalent splits. First, the categories of a feature are sorted according to + the variance of the target, for each category `k`. Once the categories are + sorted, one can consider *continuous partitions*, i.e. treat the categories + as if they were ordered continuous values (see Fisher [Fisher1958]_ for a + formal proof). As a result, only :math:`K - 1` splits need to be considered + instead of :math:`2^{K - 1} - 1`. The initial sorting is a + :math:`\mathcal{O}(K \log(K))` operation, leading to a total complexity of + :math:`\mathcal{O}(K \log(K) + K)`, instead of :math:`\mathcal{O}(2^K)`. -|details-end| +.. rubric:: Examples -.. topic:: Examples: - - * :ref:`sphx_glr_auto_examples_ensemble_plot_gradient_boosting_categorical.py` +* :ref:`sphx_glr_auto_examples_ensemble_plot_gradient_boosting_categorical.py` .. _monotonic_cst_gbdt: @@ -378,10 +366,10 @@ Also, monotonic constraints are not supported for multiclass classification. Since categories are unordered quantities, it is not possible to enforce monotonic constraints on categorical features. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_ensemble_plot_monotonic_constraints.py` - * :ref:`sphx_glr_auto_examples_ensemble_plot_hgbt_regression.py` +* :ref:`sphx_glr_auto_examples_ensemble_plot_monotonic_constraints.py` +* :ref:`sphx_glr_auto_examples_ensemble_plot_hgbt_regression.py` .. _interaction_cst_hgbt: @@ -414,16 +402,16 @@ Note that features not listed in ``interaction_cst`` are automatically assigned an interaction group for themselves. With again 3 features, this means that ``[{0}]`` is equivalent to ``[{0}, {1, 2}]``. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_inspection_plot_partial_dependence.py` +* :ref:`sphx_glr_auto_examples_inspection_plot_partial_dependence.py` -.. topic:: References +.. rubric:: References - .. [Mayer2022] M. Mayer, S.C. Bourassa, M. Hoesli, and D.F. Scognamiglio. - 2022. :doi:`Machine Learning Applications to Land and Structure Valuation - <10.3390/jrfm15050193>`. - Journal of Risk and Financial Management 15, no. 5: 193 +.. [Mayer2022] M. Mayer, S.C. Bourassa, M. Hoesli, and D.F. Scognamiglio. + 2022. :doi:`Machine Learning Applications to Land and Structure Valuation + <10.3390/jrfm15050193>`. + Journal of Risk and Financial Management 15, no. 5: 193 Low-level parallelism ^^^^^^^^^^^^^^^^^^^^^ @@ -479,18 +467,18 @@ Finally, many parts of the implementation of :class:`HistGradientBoostingClassifier` and :class:`HistGradientBoostingRegressor` are parallelized. -.. topic:: References +.. rubric:: References - .. [XGBoost] Tianqi Chen, Carlos Guestrin, :arxiv:`"XGBoost: A Scalable Tree - Boosting System" <1603.02754>` +.. [XGBoost] Tianqi Chen, Carlos Guestrin, :arxiv:`"XGBoost: A Scalable Tree + Boosting System" <1603.02754>` - .. [LightGBM] Ke et. al. `"LightGBM: A Highly Efficient Gradient - BoostingDecision Tree" `_ +.. [LightGBM] Ke et. al. `"LightGBM: A Highly Efficient Gradient + BoostingDecision Tree" `_ - .. [Fisher1958] Fisher, W.D. (1958). `"On Grouping for Maximum Homogeneity" - `_ - Journal of the American Statistical Association, 53, 789-798. +.. [Fisher1958] Fisher, W.D. (1958). `"On Grouping for Maximum Homogeneity" + `_ + Journal of the American Statistical Association, 53, 789-798. @@ -501,96 +489,88 @@ The usage and the parameters of :class:`GradientBoostingClassifier` and :class:`GradientBoostingRegressor` are described below. The 2 most important parameters of these estimators are `n_estimators` and `learning_rate`. -|details-start| -**Classification** -|details-split| - -:class:`GradientBoostingClassifier` supports both binary and multi-class -classification. -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... - -The number of weak learners (i.e. regression trees) is controlled by the -parameter ``n_estimators``; :ref:`The size of each tree -` can be controlled either by setting the tree -depth via ``max_depth`` or by setting the number of leaf nodes via -``max_leaf_nodes``. The ``learning_rate`` is a hyper-parameter in the range -(0.0, 1.0] that controls overfitting via :ref:`shrinkage -` . - -.. note:: - - Classification with more than 2 classes requires the induction - of ``n_classes`` regression trees at each iteration, - thus, the total number of induced trees equals - ``n_classes * n_estimators``. For datasets with a large number - of classes we strongly recommend to use - :class:`HistGradientBoostingClassifier` as an alternative to - :class:`GradientBoostingClassifier` . - -|details-end| - -|details-start| -**Regression** -|details-split| - -:class:`GradientBoostingRegressor` supports a number of -:ref:`different loss functions ` -for regression which can be specified via the argument -``loss``; the default loss function for regression is squared error -(``'squared_error'``). - -:: - - >>> import numpy as np - >>> from sklearn.metrics import mean_squared_error - >>> from sklearn.datasets import make_friedman1 - >>> from sklearn.ensemble import GradientBoostingRegressor - - >>> X, y = make_friedman1(n_samples=1200, random_state=0, noise=1.0) - >>> X_train, X_test = X[:200], X[200:] - >>> y_train, y_test = y[:200], y[200:] - >>> est = GradientBoostingRegressor( - ... n_estimators=100, learning_rate=0.1, max_depth=1, random_state=0, - ... loss='squared_error' - ... ).fit(X_train, y_train) - >>> mean_squared_error(y_test, est.predict(X_test)) - 5.00... - -The figure below shows the results of applying :class:`GradientBoostingRegressor` -with least squares loss and 500 base learners to the diabetes dataset -(:func:`sklearn.datasets.load_diabetes`). -The plot shows the train and test error at each iteration. -The train error at each iteration is stored in the -`train_score_` attribute of the gradient boosting model. -The test error at each iterations can be obtained -via the :meth:`~GradientBoostingRegressor.staged_predict` method which returns a -generator that yields the predictions at each stage. Plots like these can be used -to determine the optimal number of trees (i.e. ``n_estimators``) by early stopping. - -.. figure:: ../auto_examples/ensemble/images/sphx_glr_plot_gradient_boosting_regression_001.png - :target: ../auto_examples/ensemble/plot_gradient_boosting_regression.html - :align: center - :scale: 75 - -|details-end| +.. dropdown:: Classification + + :class:`GradientBoostingClassifier` supports both binary and multi-class + classification. + 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... + + The number of weak learners (i.e. regression trees) is controlled by the + parameter ``n_estimators``; :ref:`The size of each tree + ` can be controlled either by setting the tree + depth via ``max_depth`` or by setting the number of leaf nodes via + ``max_leaf_nodes``. The ``learning_rate`` is a hyper-parameter in the range + (0.0, 1.0] that controls overfitting via :ref:`shrinkage + ` . + + .. note:: + + Classification with more than 2 classes requires the induction + of ``n_classes`` regression trees at each iteration, + thus, the total number of induced trees equals + ``n_classes * n_estimators``. For datasets with a large number + of classes we strongly recommend to use + :class:`HistGradientBoostingClassifier` as an alternative to + :class:`GradientBoostingClassifier` . + +.. dropdown:: Regression + + :class:`GradientBoostingRegressor` supports a number of + :ref:`different loss functions ` + for regression which can be specified via the argument + ``loss``; the default loss function for regression is squared error + (``'squared_error'``). + + :: + + >>> import numpy as np + >>> from sklearn.metrics import mean_squared_error + >>> from sklearn.datasets import make_friedman1 + >>> from sklearn.ensemble import GradientBoostingRegressor + + >>> X, y = make_friedman1(n_samples=1200, random_state=0, noise=1.0) + >>> X_train, X_test = X[:200], X[200:] + >>> y_train, y_test = y[:200], y[200:] + >>> est = GradientBoostingRegressor( + ... n_estimators=100, learning_rate=0.1, max_depth=1, random_state=0, + ... loss='squared_error' + ... ).fit(X_train, y_train) + >>> mean_squared_error(y_test, est.predict(X_test)) + 5.00... + + The figure below shows the results of applying :class:`GradientBoostingRegressor` + with least squares loss and 500 base learners to the diabetes dataset + (:func:`sklearn.datasets.load_diabetes`). + The plot shows the train and test error at each iteration. + The train error at each iteration is stored in the + `train_score_` attribute of the gradient boosting model. + The test error at each iterations can be obtained + via the :meth:`~GradientBoostingRegressor.staged_predict` method which returns a + generator that yields the predictions at each stage. Plots like these can be used + to determine the optimal number of trees (i.e. ``n_estimators``) by early stopping. + + .. figure:: ../auto_examples/ensemble/images/sphx_glr_plot_gradient_boosting_regression_001.png + :target: ../auto_examples/ensemble/plot_gradient_boosting_regression.html + :align: center + :scale: 75 -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_ensemble_plot_gradient_boosting_regression.py` - * :ref:`sphx_glr_auto_examples_ensemble_plot_gradient_boosting_oob.py` +* :ref:`sphx_glr_auto_examples_ensemble_plot_gradient_boosting_regression.py` +* :ref:`sphx_glr_auto_examples_ensemble_plot_gradient_boosting_oob.py` .. _gradient_boosting_warm_start: @@ -660,116 +640,108 @@ Mathematical formulation We first present GBRT for regression, and then detail the classification case. -|details-start| -**Regression** -|details-split| +.. dropdown:: Regression -GBRT regressors are additive models whose prediction :math:`\hat{y}_i` for a -given input :math:`x_i` is of the following form: + GBRT regressors are additive models whose prediction :math:`\hat{y}_i` for a + given input :math:`x_i` is of the following form: -.. math:: - - \hat{y}_i = F_M(x_i) = \sum_{m=1}^{M} h_m(x_i) - -where the :math:`h_m` are estimators called *weak learners* in the context -of boosting. Gradient Tree Boosting uses :ref:`decision tree regressors -` of fixed size as weak learners. The constant M corresponds to the -`n_estimators` parameter. + .. math:: -Similar to other boosting algorithms, a GBRT is built in a greedy fashion: + \hat{y}_i = F_M(x_i) = \sum_{m=1}^{M} h_m(x_i) -.. math:: + where the :math:`h_m` are estimators called *weak learners* in the context + of boosting. Gradient Tree Boosting uses :ref:`decision tree regressors + ` of fixed size as weak learners. The constant M corresponds to the + `n_estimators` parameter. - F_m(x) = F_{m-1}(x) + h_m(x), + Similar to other boosting algorithms, a GBRT is built in a greedy fashion: -where the newly added tree :math:`h_m` is fitted in order to minimize a sum -of losses :math:`L_m`, given the previous ensemble :math:`F_{m-1}`: + .. math:: -.. math:: + F_m(x) = F_{m-1}(x) + h_m(x), - h_m = \arg\min_{h} L_m = \arg\min_{h} \sum_{i=1}^{n} - l(y_i, F_{m-1}(x_i) + h(x_i)), + where the newly added tree :math:`h_m` is fitted in order to minimize a sum + of losses :math:`L_m`, given the previous ensemble :math:`F_{m-1}`: -where :math:`l(y_i, F(x_i))` is defined by the `loss` parameter, detailed -in the next section. + .. math:: -By default, the initial model :math:`F_{0}` is chosen as the constant that -minimizes the loss: for a least-squares loss, this is the empirical mean of -the target values. The initial model can also be specified via the ``init`` -argument. + h_m = \arg\min_{h} L_m = \arg\min_{h} \sum_{i=1}^{n} + l(y_i, F_{m-1}(x_i) + h(x_i)), -Using a first-order Taylor approximation, the value of :math:`l` can be -approximated as follows: + where :math:`l(y_i, F(x_i))` is defined by the `loss` parameter, detailed + in the next section. -.. math:: + By default, the initial model :math:`F_{0}` is chosen as the constant that + minimizes the loss: for a least-squares loss, this is the empirical mean of + the target values. The initial model can also be specified via the ``init`` + argument. - l(y_i, F_{m-1}(x_i) + h_m(x_i)) \approx - l(y_i, F_{m-1}(x_i)) - + h_m(x_i) - \left[ \frac{\partial l(y_i, F(x_i))}{\partial F(x_i)} \right]_{F=F_{m - 1}}. + Using a first-order Taylor approximation, the value of :math:`l` can be + approximated as follows: -.. note:: + .. math:: - Briefly, a first-order Taylor approximation says that - :math:`l(z) \approx l(a) + (z - a) \frac{\partial l}{\partial z}(a)`. - Here, :math:`z` corresponds to :math:`F_{m - 1}(x_i) + h_m(x_i)`, and - :math:`a` corresponds to :math:`F_{m-1}(x_i)` + l(y_i, F_{m-1}(x_i) + h_m(x_i)) \approx + l(y_i, F_{m-1}(x_i)) + + h_m(x_i) + \left[ \frac{\partial l(y_i, F(x_i))}{\partial F(x_i)} \right]_{F=F_{m - 1}}. -The quantity :math:`\left[ \frac{\partial l(y_i, F(x_i))}{\partial F(x_i)} -\right]_{F=F_{m - 1}}` is the derivative of the loss with respect to its -second parameter, evaluated at :math:`F_{m-1}(x)`. It is easy to compute for -any given :math:`F_{m - 1}(x_i)` in a closed form since the loss is -differentiable. We will denote it by :math:`g_i`. + .. note:: -Removing the constant terms, we have: + Briefly, a first-order Taylor approximation says that + :math:`l(z) \approx l(a) + (z - a) \frac{\partial l}{\partial z}(a)`. + Here, :math:`z` corresponds to :math:`F_{m - 1}(x_i) + h_m(x_i)`, and + :math:`a` corresponds to :math:`F_{m-1}(x_i)` -.. math:: + The quantity :math:`\left[ \frac{\partial l(y_i, F(x_i))}{\partial F(x_i)} + \right]_{F=F_{m - 1}}` is the derivative of the loss with respect to its + second parameter, evaluated at :math:`F_{m-1}(x)`. It is easy to compute for + any given :math:`F_{m - 1}(x_i)` in a closed form since the loss is + differentiable. We will denote it by :math:`g_i`. - h_m \approx \arg\min_{h} \sum_{i=1}^{n} h(x_i) g_i + Removing the constant terms, we have: -This is minimized if :math:`h(x_i)` is fitted to predict a value that is -proportional to the negative gradient :math:`-g_i`. Therefore, at each -iteration, **the estimator** :math:`h_m` **is fitted to predict the negative -gradients of the samples**. The gradients are updated at each iteration. -This can be considered as some kind of gradient descent in a functional -space. + .. math:: -.. note:: + h_m \approx \arg\min_{h} \sum_{i=1}^{n} h(x_i) g_i - For some losses, e.g. ``'absolute_error'`` where the gradients - are :math:`\pm 1`, the values predicted by a fitted :math:`h_m` are not - accurate enough: the tree can only output integer values. As a result, the - leaves values of the tree :math:`h_m` are modified once the tree is - fitted, such that the leaves values minimize the loss :math:`L_m`. The - update is loss-dependent: for the absolute error loss, the value of - a leaf is updated to the median of the samples in that leaf. + This is minimized if :math:`h(x_i)` is fitted to predict a value that is + proportional to the negative gradient :math:`-g_i`. Therefore, at each + iteration, **the estimator** :math:`h_m` **is fitted to predict the negative + gradients of the samples**. The gradients are updated at each iteration. + This can be considered as some kind of gradient descent in a functional + space. -|details-end| + .. note:: -|details-start| -**Classification** -|details-split| + For some losses, e.g. ``'absolute_error'`` where the gradients + are :math:`\pm 1`, the values predicted by a fitted :math:`h_m` are not + accurate enough: the tree can only output integer values. As a result, the + leaves values of the tree :math:`h_m` are modified once the tree is + fitted, such that the leaves values minimize the loss :math:`L_m`. The + update is loss-dependent: for the absolute error loss, the value of + a leaf is updated to the median of the samples in that leaf. -Gradient boosting for classification is very similar to the regression case. -However, the sum of the trees :math:`F_M(x_i) = \sum_m h_m(x_i)` is not -homogeneous to a prediction: it cannot be a class, since the trees predict -continuous values. +.. dropdown:: Classification -The mapping from the value :math:`F_M(x_i)` to a class or a probability is -loss-dependent. For the log-loss, the probability that -:math:`x_i` belongs to the positive class is modeled as :math:`p(y_i = 1 | -x_i) = \sigma(F_M(x_i))` where :math:`\sigma` is the sigmoid or expit function. + Gradient boosting for classification is very similar to the regression case. + However, the sum of the trees :math:`F_M(x_i) = \sum_m h_m(x_i)` is not + homogeneous to a prediction: it cannot be a class, since the trees predict + continuous values. -For multiclass classification, K trees (for K classes) are built at each of -the :math:`M` iterations. The probability that :math:`x_i` belongs to class -k is modeled as a softmax of the :math:`F_{M,k}(x_i)` values. + The mapping from the value :math:`F_M(x_i)` to a class or a probability is + loss-dependent. For the log-loss, the probability that + :math:`x_i` belongs to the positive class is modeled as :math:`p(y_i = 1 | + x_i) = \sigma(F_M(x_i))` where :math:`\sigma` is the sigmoid or expit function. -Note that even for a classification task, the :math:`h_m` sub-estimator is -still a regressor, not a classifier. This is because the sub-estimators are -trained to predict (negative) *gradients*, which are always continuous -quantities. + For multiclass classification, K trees (for K classes) are built at each of + the :math:`M` iterations. The probability that :math:`x_i` belongs to class + k is modeled as a softmax of the :math:`F_{M,k}(x_i)` values. -|details-end| + Note that even for a classification task, the :math:`h_m` sub-estimator is + still a regressor, not a classifier. This is because the sub-estimators are + trained to predict (negative) *gradients*, which are always continuous + quantities. .. _gradient_boosting_loss: @@ -779,9 +751,7 @@ Loss Functions The following loss functions are supported and can be specified using the parameter ``loss``: -|details-start| -**Regression** -|details-split| +.. dropdown:: Regression * Squared error (``'squared_error'``): The natural choice for regression due to its superior computational properties. The initial model is @@ -798,12 +768,7 @@ the parameter ``loss``: can be used to create prediction intervals (see :ref:`sphx_glr_auto_examples_ensemble_plot_gradient_boosting_quantile.py`). -|details-end| - - -|details-start| -**Classification** -|details-split| +.. dropdown:: Classification * Binary log-loss (``'log-loss'``): The binomial negative log-likelihood loss function for binary classification. It provides @@ -821,8 +786,6 @@ the parameter ``loss``: examples than ``'log-loss'``; can only be used for binary classification. -|details-end| - .. _gradient_boosting_shrinkage: Shrinkage via learning rate @@ -889,11 +852,11 @@ the optimal number of iterations. OOB estimates are usually very pessimistic thu we recommend to use cross-validation instead and only use OOB if cross-validation is too time consuming. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_ensemble_plot_gradient_boosting_regularization.py` - * :ref:`sphx_glr_auto_examples_ensemble_plot_gradient_boosting_oob.py` - * :ref:`sphx_glr_auto_examples_ensemble_plot_ensemble_oob.py` +* :ref:`sphx_glr_auto_examples_ensemble_plot_gradient_boosting_regularization.py` +* :ref:`sphx_glr_auto_examples_ensemble_plot_gradient_boosting_oob.py` +* :ref:`sphx_glr_auto_examples_ensemble_plot_ensemble_oob.py` Interpretation with feature importance ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -936,22 +899,22 @@ Note that this computation of feature importance is based on entropy, and it is distinct from :func:`sklearn.inspection.permutation_importance` which is based on permutation of the features. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_ensemble_plot_gradient_boosting_regression.py` +* :ref:`sphx_glr_auto_examples_ensemble_plot_gradient_boosting_regression.py` -.. topic:: References +.. rubric:: References - .. [Friedman2001] Friedman, J.H. (2001). :doi:`Greedy function approximation: A gradient - boosting machine <10.1214/aos/1013203451>`. - Annals of Statistics, 29, 1189-1232. +.. [Friedman2001] Friedman, J.H. (2001). :doi:`Greedy function approximation: A gradient + boosting machine <10.1214/aos/1013203451>`. + Annals of Statistics, 29, 1189-1232. - .. [Friedman2002] Friedman, J.H. (2002). `Stochastic gradient boosting. - `_. - Computational Statistics & Data Analysis, 38, 367-378. +.. [Friedman2002] Friedman, J.H. (2002). `Stochastic gradient boosting. + `_. + Computational Statistics & Data Analysis, 38, 367-378. - .. [R2007] G. Ridgeway (2006). `Generalized Boosted Models: A guide to the gbm - package `_ +.. [R2007] G. Ridgeway (2006). `Generalized Boosted Models: A guide to the gbm + package `_ .. _forest: @@ -1035,9 +998,9 @@ characteristics of the dataset and the modeling task. It's a good idea to try both models and compare their performance and computational efficiency on your specific problem to determine which model is the best fit. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_ensemble_plot_forest_hist_grad_boosting_comparison.py` +* :ref:`sphx_glr_auto_examples_ensemble_plot_forest_hist_grad_boosting_comparison.py` Extremely Randomized Trees -------------------------- @@ -1134,20 +1097,20 @@ fast). Significant speedup can still be achieved though when building a large number of trees, or when building a single tree requires a fair amount of time (e.g., on large datasets). -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_ensemble_plot_forest_iris.py` - * :ref:`sphx_glr_auto_examples_ensemble_plot_forest_importances_faces.py` - * :ref:`sphx_glr_auto_examples_miscellaneous_plot_multioutput_face_completion.py` +* :ref:`sphx_glr_auto_examples_ensemble_plot_forest_iris.py` +* :ref:`sphx_glr_auto_examples_ensemble_plot_forest_importances_faces.py` +* :ref:`sphx_glr_auto_examples_miscellaneous_plot_multioutput_face_completion.py` -.. topic:: References +.. rubric:: References - .. [B2001] L. Breiman, "Random Forests", Machine Learning, 45(1), 5-32, 2001. +.. [B2001] L. Breiman, "Random Forests", Machine Learning, 45(1), 5-32, 2001. - .. [B1998] L. Breiman, "Arcing Classifiers", Annals of Statistics 1998. +.. [B1998] L. Breiman, "Arcing Classifiers", Annals of Statistics 1998. - * P. Geurts, D. Ernst., and L. Wehenkel, "Extremely randomized - trees", Machine Learning, 63(1), 3-42, 2006. +* P. Geurts, D. Ernst., and L. Wehenkel, "Extremely randomized + trees", Machine Learning, 63(1), 3-42, 2006. .. _random_forest_feature_importance: @@ -1199,16 +1162,16 @@ In practice those estimates are stored as an attribute named the value, the more important is the contribution of the matching feature to the prediction function. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_ensemble_plot_forest_importances_faces.py` - * :ref:`sphx_glr_auto_examples_ensemble_plot_forest_importances.py` +* :ref:`sphx_glr_auto_examples_ensemble_plot_forest_importances_faces.py` +* :ref:`sphx_glr_auto_examples_ensemble_plot_forest_importances.py` -.. topic:: References +.. rubric:: References - .. [L2014] G. Louppe, :arxiv:`"Understanding Random Forests: From Theory to - Practice" <1407.7502>`, - PhD Thesis, U. of Liege, 2014. +.. [L2014] G. Louppe, :arxiv:`"Understanding Random Forests: From Theory to + Practice" <1407.7502>`, + PhD Thesis, U. of Liege, 2014. .. _random_trees_embedding: @@ -1231,15 +1194,15 @@ As neighboring data points are more likely to lie within the same leaf of a tree, the transformation performs an implicit, non-parametric density estimation. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_ensemble_plot_random_forest_embedding.py` +* :ref:`sphx_glr_auto_examples_ensemble_plot_random_forest_embedding.py` - * :ref:`sphx_glr_auto_examples_manifold_plot_lle_digits.py` compares non-linear - dimensionality reduction techniques on handwritten digits. +* :ref:`sphx_glr_auto_examples_manifold_plot_lle_digits.py` compares non-linear + dimensionality reduction techniques on handwritten digits. - * :ref:`sphx_glr_auto_examples_ensemble_plot_feature_transformation.py` compares - supervised and unsupervised tree based feature transformations. +* :ref:`sphx_glr_auto_examples_ensemble_plot_feature_transformation.py` compares + supervised and unsupervised tree based feature transformations. .. seealso:: @@ -1335,24 +1298,23 @@ subsets of 50% of the samples and 50% of the features. >>> bagging = BaggingClassifier(KNeighborsClassifier(), ... max_samples=0.5, max_features=0.5) -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_ensemble_plot_bias_variance.py` +* :ref:`sphx_glr_auto_examples_ensemble_plot_bias_variance.py` -.. topic:: References +.. rubric:: References - .. [B1999] L. Breiman, "Pasting small votes for classification in large - databases and on-line", Machine Learning, 36(1), 85-103, 1999. +.. [B1999] L. Breiman, "Pasting small votes for classification in large + databases and on-line", Machine Learning, 36(1), 85-103, 1999. - .. [B1996] L. Breiman, "Bagging predictors", Machine Learning, 24(2), - 123-140, 1996. +.. [B1996] L. Breiman, "Bagging predictors", Machine Learning, 24(2), + 123-140, 1996. - .. [H1998] T. Ho, "The random subspace method for constructing decision - forests", Pattern Analysis and Machine Intelligence, 20(8), 832-844, - 1998. +.. [H1998] T. Ho, "The random subspace method for constructing decision + forests", Pattern Analysis and Machine Intelligence, 20(8), 832-844, 1998. - .. [LG2012] G. Louppe and P. Geurts, "Ensembles on Random Patches", - Machine Learning and Knowledge Discovery in Databases, 346-361, 2012. +.. [LG2012] G. Louppe and P. Geurts, "Ensembles on Random Patches", + Machine Learning and Knowledge Discovery in Databases, 346-361, 2012. @@ -1507,29 +1469,25 @@ Optionally, weights can be provided for the individual classifiers:: ... voting='soft', weights=[2,5,1] ... ) -|details-start| -**Using the `VotingClassifier` with `GridSearchCV`** -|details-split| - -The :class:`VotingClassifier` can also be used together with -:class:`~sklearn.model_selection.GridSearchCV` in order to tune the -hyperparameters of the individual estimators:: +.. dropdown:: Using the :class:`VotingClassifier` with :class:`~sklearn.model_selection.GridSearchCV` - >>> from sklearn.model_selection import GridSearchCV - >>> clf1 = LogisticRegression(random_state=1) - >>> clf2 = RandomForestClassifier(random_state=1) - >>> clf3 = GaussianNB() - >>> eclf = VotingClassifier( - ... estimators=[('lr', clf1), ('rf', clf2), ('gnb', clf3)], - ... voting='soft' - ... ) + The :class:`VotingClassifier` can also be used together with + :class:`~sklearn.model_selection.GridSearchCV` in order to tune the + hyperparameters of the individual estimators:: - >>> params = {'lr__C': [1.0, 100.0], 'rf__n_estimators': [20, 200]} + >>> from sklearn.model_selection import GridSearchCV + >>> clf1 = LogisticRegression(random_state=1) + >>> clf2 = RandomForestClassifier(random_state=1) + >>> clf3 = GaussianNB() + >>> eclf = VotingClassifier( + ... estimators=[('lr', clf1), ('rf', clf2), ('gnb', clf3)], + ... voting='soft' + ... ) - >>> grid = GridSearchCV(estimator=eclf, param_grid=params, cv=5) - >>> grid = grid.fit(iris.data, iris.target) + >>> params = {'lr__C': [1.0, 100.0], 'rf__n_estimators': [20, 200]} -|details-end| + >>> grid = GridSearchCV(estimator=eclf, param_grid=params, cv=5) + >>> grid = grid.fit(iris.data, iris.target) .. _voting_regressor: @@ -1567,9 +1525,9 @@ The following example shows how to fit the VotingRegressor:: :align: center :scale: 75% -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_ensemble_plot_voting_regressor.py` +* :ref:`sphx_glr_auto_examples_ensemble_plot_voting_regressor.py` .. _stacking: @@ -1688,10 +1646,10 @@ computationally expensive. ... .format(multi_layer_regressor.score(X_test, y_test))) R2 score: 0.53 -.. topic:: References +.. rubric:: References - .. [W1992] Wolpert, David H. "Stacked generalization." Neural networks 5.2 - (1992): 241-259. +.. [W1992] Wolpert, David H. "Stacked generalization." Neural networks 5.2 + (1992): 241-259. @@ -1757,27 +1715,26 @@ The main parameters to tune to obtain good results are ``n_estimators`` and the complexity of the base estimators (e.g., its depth ``max_depth`` or minimum required number of samples to consider a split ``min_samples_split``). -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_ensemble_plot_adaboost_multiclass.py` shows the performance - of AdaBoost on a multi-class problem. +* :ref:`sphx_glr_auto_examples_ensemble_plot_adaboost_multiclass.py` shows the performance + of AdaBoost on a multi-class problem. - * :ref:`sphx_glr_auto_examples_ensemble_plot_adaboost_twoclass.py` shows the decision boundary - and decision function values for a non-linearly separable two-class problem - using AdaBoost-SAMME. +* :ref:`sphx_glr_auto_examples_ensemble_plot_adaboost_twoclass.py` shows the decision boundary + and decision function values for a non-linearly separable two-class problem + using AdaBoost-SAMME. - * :ref:`sphx_glr_auto_examples_ensemble_plot_adaboost_regression.py` demonstrates regression - with the AdaBoost.R2 algorithm. +* :ref:`sphx_glr_auto_examples_ensemble_plot_adaboost_regression.py` demonstrates regression + with the AdaBoost.R2 algorithm. -.. topic:: References +.. rubric:: References - .. [FS1995] Y. Freund, and R. Schapire, "A Decision-Theoretic Generalization of - On-Line Learning and an Application to Boosting", 1997. +.. [FS1995] Y. Freund, and R. Schapire, "A Decision-Theoretic Generalization of + On-Line Learning and an Application to Boosting", 1997. - .. [ZZRH2009] J. Zhu, H. Zou, S. Rosset, T. Hastie. "Multi-class AdaBoost", - 2009. +.. [ZZRH2009] J. Zhu, H. Zou, S. Rosset, T. Hastie. "Multi-class AdaBoost", 2009. - .. [D1997] H. Drucker. "Improving Regressors using Boosting Techniques", 1997. +.. [D1997] H. Drucker. "Improving Regressors using Boosting Techniques", 1997. - .. [HTF] T. Hastie, R. Tibshirani and J. Friedman, "Elements of - Statistical Learning Ed. 2", Springer, 2009. +.. [HTF] T. Hastie, R. Tibshirani and J. Friedman, "Elements of Statistical Learning + Ed. 2", Springer, 2009. diff --git a/doc/modules/feature_extraction.rst b/doc/modules/feature_extraction.rst index 7ac538a89849b..2181014644e15 100644 --- a/doc/modules/feature_extraction.rst +++ b/doc/modules/feature_extraction.rst @@ -206,35 +206,32 @@ Note the use of a generator comprehension, which introduces laziness into the feature extraction: tokens are only processed on demand from the hasher. -|details-start| -**Implementation details** -|details-split| +.. dropdown:: Implementation details -:class:`FeatureHasher` uses the signed 32-bit variant of MurmurHash3. -As a result (and because of limitations in ``scipy.sparse``), -the maximum number of features supported is currently :math:`2^{31} - 1`. + :class:`FeatureHasher` uses the signed 32-bit variant of MurmurHash3. + As a result (and because of limitations in ``scipy.sparse``), + the maximum number of features supported is currently :math:`2^{31} - 1`. -The original formulation of the hashing trick by Weinberger et al. -used two separate hash functions :math:`h` and :math:`\xi` -to determine the column index and sign of a feature, respectively. -The present implementation works under the assumption -that the sign bit of MurmurHash3 is independent of its other bits. + The original formulation of the hashing trick by Weinberger et al. + used two separate hash functions :math:`h` and :math:`\xi` + to determine the column index and sign of a feature, respectively. + The present implementation works under the assumption + that the sign bit of MurmurHash3 is independent of its other bits. -Since a simple modulo is used to transform the hash function to a column index, -it is advisable to use a power of two as the ``n_features`` parameter; -otherwise the features will not be mapped evenly to the columns. + Since a simple modulo is used to transform the hash function to a column index, + it is advisable to use a power of two as the ``n_features`` parameter; + otherwise the features will not be mapped evenly to the columns. -.. topic:: References: + .. rubric:: References * `MurmurHash3 `_. -|details-end| -.. topic:: References: +.. rubric:: References - * Kilian Weinberger, Anirban Dasgupta, John Langford, Alex Smola and - Josh Attenberg (2009). `Feature hashing for large scale multitask learning - `_. Proc. ICML. +* Kilian Weinberger, Anirban Dasgupta, John Langford, Alex Smola and + Josh Attenberg (2009). `Feature hashing for large scale multitask learning + `_. Proc. ICML. .. _text_feature_extraction: @@ -310,7 +307,7 @@ counting in a single class:: This model has many parameters, however the default values are quite reasonable (please see the :ref:`reference documentation -` for the details):: +` for the details):: >>> vectorizer = CountVectorizer() >>> vectorizer @@ -422,12 +419,12 @@ tokenizer, so if *we've* is in ``stop_words``, but *ve* is not, *ve* will be retained from *we've* in transformed text. Our vectorizers will try to identify and warn about some kinds of inconsistencies. -.. topic:: References +.. rubric:: References - .. [NQY18] J. Nothman, H. Qin and R. Yurchak (2018). - `"Stop Word Lists in Free Open-source Software Packages" - `__. - In *Proc. Workshop for NLP Open Source Software*. +.. [NQY18] J. Nothman, H. Qin and R. Yurchak (2018). + `"Stop Word Lists in Free Open-source Software Packages" + `__. + In *Proc. Workshop for NLP Open Source Software*. .. _tfidf: @@ -492,132 +489,126 @@ class:: TfidfTransformer(smooth_idf=False) Again please see the :ref:`reference documentation -` for the details on all the parameters. - -|details-start| -**Numeric example of a tf-idf matrix** -|details-split| - -Let's take an example with the following counts. The first term is present -100% of the time hence not very interesting. The two other features only -in less than 50% of the time hence probably more representative of the -content of the documents:: - - >>> counts = [[3, 0, 1], - ... [2, 0, 0], - ... [3, 0, 0], - ... [4, 0, 0], - ... [3, 2, 0], - ... [3, 0, 2]] - ... - >>> tfidf = transformer.fit_transform(counts) - >>> tfidf - <6x3 sparse matrix of type '<... 'numpy.float64'>' - with 9 stored elements in Compressed Sparse ... format> +` for the details on all the parameters. - >>> tfidf.toarray() - array([[0.81940995, 0. , 0.57320793], - [1. , 0. , 0. ], - [1. , 0. , 0. ], - [1. , 0. , 0. ], - [0.47330339, 0.88089948, 0. ], - [0.58149261, 0. , 0.81355169]]) +.. dropdown:: Numeric example of a tf-idf matrix -Each row is normalized to have unit Euclidean norm: + Let's take an example with the following counts. The first term is present + 100% of the time hence not very interesting. The two other features only + in less than 50% of the time hence probably more representative of the + content of the documents:: -:math:`v_{norm} = \frac{v}{||v||_2} = \frac{v}{\sqrt{v{_1}^2 + -v{_2}^2 + \dots + v{_n}^2}}` + >>> counts = [[3, 0, 1], + ... [2, 0, 0], + ... [3, 0, 0], + ... [4, 0, 0], + ... [3, 2, 0], + ... [3, 0, 2]] + ... + >>> tfidf = transformer.fit_transform(counts) + >>> tfidf + <6x3 sparse matrix of type '<... 'numpy.float64'>' + with 9 stored elements in Compressed Sparse ... format> -For example, we can compute the tf-idf of the first term in the first -document in the `counts` array as follows: + >>> tfidf.toarray() + array([[0.81940995, 0. , 0.57320793], + [1. , 0. , 0. ], + [1. , 0. , 0. ], + [1. , 0. , 0. ], + [0.47330339, 0.88089948, 0. ], + [0.58149261, 0. , 0.81355169]]) -:math:`n = 6` + Each row is normalized to have unit Euclidean norm: -:math:`\text{df}(t)_{\text{term1}} = 6` + :math:`v_{norm} = \frac{v}{||v||_2} = \frac{v}{\sqrt{v{_1}^2 + + v{_2}^2 + \dots + v{_n}^2}}` -:math:`\text{idf}(t)_{\text{term1}} = -\log \frac{n}{\text{df}(t)} + 1 = \log(1)+1 = 1` + For example, we can compute the tf-idf of the first term in the first + document in the `counts` array as follows: -:math:`\text{tf-idf}_{\text{term1}} = \text{tf} \times \text{idf} = 3 \times 1 = 3` + :math:`n = 6` -Now, if we repeat this computation for the remaining 2 terms in the document, -we get + :math:`\text{df}(t)_{\text{term1}} = 6` -:math:`\text{tf-idf}_{\text{term2}} = 0 \times (\log(6/1)+1) = 0` + :math:`\text{idf}(t)_{\text{term1}} = + \log \frac{n}{\text{df}(t)} + 1 = \log(1)+1 = 1` -:math:`\text{tf-idf}_{\text{term3}} = 1 \times (\log(6/2)+1) \approx 2.0986` + :math:`\text{tf-idf}_{\text{term1}} = \text{tf} \times \text{idf} = 3 \times 1 = 3` -and the vector of raw tf-idfs: + Now, if we repeat this computation for the remaining 2 terms in the document, + we get -:math:`\text{tf-idf}_{\text{raw}} = [3, 0, 2.0986].` + :math:`\text{tf-idf}_{\text{term2}} = 0 \times (\log(6/1)+1) = 0` + :math:`\text{tf-idf}_{\text{term3}} = 1 \times (\log(6/2)+1) \approx 2.0986` -Then, applying the Euclidean (L2) norm, we obtain the following tf-idfs -for document 1: + and the vector of raw tf-idfs: -:math:`\frac{[3, 0, 2.0986]}{\sqrt{\big(3^2 + 0^2 + 2.0986^2\big)}} -= [ 0.819, 0, 0.573].` + :math:`\text{tf-idf}_{\text{raw}} = [3, 0, 2.0986].` -Furthermore, the default parameter ``smooth_idf=True`` adds "1" to the numerator -and denominator as if an extra document was seen containing every term in the -collection exactly once, which prevents zero divisions: -:math:`\text{idf}(t) = \log{\frac{1 + n}{1+\text{df}(t)}} + 1` + Then, applying the Euclidean (L2) norm, we obtain the following tf-idfs + for document 1: -Using this modification, the tf-idf of the third term in document 1 changes to -1.8473: + :math:`\frac{[3, 0, 2.0986]}{\sqrt{\big(3^2 + 0^2 + 2.0986^2\big)}} + = [ 0.819, 0, 0.573].` -:math:`\text{tf-idf}_{\text{term3}} = 1 \times \log(7/3)+1 \approx 1.8473` + Furthermore, the default parameter ``smooth_idf=True`` adds "1" to the numerator + and denominator as if an extra document was seen containing every term in the + collection exactly once, which prevents zero divisions: -And the L2-normalized tf-idf changes to + :math:`\text{idf}(t) = \log{\frac{1 + n}{1+\text{df}(t)}} + 1` -:math:`\frac{[3, 0, 1.8473]}{\sqrt{\big(3^2 + 0^2 + 1.8473^2\big)}} -= [0.8515, 0, 0.5243]`:: + Using this modification, the tf-idf of the third term in document 1 changes to + 1.8473: - >>> transformer = TfidfTransformer() - >>> transformer.fit_transform(counts).toarray() - array([[0.85151335, 0. , 0.52433293], - [1. , 0. , 0. ], - [1. , 0. , 0. ], - [1. , 0. , 0. ], - [0.55422893, 0.83236428, 0. ], - [0.63035731, 0. , 0.77630514]]) + :math:`\text{tf-idf}_{\text{term3}} = 1 \times \log(7/3)+1 \approx 1.8473` -The weights of each -feature computed by the ``fit`` method call are stored in a model -attribute:: + And the L2-normalized tf-idf changes to - >>> transformer.idf_ - array([1. ..., 2.25..., 1.84...]) + :math:`\frac{[3, 0, 1.8473]}{\sqrt{\big(3^2 + 0^2 + 1.8473^2\big)}} + = [0.8515, 0, 0.5243]`:: + >>> transformer = TfidfTransformer() + >>> transformer.fit_transform(counts).toarray() + array([[0.85151335, 0. , 0.52433293], + [1. , 0. , 0. ], + [1. , 0. , 0. ], + [1. , 0. , 0. ], + [0.55422893, 0.83236428, 0. ], + [0.63035731, 0. , 0.77630514]]) + The weights of each + feature computed by the ``fit`` method call are stored in a model + attribute:: + >>> transformer.idf_ + array([1. ..., 2.25..., 1.84...]) -As tf–idf is very often used for text features, there is also another -class called :class:`TfidfVectorizer` that combines all the options of -:class:`CountVectorizer` and :class:`TfidfTransformer` in a single model:: + As tf-idf is very often used for text features, there is also another + class called :class:`TfidfVectorizer` that combines all the options of + :class:`CountVectorizer` and :class:`TfidfTransformer` in a single model:: - >>> from sklearn.feature_extraction.text import TfidfVectorizer - >>> vectorizer = TfidfVectorizer() - >>> vectorizer.fit_transform(corpus) - <4x9 sparse matrix of type '<... 'numpy.float64'>' - with 19 stored elements in Compressed Sparse ... format> + >>> from sklearn.feature_extraction.text import TfidfVectorizer + >>> vectorizer = TfidfVectorizer() + >>> vectorizer.fit_transform(corpus) + <4x9 sparse matrix of type '<... 'numpy.float64'>' + with 19 stored elements in Compressed Sparse ... format> -While the tf–idf normalization is often very useful, there might -be cases where the binary occurrence markers might offer better -features. This can be achieved by using the ``binary`` parameter -of :class:`CountVectorizer`. In particular, some estimators such as -:ref:`bernoulli_naive_bayes` explicitly model discrete boolean random -variables. Also, very short texts are likely to have noisy tf–idf values -while the binary occurrence info is more stable. + While the tf-idf normalization is often very useful, there might + be cases where the binary occurrence markers might offer better + features. This can be achieved by using the ``binary`` parameter + of :class:`CountVectorizer`. In particular, some estimators such as + :ref:`bernoulli_naive_bayes` explicitly model discrete boolean random + variables. Also, very short texts are likely to have noisy tf-idf values + while the binary occurrence info is more stable. -As usual the best way to adjust the feature extraction parameters -is to use a cross-validated grid search, for instance by pipelining the -feature extractor with a classifier: + As usual the best way to adjust the feature extraction parameters + is to use a cross-validated grid search, for instance by pipelining the + feature extractor with a classifier: -* :ref:`sphx_glr_auto_examples_model_selection_plot_grid_search_text_feature_extraction.py` + * :ref:`sphx_glr_auto_examples_model_selection_plot_grid_search_text_feature_extraction.py` -|details-end| Decoding text files ------------------- @@ -646,64 +637,60 @@ or ``"replace"``. See the documentation for the Python function ``bytes.decode`` for more details (type ``help(bytes.decode)`` at the Python prompt). -|details-start| -**Troubleshooting decoding text** -|details-split| - -If you are having trouble decoding text, here are some things to try: - -- Find out what the actual encoding of the text is. The file might come - with a header or README that tells you the encoding, or there might be some - standard encoding you can assume based on where the text comes from. - -- You may be able to find out what kind of encoding it is in general - using the UNIX command ``file``. The Python ``chardet`` module comes with - a script called ``chardetect.py`` that will guess the specific encoding, - though you cannot rely on its guess being correct. - -- You could try UTF-8 and disregard the errors. You can decode byte - strings with ``bytes.decode(errors='replace')`` to replace all - decoding errors with a meaningless character, or set - ``decode_error='replace'`` in the vectorizer. This may damage the - usefulness of your features. - -- Real text may come from a variety of sources that may have used different - encodings, or even be sloppily decoded in a different encoding than the - one it was encoded with. This is common in text retrieved from the Web. - The Python package `ftfy`_ can automatically sort out some classes of - decoding errors, so you could try decoding the unknown text as ``latin-1`` - and then using ``ftfy`` to fix errors. - -- If the text is in a mish-mash of encodings that is simply too hard to sort - out (which is the case for the 20 Newsgroups dataset), you can fall back on - a simple single-byte encoding such as ``latin-1``. Some text may display - incorrectly, but at least the same sequence of bytes will always represent - the same feature. - -For example, the following snippet uses ``chardet`` -(not shipped with scikit-learn, must be installed separately) -to figure out the encoding of three texts. -It then vectorizes the texts and prints the learned vocabulary. -The output is not shown here. - - >>> import chardet # doctest: +SKIP - >>> text1 = b"Sei mir gegr\xc3\xbc\xc3\x9ft mein Sauerkraut" - >>> text2 = b"holdselig sind deine Ger\xfcche" - >>> text3 = b"\xff\xfeA\x00u\x00f\x00 \x00F\x00l\x00\xfc\x00g\x00e\x00l\x00n\x00 \x00d\x00e\x00s\x00 \x00G\x00e\x00s\x00a\x00n\x00g\x00e\x00s\x00,\x00 \x00H\x00e\x00r\x00z\x00l\x00i\x00e\x00b\x00c\x00h\x00e\x00n\x00,\x00 \x00t\x00r\x00a\x00g\x00 \x00i\x00c\x00h\x00 \x00d\x00i\x00c\x00h\x00 \x00f\x00o\x00r\x00t\x00" - >>> decoded = [x.decode(chardet.detect(x)['encoding']) - ... for x in (text1, text2, text3)] # doctest: +SKIP - >>> v = CountVectorizer().fit(decoded).vocabulary_ # doctest: +SKIP - >>> for term in v: print(v) # doctest: +SKIP - -(Depending on the version of ``chardet``, it might get the first one wrong.) - -For an introduction to Unicode and character encodings in general, -see Joel Spolsky's `Absolute Minimum Every Software Developer Must Know -About Unicode `_. - -.. _`ftfy`: https://github.com/LuminosoInsight/python-ftfy - -|details-end| +.. dropdown:: Troubleshooting decoding text + + If you are having trouble decoding text, here are some things to try: + + - Find out what the actual encoding of the text is. The file might come + with a header or README that tells you the encoding, or there might be some + standard encoding you can assume based on where the text comes from. + + - You may be able to find out what kind of encoding it is in general + using the UNIX command ``file``. The Python ``chardet`` module comes with + a script called ``chardetect.py`` that will guess the specific encoding, + though you cannot rely on its guess being correct. + + - You could try UTF-8 and disregard the errors. You can decode byte + strings with ``bytes.decode(errors='replace')`` to replace all + decoding errors with a meaningless character, or set + ``decode_error='replace'`` in the vectorizer. This may damage the + usefulness of your features. + + - Real text may come from a variety of sources that may have used different + encodings, or even be sloppily decoded in a different encoding than the + one it was encoded with. This is common in text retrieved from the Web. + The Python package `ftfy `__ + can automatically sort out some classes of + decoding errors, so you could try decoding the unknown text as ``latin-1`` + and then using ``ftfy`` to fix errors. + + - If the text is in a mish-mash of encodings that is simply too hard to sort + out (which is the case for the 20 Newsgroups dataset), you can fall back on + a simple single-byte encoding such as ``latin-1``. Some text may display + incorrectly, but at least the same sequence of bytes will always represent + the same feature. + + For example, the following snippet uses ``chardet`` + (not shipped with scikit-learn, must be installed separately) + to figure out the encoding of three texts. + It then vectorizes the texts and prints the learned vocabulary. + The output is not shown here. + + >>> import chardet # doctest: +SKIP + >>> text1 = b"Sei mir gegr\xc3\xbc\xc3\x9ft mein Sauerkraut" + >>> text2 = b"holdselig sind deine Ger\xfcche" + >>> text3 = b"\xff\xfeA\x00u\x00f\x00 \x00F\x00l\x00\xfc\x00g\x00e\x00l\x00n\x00 \x00d\x00e\x00s\x00 \x00G\x00e\x00s\x00a\x00n\x00g\x00e\x00s\x00,\x00 \x00H\x00e\x00r\x00z\x00l\x00i\x00e\x00b\x00c\x00h\x00e\x00n\x00,\x00 \x00t\x00r\x00a\x00g\x00 \x00i\x00c\x00h\x00 \x00d\x00i\x00c\x00h\x00 \x00f\x00o\x00r\x00t\x00" + >>> decoded = [x.decode(chardet.detect(x)['encoding']) + ... for x in (text1, text2, text3)] # doctest: +SKIP + >>> v = CountVectorizer().fit(decoded).vocabulary_ # doctest: +SKIP + >>> for term in v: print(v) # doctest: +SKIP + + (Depending on the version of ``chardet``, it might get the first one wrong.) + + For an introduction to Unicode and character encodings in general, + see Joel Spolsky's `Absolute Minimum Every Software Developer Must Know + About Unicode `_. + Applications and examples ------------------------- @@ -884,28 +871,25 @@ The :class:`HashingVectorizer` also comes with the following limitations: model. A :class:`TfidfTransformer` can be appended to it in a pipeline if required. -|details-start| -**Performing out-of-core scaling with HashingVectorizer** -|details-split| +.. dropdown:: Performing out-of-core scaling with HashingVectorizer -An interesting development of using a :class:`HashingVectorizer` is the ability -to perform `out-of-core`_ scaling. This means that we can learn from data that -does not fit into the computer's main memory. + An interesting development of using a :class:`HashingVectorizer` is the ability + to perform `out-of-core`_ scaling. This means that we can learn from data that + does not fit into the computer's main memory. -.. _out-of-core: https://en.wikipedia.org/wiki/Out-of-core_algorithm + .. _out-of-core: https://en.wikipedia.org/wiki/Out-of-core_algorithm -A strategy to implement out-of-core scaling is to stream data to the estimator -in mini-batches. Each mini-batch is vectorized using :class:`HashingVectorizer` -so as to guarantee that the input space of the estimator has always the same -dimensionality. The amount of memory used at any time is thus bounded by the -size of a mini-batch. Although there is no limit to the amount of data that can -be ingested using such an approach, from a practical point of view the learning -time is often limited by the CPU time one wants to spend on the task. + A strategy to implement out-of-core scaling is to stream data to the estimator + in mini-batches. Each mini-batch is vectorized using :class:`HashingVectorizer` + so as to guarantee that the input space of the estimator has always the same + dimensionality. The amount of memory used at any time is thus bounded by the + size of a mini-batch. Although there is no limit to the amount of data that can + be ingested using such an approach, from a practical point of view the learning + time is often limited by the CPU time one wants to spend on the task. -For a full-fledged example of out-of-core scaling in a text classification -task see :ref:`sphx_glr_auto_examples_applications_plot_out_of_core_classification.py`. + For a full-fledged example of out-of-core scaling in a text classification + task see :ref:`sphx_glr_auto_examples_applications_plot_out_of_core_classification.py`. -|details-end| Customizing the vectorizer classes ---------------------------------- @@ -945,65 +929,58 @@ parameters it is possible to derive from the class and override the ``build_preprocessor``, ``build_tokenizer`` and ``build_analyzer`` factory methods instead of passing custom functions. -|details-start| -**Tips and tricks** -|details-split| - -Some tips and tricks: - -* If documents are pre-tokenized by an external package, then store them in - files (or strings) with the tokens separated by whitespace and pass - ``analyzer=str.split`` -* Fancy token-level analysis such as stemming, lemmatizing, compound - splitting, filtering based on part-of-speech, etc. are not included in the - scikit-learn codebase, but can be added by customizing either the - tokenizer or the analyzer. - Here's a ``CountVectorizer`` with a tokenizer and lemmatizer using - `NLTK `_:: - - >>> from nltk import word_tokenize # doctest: +SKIP - >>> from nltk.stem import WordNetLemmatizer # doctest: +SKIP - >>> class LemmaTokenizer: - ... def __init__(self): - ... self.wnl = WordNetLemmatizer() - ... def __call__(self, doc): - ... return [self.wnl.lemmatize(t) for t in word_tokenize(doc)] - ... - >>> vect = CountVectorizer(tokenizer=LemmaTokenizer()) # doctest: +SKIP - - (Note that this will not filter out punctuation.) - - - The following example will, for instance, transform some British spelling - to American spelling:: - - >>> import re - >>> def to_british(tokens): - ... for t in tokens: - ... t = re.sub(r"(...)our$", r"\1or", t) - ... t = re.sub(r"([bt])re$", r"\1er", t) - ... t = re.sub(r"([iy])s(e$|ing|ation)", r"\1z\2", t) - ... t = re.sub(r"ogue$", "og", t) - ... yield t - ... - >>> class CustomVectorizer(CountVectorizer): - ... def build_tokenizer(self): - ... tokenize = super().build_tokenizer() - ... return lambda doc: list(to_british(tokenize(doc))) - ... - >>> print(CustomVectorizer().build_analyzer()(u"color colour")) - [...'color', ...'color'] - - for other styles of preprocessing; examples include stemming, lemmatization, - or normalizing numerical tokens, with the latter illustrated in: - - * :ref:`sphx_glr_auto_examples_bicluster_plot_bicluster_newsgroups.py` - - -Customizing the vectorizer can also be useful when handling Asian languages -that do not use an explicit word separator such as whitespace. - -|details-end| +.. dropdown:: Tips and tricks + :color: success + + * If documents are pre-tokenized by an external package, then store them in + files (or strings) with the tokens separated by whitespace and pass + ``analyzer=str.split`` + * Fancy token-level analysis such as stemming, lemmatizing, compound + splitting, filtering based on part-of-speech, etc. are not included in the + scikit-learn codebase, but can be added by customizing either the + tokenizer or the analyzer. + Here's a ``CountVectorizer`` with a tokenizer and lemmatizer using + `NLTK `_:: + + >>> from nltk import word_tokenize # doctest: +SKIP + >>> from nltk.stem import WordNetLemmatizer # doctest: +SKIP + >>> class LemmaTokenizer: + ... def __init__(self): + ... self.wnl = WordNetLemmatizer() + ... def __call__(self, doc): + ... return [self.wnl.lemmatize(t) for t in word_tokenize(doc)] + ... + >>> vect = CountVectorizer(tokenizer=LemmaTokenizer()) # doctest: +SKIP + + (Note that this will not filter out punctuation.) + + The following example will, for instance, transform some British spelling + to American spelling:: + + >>> import re + >>> def to_british(tokens): + ... for t in tokens: + ... t = re.sub(r"(...)our$", r"\1or", t) + ... t = re.sub(r"([bt])re$", r"\1er", t) + ... t = re.sub(r"([iy])s(e$|ing|ation)", r"\1z\2", t) + ... t = re.sub(r"ogue$", "og", t) + ... yield t + ... + >>> class CustomVectorizer(CountVectorizer): + ... def build_tokenizer(self): + ... tokenize = super().build_tokenizer() + ... return lambda doc: list(to_british(tokenize(doc))) + ... + >>> print(CustomVectorizer().build_analyzer()(u"color colour")) + [...'color', ...'color'] + + for other styles of preprocessing; examples include stemming, lemmatization, + or normalizing numerical tokens, with the latter illustrated in: + + * :ref:`sphx_glr_auto_examples_bicluster_plot_bicluster_newsgroups.py` + + Customizing the vectorizer can also be useful when handling Asian languages + that do not use an explicit word separator such as whitespace. .. _image_feature_extraction: diff --git a/doc/modules/feature_selection.rst b/doc/modules/feature_selection.rst index 1b5ce57b0074f..6746f2f65da00 100644 --- a/doc/modules/feature_selection.rst +++ b/doc/modules/feature_selection.rst @@ -114,11 +114,11 @@ applied to non-negative features, such as frequencies. feature selection as well. One needs to provide a `score_func` where `y=None`. The `score_func` should use internally `X` to compute the scores. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_feature_selection_plot_feature_selection.py` +* :ref:`sphx_glr_auto_examples_feature_selection_plot_feature_selection.py` - * :ref:`sphx_glr_auto_examples_feature_selection_plot_f_test_vs_mi.py` +* :ref:`sphx_glr_auto_examples_feature_selection_plot_f_test_vs_mi.py` .. _rfe: @@ -144,14 +144,14 @@ 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. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_feature_selection_plot_rfe_digits.py`: A recursive feature elimination example - showing the relevance of pixels in a digit classification task. +* :ref:`sphx_glr_auto_examples_feature_selection_plot_rfe_digits.py`: A recursive feature elimination example + showing the relevance of pixels in a digit classification task. - * :ref:`sphx_glr_auto_examples_feature_selection_plot_rfe_with_cross_validation.py`: A recursive feature - elimination example with automatic tuning of the number of features - selected with cross-validation. +* :ref:`sphx_glr_auto_examples_feature_selection_plot_rfe_with_cross_validation.py`: A recursive feature + elimination example with automatic tuning of the number of features + selected with cross-validation. .. _select_from_model: @@ -171,9 +171,9 @@ Available heuristics are "mean", "median" and float multiples of these like For examples on how it is to be used refer to the sections below. -.. topic:: Examples +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_feature_selection_plot_select_from_model_diabetes.py` +* :ref:`sphx_glr_auto_examples_feature_selection_plot_select_from_model_diabetes.py` .. _l1_feature_selection: @@ -207,42 +207,39 @@ With SVMs and logistic-regression, the parameter C controls the sparsity: the smaller C the fewer features selected. With Lasso, the higher the alpha parameter, the fewer features selected. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_linear_model_plot_lasso_dense_vs_sparse_data.py`. +* :ref:`sphx_glr_auto_examples_linear_model_plot_lasso_dense_vs_sparse_data.py`. .. _compressive_sensing: -|details-start| -**L1-recovery and compressive sensing** -|details-split| - -For a good choice of alpha, the :ref:`lasso` can fully recover the -exact set of non-zero variables using only few observations, provided -certain specific conditions are met. In particular, the number of -samples should be "sufficiently large", or L1 models will perform at -random, where "sufficiently large" depends on the number of non-zero -coefficients, the logarithm of the number of features, the amount of -noise, the smallest absolute value of non-zero coefficients, and the -structure of the design matrix X. In addition, the design matrix must -display certain specific properties, such as not being too correlated. - -There is no general rule to select an alpha parameter for recovery of -non-zero coefficients. It can by set by cross-validation -(:class:`~sklearn.linear_model.LassoCV` or -:class:`~sklearn.linear_model.LassoLarsCV`), though this may lead to -under-penalized models: including a small number of non-relevant variables -is not detrimental to prediction score. BIC -(:class:`~sklearn.linear_model.LassoLarsIC`) tends, on the opposite, to set -high values of alpha. - -.. topic:: Reference - - Richard G. Baraniuk "Compressive Sensing", IEEE Signal - Processing Magazine [120] July 2007 - http://users.isr.ist.utl.pt/~aguiar/CS_notes.pdf - -|details-end| +.. dropdown:: L1-recovery and compressive sensing + + For a good choice of alpha, the :ref:`lasso` can fully recover the + exact set of non-zero variables using only few observations, provided + certain specific conditions are met. In particular, the number of + samples should be "sufficiently large", or L1 models will perform at + random, where "sufficiently large" depends on the number of non-zero + coefficients, the logarithm of the number of features, the amount of + noise, the smallest absolute value of non-zero coefficients, and the + structure of the design matrix X. In addition, the design matrix must + display certain specific properties, such as not being too correlated. + + There is no general rule to select an alpha parameter for recovery of + non-zero coefficients. It can by set by cross-validation + (:class:`~sklearn.linear_model.LassoCV` or + :class:`~sklearn.linear_model.LassoLarsCV`), though this may lead to + under-penalized models: including a small number of non-relevant variables + is not detrimental to prediction score. BIC + (:class:`~sklearn.linear_model.LassoLarsIC`) tends, on the opposite, to set + high values of alpha. + + .. rubric:: References + + Richard G. Baraniuk "Compressive Sensing", IEEE Signal + Processing Magazine [120] July 2007 + http://users.isr.ist.utl.pt/~aguiar/CS_notes.pdf + Tree-based feature selection ---------------------------- @@ -268,14 +265,13 @@ meta-transformer):: >>> X_new.shape # doctest: +SKIP (150, 2) -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_ensemble_plot_forest_importances.py`: example on - synthetic data showing the recovery of the actually meaningful - features. +* :ref:`sphx_glr_auto_examples_ensemble_plot_forest_importances.py`: example on + synthetic data showing the recovery of the actually meaningful features. - * :ref:`sphx_glr_auto_examples_ensemble_plot_forest_importances_faces.py`: example - on face recognition data. +* :ref:`sphx_glr_auto_examples_ensemble_plot_forest_importances_faces.py`: example + on face recognition data. .. _sequential_feature_selection: @@ -299,38 +295,35 @@ instead of starting with no features and greedily adding features, we start with *all* the features and greedily *remove* features from the set. The `direction` parameter controls whether forward or backward SFS is used. -|details-start| -**Detail on Sequential Feature Selection** -|details-split| - -In general, forward and backward selection do not yield equivalent results. -Also, one may be much faster than the other depending on the requested number -of selected features: if we have 10 features and ask for 7 selected features, -forward selection would need to perform 7 iterations while backward selection -would only need to perform 3. - -SFS differs from :class:`~sklearn.feature_selection.RFE` and -:class:`~sklearn.feature_selection.SelectFromModel` in that it does not -require the underlying model to expose a `coef_` or `feature_importances_` -attribute. It may however be slower considering that more models need to be -evaluated, compared to the other approaches. For example in backward -selection, the iteration going from `m` features to `m - 1` features using k-fold -cross-validation requires fitting `m * k` models, while -:class:`~sklearn.feature_selection.RFE` would require only a single fit, and -:class:`~sklearn.feature_selection.SelectFromModel` always just does a single -fit and requires no iterations. - -.. topic:: Reference - - .. [sfs] Ferri et al, `Comparative study of techniques for +.. dropdown:: Details on Sequential Feature Selection + + In general, forward and backward selection do not yield equivalent results. + Also, one may be much faster than the other depending on the requested number + of selected features: if we have 10 features and ask for 7 selected features, + forward selection would need to perform 7 iterations while backward selection + would only need to perform 3. + + SFS differs from :class:`~sklearn.feature_selection.RFE` and + :class:`~sklearn.feature_selection.SelectFromModel` in that it does not + require the underlying model to expose a `coef_` or `feature_importances_` + attribute. It may however be slower considering that more models need to be + evaluated, compared to the other approaches. For example in backward + selection, the iteration going from `m` features to `m - 1` features using k-fold + cross-validation requires fitting `m * k` models, while + :class:`~sklearn.feature_selection.RFE` would require only a single fit, and + :class:`~sklearn.feature_selection.SelectFromModel` always just does a single + fit and requires no iterations. + + .. rubric:: References + + .. [sfs] Ferri et al, `Comparative study of techniques for large-scale feature selection `_. -|details-end| -.. topic:: Examples +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_feature_selection_plot_select_from_model_diabetes.py` +* :ref:`sphx_glr_auto_examples_feature_selection_plot_select_from_model_diabetes.py` Feature selection as part of a pipeline ======================================= diff --git a/doc/modules/gaussian_process.rst b/doc/modules/gaussian_process.rst index 58e56a557ed73..fb87120205f96 100644 --- a/doc/modules/gaussian_process.rst +++ b/doc/modules/gaussian_process.rst @@ -88,12 +88,12 @@ the API of standard scikit-learn estimators, :class:`GaussianProcessRegressor`: externally for other ways of selecting hyperparameters, e.g., via Markov chain Monte Carlo. -.. topic:: Examples +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_gaussian_process_plot_gpr_noisy_targets.py` - * :ref:`sphx_glr_auto_examples_gaussian_process_plot_gpr_noisy.py` - * :ref:`sphx_glr_auto_examples_gaussian_process_plot_compare_gpr_krr.py` - * :ref:`sphx_glr_auto_examples_gaussian_process_plot_gpr_co2.py` +* :ref:`sphx_glr_auto_examples_gaussian_process_plot_gpr_noisy_targets.py` +* :ref:`sphx_glr_auto_examples_gaussian_process_plot_gpr_noisy.py` +* :ref:`sphx_glr_auto_examples_gaussian_process_plot_compare_gpr_krr.py` +* :ref:`sphx_glr_auto_examples_gaussian_process_plot_gpr_co2.py` .. _gpc: @@ -239,93 +239,88 @@ also invariant to rotations in the input space. For more details, we refer to Chapter 4 of [RW2006]_. For guidance on how to best combine different kernels, we refer to [Duv2014]_. -|details-start| -**Gaussian Process Kernel API** -|details-split| - -The main usage of a :class:`Kernel` is to compute the GP's covariance between -datapoints. For this, the method ``__call__`` of the kernel can be called. This -method can either be used to compute the "auto-covariance" of all pairs of -datapoints in a 2d array X, or the "cross-covariance" of all combinations -of datapoints of a 2d array X with datapoints in a 2d array Y. The following -identity holds true for all kernels k (except for the :class:`WhiteKernel`): -``k(X) == K(X, Y=X)`` - -If only the diagonal of the auto-covariance is being used, the method ``diag()`` -of a kernel can be called, which is more computationally efficient than the -equivalent call to ``__call__``: ``np.diag(k(X, X)) == k.diag(X)`` - -Kernels are parameterized by a vector :math:`\theta` of hyperparameters. These -hyperparameters can for instance control length-scales or periodicity of a -kernel (see below). All kernels support computing analytic gradients -of the kernel's auto-covariance with respect to :math:`log(\theta)` via setting -``eval_gradient=True`` in the ``__call__`` method. -That is, a ``(len(X), len(X), len(theta))`` array is returned where the entry -``[i, j, l]`` contains :math:`\frac{\partial k_\theta(x_i, x_j)}{\partial log(\theta_l)}`. -This gradient is used by the Gaussian process (both regressor and classifier) -in computing the gradient of the log-marginal-likelihood, which in turn is used -to determine the value of :math:`\theta`, which maximizes the log-marginal-likelihood, -via gradient ascent. For each hyperparameter, the initial value and the -bounds need to be specified when creating an instance of the kernel. The -current value of :math:`\theta` can be get and set via the property -``theta`` of the kernel object. Moreover, the bounds of the hyperparameters can be -accessed by the property ``bounds`` of the kernel. Note that both properties -(theta and bounds) return log-transformed values of the internally used values -since those are typically more amenable to gradient-based optimization. -The specification of each hyperparameter is stored in the form of an instance of -:class:`Hyperparameter` in the respective kernel. Note that a kernel using a -hyperparameter with name "x" must have the attributes self.x and self.x_bounds. - -The abstract base class for all kernels is :class:`Kernel`. Kernel implements a -similar interface as :class:`~sklearn.base.BaseEstimator`, providing the -methods ``get_params()``, ``set_params()``, and ``clone()``. This allows -setting kernel values also via meta-estimators such as -:class:`~sklearn.pipeline.Pipeline` or -:class:`~sklearn.model_selection.GridSearchCV`. Note that due to the nested -structure of kernels (by applying kernel operators, see below), the names of -kernel parameters might become relatively complicated. In general, for a binary -kernel operator, parameters of the left operand are prefixed with ``k1__`` and -parameters of the right operand with ``k2__``. An additional convenience method -is ``clone_with_theta(theta)``, which returns a cloned version of the kernel -but with the hyperparameters set to ``theta``. An illustrative example: - - >>> from sklearn.gaussian_process.kernels import ConstantKernel, RBF - >>> kernel = ConstantKernel(constant_value=1.0, constant_value_bounds=(0.0, 10.0)) * RBF(length_scale=0.5, length_scale_bounds=(0.0, 10.0)) + RBF(length_scale=2.0, length_scale_bounds=(0.0, 10.0)) - >>> for hyperparameter in kernel.hyperparameters: print(hyperparameter) - Hyperparameter(name='k1__k1__constant_value', value_type='numeric', bounds=array([[ 0., 10.]]), n_elements=1, fixed=False) - Hyperparameter(name='k1__k2__length_scale', value_type='numeric', bounds=array([[ 0., 10.]]), n_elements=1, fixed=False) - Hyperparameter(name='k2__length_scale', value_type='numeric', bounds=array([[ 0., 10.]]), n_elements=1, fixed=False) - >>> params = kernel.get_params() - >>> for key in sorted(params): print("%s : %s" % (key, params[key])) - k1 : 1**2 * RBF(length_scale=0.5) - k1__k1 : 1**2 - k1__k1__constant_value : 1.0 - k1__k1__constant_value_bounds : (0.0, 10.0) - k1__k2 : RBF(length_scale=0.5) - k1__k2__length_scale : 0.5 - k1__k2__length_scale_bounds : (0.0, 10.0) - k2 : RBF(length_scale=2) - k2__length_scale : 2.0 - k2__length_scale_bounds : (0.0, 10.0) - >>> print(kernel.theta) # Note: log-transformed - [ 0. -0.69314718 0.69314718] - >>> print(kernel.bounds) # Note: log-transformed - [[ -inf 2.30258509] - [ -inf 2.30258509] - [ -inf 2.30258509]] - - -All Gaussian process kernels are interoperable with :mod:`sklearn.metrics.pairwise` -and vice versa: instances of subclasses of :class:`Kernel` can be passed as -``metric`` to ``pairwise_kernels`` from :mod:`sklearn.metrics.pairwise`. Moreover, -kernel functions from pairwise can be used as GP kernels by using the wrapper -class :class:`PairwiseKernel`. The only caveat is that the gradient of -the hyperparameters is not analytic but numeric and all those 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. - -|details-end| +.. dropdown:: Gaussian Process Kernel API + + The main usage of a :class:`Kernel` is to compute the GP's covariance between + datapoints. For this, the method ``__call__`` of the kernel can be called. This + method can either be used to compute the "auto-covariance" of all pairs of + datapoints in a 2d array X, or the "cross-covariance" of all combinations + of datapoints of a 2d array X with datapoints in a 2d array Y. The following + identity holds true for all kernels k (except for the :class:`WhiteKernel`): + ``k(X) == K(X, Y=X)`` + + If only the diagonal of the auto-covariance is being used, the method ``diag()`` + of a kernel can be called, which is more computationally efficient than the + equivalent call to ``__call__``: ``np.diag(k(X, X)) == k.diag(X)`` + + Kernels are parameterized by a vector :math:`\theta` of hyperparameters. These + hyperparameters can for instance control length-scales or periodicity of a + kernel (see below). All kernels support computing analytic gradients + of the kernel's auto-covariance with respect to :math:`log(\theta)` via setting + ``eval_gradient=True`` in the ``__call__`` method. + That is, a ``(len(X), len(X), len(theta))`` array is returned where the entry + ``[i, j, l]`` contains :math:`\frac{\partial k_\theta(x_i, x_j)}{\partial log(\theta_l)}`. + This gradient is used by the Gaussian process (both regressor and classifier) + in computing the gradient of the log-marginal-likelihood, which in turn is used + to determine the value of :math:`\theta`, which maximizes the log-marginal-likelihood, + via gradient ascent. For each hyperparameter, the initial value and the + bounds need to be specified when creating an instance of the kernel. The + current value of :math:`\theta` can be get and set via the property + ``theta`` of the kernel object. Moreover, the bounds of the hyperparameters can be + accessed by the property ``bounds`` of the kernel. Note that both properties + (theta and bounds) return log-transformed values of the internally used values + since those are typically more amenable to gradient-based optimization. + The specification of each hyperparameter is stored in the form of an instance of + :class:`Hyperparameter` in the respective kernel. Note that a kernel using a + hyperparameter with name "x" must have the attributes self.x and self.x_bounds. + + The abstract base class for all kernels is :class:`Kernel`. Kernel implements a + similar interface as :class:`~sklearn.base.BaseEstimator`, providing the + methods ``get_params()``, ``set_params()``, and ``clone()``. This allows + setting kernel values also via meta-estimators such as + :class:`~sklearn.pipeline.Pipeline` or + :class:`~sklearn.model_selection.GridSearchCV`. Note that due to the nested + structure of kernels (by applying kernel operators, see below), the names of + kernel parameters might become relatively complicated. In general, for a binary + kernel operator, parameters of the left operand are prefixed with ``k1__`` and + parameters of the right operand with ``k2__``. An additional convenience method + is ``clone_with_theta(theta)``, which returns a cloned version of the kernel + but with the hyperparameters set to ``theta``. An illustrative example: + + >>> from sklearn.gaussian_process.kernels import ConstantKernel, RBF + >>> kernel = ConstantKernel(constant_value=1.0, constant_value_bounds=(0.0, 10.0)) * RBF(length_scale=0.5, length_scale_bounds=(0.0, 10.0)) + RBF(length_scale=2.0, length_scale_bounds=(0.0, 10.0)) + >>> for hyperparameter in kernel.hyperparameters: print(hyperparameter) + Hyperparameter(name='k1__k1__constant_value', value_type='numeric', bounds=array([[ 0., 10.]]), n_elements=1, fixed=False) + Hyperparameter(name='k1__k2__length_scale', value_type='numeric', bounds=array([[ 0., 10.]]), n_elements=1, fixed=False) + Hyperparameter(name='k2__length_scale', value_type='numeric', bounds=array([[ 0., 10.]]), n_elements=1, fixed=False) + >>> params = kernel.get_params() + >>> for key in sorted(params): print("%s : %s" % (key, params[key])) + k1 : 1**2 * RBF(length_scale=0.5) + k1__k1 : 1**2 + k1__k1__constant_value : 1.0 + k1__k1__constant_value_bounds : (0.0, 10.0) + k1__k2 : RBF(length_scale=0.5) + k1__k2__length_scale : 0.5 + k1__k2__length_scale_bounds : (0.0, 10.0) + k2 : RBF(length_scale=2) + k2__length_scale : 2.0 + k2__length_scale_bounds : (0.0, 10.0) + >>> print(kernel.theta) # Note: log-transformed + [ 0. -0.69314718 0.69314718] + >>> print(kernel.bounds) # Note: log-transformed + [[ -inf 2.30258509] + [ -inf 2.30258509] + [ -inf 2.30258509]] + + All Gaussian process kernels are interoperable with :mod:`sklearn.metrics.pairwise` + and vice versa: instances of subclasses of :class:`Kernel` can be passed as + ``metric`` to ``pairwise_kernels`` from :mod:`sklearn.metrics.pairwise`. Moreover, + kernel functions from pairwise can be used as GP kernels by using the wrapper + class :class:`PairwiseKernel`. The only caveat is that the gradient of + the hyperparameters is not analytic but numeric and all those 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. Basic kernels ------------- @@ -388,42 +383,38 @@ The :class:`Matern` kernel is a stationary kernel and a generalization of the :class:`RBF` kernel. It has an additional parameter :math:`\nu` which controls the smoothness of the resulting function. 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 :math:`x` (anisotropic variant of the kernel). -|details-start| -**Mathematical implementation of Matérn kernel** -|details-split| +.. dropdown:: Mathematical implementation of Matérn kernel -The kernel is given by: - -.. math:: + The kernel is given by: - 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), + .. math:: -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. -As :math:`\nu\rightarrow\infty`, the Matérn kernel converges to the RBF kernel. -When :math:`\nu = 1/2`, the Matérn kernel becomes identical to the absolute -exponential kernel, i.e., + 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), -.. math:: - k(x_i, x_j) = \exp \Bigg(- \frac{1}{l} d(x_i , x_j ) \Bigg) \quad \quad \nu= \tfrac{1}{2} + 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. + As :math:`\nu\rightarrow\infty`, the Matérn kernel converges to the RBF kernel. + When :math:`\nu = 1/2`, the Matérn kernel becomes identical to the absolute + exponential kernel, i.e., -In particular, :math:`\nu = 3/2`: + .. math:: + k(x_i, x_j) = \exp \Bigg(- \frac{1}{l} d(x_i , x_j ) \Bigg) \quad \quad \nu= \tfrac{1}{2} -.. math:: - k(x_i, x_j) = \Bigg(1 + \frac{\sqrt{3}}{l} d(x_i , x_j )\Bigg) \exp \Bigg(-\frac{\sqrt{3}}{l} d(x_i , x_j ) \Bigg) \quad \quad \nu= \tfrac{3}{2} + In particular, :math:`\nu = 3/2`: -and :math:`\nu = 5/2`: + .. math:: + k(x_i, x_j) = \Bigg(1 + \frac{\sqrt{3}}{l} d(x_i , x_j )\Bigg) \exp \Bigg(-\frac{\sqrt{3}}{l} d(x_i , x_j ) \Bigg) \quad \quad \nu= \tfrac{3}{2} -.. math:: - k(x_i, x_j) = \Bigg(1 + \frac{\sqrt{5}}{l} d(x_i , x_j ) +\frac{5}{3l} d(x_i , x_j )^2 \Bigg) \exp \Bigg(-\frac{\sqrt{5}}{l} d(x_i , x_j ) \Bigg) \quad \quad \nu= \tfrac{5}{2} + and :math:`\nu = 5/2`: -are popular choices for learning functions that are not infinitely -differentiable (as assumed by the RBF kernel) but at least once (:math:`\nu = -3/2`) or twice differentiable (:math:`\nu = 5/2`). + .. math:: + k(x_i, x_j) = \Bigg(1 + \frac{\sqrt{5}}{l} d(x_i , x_j ) +\frac{5}{3l} d(x_i , x_j )^2 \Bigg) \exp \Bigg(-\frac{\sqrt{5}}{l} d(x_i , x_j ) \Bigg) \quad \quad \nu= \tfrac{5}{2} -The flexibility of controlling the smoothness of the learned function via :math:`\nu` -allows adapting to the properties of the true underlying functional relation. + are popular choices for learning functions that are not infinitely + differentiable (as assumed by the RBF kernel) but at least once (:math:`\nu = + 3/2`) or twice differentiable (:math:`\nu = 5/2`). -|details-end| + The flexibility of controlling the smoothness of the learned function via :math:`\nu` + allows adapting to the properties of the true underlying functional relation. The prior and posterior of a GP resulting from a Matérn kernel are shown in the following figure: diff --git a/doc/modules/grid_search.rst b/doc/modules/grid_search.rst index 01c5a5c72ee52..12ee76d8e4d39 100644 --- a/doc/modules/grid_search.rst +++ b/doc/modules/grid_search.rst @@ -72,35 +72,35 @@ evaluated and the best combination is retained. .. currentmodule:: sklearn.model_selection -.. topic:: Examples: +.. rubric:: Examples - - See :ref:`sphx_glr_auto_examples_model_selection_plot_grid_search_digits.py` for an example of - Grid Search computation on the digits dataset. +- See :ref:`sphx_glr_auto_examples_model_selection_plot_grid_search_digits.py` for an example of + Grid Search computation on the digits dataset. - - See :ref:`sphx_glr_auto_examples_model_selection_plot_grid_search_text_feature_extraction.py` for an example - of Grid Search coupling parameters from a text documents feature - extractor (n-gram count vectorizer and TF-IDF transformer) with a - classifier (here a linear SVM trained with SGD with either elastic - net or L2 penalty) using a :class:`~sklearn.pipeline.Pipeline` instance. +- See :ref:`sphx_glr_auto_examples_model_selection_plot_grid_search_text_feature_extraction.py` for an example + of Grid Search coupling parameters from a text documents feature + extractor (n-gram count vectorizer and TF-IDF transformer) with a + classifier (here a linear SVM trained with SGD with either elastic + net or L2 penalty) using a :class:`~sklearn.pipeline.Pipeline` instance. - - See :ref:`sphx_glr_auto_examples_model_selection_plot_nested_cross_validation_iris.py` - for an example of Grid Search within a cross validation loop on the iris - dataset. This is the best practice for evaluating the performance of a - model with grid search. +- See :ref:`sphx_glr_auto_examples_model_selection_plot_nested_cross_validation_iris.py` + for an example of Grid Search within a cross validation loop on the iris + dataset. This is the best practice for evaluating the performance of a + model with grid search. - - See :ref:`sphx_glr_auto_examples_model_selection_plot_multi_metric_evaluation.py` - for an example of :class:`GridSearchCV` being used to evaluate multiple - metrics simultaneously. +- See :ref:`sphx_glr_auto_examples_model_selection_plot_multi_metric_evaluation.py` + for an example of :class:`GridSearchCV` being used to evaluate multiple + metrics simultaneously. - - See :ref:`sphx_glr_auto_examples_model_selection_plot_grid_search_refit_callable.py` - for an example of using ``refit=callable`` interface in - :class:`GridSearchCV`. The example shows how this interface adds certain - amount of flexibility in identifying the "best" estimator. This interface - can also be used in multiple metrics evaluation. +- See :ref:`sphx_glr_auto_examples_model_selection_plot_grid_search_refit_callable.py` + for an example of using ``refit=callable`` interface in + :class:`GridSearchCV`. The example shows how this interface adds certain + amount of flexibility in identifying the "best" estimator. This interface + can also be used in multiple metrics evaluation. - - See :ref:`sphx_glr_auto_examples_model_selection_plot_grid_search_stats.py` - for an example of how to do a statistical comparison on the outputs of - :class:`GridSearchCV`. +- See :ref:`sphx_glr_auto_examples_model_selection_plot_grid_search_stats.py` + for an example of how to do a statistical comparison on the outputs of + :class:`GridSearchCV`. .. _randomized_parameter_search: @@ -161,16 +161,16 @@ variable that is log-uniformly distributed between ``1e0`` and ``1e3``:: 'kernel': ['rbf'], 'class_weight':['balanced', None]} -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_model_selection_plot_randomized_search.py` compares the usage and efficiency - of randomized search and grid search. +* :ref:`sphx_glr_auto_examples_model_selection_plot_randomized_search.py` compares the usage and efficiency + of randomized search and grid search. -.. topic:: References: +.. rubric:: References - * Bergstra, J. and Bengio, Y., - Random search for hyper-parameter optimization, - The Journal of Machine Learning Research (2012) +* Bergstra, J. and Bengio, Y., + Random search for hyper-parameter optimization, + The Journal of Machine Learning Research (2012) .. _successive_halving_user_guide: @@ -222,10 +222,10 @@ need to explicitly import ``enable_halving_search_cv``:: >>> from sklearn.model_selection import HalvingGridSearchCV >>> from sklearn.model_selection import HalvingRandomSearchCV -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_model_selection_plot_successive_halving_heatmap.py` - * :ref:`sphx_glr_auto_examples_model_selection_plot_successive_halving_iterations.py` +* :ref:`sphx_glr_auto_examples_model_selection_plot_successive_halving_heatmap.py` +* :ref:`sphx_glr_auto_examples_model_selection_plot_successive_halving_iterations.py` Choosing ``min_resources`` and the number of candidates ------------------------------------------------------- @@ -528,15 +528,16 @@ In the example above, the best parameter combination is ``{'criterion': since it has reached the last iteration (3) with the highest score: 0.96. -.. topic:: References: +.. rubric:: References - .. [1] K. Jamieson, A. Talwalkar, - `Non-stochastic Best Arm Identification and Hyperparameter - Optimization `_, in - proc. of Machine Learning Research, 2016. - .. [2] L. Li, K. Jamieson, G. DeSalvo, A. Rostamizadeh, A. Talwalkar, - :arxiv:`Hyperband: A Novel Bandit-Based Approach to Hyperparameter Optimization - <1603.06560>`, in Machine Learning Research 18, 2018. +.. [1] K. Jamieson, A. Talwalkar, + `Non-stochastic Best Arm Identification and Hyperparameter + Optimization `_, in + proc. of Machine Learning Research, 2016. + +.. [2] L. Li, K. Jamieson, G. DeSalvo, A. Rostamizadeh, A. Talwalkar, + :arxiv:`Hyperband: A Novel Bandit-Based Approach to Hyperparameter Optimization + <1603.06560>`, in Machine Learning Research 18, 2018. .. _grid_search_tips: diff --git a/doc/modules/impute.rst b/doc/modules/impute.rst index f5879cbffc0a5..1431f26132338 100644 --- a/doc/modules/impute.rst +++ b/doc/modules/impute.rst @@ -224,13 +224,13 @@ neighbors of samples with missing values:: For another example on usage, see :ref:`sphx_glr_auto_examples_impute_plot_missing_values.py`. -.. topic:: References +.. rubric:: References - .. [OL2001] `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. - `_ +.. [OL2001] `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. + `_ Keeping the number of features constant ======================================= diff --git a/doc/modules/isotonic.rst b/doc/modules/isotonic.rst index 6cfdc1669de5d..50fbdb24e72c7 100644 --- a/doc/modules/isotonic.rst +++ b/doc/modules/isotonic.rst @@ -32,6 +32,6 @@ thus form a function that is piecewise linear: :target: ../auto_examples/miscellaneous/plot_isotonic_regression.html :align: center -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_miscellaneous_plot_isotonic_regression.py` +* :ref:`sphx_glr_auto_examples_miscellaneous_plot_isotonic_regression.py` diff --git a/doc/modules/kernel_approximation.rst b/doc/modules/kernel_approximation.rst index 0c67c36178e3b..305c3cc6601fb 100644 --- a/doc/modules/kernel_approximation.rst +++ b/doc/modules/kernel_approximation.rst @@ -88,12 +88,12 @@ function or a precomputed kernel matrix. The number of samples used - which is also the dimensionality of the features computed - is given by the parameter ``n_components``. -.. topic:: Examples: +.. rubric:: Examples - * See the example entitled - :ref:`sphx_glr_auto_examples_applications_plot_cyclical_feature_engineering.py`, - that shows an efficient machine learning pipeline that uses a - :class:`Nystroem` kernel. +* See the example entitled + :ref:`sphx_glr_auto_examples_applications_plot_cyclical_feature_engineering.py`, + that shows an efficient machine learning pipeline that uses a + :class:`Nystroem` kernel. .. _rbf_kernel_approx: @@ -143,9 +143,9 @@ use of larger feature spaces more efficient. Comparing an exact RBF kernel (left) with the approximation (right) -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_miscellaneous_plot_kernel_approximation.py` +* :ref:`sphx_glr_auto_examples_miscellaneous_plot_kernel_approximation.py` .. _additive_chi_kernel_approx: @@ -241,9 +241,9 @@ In addition, this method can transform samples in time, where :math:`n_{\text{components}}` is the desired output dimension, determined by ``n_components``. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_kernel_approximation_plot_scalable_poly_kernels.py` +* :ref:`sphx_glr_auto_examples_kernel_approximation_plot_scalable_poly_kernels.py` .. _tensor_sketch_kernel_approx: @@ -283,29 +283,29 @@ The classes in this submodule allow to approximate the embedding or store training examples. -.. topic:: References: - - .. [WS2001] `"Using the Nyström method to speed up kernel machines" - `_ - Williams, C.K.I.; Seeger, M. - 2001. - .. [RR2007] `"Random features for large-scale kernel machines" - `_ - Rahimi, A. and Recht, B. - Advances in neural information processing 2007, - .. [LS2010] `"Random Fourier approximations for skewed multiplicative histogram kernels" - `_ - Li, F., Ionescu, C., and Sminchisescu, C. - - Pattern Recognition, DAGM 2010, Lecture Notes in Computer Science. - .. [VZ2010] `"Efficient additive kernels via explicit feature maps" - `_ - Vedaldi, A. and Zisserman, A. - Computer Vision and Pattern Recognition 2010 - .. [VVZ2010] `"Generalized RBF feature maps for Efficient Detection" - `_ - Vempati, S. and Vedaldi, A. and Zisserman, A. and Jawahar, CV - 2010 - .. [PP2013] :doi:`"Fast and scalable polynomial kernels via explicit feature maps" - <10.1145/2487575.2487591>` - Pham, N., & Pagh, R. - 2013 - .. [CCF2002] `"Finding frequent items in data streams" - `_ - Charikar, M., Chen, K., & Farach-Colton - 2002 - .. [WIKICS] `"Wikipedia: Count sketch" - `_ +.. rubric:: References + +.. [WS2001] `"Using the Nyström method to speed up kernel machines" + `_ + Williams, C.K.I.; Seeger, M. - 2001. +.. [RR2007] `"Random features for large-scale kernel machines" + `_ + Rahimi, A. and Recht, B. - Advances in neural information processing 2007, +.. [LS2010] `"Random Fourier approximations for skewed multiplicative histogram kernels" + `_ + Li, F., Ionescu, C., and Sminchisescu, C. + - Pattern Recognition, DAGM 2010, Lecture Notes in Computer Science. +.. [VZ2010] `"Efficient additive kernels via explicit feature maps" + `_ + Vedaldi, A. and Zisserman, A. - Computer Vision and Pattern Recognition 2010 +.. [VVZ2010] `"Generalized RBF feature maps for Efficient Detection" + `_ + Vempati, S. and Vedaldi, A. and Zisserman, A. and Jawahar, CV - 2010 +.. [PP2013] :doi:`"Fast and scalable polynomial kernels via explicit feature maps" + <10.1145/2487575.2487591>` + Pham, N., & Pagh, R. - 2013 +.. [CCF2002] `"Finding frequent items in data streams" + `_ + Charikar, M., Chen, K., & Farach-Colton - 2002 +.. [WIKICS] `"Wikipedia: Count sketch" + `_ diff --git a/doc/modules/kernel_ridge.rst b/doc/modules/kernel_ridge.rst index 5d25ce71f5ea1..fcc19a49628c4 100644 --- a/doc/modules/kernel_ridge.rst +++ b/doc/modules/kernel_ridge.rst @@ -55,11 +55,11 @@ dense model. :target: ../auto_examples/miscellaneous/plot_kernel_ridge_regression.html :align: center -.. topic:: Examples +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_miscellaneous_plot_kernel_ridge_regression.py` +* :ref:`sphx_glr_auto_examples_miscellaneous_plot_kernel_ridge_regression.py` -.. topic:: References: +.. rubric:: References - .. [M2012] "Machine Learning: A Probabilistic Perspective" - Murphy, K. P. - chapter 14.4.3, pp. 492-493, The MIT Press, 2012 +.. [M2012] "Machine Learning: A Probabilistic Perspective" + Murphy, K. P. - chapter 14.4.3, pp. 492-493, The MIT Press, 2012 diff --git a/doc/modules/lda_qda.rst b/doc/modules/lda_qda.rst index 850a848fe3f73..0d264ec662a9f 100644 --- a/doc/modules/lda_qda.rst +++ b/doc/modules/lda_qda.rst @@ -29,10 +29,10 @@ Discriminant Analysis can only learn linear boundaries, while Quadratic Discriminant Analysis can learn quadratic boundaries and is therefore more flexible. -.. topic:: Examples: +.. rubric:: Examples - :ref:`sphx_glr_auto_examples_classification_plot_lda_qda.py`: Comparison of LDA and QDA - on synthetic data. +* :ref:`sphx_glr_auto_examples_classification_plot_lda_qda.py`: Comparison of LDA and + QDA on synthetic data. Dimensionality reduction using Linear Discriminant Analysis =========================================================== @@ -49,10 +49,10 @@ This is implemented in the `transform` method. The desired dimensionality can be set using the ``n_components`` parameter. This parameter has no influence on the `fit` and `predict` methods. -.. topic:: Examples: +.. rubric:: Examples - :ref:`sphx_glr_auto_examples_decomposition_plot_pca_vs_lda.py`: Comparison of LDA and PCA - for dimensionality reduction of the Iris dataset +* :ref:`sphx_glr_auto_examples_decomposition_plot_pca_vs_lda.py`: Comparison of LDA and + PCA for dimensionality reduction of the Iris dataset .. _lda_qda_math: @@ -194,7 +194,7 @@ Oracle Approximating Shrinkage estimator :class:`sklearn.covariance.OAS` yields a smaller Mean Squared Error than the one given by Ledoit and Wolf's formula used with shrinkage="auto". In LDA, the data are assumed to be gaussian conditionally to the class. If these assumptions hold, using LDA with -the OAS estimator of covariance will yield a better classification +the OAS estimator of covariance will yield a better classification accuracy than if Ledoit and Wolf or the empirical covariance estimator is used. The covariance estimator can be chosen using with the ``covariance_estimator`` @@ -210,10 +210,10 @@ class. A covariance estimator should have a :term:`fit` method and a .. centered:: |shrinkage| -.. topic:: Examples: +.. rubric:: Examples - :ref:`sphx_glr_auto_examples_classification_plot_lda.py`: Comparison of LDA classifiers - with Empirical, Ledoit Wolf and OAS covariance estimator. +* :ref:`sphx_glr_auto_examples_classification_plot_lda.py`: Comparison of LDA classifiers + with Empirical, Ledoit Wolf and OAS covariance estimator. Estimation algorithms ===================== @@ -253,13 +253,13 @@ transform, and it supports shrinkage. However, the 'eigen' solver needs to compute the covariance matrix, so it might not be suitable for situations with a high number of features. -.. topic:: References: +.. rubric:: References - .. [1] "The Elements of Statistical Learning", Hastie T., Tibshirani R., - Friedman J., Section 4.3, p.106-119, 2008. +.. [1] "The Elements of Statistical Learning", Hastie T., Tibshirani R., + Friedman J., Section 4.3, p.106-119, 2008. - .. [2] Ledoit O, Wolf M. Honey, I Shrunk the Sample Covariance Matrix. - The Journal of Portfolio Management 30(4), 110-119, 2004. +.. [2] Ledoit O, Wolf M. Honey, I Shrunk the Sample Covariance Matrix. + The Journal of Portfolio Management 30(4), 110-119, 2004. - .. [3] R. O. Duda, P. E. Hart, D. G. Stork. Pattern Classification - (Second Edition), section 2.6.2. +.. [3] R. O. Duda, P. E. Hart, D. G. Stork. Pattern Classification + (Second Edition), section 2.6.2. diff --git a/doc/modules/learning_curve.rst b/doc/modules/learning_curve.rst index 3d458a1a67416..f5af5a748500a 100644 --- a/doc/modules/learning_curve.rst +++ b/doc/modules/learning_curve.rst @@ -39,11 +39,11 @@ easy to see whether the estimator suffers from bias or variance. However, in high-dimensional spaces, models can become very difficult to visualize. For this reason, it is often helpful to use the tools described below. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_model_selection_plot_underfitting_overfitting.py` - * :ref:`sphx_glr_auto_examples_model_selection_plot_validation_curve.py` - * :ref:`sphx_glr_auto_examples_model_selection_plot_learning_curve.py` +* :ref:`sphx_glr_auto_examples_model_selection_plot_underfitting_overfitting.py` +* :ref:`sphx_glr_auto_examples_model_selection_plot_validation_curve.py` +* :ref:`sphx_glr_auto_examples_model_selection_plot_learning_curve.py` .. _validation_curve: diff --git a/doc/modules/linear_model.rst b/doc/modules/linear_model.rst index 275ee01eb022f..d06101adabdb5 100644 --- a/doc/modules/linear_model.rst +++ b/doc/modules/linear_model.rst @@ -57,9 +57,9 @@ to random errors in the observed target, producing a large variance. This situation of *multicollinearity* can arise, for example, when data are collected without an experimental design. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_linear_model_plot_ols.py` +* :ref:`sphx_glr_auto_examples_linear_model_plot_ols.py` Non-Negative Least Squares -------------------------- @@ -71,9 +71,9 @@ quantities (e.g., frequency counts or prices of goods). parameter: when set to `True` `Non-Negative Least Squares `_ are then applied. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_linear_model_plot_nnls.py` +* :ref:`sphx_glr_auto_examples_linear_model_plot_nnls.py` Ordinary Least Squares Complexity --------------------------------- @@ -172,11 +172,11 @@ Machines `_ with a linear kernel. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_linear_model_plot_ridge_path.py` - * :ref:`sphx_glr_auto_examples_text_plot_document_classification_20newsgroups.py` - * :ref:`sphx_glr_auto_examples_inspection_plot_linear_model_coefficient_interpretation.py` +* :ref:`sphx_glr_auto_examples_linear_model_plot_ridge_path.py` +* :ref:`sphx_glr_auto_examples_text_plot_document_classification_20newsgroups.py` +* :ref:`sphx_glr_auto_examples_inspection_plot_linear_model_coefficient_interpretation.py` Ridge Complexity ---------------- @@ -216,13 +216,11 @@ cross-validation with :class:`~sklearn.model_selection.GridSearchCV`, for example `cv=10` for 10-fold cross-validation, rather than Leave-One-Out Cross-Validation. -.. topic:: References: - +.. dropdown:: References .. [RL2007] "Notes on Regularized Least Squares", Rifkin & Lippert (`technical report `_, - `course slides - `_). + `course slides `_). .. _lasso: @@ -262,11 +260,11 @@ for another implementation:: The function :func:`lasso_path` is useful for lower-level tasks, as it computes the coefficients along the full path of possible values. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_linear_model_plot_lasso_and_elasticnet.py` - * :ref:`sphx_glr_auto_examples_applications_plot_tomography_l1_reconstruction.py` - * :ref:`sphx_glr_auto_examples_inspection_plot_linear_model_coefficient_interpretation.py` +* :ref:`sphx_glr_auto_examples_linear_model_plot_lasso_and_elasticnet.py` +* :ref:`sphx_glr_auto_examples_applications_plot_tomography_l1_reconstruction.py` +* :ref:`sphx_glr_auto_examples_inspection_plot_linear_model_coefficient_interpretation.py` .. note:: **Feature selection with Lasso** @@ -275,23 +273,19 @@ computes the coefficients along the full path of possible values. thus be used to perform feature selection, as detailed in :ref:`l1_feature_selection`. -|details-start| -**References** -|details-split| - -The following two references explain the iterations -used in the coordinate descent solver of scikit-learn, as well as -the duality gap computation used for convergence control. +.. dropdown:: References -* "Regularization Path For Generalized linear Models by Coordinate Descent", - Friedman, Hastie & Tibshirani, J Stat Softw, 2010 (`Paper - `__). -* "An Interior-Point Method for Large-Scale L1-Regularized Least Squares," - S. J. Kim, K. Koh, M. Lustig, S. Boyd and D. Gorinevsky, - in IEEE Journal of Selected Topics in Signal Processing, 2007 - (`Paper `__) + The following two references explain the iterations + used in the coordinate descent solver of scikit-learn, as well as + the duality gap computation used for convergence control. -|details-end| + * "Regularization Path For Generalized linear Models by Coordinate Descent", + Friedman, Hastie & Tibshirani, J Stat Softw, 2010 (`Paper + `__). + * "An Interior-Point Method for Large-Scale L1-Regularized Least Squares," + S. J. Kim, K. Koh, M. Lustig, S. Boyd and D. Gorinevsky, + in IEEE Journal of Selected Topics in Signal Processing, 2007 + (`Paper `__) Setting regularization parameter -------------------------------- @@ -348,10 +342,10 @@ the problem is badly conditioned (e.g. more features than samples). :align: center :scale: 50% -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_linear_model_plot_lasso_model_selection.py` - * :ref:`sphx_glr_auto_examples_linear_model_plot_lasso_lars_ic.py` +* :ref:`sphx_glr_auto_examples_linear_model_plot_lasso_model_selection.py` +* :ref:`sphx_glr_auto_examples_linear_model_plot_lasso_lars_ic.py` .. _aic_bic: @@ -362,59 +356,57 @@ The definition of AIC (and thus BIC) might differ in the literature. In this section, we give more information regarding the criterion computed in scikit-learn. -|details-start| -**Mathematical details** -|details-split| +.. dropdown:: Mathematical details -The AIC criterion is defined as: + The AIC criterion is defined as: -.. math:: - AIC = -2 \log(\hat{L}) + 2 d + .. math:: + AIC = -2 \log(\hat{L}) + 2 d -where :math:`\hat{L}` is the maximum likelihood of the model and -:math:`d` is the number of parameters (as well referred to as degrees of -freedom in the previous section). + where :math:`\hat{L}` is the maximum likelihood of the model and + :math:`d` is the number of parameters (as well referred to as degrees of + freedom in the previous section). -The definition of BIC replace the constant :math:`2` by :math:`\log(N)`: + The definition of BIC replace the constant :math:`2` by :math:`\log(N)`: -.. math:: - BIC = -2 \log(\hat{L}) + \log(N) d + .. math:: + BIC = -2 \log(\hat{L}) + \log(N) d -where :math:`N` is the number of samples. + where :math:`N` is the number of samples. -For a linear Gaussian model, the maximum log-likelihood is defined as: + For a linear Gaussian model, the maximum log-likelihood is defined as: -.. math:: - \log(\hat{L}) = - \frac{n}{2} \log(2 \pi) - \frac{n}{2} \ln(\sigma^2) - \frac{\sum_{i=1}^{n} (y_i - \hat{y}_i)^2}{2\sigma^2} + .. math:: + \log(\hat{L}) = - \frac{n}{2} \log(2 \pi) - \frac{n}{2} \ln(\sigma^2) - \frac{\sum_{i=1}^{n} (y_i - \hat{y}_i)^2}{2\sigma^2} -where :math:`\sigma^2` is an estimate of the noise variance, -:math:`y_i` and :math:`\hat{y}_i` are respectively the true and predicted -targets, and :math:`n` is the number of samples. + where :math:`\sigma^2` is an estimate of the noise variance, + :math:`y_i` and :math:`\hat{y}_i` are respectively the true and predicted + targets, and :math:`n` is the number of samples. -Plugging the maximum log-likelihood in the AIC formula yields: + Plugging the maximum log-likelihood in the AIC formula yields: -.. math:: - AIC = n \log(2 \pi \sigma^2) + \frac{\sum_{i=1}^{n} (y_i - \hat{y}_i)^2}{\sigma^2} + 2 d + .. math:: + AIC = n \log(2 \pi \sigma^2) + \frac{\sum_{i=1}^{n} (y_i - \hat{y}_i)^2}{\sigma^2} + 2 d -The first term of the above expression is sometimes discarded since it is a -constant when :math:`\sigma^2` is provided. In addition, -it is sometimes stated that the AIC is equivalent to the :math:`C_p` statistic -[12]_. In a strict sense, however, it is equivalent only up to some constant -and a multiplicative factor. + The first term of the above expression is sometimes discarded since it is a + constant when :math:`\sigma^2` is provided. In addition, + it is sometimes stated that the AIC is equivalent to the :math:`C_p` statistic + [12]_. In a strict sense, however, it is equivalent only up to some constant + and a multiplicative factor. -At last, we mentioned above that :math:`\sigma^2` is an estimate of the -noise variance. In :class:`LassoLarsIC` when the parameter `noise_variance` is -not provided (default), the noise variance is estimated via the unbiased -estimator [13]_ defined as: + At last, we mentioned above that :math:`\sigma^2` is an estimate of the + noise variance. In :class:`LassoLarsIC` when the parameter `noise_variance` is + not provided (default), the noise variance is estimated via the unbiased + estimator [13]_ defined as: -.. math:: - \sigma^2 = \frac{\sum_{i=1}^{n} (y_i - \hat{y}_i)^2}{n - p} + .. math:: + \sigma^2 = \frac{\sum_{i=1}^{n} (y_i - \hat{y}_i)^2}{n - p} -where :math:`p` is the number of features and :math:`\hat{y}_i` is the -predicted target using an ordinary least squares regression. Note, that this -formula is valid only when `n_samples > n_features`. + where :math:`p` is the number of features and :math:`\hat{y}_i` is the + predicted target using an ordinary least squares regression. Note, that this + formula is valid only when `n_samples > n_features`. -.. topic:: References: + .. rubric:: References .. [12] :arxiv:`Zou, Hui, Trevor Hastie, and Robert Tibshirani. "On the degrees of freedom of the lasso." @@ -426,8 +418,6 @@ formula is valid only when `n_samples > n_features`. Neural computation 15.7 (2003): 1691-1714. <10.1162/089976603321891864>` -|details-end| - Comparison with the regularization parameter of SVM ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -463,33 +453,29 @@ the MultiTaskLasso are full columns. .. centered:: Fitting a time-series model, imposing that any active feature be active at all times. -.. topic:: Examples: - - * :ref:`sphx_glr_auto_examples_linear_model_plot_multi_task_lasso_support.py` +.. rubric:: Examples +* :ref:`sphx_glr_auto_examples_linear_model_plot_multi_task_lasso_support.py` -|details-start| -**Mathematical details** -|details-split| -Mathematically, it consists of a linear model trained with a mixed -:math:`\ell_1` :math:`\ell_2`-norm for regularization. -The objective function to minimize is: +.. dropdown:: Mathematical details -.. math:: \min_{W} { \frac{1}{2n_{\text{samples}}} ||X W - Y||_{\text{Fro}} ^ 2 + \alpha ||W||_{21}} + Mathematically, it consists of a linear model trained with a mixed + :math:`\ell_1` :math:`\ell_2`-norm for regularization. + The objective function to minimize is: -where :math:`\text{Fro}` indicates the Frobenius norm + .. math:: \min_{W} { \frac{1}{2n_{\text{samples}}} ||X W - Y||_{\text{Fro}} ^ 2 + \alpha ||W||_{21}} -.. math:: ||A||_{\text{Fro}} = \sqrt{\sum_{ij} a_{ij}^2} + where :math:`\text{Fro}` indicates the Frobenius norm -and :math:`\ell_1` :math:`\ell_2` reads + .. math:: ||A||_{\text{Fro}} = \sqrt{\sum_{ij} a_{ij}^2} -.. math:: ||A||_{2 1} = \sum_i \sqrt{\sum_j a_{ij}^2}. + and :math:`\ell_1` :math:`\ell_2` reads -The implementation in the class :class:`MultiTaskLasso` uses -coordinate descent as the algorithm to fit the coefficients. + .. math:: ||A||_{2 1} = \sum_i \sqrt{\sum_j a_{ij}^2}. -|details-end| + The implementation in the class :class:`MultiTaskLasso` uses + coordinate descent as the algorithm to fit the coefficients. .. _elastic_net: @@ -526,29 +512,25 @@ The objective function to minimize is in this case The class :class:`ElasticNetCV` can be used to set the parameters ``alpha`` (:math:`\alpha`) and ``l1_ratio`` (:math:`\rho`) by cross-validation. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_linear_model_plot_lasso_and_elasticnet.py` - * :ref:`sphx_glr_auto_examples_linear_model_plot_lasso_coordinate_descent_path.py` - * :ref:`sphx_glr_auto_examples_linear_model_plot_elastic_net_precomputed_gram_matrix_with_weighted_samples.py` +* :ref:`sphx_glr_auto_examples_linear_model_plot_lasso_and_elasticnet.py` +* :ref:`sphx_glr_auto_examples_linear_model_plot_lasso_coordinate_descent_path.py` +* :ref:`sphx_glr_auto_examples_linear_model_plot_elastic_net_precomputed_gram_matrix_with_weighted_samples.py` -|details-start| -**References** -|details-split| +.. dropdown:: References -The following two references explain the iterations -used in the coordinate descent solver of scikit-learn, as well as -the duality gap computation used for convergence control. + The following two references explain the iterations + used in the coordinate descent solver of scikit-learn, as well as + the duality gap computation used for convergence control. -* "Regularization Path For Generalized linear Models by Coordinate Descent", - Friedman, Hastie & Tibshirani, J Stat Softw, 2010 (`Paper - `__). -* "An Interior-Point Method for Large-Scale L1-Regularized Least Squares," - S. J. Kim, K. Koh, M. Lustig, S. Boyd and D. Gorinevsky, - in IEEE Journal of Selected Topics in Signal Processing, 2007 - (`Paper `__) - -|details-end| + * "Regularization Path For Generalized linear Models by Coordinate Descent", + Friedman, Hastie & Tibshirani, J Stat Softw, 2010 (`Paper + `__). + * "An Interior-Point Method for Large-Scale L1-Regularized Least Squares," + S. J. Kim, K. Koh, M. Lustig, S. Boyd and D. Gorinevsky, + in IEEE Journal of Selected Topics in Signal Processing, 2007 + (`Paper `__) .. _multi_task_elastic_net: @@ -641,37 +623,33 @@ function of the norm of its coefficients. >>> reg.coef_ array([0.6..., 0. ]) -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_linear_model_plot_lasso_lars.py` +* :ref:`sphx_glr_auto_examples_linear_model_plot_lasso_lars.py` The Lars algorithm provides the full path of the coefficients along the regularization parameter almost for free, thus a common operation is to retrieve the path with one of the functions :func:`lars_path` or :func:`lars_path_gram`. -|details-start| -**Mathematical formulation** -|details-split| - -The algorithm is similar to forward stepwise regression, but instead -of including features at each step, the estimated coefficients are -increased in a direction equiangular to each one's correlations with -the residual. +.. dropdown:: Mathematical formulation -Instead of giving a vector result, the LARS solution consists of a -curve denoting the solution for each value of the :math:`\ell_1` norm of the -parameter vector. The full coefficients path is stored in the array -``coef_path_`` of shape `(n_features, max_features + 1)`. The first -column is always zero. + The algorithm is similar to forward stepwise regression, but instead + of including features at each step, the estimated coefficients are + increased in a direction equiangular to each one's correlations with + the residual. -.. topic:: References: + Instead of giving a vector result, the LARS solution consists of a + curve denoting the solution for each value of the :math:`\ell_1` norm of the + parameter vector. The full coefficients path is stored in the array + ``coef_path_`` of shape `(n_features, max_features + 1)`. The first + column is always zero. - * Original Algorithm is detailed in the paper `Least Angle Regression - `_ - by Hastie et al. + .. rubric:: References -|details-end| + * Original Algorithm is detailed in the paper `Least Angle Regression + `_ + by Hastie et al. .. _omp: @@ -702,21 +680,17 @@ residual is recomputed using an orthogonal projection on the space of the previously chosen dictionary elements. -.. topic:: Examples: - - * :ref:`sphx_glr_auto_examples_linear_model_plot_omp.py` +.. rubric:: Examples -|details-start| -**References** -|details-split| +* :ref:`sphx_glr_auto_examples_linear_model_plot_omp.py` -* https://www.cs.technion.ac.il/~ronrubin/Publications/KSVD-OMP-v2.pdf +.. dropdown:: References -* `Matching pursuits with time-frequency dictionaries - `_, - S. G. Mallat, Z. Zhang, + * https://www.cs.technion.ac.il/~ronrubin/Publications/KSVD-OMP-v2.pdf -|details-end| + * `Matching pursuits with time-frequency dictionaries + `_, + S. G. Mallat, Z. Zhang, .. _bayesian_regression: @@ -755,17 +729,13 @@ The disadvantages of Bayesian regression include: - Inference of the model can be time consuming. -|details-start| -**References** -|details-split| +.. dropdown:: References -* A good introduction to Bayesian methods is given in C. Bishop: Pattern - Recognition and Machine learning + * A good introduction to Bayesian methods is given in C. Bishop: Pattern + Recognition and Machine learning -* Original Algorithm is detailed in the book `Bayesian learning for neural - networks` by Radford M. Neal - -|details-end| + * Original Algorithm is detailed in the book `Bayesian learning for neural + networks` by Radford M. Neal .. _bayesian_ridge_regression: @@ -822,21 +792,17 @@ Due to the Bayesian framework, the weights found are slightly different to the ones found by :ref:`ordinary_least_squares`. However, Bayesian Ridge Regression is more robust to ill-posed problems. -.. topic:: Examples: - - * :ref:`sphx_glr_auto_examples_linear_model_plot_bayesian_ridge_curvefit.py` +.. rubric:: Examples -|details-start| -**References** -|details-split| +* :ref:`sphx_glr_auto_examples_linear_model_plot_bayesian_ridge_curvefit.py` -* Section 3.3 in Christopher M. Bishop: Pattern Recognition and Machine Learning, 2006 +.. dropdown:: References -* David J. C. MacKay, `Bayesian Interpolation `_, 1992. + * Section 3.3 in Christopher M. Bishop: Pattern Recognition and Machine Learning, 2006 -* Michael E. Tipping, `Sparse Bayesian Learning and the Relevance Vector Machine `_, 2001. + * David J. C. MacKay, `Bayesian Interpolation `_, 1992. -|details-end| + * Michael E. Tipping, `Sparse Bayesian Learning and the Relevance Vector Machine `_, 2001. .. _automatic_relevance_determination: @@ -868,20 +834,20 @@ ARD is also known in the literature as *Sparse Bayesian Learning* and *Relevance Vector Machine* [3]_ [4]_. For a worked-out comparison between ARD and `Bayesian Ridge Regression`_, see the example below. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_linear_model_plot_ard.py` +* :ref:`sphx_glr_auto_examples_linear_model_plot_ard.py` -.. topic:: References: +.. rubric:: References - .. [1] Christopher M. Bishop: Pattern Recognition and Machine Learning, Chapter 7.2.1 +.. [1] Christopher M. Bishop: Pattern Recognition and Machine Learning, Chapter 7.2.1 - .. [2] David Wipf and Srikantan Nagarajan: `A New View of Automatic Relevance Determination `_ +.. [2] David Wipf and Srikantan Nagarajan: `A New View of Automatic Relevance Determination `_ - .. [3] Michael E. Tipping: `Sparse Bayesian Learning and the Relevance Vector Machine `_ +.. [3] Michael E. Tipping: `Sparse Bayesian Learning and the Relevance Vector Machine `_ - .. [4] Tristan Fletcher: `Relevance Vector Machines Explained `_ +.. [4] Tristan Fletcher: `Relevance Vector Machines Explained `_ .. _Logistic_regression: @@ -918,17 +884,13 @@ regularization. implemented in scikit-learn, so it expects a categorical target, making the Logistic Regression a classifier. -.. topic:: Examples - - * :ref:`sphx_glr_auto_examples_linear_model_plot_logistic_l1_l2_sparsity.py` +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_linear_model_plot_logistic_path.py` - - * :ref:`sphx_glr_auto_examples_linear_model_plot_logistic_multinomial.py` - - * :ref:`sphx_glr_auto_examples_linear_model_plot_sparse_logistic_regression_20newsgroups.py` - - * :ref:`sphx_glr_auto_examples_linear_model_plot_sparse_logistic_regression_mnist.py` +* :ref:`sphx_glr_auto_examples_linear_model_plot_logistic_l1_l2_sparsity.py` +* :ref:`sphx_glr_auto_examples_linear_model_plot_logistic_path.py` +* :ref:`sphx_glr_auto_examples_linear_model_plot_logistic_multinomial.py` +* :ref:`sphx_glr_auto_examples_linear_model_plot_sparse_logistic_regression_20newsgroups.py` +* :ref:`sphx_glr_auto_examples_linear_model_plot_sparse_logistic_regression_mnist.py` Binary Case ----------- @@ -1000,47 +962,43 @@ logistic regression, see also `log-linear model especially important when using regularization. The choice of overparameterization can be detrimental for unpenalized models since then the solution may not be unique, as shown in [16]_. -|details-start| -**Mathematical details** -|details-split| +.. dropdown:: Mathematical details -Let :math:`y_i \in {1, \ldots, K}` be the label (ordinal) encoded target variable for observation :math:`i`. -Instead of a single coefficient vector, we now have -a matrix of coefficients :math:`W` where each row vector :math:`W_k` corresponds to class -:math:`k`. We aim at predicting the class probabilities :math:`P(y_i=k|X_i)` via -:meth:`~sklearn.linear_model.LogisticRegression.predict_proba` as: + Let :math:`y_i \in {1, \ldots, K}` be the label (ordinal) encoded target variable for observation :math:`i`. + Instead of a single coefficient vector, we now have + a matrix of coefficients :math:`W` where each row vector :math:`W_k` corresponds to class + :math:`k`. We aim at predicting the class probabilities :math:`P(y_i=k|X_i)` via + :meth:`~sklearn.linear_model.LogisticRegression.predict_proba` as: -.. math:: \hat{p}_k(X_i) = \frac{\exp(X_i W_k + W_{0, k})}{\sum_{l=0}^{K-1} \exp(X_i W_l + W_{0, l})}. + .. math:: \hat{p}_k(X_i) = \frac{\exp(X_i W_k + W_{0, k})}{\sum_{l=0}^{K-1} \exp(X_i W_l + W_{0, l})}. -The objective for the optimization becomes - -.. math:: - \min_W -\frac{1}{S}\sum_{i=1}^n \sum_{k=0}^{K-1} s_{ik} [y_i = k] \log(\hat{p}_k(X_i)) - + \frac{r(W)}{S C}\,. + The objective for the optimization becomes -Where :math:`[P]` represents the Iverson bracket which evaluates to :math:`0` -if :math:`P` is false, otherwise it evaluates to :math:`1`. + .. math:: + \min_W -\frac{1}{S}\sum_{i=1}^n \sum_{k=0}^{K-1} s_{ik} [y_i = k] \log(\hat{p}_k(X_i)) + + \frac{r(W)}{S C}\,, -Again, :math:`s_{ik}` are the weights assigned by the user (multiplication of sample -weights and class weights) with their sum :math:`S = \sum_{i=1}^n \sum_{k=0}^{K-1} s_{ik}`. + where :math:`[P]` represents the Iverson bracket which evaluates to :math:`0` + if :math:`P` is false, otherwise it evaluates to :math:`1`. -We currently provide four choices -for the regularization term :math:`r(W)` via the `penalty` argument, where :math:`m` -is the number of features: + Again, :math:`s_{ik}` are the weights assigned by the user (multiplication of sample + weights and class weights) with their sum :math:`S = \sum_{i=1}^n \sum_{k=0}^{K-1} s_{ik}`. -+----------------+----------------------------------------------------------------------------------+ -| penalty | :math:`r(W)` | -+================+==================================================================================+ -| `None` | :math:`0` | -+----------------+----------------------------------------------------------------------------------+ -| :math:`\ell_1` | :math:`\|W\|_{1,1} = \sum_{i=1}^m\sum_{j=1}^{K}|W_{i,j}|` | -+----------------+----------------------------------------------------------------------------------+ -| :math:`\ell_2` | :math:`\frac{1}{2}\|W\|_F^2 = \frac{1}{2}\sum_{i=1}^m\sum_{j=1}^{K} W_{i,j}^2` | -+----------------+----------------------------------------------------------------------------------+ -| `ElasticNet` | :math:`\frac{1 - \rho}{2}\|W\|_F^2 + \rho \|W\|_{1,1}` | -+----------------+----------------------------------------------------------------------------------+ + We currently provide four choices + for the regularization term :math:`r(W)` via the `penalty` argument, where :math:`m` + is the number of features: -|details-end| + +----------------+----------------------------------------------------------------------------------+ + | penalty | :math:`r(W)` | + +================+==================================================================================+ + | `None` | :math:`0` | + +----------------+----------------------------------------------------------------------------------+ + | :math:`\ell_1` | :math:`\|W\|_{1,1} = \sum_{i=1}^m\sum_{j=1}^{K}|W_{i,j}|` | + +----------------+----------------------------------------------------------------------------------+ + | :math:`\ell_2` | :math:`\frac{1}{2}\|W\|_F^2 = \frac{1}{2}\sum_{i=1}^m\sum_{j=1}^{K} W_{i,j}^2` | + +----------------+----------------------------------------------------------------------------------+ + | `ElasticNet` | :math:`\frac{1 - \rho}{2}\|W\|_F^2 + \rho \|W\|_{1,1}` | + +----------------+----------------------------------------------------------------------------------+ Solvers ------- @@ -1097,56 +1055,54 @@ with ``fit_intercept=False`` and having many samples with ``decision_function`` zero, is likely to be a underfit, bad model and you are advised to set ``fit_intercept=True`` and increase the ``intercept_scaling``. -|details-start| -**Solvers' details** -|details-split| - -* The solver "liblinear" uses a coordinate descent (CD) algorithm, and relies - on the excellent C++ `LIBLINEAR library - `_, which is shipped with - scikit-learn. However, the CD algorithm implemented in liblinear cannot learn - a true multinomial (multiclass) model; instead, the optimization problem is - decomposed in a "one-vs-rest" fashion so separate binary classifiers are - trained for all classes. This happens under the hood, so - :class:`LogisticRegression` instances using this solver behave as multiclass - classifiers. For :math:`\ell_1` regularization :func:`sklearn.svm.l1_min_c` allows to - calculate the lower bound for C in order to get a non "null" (all feature - weights to zero) model. - -* The "lbfgs", "newton-cg" and "sag" solvers only support :math:`\ell_2` - regularization or no regularization, and are found to converge faster for some - high-dimensional data. Setting `multi_class` to "multinomial" with these solvers - learns a true multinomial logistic regression model [5]_, which means that its - probability estimates should be better calibrated than the default "one-vs-rest" - setting. - -* The "sag" solver uses Stochastic Average Gradient descent [6]_. It is faster - than other solvers for large datasets, when both the number of samples and the - number of features are large. - -* The "saga" solver [7]_ is a variant of "sag" that also supports the - non-smooth `penalty="l1"`. This is therefore the solver of choice for sparse - multinomial logistic regression. It is also the only solver that supports - `penalty="elasticnet"`. - -* The "lbfgs" is an optimization algorithm that approximates the - Broyden–Fletcher–Goldfarb–Shanno algorithm [8]_, which belongs to - quasi-Newton methods. As such, it can deal with a wide range of different training - data and is therefore the default solver. Its performance, however, suffers on poorly - scaled datasets and on datasets with one-hot encoded categorical features with rare - categories. - -* The "newton-cholesky" solver is an exact Newton solver that calculates the hessian - matrix and solves the resulting linear system. It is a very good choice for - `n_samples` >> `n_features`, but has a few shortcomings: Only :math:`\ell_2` - regularization is supported. Furthermore, because the hessian matrix is explicitly - computed, the memory usage has a quadratic dependency on `n_features` as well as on - `n_classes`. As a consequence, only the one-vs-rest scheme is implemented for the - multiclass case. - -For a comparison of some of these solvers, see [9]_. - -.. topic:: References: +.. dropdown:: Solvers' details + + * The solver "liblinear" uses a coordinate descent (CD) algorithm, and relies + on the excellent C++ `LIBLINEAR library + `_, which is shipped with + scikit-learn. However, the CD algorithm implemented in liblinear cannot learn + a true multinomial (multiclass) model; instead, the optimization problem is + decomposed in a "one-vs-rest" fashion so separate binary classifiers are + trained for all classes. This happens under the hood, so + :class:`LogisticRegression` instances using this solver behave as multiclass + classifiers. For :math:`\ell_1` regularization :func:`sklearn.svm.l1_min_c` allows to + calculate the lower bound for C in order to get a non "null" (all feature + weights to zero) model. + + * The "lbfgs", "newton-cg" and "sag" solvers only support :math:`\ell_2` + regularization or no regularization, and are found to converge faster for some + high-dimensional data. Setting `multi_class` to "multinomial" with these solvers + learns a true multinomial logistic regression model [5]_, which means that its + probability estimates should be better calibrated than the default "one-vs-rest" + setting. + + * The "sag" solver uses Stochastic Average Gradient descent [6]_. It is faster + than other solvers for large datasets, when both the number of samples and the + number of features are large. + + * The "saga" solver [7]_ is a variant of "sag" that also supports the + non-smooth `penalty="l1"`. This is therefore the solver of choice for sparse + multinomial logistic regression. It is also the only solver that supports + `penalty="elasticnet"`. + + * The "lbfgs" is an optimization algorithm that approximates the + Broyden–Fletcher–Goldfarb–Shanno algorithm [8]_, which belongs to + quasi-Newton methods. As such, it can deal with a wide range of different training + data and is therefore the default solver. Its performance, however, suffers on poorly + scaled datasets and on datasets with one-hot encoded categorical features with rare + categories. + + * The "newton-cholesky" solver is an exact Newton solver that calculates the hessian + matrix and solves the resulting linear system. It is a very good choice for + `n_samples` >> `n_features`, but has a few shortcomings: Only :math:`\ell_2` + regularization is supported. Furthermore, because the hessian matrix is explicitly + computed, the memory usage has a quadratic dependency on `n_features` as well as on + `n_classes`. As a consequence, only the one-vs-rest scheme is implemented for the + multiclass case. + + For a comparison of some of these solvers, see [9]_. + + .. rubric:: References .. [5] Christopher M. Bishop: Pattern Recognition and Machine Learning, Chapter 4.3.4 @@ -1165,8 +1121,6 @@ For a comparison of some of these solvers, see [9]_. "A Blockwise Descent Algorithm for Group-penalized Multiresponse and Multinomial Regression." <1311.6529>` -|details-end| - .. note:: **Feature selection with sparse logistic regression** @@ -1263,38 +1217,34 @@ The choice of the distribution depends on the problem at hand: used for multiclass classification. -|details-start| -**Examples of use cases** -|details-split| - -* Agriculture / weather modeling: number of rain events per year (Poisson), - amount of rainfall per event (Gamma), total rainfall per year (Tweedie / - Compound Poisson Gamma). -* Risk modeling / insurance policy pricing: number of claim events / - policyholder per year (Poisson), cost per event (Gamma), total cost per - policyholder per year (Tweedie / Compound Poisson Gamma). -* Credit Default: probability that a loan can't be paid back (Bernoulli). -* Fraud Detection: probability that a financial transaction like a cash transfer - is a fraudulent transaction (Bernoulli). -* Predictive maintenance: number of production interruption events per year - (Poisson), duration of interruption (Gamma), total interruption time per year - (Tweedie / Compound Poisson Gamma). -* Medical Drug Testing: probability of curing a patient in a set of trials or - probability that a patient will experience side effects (Bernoulli). -* News Classification: classification of news articles into three categories - namely Business News, Politics and Entertainment news (Categorical). +.. dropdown:: Examples of use cases -|details-end| + * Agriculture / weather modeling: number of rain events per year (Poisson), + amount of rainfall per event (Gamma), total rainfall per year (Tweedie / + Compound Poisson Gamma). + * Risk modeling / insurance policy pricing: number of claim events / + policyholder per year (Poisson), cost per event (Gamma), total cost per + policyholder per year (Tweedie / Compound Poisson Gamma). + * Credit Default: probability that a loan can't be paid back (Bernoulli). + * Fraud Detection: probability that a financial transaction like a cash transfer + is a fraudulent transaction (Bernoulli). + * Predictive maintenance: number of production interruption events per year + (Poisson), duration of interruption (Gamma), total interruption time per year + (Tweedie / Compound Poisson Gamma). + * Medical Drug Testing: probability of curing a patient in a set of trials or + probability that a patient will experience side effects (Bernoulli). + * News Classification: classification of news articles into three categories + namely Business News, Politics and Entertainment news (Categorical). -.. topic:: References: +.. rubric:: References - .. [10] McCullagh, Peter; Nelder, John (1989). Generalized Linear Models, - Second Edition. Boca Raton: Chapman and Hall/CRC. ISBN 0-412-31760-5. +.. [10] McCullagh, Peter; Nelder, John (1989). Generalized Linear Models, + Second Edition. Boca Raton: Chapman and Hall/CRC. ISBN 0-412-31760-5. - .. [11] Jørgensen, B. (1992). The theory of exponential dispersion models - and analysis of deviance. Monografias de matemática, no. 51. See also - `Exponential dispersion model. - `_ +.. [11] Jørgensen, B. (1992). The theory of exponential dispersion models + and analysis of deviance. Monografias de matemática, no. 51. See also + `Exponential dispersion model. + `_ Usage ----- @@ -1328,37 +1278,33 @@ Usage example:: -0.7638... -.. topic:: Examples - - * :ref:`sphx_glr_auto_examples_linear_model_plot_poisson_regression_non_normal_loss.py` - * :ref:`sphx_glr_auto_examples_linear_model_plot_tweedie_regression_insurance_claims.py` +.. rubric:: Examples -|details-start| -**Practical considerations** -|details-split| +* :ref:`sphx_glr_auto_examples_linear_model_plot_poisson_regression_non_normal_loss.py` +* :ref:`sphx_glr_auto_examples_linear_model_plot_tweedie_regression_insurance_claims.py` -The feature matrix `X` should be standardized before fitting. This ensures -that the penalty treats features equally. +.. dropdown:: Practical considerations -Since the linear predictor :math:`Xw` can be negative and Poisson, -Gamma and Inverse Gaussian distributions don't support negative values, it -is necessary to apply an inverse link function that guarantees the -non-negativeness. For example with `link='log'`, the inverse link function -becomes :math:`h(Xw)=\exp(Xw)`. + The feature matrix `X` should be standardized before fitting. This ensures + that the penalty treats features equally. -If you want to model a relative frequency, i.e. counts per exposure (time, -volume, ...) you can do so by using a Poisson distribution and passing -:math:`y=\frac{\mathrm{counts}}{\mathrm{exposure}}` as target values -together with :math:`\mathrm{exposure}` as sample weights. For a concrete -example see e.g. -:ref:`sphx_glr_auto_examples_linear_model_plot_tweedie_regression_insurance_claims.py`. + Since the linear predictor :math:`Xw` can be negative and Poisson, + Gamma and Inverse Gaussian distributions don't support negative values, it + is necessary to apply an inverse link function that guarantees the + non-negativeness. For example with `link='log'`, the inverse link function + becomes :math:`h(Xw)=\exp(Xw)`. -When performing cross-validation for the `power` parameter of -`TweedieRegressor`, it is advisable to specify an explicit `scoring` function, -because the default scorer :meth:`TweedieRegressor.score` is a function of -`power` itself. + If you want to model a relative frequency, i.e. counts per exposure (time, + volume, ...) you can do so by using a Poisson distribution and passing + :math:`y=\frac{\mathrm{counts}}{\mathrm{exposure}}` as target values + together with :math:`\mathrm{exposure}` as sample weights. For a concrete + example see e.g. + :ref:`sphx_glr_auto_examples_linear_model_plot_tweedie_regression_insurance_claims.py`. -|details-end| + When performing cross-validation for the `power` parameter of + `TweedieRegressor`, it is advisable to specify an explicit `scoring` function, + because the default scorer :meth:`TweedieRegressor.score` is a function of + `power` itself. Stochastic Gradient Descent - SGD ================================= @@ -1416,15 +1362,11 @@ For classification, :class:`PassiveAggressiveClassifier` can be used with ``loss='epsilon_insensitive'`` (PA-I) or ``loss='squared_epsilon_insensitive'`` (PA-II). -|details-start| -**References** -|details-split| +.. dropdown:: References -* `"Online Passive-Aggressive Algorithms" - `_ - K. Crammer, O. Dekel, J. Keshat, S. Shalev-Shwartz, Y. Singer - JMLR 7 (2006) - -|details-end| + * `"Online Passive-Aggressive Algorithms" + `_ + K. Crammer, O. Dekel, J. Keshat, S. Shalev-Shwartz, Y. Singer - JMLR 7 (2006) Robustness regression: outliers and modeling errors ===================================================== @@ -1534,56 +1476,48 @@ estimated only from the determined inliers. :align: center :scale: 50% -.. topic:: Examples - - * :ref:`sphx_glr_auto_examples_linear_model_plot_ransac.py` - * :ref:`sphx_glr_auto_examples_linear_model_plot_robust_fit.py` - -|details-start| -**Details of the algorithm** -|details-split| - -Each iteration performs the following steps: - -1. Select ``min_samples`` random samples from the original data and check - whether the set of data is valid (see ``is_data_valid``). -2. Fit a model to the random subset (``estimator.fit``) and check - whether the estimated model is valid (see ``is_model_valid``). -3. Classify all data as inliers or outliers by calculating the residuals - to the estimated model (``estimator.predict(X) - y``) - all data - samples with absolute residuals smaller than or equal to the - ``residual_threshold`` are considered as inliers. -4. Save fitted model as best model if number of inlier samples is - maximal. In case the current estimated model has the same number of - inliers, it is only considered as the best model if it has better score. - -These steps are performed either a maximum number of times (``max_trials``) or -until one of the special stop criteria are met (see ``stop_n_inliers`` and -``stop_score``). The final model is estimated using all inlier samples (consensus -set) of the previously determined best model. - -The ``is_data_valid`` and ``is_model_valid`` functions allow to identify and reject -degenerate combinations of random sub-samples. If the estimated model is not -needed for identifying degenerate cases, ``is_data_valid`` should be used as it -is called prior to fitting the model and thus leading to better computational -performance. - -|details-end| - -|details-start| -**References** -|details-split| - -* https://en.wikipedia.org/wiki/RANSAC -* `"Random Sample Consensus: A Paradigm for Model Fitting with Applications to - Image Analysis and Automated Cartography" - `_ - Martin A. Fischler and Robert C. Bolles - SRI International (1981) -* `"Performance Evaluation of RANSAC Family" - `_ - Sunglok Choi, Taemin Kim and Wonpil Yu - BMVC (2009) - -|details-end| +.. rubric:: Examples + +* :ref:`sphx_glr_auto_examples_linear_model_plot_ransac.py` +* :ref:`sphx_glr_auto_examples_linear_model_plot_robust_fit.py` + +.. dropdown:: Details of the algorithm + + Each iteration performs the following steps: + + 1. Select ``min_samples`` random samples from the original data and check + whether the set of data is valid (see ``is_data_valid``). + 2. Fit a model to the random subset (``estimator.fit``) and check + whether the estimated model is valid (see ``is_model_valid``). + 3. Classify all data as inliers or outliers by calculating the residuals + to the estimated model (``estimator.predict(X) - y``) - all data + samples with absolute residuals smaller than or equal to the + ``residual_threshold`` are considered as inliers. + 4. Save fitted model as best model if number of inlier samples is + maximal. In case the current estimated model has the same number of + inliers, it is only considered as the best model if it has better score. + + These steps are performed either a maximum number of times (``max_trials``) or + until one of the special stop criteria are met (see ``stop_n_inliers`` and + ``stop_score``). The final model is estimated using all inlier samples (consensus + set) of the previously determined best model. + + The ``is_data_valid`` and ``is_model_valid`` functions allow to identify and reject + degenerate combinations of random sub-samples. If the estimated model is not + needed for identifying degenerate cases, ``is_data_valid`` should be used as it + is called prior to fitting the model and thus leading to better computational + performance. + +.. dropdown:: References + + * https://en.wikipedia.org/wiki/RANSAC + * `"Random Sample Consensus: A Paradigm for Model Fitting with Applications to + Image Analysis and Automated Cartography" + `_ + Martin A. Fischler and Robert C. Bolles - SRI International (1981) + * `"Performance Evaluation of RANSAC Family" + `_ + Sunglok Choi, Taemin Kim and Wonpil Yu - BMVC (2009) .. _theil_sen_regression: @@ -1596,47 +1530,45 @@ that the robustness of the estimator decreases quickly with the dimensionality of the problem. It loses its robustness properties and becomes no better than an ordinary least squares in high dimension. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_linear_model_plot_theilsen.py` - * :ref:`sphx_glr_auto_examples_linear_model_plot_robust_fit.py` +* :ref:`sphx_glr_auto_examples_linear_model_plot_theilsen.py` +* :ref:`sphx_glr_auto_examples_linear_model_plot_robust_fit.py` -|details-start| -**Theoretical considerations** -|details-split| +.. dropdown:: Theoretical considerations -:class:`TheilSenRegressor` is comparable to the :ref:`Ordinary Least Squares -(OLS) ` in terms of asymptotic efficiency and as an -unbiased estimator. In contrast to OLS, Theil-Sen is a non-parametric -method which means it makes no assumption about the underlying -distribution of the data. Since Theil-Sen is a median-based estimator, it -is more robust against corrupted data aka outliers. In univariate -setting, Theil-Sen has a breakdown point of about 29.3% in case of a -simple linear regression which means that it can tolerate arbitrary -corrupted data of up to 29.3%. + :class:`TheilSenRegressor` is comparable to the :ref:`Ordinary Least Squares + (OLS) ` in terms of asymptotic efficiency and as an + unbiased estimator. In contrast to OLS, Theil-Sen is a non-parametric + method which means it makes no assumption about the underlying + distribution of the data. Since Theil-Sen is a median-based estimator, it + is more robust against corrupted data aka outliers. In univariate + setting, Theil-Sen has a breakdown point of about 29.3% in case of a + simple linear regression which means that it can tolerate arbitrary + corrupted data of up to 29.3%. -.. figure:: ../auto_examples/linear_model/images/sphx_glr_plot_theilsen_001.png - :target: ../auto_examples/linear_model/plot_theilsen.html - :align: center - :scale: 50% + .. figure:: ../auto_examples/linear_model/images/sphx_glr_plot_theilsen_001.png + :target: ../auto_examples/linear_model/plot_theilsen.html + :align: center + :scale: 50% -The implementation of :class:`TheilSenRegressor` in scikit-learn follows a -generalization to a multivariate linear regression model [#f1]_ using the -spatial median which is a generalization of the median to multiple -dimensions [#f2]_. + The implementation of :class:`TheilSenRegressor` in scikit-learn follows a + generalization to a multivariate linear regression model [#f1]_ using the + spatial median which is a generalization of the median to multiple + dimensions [#f2]_. -In terms of time and space complexity, Theil-Sen scales according to + In terms of time and space complexity, Theil-Sen scales according to -.. math:: - \binom{n_{\text{samples}}}{n_{\text{subsamples}}} + .. math:: + \binom{n_{\text{samples}}}{n_{\text{subsamples}}} -which makes it infeasible to be applied exhaustively to problems with a -large number of samples and features. Therefore, the magnitude of a -subpopulation can be chosen to limit the time and space complexity by -considering only a random subset of all possible combinations. + which makes it infeasible to be applied exhaustively to problems with a + large number of samples and features. Therefore, the magnitude of a + subpopulation can be chosen to limit the time and space complexity by + considering only a random subset of all possible combinations. -.. topic:: References: + .. rubric:: References .. [#f1] Xin Dang, Hanxiang Peng, Xueqin Wang and Heping Zhang: `Theil-Sen Estimators in a Multiple Linear Regression Model. `_ @@ -1644,8 +1576,6 @@ considering only a random subset of all possible combinations. Also see the `Wikipedia page `_ -|details-end| - .. _huber_regression: @@ -1664,39 +1594,35 @@ but gives a lesser weight to them. :align: center :scale: 50% -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_linear_model_plot_huber_vs_ridge.py` +* :ref:`sphx_glr_auto_examples_linear_model_plot_huber_vs_ridge.py` -|details-start| -**Mathematical details** -|details-split| +.. dropdown:: Mathematical details -The loss function that :class:`HuberRegressor` minimizes is given by + The loss function that :class:`HuberRegressor` minimizes is given by -.. math:: + .. math:: - \min_{w, \sigma} {\sum_{i=1}^n\left(\sigma + H_{\epsilon}\left(\frac{X_{i}w - y_{i}}{\sigma}\right)\sigma\right) + \alpha {||w||_2}^2} + \min_{w, \sigma} {\sum_{i=1}^n\left(\sigma + H_{\epsilon}\left(\frac{X_{i}w - y_{i}}{\sigma}\right)\sigma\right) + \alpha {||w||_2}^2} -where + where -.. math:: + .. math:: - H_{\epsilon}(z) = \begin{cases} - z^2, & \text {if } |z| < \epsilon, \\ - 2\epsilon|z| - \epsilon^2, & \text{otherwise} - \end{cases} + H_{\epsilon}(z) = \begin{cases} + z^2, & \text {if } |z| < \epsilon, \\ + 2\epsilon|z| - \epsilon^2, & \text{otherwise} + \end{cases} -It is advised to set the parameter ``epsilon`` to 1.35 to achieve 95% -statistical efficiency. + It is advised to set the parameter ``epsilon`` to 1.35 to achieve 95% + statistical efficiency. -.. topic:: References: + .. rubric:: References * Peter J. Huber, Elvezio M. Ronchetti: Robust Statistics, Concomitant scale estimates, pg 172 -|details-end| - The :class:`HuberRegressor` differs from using :class:`SGDRegressor` with loss set to `huber` in the following ways. @@ -1746,59 +1672,51 @@ Most implementations of quantile regression are based on linear programming problem. The current implementation is based on :func:`scipy.optimize.linprog`. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_linear_model_plot_quantile_regression.py` +* :ref:`sphx_glr_auto_examples_linear_model_plot_quantile_regression.py` -|details-start| -**Mathematical details** -|details-split| +.. dropdown:: Mathematical details -As a linear model, the :class:`QuantileRegressor` gives linear predictions -:math:`\hat{y}(w, X) = Xw` for the :math:`q`-th quantile, :math:`q \in (0, 1)`. -The weights or coefficients :math:`w` are then found by the following -minimization problem: + As a linear model, the :class:`QuantileRegressor` gives linear predictions + :math:`\hat{y}(w, X) = Xw` for the :math:`q`-th quantile, :math:`q \in (0, 1)`. + The weights or coefficients :math:`w` are then found by the following + minimization problem: -.. math:: - \min_{w} {\frac{1}{n_{\text{samples}}} - \sum_i PB_q(y_i - X_i w) + \alpha ||w||_1}. + .. math:: + \min_{w} {\frac{1}{n_{\text{samples}}} + \sum_i PB_q(y_i - X_i w) + \alpha ||w||_1}. -This consists of the pinball loss (also known as linear loss), -see also :class:`~sklearn.metrics.mean_pinball_loss`, + This consists of the pinball loss (also known as linear loss), + see also :class:`~sklearn.metrics.mean_pinball_loss`, -.. math:: - PB_q(t) = q \max(t, 0) + (1 - q) \max(-t, 0) = - \begin{cases} - q t, & t > 0, \\ - 0, & t = 0, \\ - (q-1) t, & t < 0 - \end{cases} - -and the L1 penalty controlled by parameter ``alpha``, similar to -:class:`Lasso`. + .. math:: + PB_q(t) = q \max(t, 0) + (1 - q) \max(-t, 0) = + \begin{cases} + q t, & t > 0, \\ + 0, & t = 0, \\ + (q-1) t, & t < 0 + \end{cases} -As the pinball loss is only linear in the residuals, quantile regression is -much more robust to outliers than squared error based estimation of the mean. -Somewhat in between is the :class:`HuberRegressor`. + and the L1 penalty controlled by parameter ``alpha``, similar to + :class:`Lasso`. -|details-end| + As the pinball loss is only linear in the residuals, quantile regression is + much more robust to outliers than squared error based estimation of the mean. + Somewhat in between is the :class:`HuberRegressor`. -|details-start| -**References** -|details-split| +.. dropdown:: References -* Koenker, R., & Bassett Jr, G. (1978). `Regression quantiles. - `_ - Econometrica: journal of the Econometric Society, 33-50. + * Koenker, R., & Bassett Jr, G. (1978). `Regression quantiles. + `_ + Econometrica: journal of the Econometric Society, 33-50. -* Portnoy, S., & Koenker, R. (1997). :doi:`The Gaussian hare and the Laplacian - tortoise: computability of squared-error versus absolute-error estimators. - Statistical Science, 12, 279-300 <10.1214/ss/1030037960>`. + * Portnoy, S., & Koenker, R. (1997). :doi:`The Gaussian hare and the Laplacian + tortoise: computability of squared-error versus absolute-error estimators. + Statistical Science, 12, 279-300 <10.1214/ss/1030037960>`. -* Koenker, R. (2005). :doi:`Quantile Regression <10.1017/CBO9780511754098>`. - Cambridge University Press. - -|details-end| + * Koenker, R. (2005). :doi:`Quantile Regression <10.1017/CBO9780511754098>`. + Cambridge University Press. .. _polynomial_regression: @@ -1813,38 +1731,34 @@ on nonlinear functions of the data. This approach maintains the generally fast performance of linear methods, while allowing them to fit a much wider range of data. -|details-start| -**Mathematical details** -|details-split| - -For example, a simple linear regression can be extended by constructing -**polynomial features** from the coefficients. In the standard linear -regression case, you might have a model that looks like this for -two-dimensional data: +.. dropdown:: Mathematical details -.. math:: \hat{y}(w, x) = w_0 + w_1 x_1 + w_2 x_2 + For example, a simple linear regression can be extended by constructing + **polynomial features** from the coefficients. In the standard linear + regression case, you might have a model that looks like this for + two-dimensional data: -If we want to fit a paraboloid to the data instead of a plane, we can combine -the features in second-order polynomials, so that the model looks like this: + .. math:: \hat{y}(w, x) = w_0 + w_1 x_1 + w_2 x_2 -.. math:: \hat{y}(w, x) = w_0 + w_1 x_1 + w_2 x_2 + w_3 x_1 x_2 + w_4 x_1^2 + w_5 x_2^2 + If we want to fit a paraboloid to the data instead of a plane, we can combine + the features in second-order polynomials, so that the model looks like this: -The (sometimes surprising) observation is that this is *still a linear model*: -to see this, imagine creating a new set of features + .. math:: \hat{y}(w, x) = w_0 + w_1 x_1 + w_2 x_2 + w_3 x_1 x_2 + w_4 x_1^2 + w_5 x_2^2 -.. math:: z = [x_1, x_2, x_1 x_2, x_1^2, x_2^2] + The (sometimes surprising) observation is that this is *still a linear model*: + to see this, imagine creating a new set of features -With this re-labeling of the data, our problem can be written + .. math:: z = [x_1, x_2, x_1 x_2, x_1^2, x_2^2] -.. math:: \hat{y}(w, z) = w_0 + w_1 z_1 + w_2 z_2 + w_3 z_3 + w_4 z_4 + w_5 z_5 + With this re-labeling of the data, our problem can be written -We see that the resulting *polynomial regression* is in the same class of -linear models we considered above (i.e. the model is linear in :math:`w`) -and can be solved by the same techniques. By considering linear fits within -a higher-dimensional space built with these basis functions, the model has the -flexibility to fit a much broader range of data. + .. math:: \hat{y}(w, z) = w_0 + w_1 z_1 + w_2 z_2 + w_3 z_3 + w_4 z_4 + w_5 z_5 -|details-end| + We see that the resulting *polynomial regression* is in the same class of + linear models we considered above (i.e. the model is linear in :math:`w`) + and can be solved by the same techniques. By considering linear fits within + a higher-dimensional space built with these basis functions, the model has the + flexibility to fit a much broader range of data. Here is an example of applying this idea to one-dimensional data, using polynomial features of varying degrees: diff --git a/doc/modules/manifold.rst b/doc/modules/manifold.rst index 7cc6776e37daa..785fba3097edf 100644 --- a/doc/modules/manifold.rst +++ b/doc/modules/manifold.rst @@ -102,13 +102,13 @@ unsupervised: it learns the high-dimensional structure of the data from the data itself, without the use of predetermined classifications. -.. topic:: Examples: +.. rubric:: Examples - * See :ref:`sphx_glr_auto_examples_manifold_plot_lle_digits.py` for an example of - dimensionality reduction on handwritten digits. +* See :ref:`sphx_glr_auto_examples_manifold_plot_lle_digits.py` for an example of + dimensionality reduction on handwritten digits. - * See :ref:`sphx_glr_auto_examples_manifold_plot_compare_methods.py` for an example of - dimensionality reduction on a toy "S-curve" dataset. +* See :ref:`sphx_glr_auto_examples_manifold_plot_compare_methods.py` for an example of + dimensionality reduction on a toy "S-curve" dataset. The manifold learning implementations available in scikit-learn are summarized below @@ -130,47 +130,43 @@ distances between all points. Isomap can be performed with the object :align: center :scale: 50 -|details-start| -**Complexity** -|details-split| +.. dropdown:: Complexity -The Isomap algorithm comprises three stages: + The Isomap algorithm comprises three stages: -1. **Nearest neighbor search.** Isomap uses - :class:`~sklearn.neighbors.BallTree` for efficient neighbor search. - The cost is approximately :math:`O[D \log(k) N \log(N)]`, for :math:`k` - nearest neighbors of :math:`N` points in :math:`D` dimensions. + 1. **Nearest neighbor search.** Isomap uses + :class:`~sklearn.neighbors.BallTree` for efficient neighbor search. + The cost is approximately :math:`O[D \log(k) N \log(N)]`, for :math:`k` + nearest neighbors of :math:`N` points in :math:`D` dimensions. -2. **Shortest-path graph search.** The most efficient known algorithms - for this are *Dijkstra's Algorithm*, which is approximately - :math:`O[N^2(k + \log(N))]`, or the *Floyd-Warshall algorithm*, which - is :math:`O[N^3]`. The algorithm can be selected by the user with - the ``path_method`` keyword of ``Isomap``. If unspecified, the code - attempts to choose the best algorithm for the input data. + 2. **Shortest-path graph search.** The most efficient known algorithms + for this are *Dijkstra's Algorithm*, which is approximately + :math:`O[N^2(k + \log(N))]`, or the *Floyd-Warshall algorithm*, which + is :math:`O[N^3]`. The algorithm can be selected by the user with + the ``path_method`` keyword of ``Isomap``. If unspecified, the code + attempts to choose the best algorithm for the input data. -3. **Partial eigenvalue decomposition.** The embedding is encoded in the - eigenvectors corresponding to the :math:`d` largest eigenvalues of the - :math:`N \times N` isomap kernel. For a dense solver, the cost is - approximately :math:`O[d N^2]`. This cost can often be improved using - the ``ARPACK`` solver. The eigensolver can be specified by the user - with the ``eigen_solver`` keyword of ``Isomap``. If unspecified, the - code attempts to choose the best algorithm for the input data. + 3. **Partial eigenvalue decomposition.** The embedding is encoded in the + eigenvectors corresponding to the :math:`d` largest eigenvalues of the + :math:`N \times N` isomap kernel. For a dense solver, the cost is + approximately :math:`O[d N^2]`. This cost can often be improved using + the ``ARPACK`` solver. The eigensolver can be specified by the user + with the ``eigen_solver`` keyword of ``Isomap``. If unspecified, the + code attempts to choose the best algorithm for the input data. -The overall complexity of Isomap is -:math:`O[D \log(k) N \log(N)] + O[N^2(k + \log(N))] + O[d N^2]`. + The overall complexity of Isomap is + :math:`O[D \log(k) N \log(N)] + O[N^2(k + \log(N))] + O[d N^2]`. -* :math:`N` : number of training data points -* :math:`D` : input dimension -* :math:`k` : number of nearest neighbors -* :math:`d` : output dimension + * :math:`N` : number of training data points + * :math:`D` : input dimension + * :math:`k` : number of nearest neighbors + * :math:`d` : output dimension -|details-end| +.. rubric:: References -.. topic:: References: - - * `"A global geometric framework for nonlinear dimensionality reduction" - `_ - Tenenbaum, J.B.; De Silva, V.; & Langford, J.C. Science 290 (5500) +* `"A global geometric framework for nonlinear dimensionality reduction" + `_ + Tenenbaum, J.B.; De Silva, V.; & Langford, J.C. Science 290 (5500) .. _locally_linear_embedding: @@ -191,36 +187,32 @@ Locally linear embedding can be performed with function :align: center :scale: 50 -|details-start| -**Complexity** -|details-split| - -The standard LLE algorithm comprises three stages: +.. dropdown:: Complexity -1. **Nearest Neighbors Search**. See discussion under Isomap above. + The standard LLE algorithm comprises three stages: -2. **Weight Matrix Construction**. :math:`O[D N k^3]`. - The construction of the LLE weight matrix involves the solution of a - :math:`k \times k` linear equation for each of the :math:`N` local - neighborhoods + 1. **Nearest Neighbors Search**. See discussion under Isomap above. -3. **Partial Eigenvalue Decomposition**. See discussion under Isomap above. + 2. **Weight Matrix Construction**. :math:`O[D N k^3]`. + The construction of the LLE weight matrix involves the solution of a + :math:`k \times k` linear equation for each of the :math:`N` local + neighborhoods. -The overall complexity of standard LLE is -:math:`O[D \log(k) N \log(N)] + O[D N k^3] + O[d N^2]`. + 3. **Partial Eigenvalue Decomposition**. See discussion under Isomap above. -* :math:`N` : number of training data points -* :math:`D` : input dimension -* :math:`k` : number of nearest neighbors -* :math:`d` : output dimension + The overall complexity of standard LLE is + :math:`O[D \log(k) N \log(N)] + O[D N k^3] + O[d N^2]`. -|details-end| + * :math:`N` : number of training data points + * :math:`D` : input dimension + * :math:`k` : number of nearest neighbors + * :math:`d` : output dimension -.. topic:: References: +.. rubric:: References - * `"Nonlinear dimensionality reduction by locally linear embedding" - `_ - Roweis, S. & Saul, L. Science 290:2323 (2000) +* `"Nonlinear dimensionality reduction by locally linear embedding" + `_ + Roweis, S. & Saul, L. Science 290:2323 (2000) Modified Locally Linear Embedding @@ -248,38 +240,34 @@ It requires ``n_neighbors > n_components``. :align: center :scale: 50 -|details-start| -**Complexity** -|details-split| - -The MLLE algorithm comprises three stages: +.. dropdown:: Complexity -1. **Nearest Neighbors Search**. Same as standard LLE + The MLLE algorithm comprises three stages: -2. **Weight Matrix Construction**. Approximately - :math:`O[D N k^3] + O[N (k-D) k^2]`. The first term is exactly equivalent - to that of standard LLE. The second term has to do with constructing the - weight matrix from multiple weights. In practice, the added cost of - constructing the MLLE weight matrix is relatively small compared to the - cost of stages 1 and 3. + 1. **Nearest Neighbors Search**. Same as standard LLE -3. **Partial Eigenvalue Decomposition**. Same as standard LLE + 2. **Weight Matrix Construction**. Approximately + :math:`O[D N k^3] + O[N (k-D) k^2]`. The first term is exactly equivalent + to that of standard LLE. The second term has to do with constructing the + weight matrix from multiple weights. In practice, the added cost of + constructing the MLLE weight matrix is relatively small compared to the + cost of stages 1 and 3. -The overall complexity of MLLE is -:math:`O[D \log(k) N \log(N)] + O[D N k^3] + O[N (k-D) k^2] + O[d N^2]`. + 3. **Partial Eigenvalue Decomposition**. Same as standard LLE -* :math:`N` : number of training data points -* :math:`D` : input dimension -* :math:`k` : number of nearest neighbors -* :math:`d` : output dimension + The overall complexity of MLLE is + :math:`O[D \log(k) N \log(N)] + O[D N k^3] + O[N (k-D) k^2] + O[d N^2]`. -|details-end| + * :math:`N` : number of training data points + * :math:`D` : input dimension + * :math:`k` : number of nearest neighbors + * :math:`d` : output dimension -.. topic:: References: +.. rubric:: References - * `"MLLE: Modified Locally Linear Embedding Using Multiple Weights" - `_ - Zhang, Z. & Wang, J. +* `"MLLE: Modified Locally Linear Embedding Using Multiple Weights" + `_ + Zhang, Z. & Wang, J. Hessian Eigenmapping @@ -301,36 +289,32 @@ It requires ``n_neighbors > n_components * (n_components + 3) / 2``. :align: center :scale: 50 -|details-start| -**Complexity** -|details-split| +.. dropdown:: Complexity The HLLE algorithm comprises three stages: -1. **Nearest Neighbors Search**. Same as standard LLE + 1. **Nearest Neighbors Search**. Same as standard LLE -2. **Weight Matrix Construction**. Approximately - :math:`O[D N k^3] + O[N d^6]`. The first term reflects a similar - cost to that of standard LLE. The second term comes from a QR - decomposition of the local hessian estimator. + 2. **Weight Matrix Construction**. Approximately + :math:`O[D N k^3] + O[N d^6]`. The first term reflects a similar + cost to that of standard LLE. The second term comes from a QR + decomposition of the local hessian estimator. -3. **Partial Eigenvalue Decomposition**. Same as standard LLE + 3. **Partial Eigenvalue Decomposition**. Same as standard LLE -The overall complexity of standard HLLE is -:math:`O[D \log(k) N \log(N)] + O[D N k^3] + O[N d^6] + O[d N^2]`. + The overall complexity of standard HLLE is + :math:`O[D \log(k) N \log(N)] + O[D N k^3] + O[N d^6] + O[d N^2]`. -* :math:`N` : number of training data points -* :math:`D` : input dimension -* :math:`k` : number of nearest neighbors -* :math:`d` : output dimension + * :math:`N` : number of training data points + * :math:`D` : input dimension + * :math:`k` : number of nearest neighbors + * :math:`d` : output dimension -|details-end| +.. rubric:: References -.. topic:: References: - - * `"Hessian Eigenmaps: Locally linear embedding techniques for - high-dimensional data" `_ - Donoho, D. & Grimes, C. Proc Natl Acad Sci USA. 100:5591 (2003) +* `"Hessian Eigenmaps: Locally linear embedding techniques for + high-dimensional data" `_ + Donoho, D. & Grimes, C. Proc Natl Acad Sci USA. 100:5591 (2003) .. _spectral_embedding: @@ -348,38 +332,34 @@ preserving local distances. Spectral embedding can be performed with the function :func:`spectral_embedding` or its object-oriented counterpart :class:`SpectralEmbedding`. -|details-start| -**Complexity** -|details-split| - -The Spectral Embedding (Laplacian Eigenmaps) algorithm comprises three stages: +.. dropdown:: Complexity -1. **Weighted Graph Construction**. Transform the raw input data into - graph representation using affinity (adjacency) matrix representation. + The Spectral Embedding (Laplacian Eigenmaps) algorithm comprises three stages: -2. **Graph Laplacian Construction**. unnormalized Graph Laplacian - is constructed as :math:`L = D - A` for and normalized one as - :math:`L = D^{-\frac{1}{2}} (D - A) D^{-\frac{1}{2}}`. + 1. **Weighted Graph Construction**. Transform the raw input data into + graph representation using affinity (adjacency) matrix representation. -3. **Partial Eigenvalue Decomposition**. Eigenvalue decomposition is - done on graph Laplacian + 2. **Graph Laplacian Construction**. unnormalized Graph Laplacian + is constructed as :math:`L = D - A` for and normalized one as + :math:`L = D^{-\frac{1}{2}} (D - A) D^{-\frac{1}{2}}`. -The overall complexity of spectral embedding is -:math:`O[D \log(k) N \log(N)] + O[D N k^3] + O[d N^2]`. + 3. **Partial Eigenvalue Decomposition**. Eigenvalue decomposition is + done on graph Laplacian. -* :math:`N` : number of training data points -* :math:`D` : input dimension -* :math:`k` : number of nearest neighbors -* :math:`d` : output dimension + The overall complexity of spectral embedding is + :math:`O[D \log(k) N \log(N)] + O[D N k^3] + O[d N^2]`. -|details-end| + * :math:`N` : number of training data points + * :math:`D` : input dimension + * :math:`k` : number of nearest neighbors + * :math:`d` : output dimension -.. topic:: References: +.. rubric:: References - * `"Laplacian Eigenmaps for Dimensionality Reduction - and Data Representation" - `_ - M. Belkin, P. Niyogi, Neural Computation, June 2003; 15 (6):1373-1396 +* `"Laplacian Eigenmaps for Dimensionality Reduction + and Data Representation" + `_ + M. Belkin, P. Niyogi, Neural Computation, June 2003; 15 (6):1373-1396 Local Tangent Space Alignment @@ -399,36 +379,32 @@ tangent spaces to learn the embedding. LTSA can be performed with function :align: center :scale: 50 -|details-start| -**Complexity** -|details-split| +.. dropdown:: Complexity -The LTSA algorithm comprises three stages: + The LTSA algorithm comprises three stages: -1. **Nearest Neighbors Search**. Same as standard LLE + 1. **Nearest Neighbors Search**. Same as standard LLE -2. **Weight Matrix Construction**. Approximately - :math:`O[D N k^3] + O[k^2 d]`. The first term reflects a similar - cost to that of standard LLE. + 2. **Weight Matrix Construction**. Approximately + :math:`O[D N k^3] + O[k^2 d]`. The first term reflects a similar + cost to that of standard LLE. -3. **Partial Eigenvalue Decomposition**. Same as standard LLE + 3. **Partial Eigenvalue Decomposition**. Same as standard LLE -The overall complexity of standard LTSA is -:math:`O[D \log(k) N \log(N)] + O[D N k^3] + O[k^2 d] + O[d N^2]`. + The overall complexity of standard LTSA is + :math:`O[D \log(k) N \log(N)] + O[D N k^3] + O[k^2 d] + O[d N^2]`. -* :math:`N` : number of training data points -* :math:`D` : input dimension -* :math:`k` : number of nearest neighbors -* :math:`d` : output dimension + * :math:`N` : number of training data points + * :math:`D` : input dimension + * :math:`k` : number of nearest neighbors + * :math:`d` : output dimension -|details-end| +.. rubric:: References -.. topic:: References: - - * :arxiv:`"Principal manifolds and nonlinear dimensionality reduction via - tangent space alignment" - ` - Zhang, Z. & Zha, H. Journal of Shanghai Univ. 8:406 (2004) +* :arxiv:`"Principal manifolds and nonlinear dimensionality reduction via + tangent space alignment" + ` + Zhang, Z. & Zha, H. Journal of Shanghai Univ. 8:406 (2004) .. _multidimensional_scaling: @@ -467,67 +443,59 @@ the similarities chosen in some optimal ways. The objective, called the stress, is then defined by :math:`\sum_{i < j} d_{ij}(X) - \hat{d}_{ij}(X)` -|details-start| -**Metric MDS** -|details-split| - -The simplest metric :class:`MDS` model, called *absolute MDS*, disparities are defined by -:math:`\hat{d}_{ij} = S_{ij}`. With absolute MDS, the value :math:`S_{ij}` -should then correspond exactly to the distance between point :math:`i` and -:math:`j` in the embedding point. +.. dropdown:: Metric MDS -Most commonly, disparities are set to :math:`\hat{d}_{ij} = b S_{ij}`. + The simplest metric :class:`MDS` model, called *absolute MDS*, disparities are defined by + :math:`\hat{d}_{ij} = S_{ij}`. With absolute MDS, the value :math:`S_{ij}` + should then correspond exactly to the distance between point :math:`i` and + :math:`j` in the embedding point. -|details-end| + Most commonly, disparities are set to :math:`\hat{d}_{ij} = b S_{ij}`. -|details-start| -**Nonmetric MDS** -|details-split| +.. dropdown:: Nonmetric MDS -Non metric :class:`MDS` focuses on the ordination of the data. If -:math:`S_{ij} > S_{jk}`, then the embedding should enforce :math:`d_{ij} < -d_{jk}`. For this reason, we discuss it in terms of dissimilarities -(:math:`\delta_{ij}`) instead of similarities (:math:`S_{ij}`). Note that -dissimilarities can easily be obtained from similarities through a simple -transform, e.g. :math:`\delta_{ij}=c_1-c_2 S_{ij}` for some real constants -:math:`c_1, c_2`. A simple algorithm to enforce proper ordination is to use a -monotonic regression of :math:`d_{ij}` on :math:`\delta_{ij}`, yielding -disparities :math:`\hat{d}_{ij}` in the same order as :math:`\delta_{ij}`. + Non metric :class:`MDS` focuses on the ordination of the data. If + :math:`S_{ij} > S_{jk}`, then the embedding should enforce :math:`d_{ij} < + d_{jk}`. For this reason, we discuss it in terms of dissimilarities + (:math:`\delta_{ij}`) instead of similarities (:math:`S_{ij}`). Note that + dissimilarities can easily be obtained from similarities through a simple + transform, e.g. :math:`\delta_{ij}=c_1-c_2 S_{ij}` for some real constants + :math:`c_1, c_2`. A simple algorithm to enforce proper ordination is to use a + monotonic regression of :math:`d_{ij}` on :math:`\delta_{ij}`, yielding + disparities :math:`\hat{d}_{ij}` in the same order as :math:`\delta_{ij}`. -A trivial solution to this problem is to set all the points on the origin. In -order to avoid that, the disparities :math:`\hat{d}_{ij}` are normalized. Note -that since we only care about relative ordering, our objective should be -invariant to simple translation and scaling, however the stress used in metric -MDS is sensitive to scaling. To address this, non-metric MDS may use a -normalized stress, known as Stress-1 defined as + A trivial solution to this problem is to set all the points on the origin. In + order to avoid that, the disparities :math:`\hat{d}_{ij}` are normalized. Note + that since we only care about relative ordering, our objective should be + invariant to simple translation and scaling, however the stress used in metric + MDS is sensitive to scaling. To address this, non-metric MDS may use a + normalized stress, known as Stress-1 defined as -.. math:: - \sqrt{\frac{\sum_{i < j} (d_{ij} - \hat{d}_{ij})^2}{\sum_{i < j} d_{ij}^2}}. + .. math:: + \sqrt{\frac{\sum_{i < j} (d_{ij} - \hat{d}_{ij})^2}{\sum_{i < j} d_{ij}^2}}. -The use of normalized Stress-1 can be enabled by setting `normalized_stress=True`, -however it is only compatible with the non-metric MDS problem and will be ignored -in the metric case. - -.. figure:: ../auto_examples/manifold/images/sphx_glr_plot_mds_001.png - :target: ../auto_examples/manifold/plot_mds.html - :align: center - :scale: 60 + The use of normalized Stress-1 can be enabled by setting `normalized_stress=True`, + however it is only compatible with the non-metric MDS problem and will be ignored + in the metric case. -|details-end| + .. figure:: ../auto_examples/manifold/images/sphx_glr_plot_mds_001.png + :target: ../auto_examples/manifold/plot_mds.html + :align: center + :scale: 60 -.. topic:: References: +.. rubric:: References - * `"Modern Multidimensional Scaling - Theory and Applications" - `_ - Borg, I.; Groenen P. Springer Series in Statistics (1997) +* `"Modern Multidimensional Scaling - Theory and Applications" + `_ + Borg, I.; Groenen P. Springer Series in Statistics (1997) - * `"Nonmetric multidimensional scaling: a numerical method" - `_ - Kruskal, J. Psychometrika, 29 (1964) +* `"Nonmetric multidimensional scaling: a numerical method" + `_ + Kruskal, J. Psychometrika, 29 (1964) - * `"Multidimensional scaling by optimizing goodness of fit to a nonmetric hypothesis" - `_ - Kruskal, J. Psychometrika, 29, (1964) +* `"Multidimensional scaling by optimizing goodness of fit to a nonmetric hypothesis" + `_ + Kruskal, J. Psychometrika, 29, (1964) .. _t_sne: @@ -575,120 +543,110 @@ The disadvantages to using t-SNE are roughly: :align: center :scale: 50 -|details-start| -**Optimizing t-SNE** -|details-split| - -The main purpose of t-SNE is visualization of high-dimensional data. Hence, -it works best when the data will be embedded on two or three dimensions. - -Optimizing the KL divergence can be a little bit tricky sometimes. There are -five parameters that control the optimization of t-SNE and therefore possibly -the quality of the resulting embedding: - -* perplexity -* early exaggeration factor -* learning rate -* maximum number of iterations -* angle (not used in the exact method) - -The perplexity is defined as :math:`k=2^{(S)}` where :math:`S` is the Shannon -entropy of the conditional probability distribution. The perplexity of a -:math:`k`-sided die is :math:`k`, so that :math:`k` is effectively the number of -nearest neighbors t-SNE considers when generating the conditional probabilities. -Larger perplexities lead to more nearest neighbors and less sensitive to small -structure. Conversely a lower perplexity considers a smaller number of -neighbors, and thus ignores more global information in favour of the -local neighborhood. As dataset sizes get larger more points will be -required to get a reasonable sample of the local neighborhood, and hence -larger perplexities may be required. Similarly noisier datasets will require -larger perplexity values to encompass enough local neighbors to see beyond -the background noise. - -The maximum number of iterations is usually high enough and does not need -any tuning. The optimization consists of two phases: the early exaggeration -phase and the final optimization. During early exaggeration the joint -probabilities in the original space will be artificially increased by -multiplication with a given factor. Larger factors result in larger gaps -between natural clusters in the data. If the factor is too high, the KL -divergence could increase during this phase. Usually it does not have to be -tuned. A critical parameter is the learning rate. If it is too low gradient -descent will get stuck in a bad local minimum. If it is too high the KL -divergence will increase during optimization. A heuristic suggested in -Belkina et al. (2019) is to set the learning rate to the sample size -divided by the early exaggeration factor. We implement this heuristic -as `learning_rate='auto'` argument. More tips can be found in -Laurens van der Maaten's FAQ (see references). The last parameter, angle, -is a tradeoff between performance and accuracy. Larger angles imply that we -can approximate larger regions by a single point, leading to better speed -but less accurate results. - -`"How to Use t-SNE Effectively" `_ -provides a good discussion of the effects of the various parameters, as well -as interactive plots to explore the effects of different parameters. - -|details-end| - -|details-start| -**Barnes-Hut t-SNE** -|details-split| - -The Barnes-Hut t-SNE that has been implemented here is usually much slower than -other manifold learning algorithms. The optimization is quite difficult -and the computation of the gradient is :math:`O[d N log(N)]`, where :math:`d` -is the number of output dimensions and :math:`N` is the number of samples. The -Barnes-Hut method improves on the exact method where t-SNE complexity is -:math:`O[d N^2]`, but has several other notable differences: - -* The Barnes-Hut implementation only works when the target dimensionality is 3 - or less. The 2D case is typical when building visualizations. -* Barnes-Hut only works with dense input data. Sparse data matrices can only be - embedded with the exact method or can be approximated by a dense low rank - projection for instance using :class:`~sklearn.decomposition.PCA` -* Barnes-Hut is an approximation of the exact method. The approximation is - parameterized with the angle parameter, therefore the angle parameter is - unused when method="exact" -* Barnes-Hut is significantly more scalable. Barnes-Hut can be used to embed - hundred of thousands of data points while the exact method can handle - thousands of samples before becoming computationally intractable - -For visualization purpose (which is the main use case of t-SNE), using the -Barnes-Hut method is strongly recommended. The exact t-SNE method is useful -for checking the theoretically properties of the embedding possibly in higher -dimensional space but limit to small datasets due to computational constraints. - -Also note that the digits labels roughly match the natural grouping found by -t-SNE while the linear 2D projection of the PCA model yields a representation -where label regions largely overlap. This is a strong clue that this data can -be well separated by non linear methods that focus on the local structure (e.g. -an SVM with a Gaussian RBF kernel). However, failing to visualize well -separated homogeneously labeled groups with t-SNE in 2D does not necessarily -imply that the data cannot be correctly classified by a supervised model. It -might be the case that 2 dimensions are not high enough to accurately represent -the internal structure of the data. - -|details-end| - -.. topic:: References: - - * `"Visualizing High-Dimensional Data Using t-SNE" - `_ - van der Maaten, L.J.P.; Hinton, G. Journal of Machine Learning Research - (2008) - - * `"t-Distributed Stochastic Neighbor Embedding" - `_ - van der Maaten, L.J.P. - - * `"Accelerating t-SNE using Tree-Based Algorithms" - `_ - van der Maaten, L.J.P.; Journal of Machine Learning Research 15(Oct):3221-3245, 2014. - - * `"Automated optimized parameters for T-distributed stochastic neighbor - embedding improve visualization and analysis of large datasets" - `_ - Belkina, A.C., Ciccolella, C.O., Anno, R., Halpert, R., Spidlen, J., - Snyder-Cappione, J.E., Nature Communications 10, 5415 (2019). +.. dropdown:: Optimizing t-SNE + + The main purpose of t-SNE is visualization of high-dimensional data. Hence, + it works best when the data will be embedded on two or three dimensions. + + Optimizing the KL divergence can be a little bit tricky sometimes. There are + five parameters that control the optimization of t-SNE and therefore possibly + the quality of the resulting embedding: + + * perplexity + * early exaggeration factor + * learning rate + * maximum number of iterations + * angle (not used in the exact method) + + The perplexity is defined as :math:`k=2^{(S)}` where :math:`S` is the Shannon + entropy of the conditional probability distribution. The perplexity of a + :math:`k`-sided die is :math:`k`, so that :math:`k` is effectively the number of + nearest neighbors t-SNE considers when generating the conditional probabilities. + Larger perplexities lead to more nearest neighbors and less sensitive to small + structure. Conversely a lower perplexity considers a smaller number of + neighbors, and thus ignores more global information in favour of the + local neighborhood. As dataset sizes get larger more points will be + required to get a reasonable sample of the local neighborhood, and hence + larger perplexities may be required. Similarly noisier datasets will require + larger perplexity values to encompass enough local neighbors to see beyond + the background noise. + + The maximum number of iterations is usually high enough and does not need + any tuning. The optimization consists of two phases: the early exaggeration + phase and the final optimization. During early exaggeration the joint + probabilities in the original space will be artificially increased by + multiplication with a given factor. Larger factors result in larger gaps + between natural clusters in the data. If the factor is too high, the KL + divergence could increase during this phase. Usually it does not have to be + tuned. A critical parameter is the learning rate. If it is too low gradient + descent will get stuck in a bad local minimum. If it is too high the KL + divergence will increase during optimization. A heuristic suggested in + Belkina et al. (2019) is to set the learning rate to the sample size + divided by the early exaggeration factor. We implement this heuristic + as `learning_rate='auto'` argument. More tips can be found in + Laurens van der Maaten's FAQ (see references). The last parameter, angle, + is a tradeoff between performance and accuracy. Larger angles imply that we + can approximate larger regions by a single point, leading to better speed + but less accurate results. + + `"How to Use t-SNE Effectively" `_ + provides a good discussion of the effects of the various parameters, as well + as interactive plots to explore the effects of different parameters. + +.. dropdown:: Barnes-Hut t-SNE + + The Barnes-Hut t-SNE that has been implemented here is usually much slower than + other manifold learning algorithms. The optimization is quite difficult + and the computation of the gradient is :math:`O[d N log(N)]`, where :math:`d` + is the number of output dimensions and :math:`N` is the number of samples. The + Barnes-Hut method improves on the exact method where t-SNE complexity is + :math:`O[d N^2]`, but has several other notable differences: + + * The Barnes-Hut implementation only works when the target dimensionality is 3 + or less. The 2D case is typical when building visualizations. + * Barnes-Hut only works with dense input data. Sparse data matrices can only be + embedded with the exact method or can be approximated by a dense low rank + projection for instance using :class:`~sklearn.decomposition.PCA` + * Barnes-Hut is an approximation of the exact method. The approximation is + parameterized with the angle parameter, therefore the angle parameter is + unused when method="exact" + * Barnes-Hut is significantly more scalable. Barnes-Hut can be used to embed + hundred of thousands of data points while the exact method can handle + thousands of samples before becoming computationally intractable + + For visualization purpose (which is the main use case of t-SNE), using the + Barnes-Hut method is strongly recommended. The exact t-SNE method is useful + for checking the theoretically properties of the embedding possibly in higher + dimensional space but limit to small datasets due to computational constraints. + + Also note that the digits labels roughly match the natural grouping found by + t-SNE while the linear 2D projection of the PCA model yields a representation + where label regions largely overlap. This is a strong clue that this data can + be well separated by non linear methods that focus on the local structure (e.g. + an SVM with a Gaussian RBF kernel). However, failing to visualize well + separated homogeneously labeled groups with t-SNE in 2D does not necessarily + imply that the data cannot be correctly classified by a supervised model. It + might be the case that 2 dimensions are not high enough to accurately represent + the internal structure of the data. + +.. rubric:: References + +* `"Visualizing High-Dimensional Data Using t-SNE" + `_ + van der Maaten, L.J.P.; Hinton, G. Journal of Machine Learning Research (2008) + +* `"t-Distributed Stochastic Neighbor Embedding" + `_ van der Maaten, L.J.P. + +* `"Accelerating t-SNE using Tree-Based Algorithms" + `_ + van der Maaten, L.J.P.; Journal of Machine Learning Research 15(Oct):3221-3245, 2014. + +* `"Automated optimized parameters for T-distributed stochastic neighbor + embedding improve visualization and analysis of large datasets" + `_ + Belkina, A.C., Ciccolella, C.O., Anno, R., Halpert, R., Spidlen, J., + Snyder-Cappione, J.E., Nature Communications 10, 5415 (2019). Tips on practical use ===================== diff --git a/doc/modules/metrics.rst b/doc/modules/metrics.rst index caea39319e869..63ea797223c22 100644 --- a/doc/modules/metrics.rst +++ b/doc/modules/metrics.rst @@ -87,11 +87,11 @@ represented as tf-idf vectors. can produce normalized vectors, in which case :func:`cosine_similarity` is equivalent to :func:`linear_kernel`, only slower.) -.. topic:: References: +.. rubric:: References - * C.D. Manning, P. Raghavan and H. Schütze (2008). Introduction to - Information Retrieval. Cambridge University Press. - https://nlp.stanford.edu/IR-book/html/htmledition/the-vector-space-model-for-scoring-1.html +* C.D. Manning, P. Raghavan and H. Schütze (2008). Introduction to + Information Retrieval. Cambridge University Press. + https://nlp.stanford.edu/IR-book/html/htmledition/the-vector-space-model-for-scoring-1.html .. _linear_kernel: @@ -222,10 +222,10 @@ which is a distance between discrete probability distributions. The chi squared kernel is most commonly used on histograms (bags) of visual words. -.. topic:: References: +.. rubric:: References - * 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 +* 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 diff --git a/doc/modules/mixture.rst b/doc/modules/mixture.rst index df5d8020a1369..1fd72c3158336 100644 --- a/doc/modules/mixture.rst +++ b/doc/modules/mixture.rst @@ -60,128 +60,111 @@ full covariance. :align: center :scale: 75% -.. topic:: Examples: +.. rubric:: Examples - * See :ref:`sphx_glr_auto_examples_mixture_plot_gmm_covariances.py` for an example of - using the Gaussian mixture as clustering on the iris dataset. +* See :ref:`sphx_glr_auto_examples_mixture_plot_gmm_covariances.py` for an example of + using the Gaussian mixture as clustering on the iris dataset. - * See :ref:`sphx_glr_auto_examples_mixture_plot_gmm_pdf.py` for an example on plotting the - density estimation. +* See :ref:`sphx_glr_auto_examples_mixture_plot_gmm_pdf.py` for an example on plotting the + density estimation. -|details-start| -**Pros and cons of class GaussianMixture** -|details-split| +.. dropdown:: Pros and cons of class GaussianMixture -.. topic:: Pros: + .. rubric:: Pros - :Speed: It is the fastest algorithm for learning mixture models + :Speed: It is the fastest algorithm for learning mixture models - :Agnostic: As this algorithm maximizes only the likelihood, it - will not bias the means towards zero, or bias the cluster sizes to - have specific structures that might or might not apply. + :Agnostic: As this algorithm maximizes only the likelihood, it + will not bias the means towards zero, or bias the cluster sizes to + have specific structures that might or might not apply. -.. topic:: Cons: + .. rubric:: Cons - :Singularities: When one has insufficiently many points per - mixture, estimating the covariance matrices becomes difficult, - and the algorithm is known to diverge and find solutions with - infinite likelihood unless one regularizes the covariances artificially. + :Singularities: When one has insufficiently many points per + mixture, estimating the covariance matrices becomes difficult, + and the algorithm is known to diverge and find solutions with + infinite likelihood unless one regularizes the covariances artificially. - :Number of components: This algorithm will always use all the - components it has access to, needing held-out data - or information theoretical criteria to decide how many components to use - in the absence of external cues. + :Number of components: This algorithm will always use all the + components it has access to, needing held-out data + or information theoretical criteria to decide how many components to use + in the absence of external cues. -|details-end| +.. dropdown:: Selecting the number of components in a classical Gaussian Mixture model + The BIC criterion can be used to select the number of components in a Gaussian + Mixture in an efficient way. In theory, it recovers the true number of + components only in the asymptotic regime (i.e. if much data is available and + assuming that the data was actually generated i.i.d. from a mixture of Gaussian + distribution). Note that using a :ref:`Variational Bayesian Gaussian mixture ` + avoids the specification of the number of components for a Gaussian mixture + model. -|details-start| -**Selecting the number of components in a classical Gaussian Mixture model** -|details-split| + .. figure:: ../auto_examples/mixture/images/sphx_glr_plot_gmm_selection_002.png + :target: ../auto_examples/mixture/plot_gmm_selection.html + :align: center + :scale: 50% -The BIC criterion can be used to select the number of components in a Gaussian -Mixture in an efficient way. In theory, it recovers the true number of -components only in the asymptotic regime (i.e. if much data is available and -assuming that the data was actually generated i.i.d. from a mixture of Gaussian -distribution). Note that using a :ref:`Variational Bayesian Gaussian mixture ` -avoids the specification of the number of components for a Gaussian mixture -model. + .. rubric:: Examples -.. figure:: ../auto_examples/mixture/images/sphx_glr_plot_gmm_selection_002.png - :target: ../auto_examples/mixture/plot_gmm_selection.html - :align: center - :scale: 50% - -.. topic:: Examples: - - * See :ref:`sphx_glr_auto_examples_mixture_plot_gmm_selection.py` for an example - of model selection performed with classical Gaussian mixture. - -|details-end| + * See :ref:`sphx_glr_auto_examples_mixture_plot_gmm_selection.py` for an example + of model selection performed with classical Gaussian mixture. .. _expectation_maximization: -|details-start| -**Estimation algorithm expectation-maximization** -|details-split| - -The main difficulty in learning Gaussian mixture models from unlabeled -data is that one usually doesn't know which points came from -which latent component (if one has access to this information it gets -very easy to fit a separate Gaussian distribution to each set of -points). `Expectation-maximization -`_ -is a well-founded statistical -algorithm to get around this problem by an iterative process. First -one assumes random components (randomly centered on data points, -learned from k-means, or even just normally distributed around the -origin) and computes for each point a probability of being generated by -each component of the model. Then, one tweaks the -parameters to maximize the likelihood of the data given those -assignments. Repeating this process is guaranteed to always converge -to a local optimum. - -|details-end| - -|details-start| -**Choice of the Initialization method** -|details-split| - -There is a choice of four initialization methods (as well as inputting user defined -initial means) to generate the initial centers for the model components: - -k-means (default) - This applies a traditional k-means clustering algorithm. - This can be computationally expensive compared to other initialization methods. - -k-means++ - This uses the initialization method of k-means clustering: k-means++. - This will pick the first center at random from the data. Subsequent centers will be - chosen from a weighted distribution of the data favouring points further away from - existing centers. k-means++ is the default initialization for k-means so will be - quicker than running a full k-means but can still take a significant amount of - time for large data sets with many components. - -random_from_data - This will pick random data points from the input data as the initial - centers. This is a very fast method of initialization but can produce non-convergent - results if the chosen points are too close to each other. - -random - Centers are chosen as a small perturbation away from the mean of all data. - This method is simple but can lead to the model taking longer to converge. - -.. figure:: ../auto_examples/mixture/images/sphx_glr_plot_gmm_init_001.png - :target: ../auto_examples/mixture/plot_gmm_init.html - :align: center - :scale: 50% - -.. topic:: Examples: - - * See :ref:`sphx_glr_auto_examples_mixture_plot_gmm_init.py` for an example of - using different initializations in Gaussian Mixture. - -|details-end| +.. dropdown:: Estimation algorithm expectation-maximization + + The main difficulty in learning Gaussian mixture models from unlabeled + data is that one usually doesn't know which points came from + which latent component (if one has access to this information it gets + very easy to fit a separate Gaussian distribution to each set of + points). `Expectation-maximization + `_ + is a well-founded statistical + algorithm to get around this problem by an iterative process. First + one assumes random components (randomly centered on data points, + learned from k-means, or even just normally distributed around the + origin) and computes for each point a probability of being generated by + each component of the model. Then, one tweaks the + parameters to maximize the likelihood of the data given those + assignments. Repeating this process is guaranteed to always converge + to a local optimum. + +.. dropdown:: Choice of the Initialization method + + There is a choice of four initialization methods (as well as inputting user defined + initial means) to generate the initial centers for the model components: + + k-means (default) + This applies a traditional k-means clustering algorithm. + This can be computationally expensive compared to other initialization methods. + + k-means++ + This uses the initialization method of k-means clustering: k-means++. + This will pick the first center at random from the data. Subsequent centers will be + chosen from a weighted distribution of the data favouring points further away from + existing centers. k-means++ is the default initialization for k-means so will be + quicker than running a full k-means but can still take a significant amount of + time for large data sets with many components. + + random_from_data + This will pick random data points from the input data as the initial + centers. This is a very fast method of initialization but can produce non-convergent + results if the chosen points are too close to each other. + + random + Centers are chosen as a small perturbation away from the mean of all data. + This method is simple but can lead to the model taking longer to converge. + + .. figure:: ../auto_examples/mixture/images/sphx_glr_plot_gmm_init_001.png + :target: ../auto_examples/mixture/plot_gmm_init.html + :align: center + :scale: 50% + + .. rubric:: Examples + + * See :ref:`sphx_glr_auto_examples_mixture_plot_gmm_init.py` for an example of + using different initializations in Gaussian Mixture. .. _bgmm: @@ -276,63 +259,58 @@ from the two resulting mixtures. -.. topic:: Examples: - - * See :ref:`sphx_glr_auto_examples_mixture_plot_gmm.py` for an example on - plotting the confidence ellipsoids for both :class:`GaussianMixture` - and :class:`BayesianGaussianMixture`. - - * :ref:`sphx_glr_auto_examples_mixture_plot_gmm_sin.py` shows using - :class:`GaussianMixture` and :class:`BayesianGaussianMixture` to fit a - sine wave. +.. rubric:: Examples - * See :ref:`sphx_glr_auto_examples_mixture_plot_concentration_prior.py` - for an example plotting the confidence ellipsoids for the - :class:`BayesianGaussianMixture` with different - ``weight_concentration_prior_type`` for different values of the parameter - ``weight_concentration_prior``. +* See :ref:`sphx_glr_auto_examples_mixture_plot_gmm.py` for an example on + plotting the confidence ellipsoids for both :class:`GaussianMixture` + and :class:`BayesianGaussianMixture`. -|details-start| -**Pros and cons of variational inference with BayesianGaussianMixture** -|details-split| +* :ref:`sphx_glr_auto_examples_mixture_plot_gmm_sin.py` shows using + :class:`GaussianMixture` and :class:`BayesianGaussianMixture` to fit a + sine wave. -.. topic:: Pros: +* See :ref:`sphx_glr_auto_examples_mixture_plot_concentration_prior.py` + for an example plotting the confidence ellipsoids for the + :class:`BayesianGaussianMixture` with different + ``weight_concentration_prior_type`` for different values of the parameter + ``weight_concentration_prior``. - :Automatic selection: when ``weight_concentration_prior`` is small enough and - ``n_components`` is larger than what is found necessary by the model, the - Variational Bayesian mixture model has a natural tendency to set some mixture - weights values close to zero. This makes it possible to let the model choose - a suitable number of effective components automatically. Only an upper bound - of this number needs to be provided. Note however that the "ideal" number of - active components is very application specific and is typically ill-defined - in a data exploration setting. +.. dropdown:: Pros and cons of variational inference with BayesianGaussianMixture - :Less sensitivity to the number of parameters: unlike finite models, which will - almost always use all components as much as they can, and hence will produce - wildly different solutions for different numbers of components, the - variational inference with a Dirichlet process prior - (``weight_concentration_prior_type='dirichlet_process'``) won't change much - with changes to the parameters, leading to more stability and less tuning. + .. rubric:: Pros - :Regularization: due to the incorporation of prior information, - variational solutions have less pathological special cases than - expectation-maximization solutions. + :Automatic selection: when ``weight_concentration_prior`` is small enough and + ``n_components`` is larger than what is found necessary by the model, the + Variational Bayesian mixture model has a natural tendency to set some mixture + weights values close to zero. This makes it possible to let the model choose + a suitable number of effective components automatically. Only an upper bound + of this number needs to be provided. Note however that the "ideal" number of + active components is very application specific and is typically ill-defined + in a data exploration setting. + :Less sensitivity to the number of parameters: unlike finite models, which will + almost always use all components as much as they can, and hence will produce + wildly different solutions for different numbers of components, the + variational inference with a Dirichlet process prior + (``weight_concentration_prior_type='dirichlet_process'``) won't change much + with changes to the parameters, leading to more stability and less tuning. -.. topic:: Cons: + :Regularization: due to the incorporation of prior information, + variational solutions have less pathological special cases than + expectation-maximization solutions. - :Speed: the extra parametrization necessary for variational inference makes - inference slower, although not by much. + .. rubric:: Cons - :Hyperparameters: this algorithm needs an extra hyperparameter - that might need experimental tuning via cross-validation. + :Speed: the extra parametrization necessary for variational inference makes + inference slower, although not by much. - :Bias: there are many implicit biases in the inference algorithms (and also in - the Dirichlet process if used), and whenever there is a mismatch between - these biases and the data it might be possible to fit better models using a - finite mixture. + :Hyperparameters: this algorithm needs an extra hyperparameter + that might need experimental tuning via cross-validation. -|details-end| + :Bias: there are many implicit biases in the inference algorithms (and also in + the Dirichlet process if used), and whenever there is a mismatch between + these biases and the data it might be possible to fit better models using a + finite mixture. .. _dirichlet_process: diff --git a/doc/modules/model_evaluation.rst b/doc/modules/model_evaluation.rst index 056bf9a56d42c..81615b4419bba 100644 --- a/doc/modules/model_evaluation.rst +++ b/doc/modules/model_evaluation.rst @@ -172,58 +172,53 @@ measuring a prediction error given ground truth and prediction: parameter description below). -|details-start| -**Custom scorer objects** -|details-split| - - -The second use case is to build a completely custom scorer object -from a simple python function using :func:`make_scorer`, which can -take several parameters: - -* the python function you want to use (``my_custom_loss_func`` - in the example below) - -* whether the python function returns a score (``greater_is_better=True``, - the default) or a loss (``greater_is_better=False``). If a loss, the output - of the python function is negated by the scorer object, conforming to - the cross validation convention that scorers return higher values for better models. - -* for classification metrics only: whether the python function you provided requires - continuous decision certainties. If the scoring function only accepts probability - estimates (e.g. :func:`metrics.log_loss`) then one needs to set the parameter - `response_method`, thus in this case `response_method="predict_proba"`. Some scoring - function do not necessarily require probability estimates but rather non-thresholded - decision values (e.g. :func:`metrics.roc_auc_score`). In this case, one provides a - list such as `response_method=["decision_function", "predict_proba"]`. In this case, - the scorer will use the first available method, in the order given in the list, - to compute the scores. - -* any additional parameters, such as ``beta`` or ``labels`` in :func:`f1_score`. - -Here is an example of building custom scorers, and of using the -``greater_is_better`` parameter:: - - >>> import numpy as np - >>> def my_custom_loss_func(y_true, y_pred): - ... diff = np.abs(y_true - y_pred).max() - ... return np.log1p(diff) - ... - >>> # score will negate the return value of my_custom_loss_func, - >>> # which will be np.log(2), 0.693, given the values for X - >>> # and y defined below. - >>> score = make_scorer(my_custom_loss_func, greater_is_better=False) - >>> X = [[1], [1]] - >>> y = [0, 1] - >>> from sklearn.dummy import DummyClassifier - >>> clf = DummyClassifier(strategy='most_frequent', random_state=0) - >>> clf = clf.fit(X, y) - >>> my_custom_loss_func(y, clf.predict(X)) - 0.69... - >>> score(clf, X, y) - -0.69... - -|details-end| +.. dropdown:: Custom scorer objects + + The second use case is to build a completely custom scorer object + from a simple python function using :func:`make_scorer`, which can + take several parameters: + + * the python function you want to use (``my_custom_loss_func`` + in the example below) + + * whether the python function returns a score (``greater_is_better=True``, + the default) or a loss (``greater_is_better=False``). If a loss, the output + of the python function is negated by the scorer object, conforming to + the cross validation convention that scorers return higher values for better models. + + * for classification metrics only: whether the python function you provided requires + continuous decision certainties. If the scoring function only accepts probability + estimates (e.g. :func:`metrics.log_loss`) then one needs to set the parameter + `response_method`, thus in this case `response_method="predict_proba"`. Some scoring + function do not necessarily require probability estimates but rather non-thresholded + decision values (e.g. :func:`metrics.roc_auc_score`). In this case, one provides a + list such as `response_method=["decision_function", "predict_proba"]`. In this case, + the scorer will use the first available method, in the order given in the list, + to compute the scores. + + * any additional parameters, such as ``beta`` or ``labels`` in :func:`f1_score`. + + Here is an example of building custom scorers, and of using the + ``greater_is_better`` parameter:: + + >>> import numpy as np + >>> def my_custom_loss_func(y_true, y_pred): + ... diff = np.abs(y_true - y_pred).max() + ... return np.log1p(diff) + ... + >>> # score will negate the return value of my_custom_loss_func, + >>> # which will be np.log(2), 0.693, given the values for X + >>> # and y defined below. + >>> score = make_scorer(my_custom_loss_func, greater_is_better=False) + >>> X = [[1], [1]] + >>> y = [0, 1] + >>> from sklearn.dummy import DummyClassifier + >>> clf = DummyClassifier(strategy='most_frequent', random_state=0) + >>> clf = clf.fit(X, y) + >>> my_custom_loss_func(y, clf.predict(X)) + 0.69... + >>> score(clf, X, y) + -0.69... .. _diy_scoring: @@ -234,51 +229,47 @@ You can generate even more flexible model scorers by constructing your own scoring object from scratch, without using the :func:`make_scorer` factory. -|details-start| -**How to build a scorer from scratch** -|details-split| +.. dropdown:: How to build a scorer from scratch -For a callable to be a scorer, it needs to meet the protocol specified by -the following two rules: + For a callable to be a scorer, it needs to meet the protocol specified by + the following two rules: -- It can be called with parameters ``(estimator, X, y)``, where ``estimator`` - is the model that should be evaluated, ``X`` is validation data, and ``y`` is - the ground truth target for ``X`` (in the supervised case) or ``None`` (in the - unsupervised case). + - It can be called with parameters ``(estimator, X, y)``, where ``estimator`` + is the model that should be evaluated, ``X`` is validation data, and ``y`` is + the ground truth target for ``X`` (in the supervised case) or ``None`` (in the + unsupervised case). -- It returns a floating point number that quantifies the - ``estimator`` prediction quality on ``X``, with reference to ``y``. - Again, by convention higher numbers are better, so if your scorer - returns loss, that value should be negated. + - It returns a floating point number that quantifies the + ``estimator`` prediction quality on ``X``, with reference to ``y``. + Again, by convention higher numbers are better, so if your scorer + returns loss, that value should be negated. -- Advanced: If it requires extra metadata to be passed to it, it should expose - a ``get_metadata_routing`` method returning the requested metadata. The user - should be able to set the requested metadata via a ``set_score_request`` - method. Please see :ref:`User Guide ` and :ref:`Developer - Guide ` for - more details. + - Advanced: If it requires extra metadata to be passed to it, it should expose + a ``get_metadata_routing`` method returning the requested metadata. The user + should be able to set the requested metadata via a ``set_score_request`` + method. Please see :ref:`User Guide ` and :ref:`Developer + Guide ` for + more details. -.. note:: **Using custom scorers in functions where n_jobs > 1** + .. note:: **Using custom scorers in functions where n_jobs > 1** - While defining the custom scoring function alongside the calling function - should work out of the box with the default joblib backend (loky), - importing it from another module will be a more robust approach and work - independently of the joblib backend. + While defining the custom scoring function alongside the calling function + should work out of the box with the default joblib backend (loky), + importing it from another module will be a more robust approach and work + independently of the joblib backend. - For example, to use ``n_jobs`` greater than 1 in the example below, - ``custom_scoring_function`` function is saved in a user-created module - (``custom_scorer_module.py``) and imported:: + For example, to use ``n_jobs`` greater than 1 in the example below, + ``custom_scoring_function`` function is saved in a user-created module + (``custom_scorer_module.py``) and imported:: - >>> from custom_scorer_module import custom_scoring_function # doctest: +SKIP - >>> cross_val_score(model, - ... X_train, - ... y_train, - ... scoring=make_scorer(custom_scoring_function, greater_is_better=False), - ... cv=5, - ... n_jobs=-1) # doctest: +SKIP - -|details-end| + >>> from custom_scorer_module import custom_scoring_function # doctest: +SKIP + >>> cross_val_score(model, + ... X_train, + ... y_train, + ... scoring=make_scorer(custom_scoring_function, greater_is_better=False), + ... cv=5, + ... n_jobs=-1) # doctest: +SKIP .. _multimetric_scoring: @@ -474,11 +465,11 @@ In the multilabel case with binary label indicators:: >>> accuracy_score(np.array([[0, 1], [1, 1]]), np.ones((2, 2))) 0.5 -.. topic:: Example: +.. rubric:: Examples - * See :ref:`sphx_glr_auto_examples_model_selection_plot_permutation_tests_for_classification.py` - for an example of accuracy score usage using permutations of - the dataset. +* See :ref:`sphx_glr_auto_examples_model_selection_plot_permutation_tests_for_classification.py` + for an example of accuracy score usage using permutations of + the dataset. .. _top_k_accuracy_score: @@ -589,22 +580,20 @@ or *informedness*. * Balanced Accuracy as described in [Urbanowicz2015]_: the average of sensitivity and specificity is computed for each class and then averaged over total number of classes. -.. topic:: References: - - .. [Guyon2015] I. Guyon, K. Bennett, G. Cawley, H.J. Escalante, S. Escalera, T.K. Ho, N. Macià, - B. Ray, M. Saeed, A.R. Statnikov, E. Viegas, `Design of the 2015 ChaLearn AutoML Challenge - `_, - IJCNN 2015. - .. [Mosley2013] L. Mosley, `A balanced approach to the multi-class imbalance problem - `_, - IJCV 2010. - .. [Kelleher2015] John. D. Kelleher, Brian Mac Namee, Aoife D'Arcy, `Fundamentals of - Machine Learning for Predictive Data Analytics: Algorithms, Worked Examples, - and Case Studies `_, - 2015. - .. [Urbanowicz2015] Urbanowicz R.J., Moore, J.H. :doi:`ExSTraCS 2.0: description - and evaluation of a scalable learning classifier - system <10.1007/s12065-015-0128-8>`, Evol. Intel. (2015) 8: 89. +.. rubric:: References + +.. [Guyon2015] I. Guyon, K. Bennett, G. Cawley, H.J. Escalante, S. Escalera, T.K. Ho, N. Macià, + B. Ray, M. Saeed, A.R. Statnikov, E. Viegas, `Design of the 2015 ChaLearn AutoML Challenge + `_, IJCNN 2015. +.. [Mosley2013] L. Mosley, `A balanced approach to the multi-class imbalance problem + `_, IJCV 2010. +.. [Kelleher2015] John. D. Kelleher, Brian Mac Namee, Aoife D'Arcy, `Fundamentals of + Machine Learning for Predictive Data Analytics: Algorithms, Worked Examples, + and Case Studies `_, + 2015. +.. [Urbanowicz2015] Urbanowicz R.J., Moore, J.H. :doi:`ExSTraCS 2.0: description + and evaluation of a scalable learning classifier + system <10.1007/s12065-015-0128-8>`, Evol. Intel. (2015) 8: 89. .. _cohen_kappa: @@ -683,19 +672,19 @@ false negatives and true positives as follows:: >>> tn, fp, fn, tp (2, 1, 2, 3) -.. topic:: Example: +.. rubric:: Examples - * See :ref:`sphx_glr_auto_examples_model_selection_plot_confusion_matrix.py` - for an example of using a confusion matrix to evaluate classifier output - quality. +* See :ref:`sphx_glr_auto_examples_model_selection_plot_confusion_matrix.py` + for an example of using a confusion matrix to evaluate classifier output + quality. - * See :ref:`sphx_glr_auto_examples_classification_plot_digits_classification.py` - for an example of using a confusion matrix to classify - hand-written digits. +* See :ref:`sphx_glr_auto_examples_classification_plot_digits_classification.py` + for an example of using a confusion matrix to classify + hand-written digits. - * See :ref:`sphx_glr_auto_examples_text_plot_document_classification_20newsgroups.py` - for an example of using a confusion matrix to classify text - documents. +* See :ref:`sphx_glr_auto_examples_text_plot_document_classification_20newsgroups.py` + for an example of using a confusion matrix to classify text + documents. .. _classification_report: @@ -722,15 +711,15 @@ and inferred labels:: weighted avg 0.67 0.60 0.59 5 -.. topic:: Example: +.. rubric:: Examples - * See :ref:`sphx_glr_auto_examples_classification_plot_digits_classification.py` - for an example of classification report usage for - hand-written digits. +* See :ref:`sphx_glr_auto_examples_classification_plot_digits_classification.py` + for an example of classification report usage for + hand-written digits. - * See :ref:`sphx_glr_auto_examples_model_selection_plot_grid_search_digits.py` - for an example of classification report usage for - grid search with nested cross-validation. +* See :ref:`sphx_glr_auto_examples_model_selection_plot_grid_search_digits.py` + for an example of classification report usage for + grid search with nested cross-validation. .. _hamming_loss: @@ -848,31 +837,31 @@ precision-recall curve as follows. :scale: 75 :align: center -.. topic:: Examples: +.. rubric:: Examples - * See :ref:`sphx_glr_auto_examples_model_selection_plot_grid_search_digits.py` - for an example of :func:`precision_score` and :func:`recall_score` usage - to estimate parameters using grid search with nested cross-validation. +* See :ref:`sphx_glr_auto_examples_model_selection_plot_grid_search_digits.py` + for an example of :func:`precision_score` and :func:`recall_score` usage + to estimate parameters using grid search with nested cross-validation. - * See :ref:`sphx_glr_auto_examples_model_selection_plot_precision_recall.py` - for an example of :func:`precision_recall_curve` usage to evaluate - classifier output quality. +* See :ref:`sphx_glr_auto_examples_model_selection_plot_precision_recall.py` + for an example of :func:`precision_recall_curve` usage to evaluate + classifier output quality. -.. topic:: References: +.. rubric:: References - .. [Manning2008] C.D. Manning, P. Raghavan, H. Schütze, `Introduction to Information Retrieval - `_, - 2008. - .. [Everingham2010] M. Everingham, L. Van Gool, C.K.I. Williams, J. Winn, A. Zisserman, - `The Pascal Visual Object Classes (VOC) Challenge - `_, - IJCV 2010. - .. [Davis2006] J. Davis, M. Goadrich, `The Relationship Between Precision-Recall and ROC Curves - `_, - ICML 2006. - .. [Flach2015] P.A. Flach, M. Kull, `Precision-Recall-Gain Curves: PR Analysis Done Right - `_, - NIPS 2015. +.. [Manning2008] C.D. Manning, P. Raghavan, H. Schütze, `Introduction to Information Retrieval + `_, + 2008. +.. [Everingham2010] M. Everingham, L. Van Gool, C.K.I. Williams, J. Winn, A. Zisserman, + `The Pascal Visual Object Classes (VOC) Challenge + `_, + IJCV 2010. +.. [Davis2006] J. Davis, M. Goadrich, `The Relationship Between Precision-Recall and ROC Curves + `_, + ICML 2006. +.. [Flach2015] P.A. Flach, M. Kull, `Precision-Recall-Gain Curves: PR Analysis Done Right + `_, + NIPS 2015. Binary classification ^^^^^^^^^^^^^^^^^^^^^ @@ -1041,10 +1030,10 @@ Similarly, labels not present in the data sample may be accounted for in macro-a >>> metrics.precision_score(y_true, y_pred, labels=[0, 1, 2, 3], average='macro') 0.166... -.. topic:: References: +.. rubric:: References - .. [OB2019] :arxiv:`Opitz, J., & Burst, S. (2019). "Macro f1 and macro f1." - <1911.03347>` +.. [OB2019] :arxiv:`Opitz, J., & Burst, S. (2019). "Macro f1 and macro f1." + <1911.03347>` .. _jaccard_similarity_score: @@ -1496,65 +1485,57 @@ correspond to the probability estimates that a sample belongs to a particular class. The OvO and OvR algorithms support weighting uniformly (``average='macro'``) and by prevalence (``average='weighted'``). -|details-start| -**One-vs-one Algorithm** -|details-split| +.. dropdown:: One-vs-one Algorithm -Computes the average AUC of all possible pairwise -combinations of classes. [HT2001]_ defines a multiclass AUC metric weighted -uniformly: + Computes the average AUC of all possible pairwise + combinations of classes. [HT2001]_ defines a multiclass AUC metric weighted + uniformly: -.. math:: + .. math:: - \frac{1}{c(c-1)}\sum_{j=1}^{c}\sum_{k > j}^c (\text{AUC}(j | k) + - \text{AUC}(k | j)) + \frac{1}{c(c-1)}\sum_{j=1}^{c}\sum_{k > j}^c (\text{AUC}(j | k) + + \text{AUC}(k | j)) -where :math:`c` is the number of classes and :math:`\text{AUC}(j | k)` is the -AUC with class :math:`j` as the positive class and class :math:`k` as the -negative class. In general, -:math:`\text{AUC}(j | k) \neq \text{AUC}(k | j))` in the multiclass -case. This algorithm is used by setting the keyword argument ``multiclass`` -to ``'ovo'`` and ``average`` to ``'macro'``. + where :math:`c` is the number of classes and :math:`\text{AUC}(j | k)` is the + AUC with class :math:`j` as the positive class and class :math:`k` as the + negative class. In general, + :math:`\text{AUC}(j | k) \neq \text{AUC}(k | j))` in the multiclass + case. This algorithm is used by setting the keyword argument ``multiclass`` + to ``'ovo'`` and ``average`` to ``'macro'``. -The [HT2001]_ multiclass AUC metric can be extended to be weighted by the -prevalence: + The [HT2001]_ multiclass AUC metric can be extended to be weighted by the + prevalence: -.. math:: + .. math:: - \frac{1}{c(c-1)}\sum_{j=1}^{c}\sum_{k > j}^c p(j \cup k)( - \text{AUC}(j | k) + \text{AUC}(k | j)) + \frac{1}{c(c-1)}\sum_{j=1}^{c}\sum_{k > j}^c p(j \cup k)( + \text{AUC}(j | k) + \text{AUC}(k | j)) -where :math:`c` is the number of classes. This algorithm is used by setting -the keyword argument ``multiclass`` to ``'ovo'`` and ``average`` to -``'weighted'``. The ``'weighted'`` option returns a prevalence-weighted average -as described in [FC2009]_. + where :math:`c` is the number of classes. This algorithm is used by setting + the keyword argument ``multiclass`` to ``'ovo'`` and ``average`` to + ``'weighted'``. The ``'weighted'`` option returns a prevalence-weighted average + as described in [FC2009]_. -|details-end| +.. dropdown:: One-vs-rest Algorithm -|details-start| -**One-vs-rest Algorithm** -|details-split| + Computes the AUC of each class against the rest + [PD2000]_. The algorithm is functionally the same as the multilabel case. To + enable this algorithm set the keyword argument ``multiclass`` to ``'ovr'``. + Additionally to ``'macro'`` [F2006]_ and ``'weighted'`` [F2001]_ averaging, OvR + supports ``'micro'`` averaging. -Computes the AUC of each class against the rest -[PD2000]_. The algorithm is functionally the same as the multilabel case. To -enable this algorithm set the keyword argument ``multiclass`` to ``'ovr'``. -Additionally to ``'macro'`` [F2006]_ and ``'weighted'`` [F2001]_ averaging, OvR -supports ``'micro'`` averaging. + In applications where a high false positive rate is not tolerable the parameter + ``max_fpr`` of :func:`roc_auc_score` can be used to summarize the ROC curve up + to the given limit. -In applications where a high false positive rate is not tolerable the parameter -``max_fpr`` of :func:`roc_auc_score` can be used to summarize the ROC curve up -to the given limit. + The following figure shows the micro-averaged ROC curve and its corresponding + ROC-AUC score for a classifier aimed to distinguish the different species in + the :ref:`iris_dataset`: -The following figure shows the micro-averaged ROC curve and its corresponding -ROC-AUC score for a classifier aimed to distinguish the different species in -the :ref:`iris_dataset`: - -.. image:: ../auto_examples/model_selection/images/sphx_glr_plot_roc_002.png - :target: ../auto_examples/model_selection/plot_roc.html - :scale: 75 - :align: center - -|details-end| + .. image:: ../auto_examples/model_selection/images/sphx_glr_plot_roc_002.png + :target: ../auto_examples/model_selection/plot_roc.html + :scale: 75 + :align: center .. _roc_auc_multilabel: @@ -1584,46 +1565,43 @@ And the decision values do not require such processing. >>> roc_auc_score(y, y_score, average=None) array([0.81..., 0.84... , 0.93..., 0.87..., 0.94...]) -.. topic:: Examples: +.. rubric:: Examples - * See :ref:`sphx_glr_auto_examples_model_selection_plot_roc.py` - for an example of using ROC to - evaluate the quality of the output of a classifier. +* See :ref:`sphx_glr_auto_examples_model_selection_plot_roc.py` for an example of + using ROC to evaluate the quality of the output of a classifier. - * See :ref:`sphx_glr_auto_examples_model_selection_plot_roc_crossval.py` - for an example of using ROC to - evaluate classifier output quality, using cross-validation. +* See :ref:`sphx_glr_auto_examples_model_selection_plot_roc_crossval.py` for an + example of using ROC to evaluate classifier output quality, using cross-validation. - * See :ref:`sphx_glr_auto_examples_applications_plot_species_distribution_modeling.py` - for an example of using ROC to - model species distribution. +* See :ref:`sphx_glr_auto_examples_applications_plot_species_distribution_modeling.py` + for an example of using ROC to model species distribution. -.. topic:: References: +.. rubric:: References - .. [HT2001] Hand, D.J. and Till, R.J., (2001). `A simple generalisation - of the area under the ROC curve for multiple class classification problems. - `_ - Machine learning, 45(2), pp. 171-186. +.. [HT2001] Hand, D.J. and Till, R.J., (2001). `A simple generalisation + of the area under the ROC curve for multiple class classification problems. + `_ + Machine learning, 45(2), pp. 171-186. - .. [FC2009] Ferri, Cèsar & Hernandez-Orallo, Jose & Modroiu, R. (2009). - `An Experimental Comparison of Performance Measures for Classification. - `_ - Pattern Recognition Letters. 30. 27-38. +.. [FC2009] Ferri, Cèsar & Hernandez-Orallo, Jose & Modroiu, R. (2009). + `An Experimental Comparison of Performance Measures for Classification. + `_ + Pattern Recognition Letters. 30. 27-38. - .. [PD2000] 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. +.. [PD2000] 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. - .. [F2006] Fawcett, T., 2006. `An introduction to ROC analysis. - `_ - Pattern Recognition Letters, 27(8), pp. 861-874. +.. [F2006] Fawcett, T., 2006. `An introduction to ROC analysis. + `_ + Pattern Recognition Letters, 27(8), pp. 861-874. - .. [F2001] Fawcett, T., 2001. `Using rule sets to maximize - ROC performance `_ - In Data Mining, 2001. - Proceedings IEEE International Conference, pp. 131-138. +.. [F2001] Fawcett, T., 2001. `Using rule sets to maximize + ROC performance `_ + In Data Mining, 2001. + Proceedings IEEE International Conference, pp. 131-138. .. _det_curve: @@ -1659,67 +1637,57 @@ same classification task: :scale: 75 :align: center -.. topic:: Examples: - - * See :ref:`sphx_glr_auto_examples_model_selection_plot_det.py` - for an example comparison between receiver operating characteristic (ROC) - curves and Detection error tradeoff (DET) curves. +.. dropdown:: Properties -|details-start| -**Properties** -|details-split| + * DET curves form a linear curve in normal deviate scale if the detection + scores are normally (or close-to normally) distributed. + It was shown by [Navratil2007]_ that the reverse is not necessarily true and + even more general distributions are able to produce linear DET curves. -* DET curves form a linear curve in normal deviate scale if the detection - scores are normally (or close-to normally) distributed. - It was shown by [Navratil2007]_ that the reverse is not necessarily true and - even more general distributions are able to produce linear DET curves. + * The normal deviate scale transformation spreads out the points such that a + comparatively larger space of plot is occupied. + Therefore curves with similar classification performance might be easier to + distinguish on a DET plot. -* The normal deviate scale transformation spreads out the points such that a - comparatively larger space of plot is occupied. - Therefore curves with similar classification performance might be easier to - distinguish on a DET plot. + * With False Negative Rate being "inverse" to True Positive Rate the point + of perfection for DET curves is the origin (in contrast to the top left + corner for ROC curves). -* With False Negative Rate being "inverse" to True Positive Rate the point - of perfection for DET curves is the origin (in contrast to the top left - corner for ROC curves). +.. dropdown:: Applications and limitations -|details-end| + DET curves are intuitive to read and hence allow quick visual assessment of a + classifier's performance. + Additionally DET curves can be consulted for threshold analysis and operating + point selection. + This is particularly helpful if a comparison of error types is required. -|details-start| -**Applications and limitations** -|details-split| + On the other hand DET curves do not provide their metric as a single number. + Therefore for either automated evaluation or comparison to other + classification tasks metrics like the derived area under ROC curve might be + better suited. -DET curves are intuitive to read and hence allow quick visual assessment of a -classifier's performance. -Additionally DET curves can be consulted for threshold analysis and operating -point selection. -This is particularly helpful if a comparison of error types is required. +.. rubric:: Examples -On the other hand DET curves do not provide their metric as a single number. -Therefore for either automated evaluation or comparison to other -classification tasks metrics like the derived area under ROC curve might be -better suited. +* See :ref:`sphx_glr_auto_examples_model_selection_plot_det.py` + for an example comparison between receiver operating characteristic (ROC) + curves and Detection error tradeoff (DET) curves. -|details-end| +.. rubric:: References -.. topic:: References: +.. [WikipediaDET2017] Wikipedia contributors. Detection error tradeoff. + Wikipedia, The Free Encyclopedia. September 4, 2017, 23:33 UTC. + Available at: https://en.wikipedia.org/w/index.php?title=Detection_error_tradeoff&oldid=798982054. + Accessed February 19, 2018. - .. [WikipediaDET2017] Wikipedia contributors. Detection error tradeoff. - Wikipedia, The Free Encyclopedia. September 4, 2017, 23:33 UTC. - Available at: https://en.wikipedia.org/w/index.php?title=Detection_error_tradeoff&oldid=798982054. - Accessed February 19, 2018. +.. [Martin1997] A. Martin, G. Doddington, T. Kamm, M. Ordowski, and M. Przybocki, + `The DET Curve in Assessment of Detection Task Performance + `_, NIST 1997. - .. [Martin1997] A. Martin, G. Doddington, T. Kamm, M. Ordowski, and M. Przybocki, - `The DET Curve in Assessment of Detection Task Performance - `_, - NIST 1997. - - .. [Navratil2007] J. Navractil and D. Klusacek, - "`On Linear DETs, - `_" - 2007 IEEE International Conference on Acoustics, - Speech and Signal Processing - ICASSP '07, Honolulu, - HI, 2007, pp. IV-229-IV-232. +.. [Navratil2007] J. Navractil and D. Klusacek, + `"On Linear DETs" `_, + 2007 IEEE International Conference on Acoustics, + Speech and Signal Processing - ICASSP '07, Honolulu, + HI, 2007, pp. IV-229-IV-232. .. _zero_one_loss: @@ -1767,11 +1735,11 @@ set [0,1] has an error:: >>> zero_one_loss(np.array([[0, 1], [1, 1]]), np.ones((2, 2)), normalize=False) 1.0 -.. topic:: Example: +.. rubric:: Examples - * See :ref:`sphx_glr_auto_examples_feature_selection_plot_rfe_with_cross_validation.py` - for an example of zero one loss usage to perform recursive feature - elimination with cross-validation. +* See :ref:`sphx_glr_auto_examples_feature_selection_plot_rfe_with_cross_validation.py` + for an example of zero one loss usage to perform recursive feature + elimination with cross-validation. .. _brier_score_loss: @@ -1827,28 +1795,27 @@ necessarily mean a better calibrated model. "Only when refinement loss remains the same does a lower Brier score loss always mean better calibration" [Bella2012]_, [Flach2008]_. -.. topic:: Example: +.. rubric:: Examples - * See :ref:`sphx_glr_auto_examples_calibration_plot_calibration.py` - for an example of Brier score loss usage to perform probability - calibration of classifiers. +* See :ref:`sphx_glr_auto_examples_calibration_plot_calibration.py` + for an example of Brier score loss usage to perform probability + calibration of classifiers. -.. topic:: References: +.. rubric:: References - .. [Brier1950] G. Brier, `Verification of forecasts expressed in terms of - probability - `_, - Monthly weather review 78.1 (1950) +.. [Brier1950] G. Brier, `Verification of forecasts expressed in terms of probability + `_, + Monthly weather review 78.1 (1950) - .. [Bella2012] Bella, Ferri, Hernández-Orallo, and Ramírez-Quintana - `"Calibration of Machine Learning Models" - `_ - in Khosrow-Pour, M. "Machine learning: concepts, methodologies, tools - and applications." Hershey, PA: Information Science Reference (2012). +.. [Bella2012] Bella, Ferri, Hernández-Orallo, and Ramírez-Quintana + `"Calibration of Machine Learning Models" + `_ + in Khosrow-Pour, M. "Machine learning: concepts, methodologies, tools + and applications." Hershey, PA: Information Science Reference (2012). - .. [Flach2008] Flach, Peter, and Edson Matsubara. `"On classification, ranking, - and probability estimation." `_ - Dagstuhl Seminar Proceedings. Schloss Dagstuhl-Leibniz-Zentrum fr Informatik (2008). +.. [Flach2008] Flach, Peter, and Edson Matsubara. `"On classification, ranking, + and probability estimation." `_ + Dagstuhl Seminar Proceedings. Schloss Dagstuhl-Leibniz-Zentrum fr Informatik (2008). .. _class_likelihood_ratios: @@ -1901,82 +1868,72 @@ counts ``tp`` (see `the wikipedia page `_ for the actual formulas). -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_model_selection_plot_likelihood_ratios.py` +* :ref:`sphx_glr_auto_examples_model_selection_plot_likelihood_ratios.py` -|details-start| -**Interpretation across varying prevalence** -|details-split| +.. dropdown:: Interpretation across varying prevalence -Both class likelihood ratios are interpretable in terms of an odds ratio -(pre-test and post-tests): + Both class likelihood ratios are interpretable in terms of an odds ratio + (pre-test and post-tests): -.. math:: + .. math:: - \text{post-test odds} = \text{Likelihood ratio} \times \text{pre-test odds}. + \text{post-test odds} = \text{Likelihood ratio} \times \text{pre-test odds}. -Odds are in general related to probabilities via + Odds are in general related to probabilities via -.. math:: + .. math:: - \text{odds} = \frac{\text{probability}}{1 - \text{probability}}, + \text{odds} = \frac{\text{probability}}{1 - \text{probability}}, -or equivalently + or equivalently -.. math:: + .. math:: - \text{probability} = \frac{\text{odds}}{1 + \text{odds}}. + \text{probability} = \frac{\text{odds}}{1 + \text{odds}}. -On a given population, the pre-test probability is given by the prevalence. By -converting odds to probabilities, the likelihood ratios can be translated into a -probability of truly belonging to either class before and after a classifier -prediction: + On a given population, the pre-test probability is given by the prevalence. By + converting odds to probabilities, the likelihood ratios can be translated into a + probability of truly belonging to either class before and after a classifier + prediction: -.. math:: + .. math:: - \text{post-test odds} = \text{Likelihood ratio} \times - \frac{\text{pre-test probability}}{1 - \text{pre-test probability}}, + \text{post-test odds} = \text{Likelihood ratio} \times + \frac{\text{pre-test probability}}{1 - \text{pre-test probability}}, -.. math:: - - \text{post-test probability} = \frac{\text{post-test odds}}{1 + \text{post-test odds}}. - -|details-end| + .. math:: -|details-start| -**Mathematical divergences** -|details-split| + \text{post-test probability} = \frac{\text{post-test odds}}{1 + \text{post-test odds}}. -The positive likelihood ratio is undefined when :math:`fp = 0`, which can be -interpreted as the classifier perfectly identifying positive cases. If :math:`fp -= 0` and additionally :math:`tp = 0`, this leads to a zero/zero division. This -happens, for instance, when using a `DummyClassifier` that always predicts the -negative class and therefore the interpretation as a perfect classifier is lost. +.. dropdown:: Mathematical divergences -The negative likelihood ratio is undefined when :math:`tn = 0`. Such divergence -is invalid, as :math:`LR_- > 1` would indicate an increase in the odds of a -sample belonging to the positive class after being classified as negative, as if -the act of classifying caused the positive condition. This includes the case of -a `DummyClassifier` that always predicts the positive class (i.e. when -:math:`tn=fn=0`). + The positive likelihood ratio is undefined when :math:`fp = 0`, which can be + interpreted as the classifier perfectly identifying positive cases. If :math:`fp + = 0` and additionally :math:`tp = 0`, this leads to a zero/zero division. This + happens, for instance, when using a `DummyClassifier` that always predicts the + negative class and therefore the interpretation as a perfect classifier is lost. -Both class likelihood ratios are undefined when :math:`tp=fn=0`, which means -that no samples of the positive class were present in the testing set. This can -also happen when cross-validating highly imbalanced data. + The negative likelihood ratio is undefined when :math:`tn = 0`. Such divergence + is invalid, as :math:`LR_- > 1` would indicate an increase in the odds of a + sample belonging to the positive class after being classified as negative, as if + the act of classifying caused the positive condition. This includes the case of + a `DummyClassifier` that always predicts the positive class (i.e. when + :math:`tn=fn=0`). -In all the previous cases the :func:`class_likelihood_ratios` function raises by -default an appropriate warning message and returns `nan` to avoid pollution when -averaging over cross-validation folds. + Both class likelihood ratios are undefined when :math:`tp=fn=0`, which means + that no samples of the positive class were present in the testing set. This can + also happen when cross-validating highly imbalanced data. -For a worked-out demonstration of the :func:`class_likelihood_ratios` function, -see the example below. + In all the previous cases the :func:`class_likelihood_ratios` function raises by + default an appropriate warning message and returns `nan` to avoid pollution when + averaging over cross-validation folds. -|details-end| + For a worked-out demonstration of the :func:`class_likelihood_ratios` function, + see the example below. -|details-start| -**References** -|details-split| +.. dropdown:: References * `Wikipedia entry for Likelihood ratios in diagnostic testing `_ @@ -1986,7 +1943,6 @@ see the example below. values with disease prevalence. Statistics in medicine, 16(9), 981-991. -|details-end| .. _d2_score_classification: @@ -2011,47 +1967,44 @@ model can be arbitrarily worse). A constant model that always predicts :math:`y_{\text{null}}`, disregarding the input features, would get a D² score of 0.0. -|details-start| -**D2 log loss score** -|details-split| +.. dropdown:: D2 log loss score -The :func:`d2_log_loss_score` function implements the special case -of D² with the log loss, see :ref:`log_loss`, i.e.: + The :func:`d2_log_loss_score` function implements the special case + of D² with the log loss, see :ref:`log_loss`, i.e.: -.. math:: + .. math:: - \text{dev}(y, \hat{y}) = \text{log_loss}(y, \hat{y}). + \text{dev}(y, \hat{y}) = \text{log_loss}(y, \hat{y}). -Here are some usage examples of the :func:`d2_log_loss_score` function:: + Here are some usage examples of the :func:`d2_log_loss_score` function:: - >>> from sklearn.metrics import d2_log_loss_score - >>> y_true = [1, 1, 2, 3] - >>> y_pred = [ - ... [0.5, 0.25, 0.25], - ... [0.5, 0.25, 0.25], - ... [0.5, 0.25, 0.25], - ... [0.5, 0.25, 0.25], - ... ] - >>> d2_log_loss_score(y_true, y_pred) - 0.0 - >>> y_true = [1, 2, 3] - >>> y_pred = [ - ... [0.98, 0.01, 0.01], - ... [0.01, 0.98, 0.01], - ... [0.01, 0.01, 0.98], - ... ] - >>> d2_log_loss_score(y_true, y_pred) - 0.981... - >>> y_true = [1, 2, 3] - >>> y_pred = [ - ... [0.1, 0.6, 0.3], - ... [0.1, 0.6, 0.3], - ... [0.4, 0.5, 0.1], - ... ] - >>> d2_log_loss_score(y_true, y_pred) - -0.552... + >>> from sklearn.metrics import d2_log_loss_score + >>> y_true = [1, 1, 2, 3] + >>> y_pred = [ + ... [0.5, 0.25, 0.25], + ... [0.5, 0.25, 0.25], + ... [0.5, 0.25, 0.25], + ... [0.5, 0.25, 0.25], + ... ] + >>> d2_log_loss_score(y_true, y_pred) + 0.0 + >>> y_true = [1, 2, 3] + >>> y_pred = [ + ... [0.98, 0.01, 0.01], + ... [0.01, 0.98, 0.01], + ... [0.01, 0.01, 0.98], + ... ] + >>> d2_log_loss_score(y_true, y_pred) + 0.981... + >>> y_true = [1, 2, 3] + >>> y_pred = [ + ... [0.1, 0.6, 0.3], + ... [0.1, 0.6, 0.3], + ... [0.4, 0.5, 0.1], + ... ] + >>> d2_log_loss_score(y_true, y_pred) + -0.552... -|details-end| .. _multilabel_ranking_metrics: @@ -2191,14 +2144,11 @@ Here is a small example of usage of this function:: 0.0 -|details-start| -**References** -|details-split| +.. dropdown:: References * Tsoumakas, G., Katakis, I., & Vlahavas, I. (2010). Mining multi-label data. In Data mining and knowledge discovery handbook (pp. 667-685). Springer US. -|details-end| .. _ndcg: @@ -2244,9 +2194,7 @@ DCG score is and the NDCG score is the DCG score divided by the DCG score obtained for :math:`y`. -|details-start| -**References** -|details-split| +.. dropdown:: References * `Wikipedia entry for Discounted Cumulative Gain `_ @@ -2264,7 +2212,6 @@ and the NDCG score is the DCG score divided by the DCG score obtained for European conference on information retrieval (pp. 414-421). Springer, Berlin, Heidelberg. -|details-end| .. _regression_metrics: @@ -2374,11 +2321,11 @@ Here is a small example of usage of the :func:`r2_score` function:: >>> r2_score(y_true, y_pred, force_finite=False) -inf -.. topic:: Example: +.. rubric:: Examples - * See :ref:`sphx_glr_auto_examples_linear_model_plot_lasso_and_elasticnet.py` - for an example of R² score usage to - evaluate Lasso and Elastic Net on sparse signals. +* See :ref:`sphx_glr_auto_examples_linear_model_plot_lasso_and_elasticnet.py` + for an example of R² score usage to + evaluate Lasso and Elastic Net on sparse signals. .. _mean_absolute_error: @@ -2445,11 +2392,10 @@ function:: >>> mean_squared_error(y_true, y_pred) 0.7083... -.. topic:: Examples: +.. rubric:: Examples - * See :ref:`sphx_glr_auto_examples_ensemble_plot_gradient_boosting_regression.py` - for an example of mean squared error usage to - evaluate gradient boosting regression. +* See :ref:`sphx_glr_auto_examples_ensemble_plot_gradient_boosting_regression.py` + for an example of mean squared error usage to evaluate gradient boosting regression. Taking the square root of the MSE, called the root mean squared error (RMSE), is another common metric that provides a measure in the same units as the target variable. RSME is @@ -2787,12 +2733,12 @@ It is also possible to build scorer objects for hyper-parameter tuning. The sign of the loss must be switched to ensure that greater means better as explained in the example linked below. -.. topic:: Example: +.. rubric:: Examples - * See :ref:`sphx_glr_auto_examples_ensemble_plot_gradient_boosting_quantile.py` - for an example of using the pinball loss to evaluate and tune the - hyper-parameters of quantile regression models on data with non-symmetric - noise and outliers. +* See :ref:`sphx_glr_auto_examples_ensemble_plot_gradient_boosting_quantile.py` + for an example of using the pinball loss to evaluate and tune the + hyper-parameters of quantile regression models on data with non-symmetric + noise and outliers. .. _d2_score: @@ -2818,77 +2764,66 @@ model can be arbitrarily worse). A constant model that always predicts :math:`y_{\text{null}}`, disregarding the input features, would get a D² score of 0.0. -|details-start| -**D² Tweedie score** -|details-split| - -The :func:`d2_tweedie_score` function implements the special case of D² -where :math:`\text{dev}(y, \hat{y})` is the Tweedie deviance, see :ref:`mean_tweedie_deviance`. -It is also known as D² Tweedie and is related to McFadden's likelihood ratio index. +.. dropdown:: D² Tweedie score -The argument ``power`` defines the Tweedie power as for -:func:`mean_tweedie_deviance`. Note that for `power=0`, -:func:`d2_tweedie_score` equals :func:`r2_score` (for single targets). + The :func:`d2_tweedie_score` function implements the special case of D² + where :math:`\text{dev}(y, \hat{y})` is the Tweedie deviance, see :ref:`mean_tweedie_deviance`. + It is also known as D² Tweedie and is related to McFadden's likelihood ratio index. -A scorer object with a specific choice of ``power`` can be built by:: + The argument ``power`` defines the Tweedie power as for + :func:`mean_tweedie_deviance`. Note that for `power=0`, + :func:`d2_tweedie_score` equals :func:`r2_score` (for single targets). - >>> from sklearn.metrics import d2_tweedie_score, make_scorer - >>> d2_tweedie_score_15 = make_scorer(d2_tweedie_score, power=1.5) + A scorer object with a specific choice of ``power`` can be built by:: -|details-end| + >>> from sklearn.metrics import d2_tweedie_score, make_scorer + >>> d2_tweedie_score_15 = make_scorer(d2_tweedie_score, power=1.5) -|details-start| -**D² pinball score** -|details-split| +.. dropdown:: D² pinball score -The :func:`d2_pinball_score` function implements the special case -of D² with the pinball loss, see :ref:`pinball_loss`, i.e.: - -.. math:: + The :func:`d2_pinball_score` function implements the special case + of D² with the pinball loss, see :ref:`pinball_loss`, i.e.: - \text{dev}(y, \hat{y}) = \text{pinball}(y, \hat{y}). + .. math:: -The argument ``alpha`` defines the slope of the pinball loss as for -:func:`mean_pinball_loss` (:ref:`pinball_loss`). It determines the -quantile level ``alpha`` for which the pinball loss and also D² -are optimal. Note that for `alpha=0.5` (the default) :func:`d2_pinball_score` -equals :func:`d2_absolute_error_score`. + \text{dev}(y, \hat{y}) = \text{pinball}(y, \hat{y}). -A scorer object with a specific choice of ``alpha`` can be built by:: + The argument ``alpha`` defines the slope of the pinball loss as for + :func:`mean_pinball_loss` (:ref:`pinball_loss`). It determines the + quantile level ``alpha`` for which the pinball loss and also D² + are optimal. Note that for `alpha=0.5` (the default) :func:`d2_pinball_score` + equals :func:`d2_absolute_error_score`. - >>> from sklearn.metrics import d2_pinball_score, make_scorer - >>> d2_pinball_score_08 = make_scorer(d2_pinball_score, alpha=0.8) + A scorer object with a specific choice of ``alpha`` can be built by:: -|details-end| + >>> from sklearn.metrics import d2_pinball_score, make_scorer + >>> d2_pinball_score_08 = make_scorer(d2_pinball_score, alpha=0.8) -|details-start| -**D² absolute error score** -|details-split| +.. dropdown:: D² absolute error score -The :func:`d2_absolute_error_score` function implements the special case of -the :ref:`mean_absolute_error`: + The :func:`d2_absolute_error_score` function implements the special case of + the :ref:`mean_absolute_error`: -.. math:: + .. math:: - \text{dev}(y, \hat{y}) = \text{MAE}(y, \hat{y}). + \text{dev}(y, \hat{y}) = \text{MAE}(y, \hat{y}). -Here are some usage examples of the :func:`d2_absolute_error_score` function:: + Here are some usage examples of the :func:`d2_absolute_error_score` function:: - >>> from sklearn.metrics import d2_absolute_error_score - >>> y_true = [3, -0.5, 2, 7] - >>> y_pred = [2.5, 0.0, 2, 8] - >>> d2_absolute_error_score(y_true, y_pred) - 0.764... - >>> y_true = [1, 2, 3] - >>> y_pred = [1, 2, 3] - >>> d2_absolute_error_score(y_true, y_pred) - 1.0 - >>> y_true = [1, 2, 3] - >>> y_pred = [2, 2, 2] - >>> d2_absolute_error_score(y_true, y_pred) - 0.0 + >>> from sklearn.metrics import d2_absolute_error_score + >>> y_true = [3, -0.5, 2, 7] + >>> y_pred = [2.5, 0.0, 2, 8] + >>> d2_absolute_error_score(y_true, y_pred) + 0.764... + >>> y_true = [1, 2, 3] + >>> y_pred = [1, 2, 3] + >>> d2_absolute_error_score(y_true, y_pred) + 1.0 + >>> y_true = [1, 2, 3] + >>> y_pred = [2, 2, 2] + >>> d2_absolute_error_score(y_true, y_pred) + 0.0 -|details-end| .. _visualization_regression_evaluation: @@ -2958,12 +2893,12 @@ model might be useful. Refer to the example below to see a model evaluation that makes use of this display. -.. topic:: Example: +.. rubric:: Examples - * See :ref:`sphx_glr_auto_examples_compose_plot_transformed_target.py` for - an example on how to use :class:`~sklearn.metrics.PredictionErrorDisplay` - to visualize the prediction quality improvement of a regression model - obtained by transforming the target before learning. +* See :ref:`sphx_glr_auto_examples_compose_plot_transformed_target.py` for + an example on how to use :class:`~sklearn.metrics.PredictionErrorDisplay` + to visualize the prediction quality improvement of a regression model + obtained by transforming the target before learning. .. _clustering_metrics: diff --git a/doc/modules/multiclass.rst b/doc/modules/multiclass.rst index 42762690ce8f7..b5f7611bdfd91 100644 --- a/doc/modules/multiclass.rst +++ b/doc/modules/multiclass.rst @@ -222,9 +222,9 @@ in which cell [i, j] indicates the presence of label j in sample i. :scale: 75% -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_miscellaneous_plot_multilabel.py` +* :ref:`sphx_glr_auto_examples_miscellaneous_plot_multilabel.py` .. _ovo_classification: @@ -263,10 +263,10 @@ Below is an example of multiclass learning using OvO:: 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]) -.. topic:: References: +.. rubric:: References - * "Pattern Recognition and Machine Learning. Springer", - Christopher M. Bishop, page 183, (First Edition) +* "Pattern Recognition and Machine Learning. Springer", + Christopher M. Bishop, page 183, (First Edition) .. _ecoc: @@ -321,21 +321,16 @@ Below is an example of multiclass learning using Output-Codes:: 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]) -.. topic:: References: +.. rubric:: References - * "Solving multiclass learning problems via error-correcting output codes", - Dietterich T., Bakiri G., - Journal of Artificial Intelligence Research 2, - 1995. +* "Solving multiclass learning problems via error-correcting output codes", + Dietterich T., Bakiri G., Journal of Artificial Intelligence Research 2, 1995. - .. [3] "The error coding method and PICTs", - James G., Hastie T., - Journal of Computational and Graphical statistics 7, - 1998. +.. [3] "The error coding method and PICTs", James G., Hastie T., + Journal of Computational and Graphical statistics 7, 1998. - * "The Elements of Statistical Learning", - Hastie T., Tibshirani R., Friedman J., page 606 (second-edition) - 2008. +* "The Elements of Statistical Learning", + Hastie T., Tibshirani R., Friedman J., page 606 (second-edition), 2008. .. _multilabel_classification: @@ -432,10 +427,10 @@ one does not know the optimal ordering of the models in the chain so typically many randomly ordered chains are fit and their predictions are averaged together. -.. topic:: References: +.. rubric:: References - Jesse Read, Bernhard Pfahringer, Geoff Holmes, Eibe Frank, - "Classifier Chains for Multi-label Classification", 2009. +* Jesse Read, Bernhard Pfahringer, Geoff Holmes, Eibe Frank, + "Classifier Chains for Multi-label Classification", 2009. .. _multiclass_multioutput_classification: diff --git a/doc/modules/naive_bayes.rst b/doc/modules/naive_bayes.rst index 05ca928dfae0b..6e80ec6145919 100644 --- a/doc/modules/naive_bayes.rst +++ b/doc/modules/naive_bayes.rst @@ -69,15 +69,11 @@ On the flip side, although naive Bayes is known as a decent classifier, it is known to be a bad estimator, so the probability outputs from ``predict_proba`` are not to be taken too seriously. -|details-start| -**References** -|details-split| +.. dropdown:: References -* H. Zhang (2004). `The optimality of Naive Bayes. - `_ - Proc. FLAIRS. - -|details-end| + * H. Zhang (2004). `The optimality of Naive Bayes. + `_ + Proc. FLAIRS. .. _gaussian_naive_bayes: @@ -153,47 +149,40 @@ The inventors of CNB show empirically that the parameter estimates for CNB are more stable than those for MNB. Further, CNB regularly outperforms MNB (often by a considerable margin) on text classification tasks. -|details-start| -**Weights calculation** -|details-split| - -The procedure for calculating the weights is as follows: +.. dropdown:: Weights calculation -.. math:: + The procedure for calculating the weights is as follows: - \hat{\theta}_{ci} = \frac{\alpha_i + \sum_{j:y_j \neq c} d_{ij}} - {\alpha + \sum_{j:y_j \neq c} \sum_{k} d_{kj}} + .. math:: - w_{ci} = \log \hat{\theta}_{ci} + \hat{\theta}_{ci} = \frac{\alpha_i + \sum_{j:y_j \neq c} d_{ij}} + {\alpha + \sum_{j:y_j \neq c} \sum_{k} d_{kj}} - w_{ci} = \frac{w_{ci}}{\sum_{j} |w_{cj}|} + w_{ci} = \log \hat{\theta}_{ci} -where the summations are over all documents :math:`j` not in class :math:`c`, -:math:`d_{ij}` is either the count or tf-idf value of term :math:`i` in document -:math:`j`, :math:`\alpha_i` is a smoothing hyperparameter like that found in -MNB, and :math:`\alpha = \sum_{i} \alpha_i`. The second normalization addresses -the tendency for longer documents to dominate parameter estimates in MNB. The -classification rule is: + w_{ci} = \frac{w_{ci}}{\sum_{j} |w_{cj}|} -.. math:: + where the summations are over all documents :math:`j` not in class :math:`c`, + :math:`d_{ij}` is either the count or tf-idf value of term :math:`i` in document + :math:`j`, :math:`\alpha_i` is a smoothing hyperparameter like that found in + MNB, and :math:`\alpha = \sum_{i} \alpha_i`. The second normalization addresses + the tendency for longer documents to dominate parameter estimates in MNB. The + classification rule is: - \hat{c} = \arg\min_c \sum_{i} t_i w_{ci} + .. math:: -i.e., a document is assigned to the class that is the *poorest* complement -match. + \hat{c} = \arg\min_c \sum_{i} t_i w_{ci} -|details-end| + i.e., a document is assigned to the class that is the *poorest* complement + match. -|details-start| -**References** -|details-split| +.. dropdown:: References -* 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). + * 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). -|details-end| .. _bernoulli_naive_bayes: @@ -224,24 +213,21 @@ count vectors) may be used to train and use this classifier. :class:`BernoulliNB might perform better on some datasets, especially those with shorter documents. It is advisable to evaluate both models, if time permits. -|details-start| -**References** -|details-split| +.. dropdown:: References -* C.D. Manning, P. Raghavan and H. Schütze (2008). Introduction to - Information Retrieval. Cambridge University Press, pp. 234-265. + * C.D. Manning, P. Raghavan and H. Schütze (2008). Introduction to + Information Retrieval. Cambridge University Press, pp. 234-265. -* 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. + * 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). + * V. Metsis, I. Androutsopoulos and G. Paliouras (2006). + `Spam filtering with Naive Bayes -- Which Naive Bayes? + `_ + 3rd Conf. on Email and Anti-Spam (CEAS). -|details-end| .. _categorical_naive_bayes: @@ -258,25 +244,21 @@ For each feature :math:`i` in the training set :math:`X`, of X conditioned on the class y. The index set of the samples is defined as :math:`J = \{ 1, \dots, m \}`, with :math:`m` as the number of samples. -|details-start| -**Probability calculation** -|details-split| - -The probability of category :math:`t` in feature :math:`i` given class -:math:`c` is estimated as: +.. dropdown:: Probability calculation -.. math:: + The probability of category :math:`t` in feature :math:`i` given class + :math:`c` is estimated as: - P(x_i = t \mid y = c \: ;\, \alpha) = \frac{ N_{tic} + \alpha}{N_{c} + - \alpha n_i}, + .. math:: -where :math:`N_{tic} = |\{j \in J \mid x_{ij} = t, y_j = c\}|` is the number -of times category :math:`t` appears in the samples :math:`x_{i}`, which belong -to class :math:`c`, :math:`N_{c} = |\{ j \in J\mid y_j = c\}|` is the number -of samples with class c, :math:`\alpha` is a smoothing parameter and -:math:`n_i` is the number of available categories of feature :math:`i`. + P(x_i = t \mid y = c \: ;\, \alpha) = \frac{ N_{tic} + \alpha}{N_{c} + + \alpha n_i}, -|details-end| + where :math:`N_{tic} = |\{j \in J \mid x_{ij} = t, y_j = c\}|` is the number + of times category :math:`t` appears in the samples :math:`x_{i}`, which belong + to class :math:`c`, :math:`N_{c} = |\{ j \in J\mid y_j = c\}|` is the number + of samples with class c, :math:`\alpha` is a smoothing parameter and + :math:`n_i` is the number of available categories of feature :math:`i`. :class:`CategoricalNB` assumes that the sample matrix :math:`X` is encoded (for instance with the help of :class:`~sklearn.preprocessing.OrdinalEncoder`) such diff --git a/doc/modules/neighbors.rst b/doc/modules/neighbors.rst index b081b29572d8a..de0eff67018bc 100644 --- a/doc/modules/neighbors.rst +++ b/doc/modules/neighbors.rst @@ -192,10 +192,10 @@ distance can be supplied to compute the weights. .. centered:: |classification_1| -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_neighbors_plot_classification.py`: an example of - classification using nearest neighbors. +* :ref:`sphx_glr_auto_examples_neighbors_plot_classification.py`: an example of + classification using nearest neighbors. .. _regression: @@ -241,13 +241,13 @@ the lower half of those faces. :align: center -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_neighbors_plot_regression.py`: an example of regression - using nearest neighbors. +* :ref:`sphx_glr_auto_examples_neighbors_plot_regression.py`: an example of regression + using nearest neighbors. - * :ref:`sphx_glr_auto_examples_miscellaneous_plot_multioutput_face_completion.py`: an example of - multi-output regression using nearest neighbors. +* :ref:`sphx_glr_auto_examples_miscellaneous_plot_multioutput_face_completion.py`: + an example of multi-output regression using nearest neighbors. Nearest Neighbor Algorithms @@ -304,15 +304,13 @@ In scikit-learn, KD tree neighbors searches are specified using the keyword ``algorithm = 'kd_tree'``, and are computed using the class :class:`KDTree`. -|details-start| -**References** -|details-split| - * `"Multidimensional binary search trees used for associative searching" - `_, - Bentley, J.L., Communications of the ACM (1975) +.. dropdown:: References + + * `"Multidimensional binary search trees used for associative searching" + `_, + Bentley, J.L., Communications of the ACM (1975) -|details-end| .. _ball_tree: @@ -345,156 +343,142 @@ neighbors searches are specified using the keyword ``algorithm = 'ball_tree'``, and are computed using the class :class:`BallTree`. Alternatively, the user can work with the :class:`BallTree` class directly. -|details-start| -**References** -|details-split| - - * `"Five Balltree Construction Algorithms" - `_, - Omohundro, S.M., International Computer Science Institute - Technical Report (1989) - -|details-end| - -|details-start| -**Choice of Nearest Neighbors Algorithm** -|details-split| - -The optimal algorithm for a given dataset is a complicated choice, and -depends on a number of factors: - -* number of samples :math:`N` (i.e. ``n_samples``) and dimensionality - :math:`D` (i.e. ``n_features``). - - * *Brute force* query time grows as :math:`O[D N]` - * *Ball tree* query time grows as approximately :math:`O[D \log(N)]` - * *KD tree* query time changes with :math:`D` in a way that is difficult - to precisely characterise. For small :math:`D` (less than 20 or so) - the cost is approximately :math:`O[D\log(N)]`, and the KD tree - query can be very efficient. - For larger :math:`D`, the cost increases to nearly :math:`O[DN]`, and - the overhead due to the tree - structure can lead to queries which are slower than brute force. - - For small data sets (:math:`N` less than 30 or so), :math:`\log(N)` is - comparable to :math:`N`, and brute force algorithms can be more efficient - than a tree-based approach. Both :class:`KDTree` and :class:`BallTree` - address this through providing a *leaf size* parameter: this controls the - number of samples at which a query switches to brute-force. This allows both - algorithms to approach the efficiency of a brute-force computation for small - :math:`N`. - -* data structure: *intrinsic dimensionality* of the data and/or *sparsity* - of the data. Intrinsic dimensionality refers to the dimension - :math:`d \le D` of a manifold on which the data lies, which can be linearly - or non-linearly embedded in the parameter space. Sparsity refers to the - degree to which the data fills the parameter space (this is to be - distinguished from the concept as used in "sparse" matrices. The data - matrix may have no zero entries, but the **structure** can still be - "sparse" in this sense). - - * *Brute force* query time is unchanged by data structure. - * *Ball tree* and *KD tree* query times can be greatly influenced - by data structure. In general, sparser data with a smaller intrinsic - dimensionality leads to faster query times. Because the KD tree - internal representation is aligned with the parameter axes, it will not - generally show as much improvement as ball tree for arbitrarily - structured data. - - Datasets used in machine learning tend to be very structured, and are - very well-suited for tree-based queries. - -* number of neighbors :math:`k` requested for a query point. - - * *Brute force* query time is largely unaffected by the value of :math:`k` - * *Ball tree* and *KD tree* query time will become slower as :math:`k` - increases. This is due to two effects: first, a larger :math:`k` leads - to the necessity to search a larger portion of the parameter space. - Second, using :math:`k > 1` requires internal queueing of results - as the tree is traversed. - - As :math:`k` becomes large compared to :math:`N`, the ability to prune - branches in a tree-based query is reduced. In this situation, Brute force - queries can be more efficient. - -* number of query points. Both the ball tree and the KD Tree - require a construction phase. The cost of this construction becomes - negligible when amortized over many queries. If only a small number of - queries will be performed, however, the construction can make up - a significant fraction of the total cost. If very few query points - will be required, brute force is better than a tree-based method. - -Currently, ``algorithm = 'auto'`` selects ``'brute'`` if any of the following -conditions are verified: - -* input data is sparse -* ``metric = 'precomputed'`` -* :math:`D > 15` -* :math:`k >= N/2` -* ``effective_metric_`` isn't in the ``VALID_METRICS`` list for either - ``'kd_tree'`` or ``'ball_tree'`` - -Otherwise, it selects the first out of ``'kd_tree'`` and ``'ball_tree'`` that -has ``effective_metric_`` in its ``VALID_METRICS`` list. This heuristic is -based on the following assumptions: - -* the number of query points is at least the same order as the number of - training points -* ``leaf_size`` is close to its default value of ``30`` -* when :math:`D > 15`, the intrinsic dimensionality of the data is generally - too high for tree-based methods - -|details-end| - -|details-start| -**Effect of ``leaf_size``** -|details-split| - -As noted above, for small sample sizes a brute force search can be more -efficient than a tree-based query. This fact is accounted for in the ball -tree and KD tree by internally switching to brute force searches within -leaf nodes. The level of this switch can be specified with the parameter -``leaf_size``. This parameter choice has many effects: - -**construction time** - A larger ``leaf_size`` leads to a faster tree construction time, because - fewer nodes need to be created - -**query time** - Both a large or small ``leaf_size`` can lead to suboptimal query cost. - For ``leaf_size`` approaching 1, the overhead involved in traversing - nodes can significantly slow query times. For ``leaf_size`` approaching - the size of the training set, queries become essentially brute force. - A good compromise between these is ``leaf_size = 30``, the default value - of the parameter. - -**memory** - As ``leaf_size`` increases, the memory required to store a tree structure - decreases. This is especially important in the case of ball tree, which - stores a :math:`D`-dimensional centroid for each node. The required - storage space for :class:`BallTree` is approximately ``1 / leaf_size`` times - the size of the training set. - -``leaf_size`` is not referenced for brute force queries. -|details-end| - -|details-start| -**Valid Metrics for Nearest Neighbor Algorithms** -|details-split| - -For a list of available metrics, see the documentation of the -:class:`~sklearn.metrics.DistanceMetric` class and the metrics listed in -`sklearn.metrics.pairwise.PAIRWISE_DISTANCE_FUNCTIONS`. Note that the "cosine" -metric uses :func:`~sklearn.metrics.pairwise.cosine_distances`. - -A list of valid metrics for any of the above algorithms can be obtained by using their -``valid_metric`` attribute. For example, valid metrics for ``KDTree`` can be generated by: - - >>> from sklearn.neighbors import KDTree - >>> print(sorted(KDTree.valid_metrics)) - ['chebyshev', 'cityblock', 'euclidean', 'infinity', 'l1', 'l2', 'manhattan', 'minkowski', 'p'] -|details-end| +.. dropdown:: References + + * `"Five Balltree Construction Algorithms" + `_, + Omohundro, S.M., International Computer Science Institute + Technical Report (1989) + +.. dropdown:: Choice of Nearest Neighbors Algorithm + + The optimal algorithm for a given dataset is a complicated choice, and + depends on a number of factors: + + * number of samples :math:`N` (i.e. ``n_samples``) and dimensionality + :math:`D` (i.e. ``n_features``). + + * *Brute force* query time grows as :math:`O[D N]` + * *Ball tree* query time grows as approximately :math:`O[D \log(N)]` + * *KD tree* query time changes with :math:`D` in a way that is difficult + to precisely characterise. For small :math:`D` (less than 20 or so) + the cost is approximately :math:`O[D\log(N)]`, and the KD tree + query can be very efficient. + For larger :math:`D`, the cost increases to nearly :math:`O[DN]`, and + the overhead due to the tree + structure can lead to queries which are slower than brute force. + + For small data sets (:math:`N` less than 30 or so), :math:`\log(N)` is + comparable to :math:`N`, and brute force algorithms can be more efficient + than a tree-based approach. Both :class:`KDTree` and :class:`BallTree` + address this through providing a *leaf size* parameter: this controls the + number of samples at which a query switches to brute-force. This allows both + algorithms to approach the efficiency of a brute-force computation for small + :math:`N`. + + * data structure: *intrinsic dimensionality* of the data and/or *sparsity* + of the data. Intrinsic dimensionality refers to the dimension + :math:`d \le D` of a manifold on which the data lies, which can be linearly + or non-linearly embedded in the parameter space. Sparsity refers to the + degree to which the data fills the parameter space (this is to be + distinguished from the concept as used in "sparse" matrices. The data + matrix may have no zero entries, but the **structure** can still be + "sparse" in this sense). + + * *Brute force* query time is unchanged by data structure. + * *Ball tree* and *KD tree* query times can be greatly influenced + by data structure. In general, sparser data with a smaller intrinsic + dimensionality leads to faster query times. Because the KD tree + internal representation is aligned with the parameter axes, it will not + generally show as much improvement as ball tree for arbitrarily + structured data. + + Datasets used in machine learning tend to be very structured, and are + very well-suited for tree-based queries. + + * number of neighbors :math:`k` requested for a query point. + + * *Brute force* query time is largely unaffected by the value of :math:`k` + * *Ball tree* and *KD tree* query time will become slower as :math:`k` + increases. This is due to two effects: first, a larger :math:`k` leads + to the necessity to search a larger portion of the parameter space. + Second, using :math:`k > 1` requires internal queueing of results + as the tree is traversed. + + As :math:`k` becomes large compared to :math:`N`, the ability to prune + branches in a tree-based query is reduced. In this situation, Brute force + queries can be more efficient. + + * number of query points. Both the ball tree and the KD Tree + require a construction phase. The cost of this construction becomes + negligible when amortized over many queries. If only a small number of + queries will be performed, however, the construction can make up + a significant fraction of the total cost. If very few query points + will be required, brute force is better than a tree-based method. + + Currently, ``algorithm = 'auto'`` selects ``'brute'`` if any of the following + conditions are verified: + + * input data is sparse + * ``metric = 'precomputed'`` + * :math:`D > 15` + * :math:`k >= N/2` + * ``effective_metric_`` isn't in the ``VALID_METRICS`` list for either + ``'kd_tree'`` or ``'ball_tree'`` + + Otherwise, it selects the first out of ``'kd_tree'`` and ``'ball_tree'`` that + has ``effective_metric_`` in its ``VALID_METRICS`` list. This heuristic is + based on the following assumptions: + + * the number of query points is at least the same order as the number of + training points + * ``leaf_size`` is close to its default value of ``30`` + * when :math:`D > 15`, the intrinsic dimensionality of the data is generally + too high for tree-based methods + +.. dropdown:: Effect of ``leaf_size`` + + As noted above, for small sample sizes a brute force search can be more + efficient than a tree-based query. This fact is accounted for in the ball + tree and KD tree by internally switching to brute force searches within + leaf nodes. The level of this switch can be specified with the parameter + ``leaf_size``. This parameter choice has many effects: + + **construction time** + A larger ``leaf_size`` leads to a faster tree construction time, because + fewer nodes need to be created + + **query time** + Both a large or small ``leaf_size`` can lead to suboptimal query cost. + For ``leaf_size`` approaching 1, the overhead involved in traversing + nodes can significantly slow query times. For ``leaf_size`` approaching + the size of the training set, queries become essentially brute force. + A good compromise between these is ``leaf_size = 30``, the default value + of the parameter. + + **memory** + As ``leaf_size`` increases, the memory required to store a tree structure + decreases. This is especially important in the case of ball tree, which + stores a :math:`D`-dimensional centroid for each node. The required + storage space for :class:`BallTree` is approximately ``1 / leaf_size`` times + the size of the training set. + + ``leaf_size`` is not referenced for brute force queries. + +.. dropdown:: Valid Metrics for Nearest Neighbor Algorithms + + For a list of available metrics, see the documentation of the + :class:`~sklearn.metrics.DistanceMetric` class and the metrics listed in + `sklearn.metrics.pairwise.PAIRWISE_DISTANCE_FUNCTIONS`. Note that the "cosine" + metric uses :func:`~sklearn.metrics.pairwise.cosine_distances`. + + A list of valid metrics for any of the above algorithms can be obtained by using their + ``valid_metric`` attribute. For example, valid metrics for ``KDTree`` can be generated by: + + >>> from sklearn.neighbors import KDTree + >>> print(sorted(KDTree.valid_metrics)) + ['chebyshev', 'cityblock', 'euclidean', 'infinity', 'l1', 'l2', 'manhattan', 'minkowski', 'p'] .. _nearest_centroid_classifier: @@ -547,10 +531,10 @@ the model from 0.81 to 0.82. .. centered:: |nearest_centroid_1| |nearest_centroid_2| -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_neighbors_plot_nearest_centroid.py`: an example of - classification using nearest centroid with different shrink thresholds. +* :ref:`sphx_glr_auto_examples_neighbors_plot_nearest_centroid.py`: an example of + classification using nearest centroid with different shrink thresholds. .. _neighbors_transformer: @@ -635,17 +619,17 @@ implementation with special data types. The precomputed neighbors include one extra neighbor in a custom nearest neighbors estimator, since unnecessary neighbors will be filtered by following estimators. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_neighbors_approximate_nearest_neighbors.py`: - an example of pipelining :class:`KNeighborsTransformer` and - :class:`~sklearn.manifold.TSNE`. Also proposes two custom nearest neighbors - estimators based on external packages. +* :ref:`sphx_glr_auto_examples_neighbors_approximate_nearest_neighbors.py`: + an example of pipelining :class:`KNeighborsTransformer` and + :class:`~sklearn.manifold.TSNE`. Also proposes two custom nearest neighbors + estimators based on external packages. - * :ref:`sphx_glr_auto_examples_neighbors_plot_caching_nearest_neighbors.py`: - an example of pipelining :class:`KNeighborsTransformer` and - :class:`KNeighborsClassifier` to enable caching of the neighbors graph - during a hyper-parameter grid-search. +* :ref:`sphx_glr_auto_examples_neighbors_plot_caching_nearest_neighbors.py`: + an example of pipelining :class:`KNeighborsTransformer` and + :class:`KNeighborsClassifier` to enable caching of the neighbors graph + during a hyper-parameter grid-search. .. _nca: @@ -769,11 +753,11 @@ by each method. Each data sample belongs to one of 10 classes. .. centered:: |nca_dim_reduction_1| |nca_dim_reduction_2| |nca_dim_reduction_3| -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_neighbors_plot_nca_classification.py` - * :ref:`sphx_glr_auto_examples_neighbors_plot_nca_dim_reduction.py` - * :ref:`sphx_glr_auto_examples_manifold_plot_lle_digits.py` +* :ref:`sphx_glr_auto_examples_neighbors_plot_nca_classification.py` +* :ref:`sphx_glr_auto_examples_neighbors_plot_nca_dim_reduction.py` +* :ref:`sphx_glr_auto_examples_manifold_plot_lle_digits.py` .. _nca_mathematical_formulation: @@ -806,20 +790,17 @@ space: p_{i j} = \frac{\exp(-||L x_i - L x_j||^2)}{\sum\limits_{k \ne i} {\exp{-(||L x_i - L x_k||^2)}}} , \quad p_{i i} = 0 -|details-start| -**Mahalanobis distance** -|details-split| +.. dropdown:: Mahalanobis distance -NCA can be seen as learning a (squared) Mahalanobis distance metric: + NCA can be seen as learning a (squared) Mahalanobis distance metric: -.. math:: + .. math:: - || L(x_i - x_j)||^2 = (x_i - x_j)^TM(x_i - x_j), + || L(x_i - x_j)||^2 = (x_i - x_j)^TM(x_i - x_j), -where :math:`M = L^T L` is a symmetric positive semi-definite matrix of size -``(n_features, n_features)``. + where :math:`M = L^T L` is a symmetric positive semi-definite matrix of size + ``(n_features, n_features)``. -|details-end| Implementation -------------- @@ -851,14 +832,12 @@ complexity equals ``n_components * n_features * n_samples_test``. There is no added space complexity in the operation. -.. topic:: References: - - .. [1] `"Neighbourhood Components Analysis" - `_, - J. Goldberger, S. Roweis, G. Hinton, R. Salakhutdinov, Advances in - Neural Information Processing Systems, Vol. 17, May 2005, pp. 513-520. +.. rubric:: References - `Wikipedia entry on Neighborhood Components Analysis - `_ +.. [1] `"Neighbourhood Components Analysis" + `_, + J. Goldberger, S. Roweis, G. Hinton, R. Salakhutdinov, Advances in + Neural Information Processing Systems, Vol. 17, May 2005, pp. 513-520. -|details-end| +* `Wikipedia entry on Neighborhood Components Analysis + `_ diff --git a/doc/modules/neural_networks_supervised.rst b/doc/modules/neural_networks_supervised.rst index 7ee2387068c81..5c6baecb7e2ff 100644 --- a/doc/modules/neural_networks_supervised.rst +++ b/doc/modules/neural_networks_supervised.rst @@ -49,33 +49,30 @@ The module contains the public attributes ``coefs_`` and ``intercepts_``. :math:`i+1`. ``intercepts_`` is a list of bias vectors, where the vector at index :math:`i` represents the bias values added to layer :math:`i+1`. -|details-start| -**Advantages and disadvantages of Multi-layer Perceptron** -|details-split| +.. dropdown:: Advantages and disadvantages of Multi-layer Perceptron -The advantages of Multi-layer Perceptron are: + The advantages of Multi-layer Perceptron are: -+ Capability to learn non-linear models. + + Capability to learn non-linear models. -+ Capability to learn models in real-time (on-line learning) - using ``partial_fit``. + + Capability to learn models in real-time (on-line learning) + using ``partial_fit``. -The disadvantages of Multi-layer Perceptron (MLP) include: + The disadvantages of Multi-layer Perceptron (MLP) include: -+ MLP with hidden layers have a non-convex loss function where there exists - more than one local minimum. Therefore different random weight - initializations can lead to different validation accuracy. + + MLP with hidden layers have a non-convex loss function where there exists + more than one local minimum. Therefore different random weight + initializations can lead to different validation accuracy. -+ MLP requires tuning a number of hyperparameters such as the number of - hidden neurons, layers, and iterations. + + MLP requires tuning a number of hyperparameters such as the number of + hidden neurons, layers, and iterations. -+ MLP is sensitive to feature scaling. + + MLP is sensitive to feature scaling. -Please see :ref:`Tips on Practical Use ` section that addresses -some of these disadvantages. + Please see :ref:`Tips on Practical Use ` section that addresses + some of these disadvantages. -|details-end| Classification ============== @@ -148,11 +145,11 @@ indices where the value is `1` represents the assigned classes of that sample:: See the examples below and the docstring of :meth:`MLPClassifier.fit` for further information. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_neural_networks_plot_mlp_training_curves.py` - * See :ref:`sphx_glr_auto_examples_neural_networks_plot_mnist_filters.py` for - visualized representation of trained weights. +* :ref:`sphx_glr_auto_examples_neural_networks_plot_mlp_training_curves.py` +* See :ref:`sphx_glr_auto_examples_neural_networks_plot_mnist_filters.py` for + visualized representation of trained weights. Regression ========== @@ -181,9 +178,9 @@ decision function with value of alpha. See the examples below for further information. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_neural_networks_plot_mlp_alpha.py` +* :ref:`sphx_glr_auto_examples_neural_networks_plot_mlp_alpha.py` Algorithms ========== @@ -234,83 +231,78 @@ of iterations. Since backpropagation has a high time complexity, it is advisable to start with smaller number of hidden neurons and few hidden layers for training. -|details-start| -Mathematical formulation -|details-split| +.. dropdown:: Mathematical formulation -Given a set of training examples :math:`(x_1, y_1), (x_2, y_2), \ldots, (x_n, y_n)` -where :math:`x_i \in \mathbf{R}^n` and :math:`y_i \in \{0, 1\}`, a one hidden -layer one hidden neuron MLP learns the function :math:`f(x) = W_2 g(W_1^T x + b_1) + b_2` -where :math:`W_1 \in \mathbf{R}^m` and :math:`W_2, b_1, b_2 \in \mathbf{R}` are -model parameters. :math:`W_1, W_2` represent the weights of the input layer and -hidden layer, respectively; and :math:`b_1, b_2` represent the bias added to -the hidden layer and the output layer, respectively. -:math:`g(\cdot) : R \rightarrow R` is the activation function, set by default as -the hyperbolic tan. It is given as, + Given a set of training examples :math:`(x_1, y_1), (x_2, y_2), \ldots, (x_n, y_n)` + where :math:`x_i \in \mathbf{R}^n` and :math:`y_i \in \{0, 1\}`, a one hidden + layer one hidden neuron MLP learns the function :math:`f(x) = W_2 g(W_1^T x + b_1) + b_2` + where :math:`W_1 \in \mathbf{R}^m` and :math:`W_2, b_1, b_2 \in \mathbf{R}` are + model parameters. :math:`W_1, W_2` represent the weights of the input layer and + hidden layer, respectively; and :math:`b_1, b_2` represent the bias added to + the hidden layer and the output layer, respectively. + :math:`g(\cdot) : R \rightarrow R` is the activation function, set by default as + the hyperbolic tan. It is given as, -.. math:: - g(z)= \frac{e^z-e^{-z}}{e^z+e^{-z}} - -For binary classification, :math:`f(x)` passes through the logistic function -:math:`g(z)=1/(1+e^{-z})` to obtain output values between zero and one. A -threshold, set to 0.5, would assign samples of outputs larger or equal 0.5 -to the positive class, and the rest to the negative class. + .. math:: + g(z)= \frac{e^z-e^{-z}}{e^z+e^{-z}} -If there are more than two classes, :math:`f(x)` itself would be a vector of -size (n_classes,). Instead of passing through logistic function, it passes -through the softmax function, which is written as, - -.. math:: - \text{softmax}(z)_i = \frac{\exp(z_i)}{\sum_{l=1}^k\exp(z_l)} + For binary classification, :math:`f(x)` passes through the logistic function + :math:`g(z)=1/(1+e^{-z})` to obtain output values between zero and one. A + threshold, set to 0.5, would assign samples of outputs larger or equal 0.5 + to the positive class, and the rest to the negative class. -where :math:`z_i` represents the :math:`i` th element of the input to softmax, -which corresponds to class :math:`i`, and :math:`K` is the number of classes. -The result is a vector containing the probabilities that sample :math:`x` -belong to each class. The output is the class with the highest probability. + If there are more than two classes, :math:`f(x)` itself would be a vector of + size (n_classes,). Instead of passing through logistic function, it passes + through the softmax function, which is written as, -In regression, the output remains as :math:`f(x)`; therefore, output activation -function is just the identity function. + .. math:: + \text{softmax}(z)_i = \frac{\exp(z_i)}{\sum_{l=1}^k\exp(z_l)} -MLP uses different loss functions depending on the problem type. The loss -function for classification is Average Cross-Entropy, which in binary case is -given as, + where :math:`z_i` represents the :math:`i` th element of the input to softmax, + which corresponds to class :math:`i`, and :math:`K` is the number of classes. + The result is a vector containing the probabilities that sample :math:`x` + belong to each class. The output is the class with the highest probability. -.. math:: + In regression, the output remains as :math:`f(x)`; therefore, output activation + function is just the identity function. - Loss(\hat{y},y,W) = -\dfrac{1}{n}\sum_{i=0}^n(y_i \ln {\hat{y_i}} + (1-y_i) \ln{(1-\hat{y_i})}) + \dfrac{\alpha}{2n} ||W||_2^2 + MLP uses different loss functions depending on the problem type. The loss + function for classification is Average Cross-Entropy, which in binary case is + given as, -where :math:`\alpha ||W||_2^2` is an L2-regularization term (aka penalty) -that penalizes complex models; and :math:`\alpha > 0` is a non-negative -hyperparameter that controls the magnitude of the penalty. + .. math:: -For regression, MLP uses the Mean Square Error loss function; written as, + Loss(\hat{y},y,W) = -\dfrac{1}{n}\sum_{i=0}^n(y_i \ln {\hat{y_i}} + (1-y_i) \ln{(1-\hat{y_i})}) + \dfrac{\alpha}{2n} ||W||_2^2 -.. math:: + where :math:`\alpha ||W||_2^2` is an L2-regularization term (aka penalty) + that penalizes complex models; and :math:`\alpha > 0` is a non-negative + hyperparameter that controls the magnitude of the penalty. - Loss(\hat{y},y,W) = \frac{1}{2n}\sum_{i=0}^n||\hat{y}_i - y_i ||_2^2 + \frac{\alpha}{2n} ||W||_2^2 + For regression, MLP uses the Mean Square Error loss function; written as, + .. math:: -Starting from initial random weights, multi-layer perceptron (MLP) minimizes -the loss function by repeatedly updating these weights. After computing the -loss, a backward pass propagates it from the output layer to the previous -layers, providing each weight parameter with an update value meant to decrease -the loss. + Loss(\hat{y},y,W) = \frac{1}{2n}\sum_{i=0}^n||\hat{y}_i - y_i ||_2^2 + \frac{\alpha}{2n} ||W||_2^2 -In gradient descent, the gradient :math:`\nabla Loss_{W}` of the loss with respect -to the weights is computed and deducted from :math:`W`. -More formally, this is expressed as, + Starting from initial random weights, multi-layer perceptron (MLP) minimizes + the loss function by repeatedly updating these weights. After computing the + loss, a backward pass propagates it from the output layer to the previous + layers, providing each weight parameter with an update value meant to decrease + the loss. -.. math:: - W^{i+1} = W^i - \epsilon \nabla {Loss}_{W}^{i} + In gradient descent, the gradient :math:`\nabla Loss_{W}` of the loss with respect + to the weights is computed and deducted from :math:`W`. + More formally, this is expressed as, + .. math:: + W^{i+1} = W^i - \epsilon \nabla {Loss}_{W}^{i} -where :math:`i` is the iteration step, and :math:`\epsilon` is the learning rate -with a value larger than 0. + where :math:`i` is the iteration step, and :math:`\epsilon` is the learning rate + with a value larger than 0. -The algorithm stops when it reaches a preset maximum number of iterations; or -when the improvement in loss is below a certain, small number. + The algorithm stops when it reaches a preset maximum number of iterations; or + when the improvement in loss is below a certain, small number. -|details-end| .. _mlp_tips: @@ -361,25 +353,19 @@ or want to do additional monitoring, using ``warm_start=True`` and ... # additional monitoring / inspection MLPClassifier(... -|details-start| -**References** -|details-split| - - * `"Learning representations by back-propagating errors." - `_ - Rumelhart, David E., Geoffrey E. Hinton, and Ronald J. Williams. +.. dropdown:: References - * `"Stochastic Gradient Descent" `_ L. Bottou - Website, 2010. + * `"Learning representations by back-propagating errors." + `_ + Rumelhart, David E., Geoffrey E. Hinton, and Ronald J. Williams. - * `"Backpropagation" `_ - Andrew Ng, Jiquan Ngiam, Chuan Yu Foo, Yifan Mai, Caroline Suen - Website, 2011. + * `"Stochastic Gradient Descent" `_ L. Bottou - Website, 2010. - * `"Efficient BackProp" `_ - Y. LeCun, L. Bottou, G. Orr, K. Müller - In Neural Networks: Tricks - of the Trade 1998. + * `"Backpropagation" `_ + Andrew Ng, Jiquan Ngiam, Chuan Yu Foo, Yifan Mai, Caroline Suen - Website, 2011. - * :arxiv:`"Adam: A method for stochastic optimization." - <1412.6980>` - Kingma, Diederik, and Jimmy Ba (2014) + * `"Efficient BackProp" `_ + Y. LeCun, L. Bottou, G. Orr, K. Müller - In Neural Networks: Tricks of the Trade 1998. -|details-end| + * :arxiv:`"Adam: A method for stochastic optimization." <1412.6980>` + Kingma, Diederik, and Jimmy Ba (2014) diff --git a/doc/modules/neural_networks_unsupervised.rst b/doc/modules/neural_networks_unsupervised.rst index aca56ae8aaf2e..7f6c0016d183b 100644 --- a/doc/modules/neural_networks_unsupervised.rst +++ b/doc/modules/neural_networks_unsupervised.rst @@ -37,9 +37,9 @@ weights of independent RBMs. This method is known as unsupervised pre-training. :align: center :scale: 100% -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_neural_networks_plot_rbm_logistic_classification.py` +* :ref:`sphx_glr_auto_examples_neural_networks_plot_rbm_logistic_classification.py` Graphical model and parametrization @@ -57,7 +57,7 @@ visible and hidden unit, omitted from the image for simplicity. The energy function measures the quality of a joint assignment: -.. math:: +.. math:: E(\mathbf{v}, \mathbf{h}) = -\sum_i \sum_j w_{ij}v_ih_j - \sum_i b_iv_i - \sum_j c_jh_j @@ -149,13 +149,13 @@ step, in PCD we keep a number of chains (fantasy particles) that are updated :math:`k` Gibbs steps after each weight update. This allows the particles to explore the space more thoroughly. -.. topic:: References: +.. rubric:: References - * `"A fast learning algorithm for deep belief nets" - `_ - G. Hinton, S. Osindero, Y.-W. Teh, 2006 +* `"A fast learning algorithm for deep belief nets" + `_, + G. Hinton, S. Osindero, Y.-W. Teh, 2006 - * `"Training Restricted Boltzmann Machines using Approximations to - the Likelihood Gradient" - `_ - T. Tieleman, 2008 +* `"Training Restricted Boltzmann Machines using Approximations to + the Likelihood Gradient" + `_, + T. Tieleman, 2008 diff --git a/doc/modules/outlier_detection.rst b/doc/modules/outlier_detection.rst index d003b645eb19c..0c6891ed119bd 100644 --- a/doc/modules/outlier_detection.rst +++ b/doc/modules/outlier_detection.rst @@ -123,19 +123,19 @@ refer to the example :ref:`sphx_glr_auto_examples_miscellaneous_plot_anomaly_comparison.py` and the sections hereunder. -.. topic:: Examples: +.. rubric:: Examples - * See :ref:`sphx_glr_auto_examples_miscellaneous_plot_anomaly_comparison.py` - for a comparison of the :class:`svm.OneClassSVM`, the - :class:`ensemble.IsolationForest`, the - :class:`neighbors.LocalOutlierFactor` and - :class:`covariance.EllipticEnvelope`. +* See :ref:`sphx_glr_auto_examples_miscellaneous_plot_anomaly_comparison.py` + for a comparison of the :class:`svm.OneClassSVM`, the + :class:`ensemble.IsolationForest`, the + :class:`neighbors.LocalOutlierFactor` and + :class:`covariance.EllipticEnvelope`. - * See :ref:`sphx_glr_auto_examples_miscellaneous_plot_outlier_detection_bench.py` - for an example showing how to evaluate outlier detection estimators, - the :class:`neighbors.LocalOutlierFactor` and the - :class:`ensemble.IsolationForest`, using ROC curves from - :class:`metrics.RocCurveDisplay`. +* See :ref:`sphx_glr_auto_examples_miscellaneous_plot_outlier_detection_bench.py` + for an example showing how to evaluate outlier detection estimators, + the :class:`neighbors.LocalOutlierFactor` and the + :class:`ensemble.IsolationForest`, using ROC curves from + :class:`metrics.RocCurveDisplay`. Novelty Detection ================= @@ -167,18 +167,18 @@ implementation. The `nu` parameter, also known as the margin of the One-Class SVM, corresponds to the probability of finding a new, but regular, observation outside the frontier. -.. topic:: References: +.. rubric:: References - * `Estimating the support of a high-dimensional distribution - `_ - Schölkopf, Bernhard, et al. Neural computation 13.7 (2001): 1443-1471. +* `Estimating the support of a high-dimensional distribution + `_ + Schölkopf, Bernhard, et al. Neural computation 13.7 (2001): 1443-1471. -.. topic:: Examples: +.. rubric:: Examples - * See :ref:`sphx_glr_auto_examples_svm_plot_oneclass.py` for visualizing the - frontier learned around some data by a - :class:`svm.OneClassSVM` object. - * :ref:`sphx_glr_auto_examples_applications_plot_species_distribution_modeling.py` +* See :ref:`sphx_glr_auto_examples_svm_plot_oneclass.py` for visualizing the + frontier learned around some data by a :class:`svm.OneClassSVM` object. + +* :ref:`sphx_glr_auto_examples_applications_plot_species_distribution_modeling.py` .. figure:: ../auto_examples/svm/images/sphx_glr_plot_oneclass_001.png :target: ../auto_examples/svm/plot_oneclass.html @@ -196,11 +196,11 @@ approximate the solution of a kernelized :class:`svm.OneClassSVM` whose complexity is at best quadratic in the number of samples. See section :ref:`sgd_online_one_class_svm` for more details. -.. topic:: Examples: +.. rubric:: Examples - * See :ref:`sphx_glr_auto_examples_linear_model_plot_sgdocsvm_vs_ocsvm.py` - for an illustration of the approximation of a kernelized One-Class SVM - with the `linear_model.SGDOneClassSVM` combined with kernel approximation. +* See :ref:`sphx_glr_auto_examples_linear_model_plot_sgdocsvm_vs_ocsvm.py` + for an illustration of the approximation of a kernelized One-Class SVM + with the `linear_model.SGDOneClassSVM` combined with kernel approximation. Outlier Detection @@ -238,18 +238,18 @@ This strategy is illustrated below. :align: center :scale: 75% -.. topic:: Examples: +.. rubric:: Examples - * See :ref:`sphx_glr_auto_examples_covariance_plot_mahalanobis_distances.py` for - an illustration of the difference between using a standard - (:class:`covariance.EmpiricalCovariance`) or a robust estimate - (:class:`covariance.MinCovDet`) of location and covariance to - assess the degree of outlyingness of an observation. +* See :ref:`sphx_glr_auto_examples_covariance_plot_mahalanobis_distances.py` for + an illustration of the difference between using a standard + (:class:`covariance.EmpiricalCovariance`) or a robust estimate + (:class:`covariance.MinCovDet`) of location and covariance to + assess the degree of outlyingness of an observation. -.. topic:: References: +.. rubric:: References - * Rousseeuw, P.J., Van Driessen, K. "A fast algorithm for the minimum - covariance determinant estimator" Technometrics 41(3), 212 (1999) +* Rousseeuw, P.J., Van Driessen, K. "A fast algorithm for the minimum + covariance determinant estimator" Technometrics 41(3), 212 (1999) .. _isolation_forest: @@ -299,22 +299,22 @@ allows you to add more trees to an already fitted model:: >>> clf.set_params(n_estimators=20) # add 10 more trees # doctest: +SKIP >>> clf.fit(X) # fit the added trees # doctest: +SKIP -.. topic:: Examples: +.. rubric:: Examples - * See :ref:`sphx_glr_auto_examples_ensemble_plot_isolation_forest.py` for - an illustration of the use of IsolationForest. +* See :ref:`sphx_glr_auto_examples_ensemble_plot_isolation_forest.py` for + an illustration of the use of IsolationForest. - * See :ref:`sphx_glr_auto_examples_miscellaneous_plot_anomaly_comparison.py` - for a comparison of :class:`ensemble.IsolationForest` with - :class:`neighbors.LocalOutlierFactor`, - :class:`svm.OneClassSVM` (tuned to perform like an outlier detection - method), :class:`linear_model.SGDOneClassSVM`, and a covariance-based - outlier detection with :class:`covariance.EllipticEnvelope`. +* See :ref:`sphx_glr_auto_examples_miscellaneous_plot_anomaly_comparison.py` + for a comparison of :class:`ensemble.IsolationForest` with + :class:`neighbors.LocalOutlierFactor`, + :class:`svm.OneClassSVM` (tuned to perform like an outlier detection + method), :class:`linear_model.SGDOneClassSVM`, and a covariance-based + outlier detection with :class:`covariance.EllipticEnvelope`. -.. topic:: References: +.. rubric:: References - * Liu, Fei Tony, Ting, Kai Ming and Zhou, Zhi-Hua. "Isolation forest." - Data Mining, 2008. ICDM'08. Eighth IEEE International Conference on. +* Liu, Fei Tony, Ting, Kai Ming and Zhou, Zhi-Hua. "Isolation forest." + Data Mining, 2008. ICDM'08. Eighth IEEE International Conference on. .. _local_outlier_factor: @@ -370,20 +370,20 @@ This strategy is illustrated below. :align: center :scale: 75% -.. topic:: Examples: +.. rubric:: Examples - * See :ref:`sphx_glr_auto_examples_neighbors_plot_lof_outlier_detection.py` - for an illustration of the use of :class:`neighbors.LocalOutlierFactor`. +* See :ref:`sphx_glr_auto_examples_neighbors_plot_lof_outlier_detection.py` + for an illustration of the use of :class:`neighbors.LocalOutlierFactor`. - * See :ref:`sphx_glr_auto_examples_miscellaneous_plot_anomaly_comparison.py` - for a comparison with other anomaly detection methods. +* See :ref:`sphx_glr_auto_examples_miscellaneous_plot_anomaly_comparison.py` + for a comparison with other anomaly detection methods. -.. topic:: References: +.. rubric:: References - * Breunig, Kriegel, Ng, and Sander (2000) - `LOF: identifying density-based local outliers. - `_ - Proc. ACM SIGMOD +* Breunig, Kriegel, Ng, and Sander (2000) + `LOF: identifying density-based local outliers. + `_ + Proc. ACM SIGMOD .. _novelty_with_lof: diff --git a/doc/modules/partial_dependence.rst b/doc/modules/partial_dependence.rst index 94f7206140b90..40f691a9e6dcc 100644 --- a/doc/modules/partial_dependence.rst +++ b/doc/modules/partial_dependence.rst @@ -79,25 +79,21 @@ parameter takes a list of indices, names of the categorical features or a boolea mask. The graphical representation of partial dependence for categorical features is a bar plot or a 2D heatmap. -|details-start| -**PDPs for multi-class classification** -|details-split| - -For multi-class classification, you need to set the class label for which -the PDPs should be created via the ``target`` argument:: - - >>> from sklearn.datasets import load_iris - >>> iris = load_iris() - >>> mc_clf = GradientBoostingClassifier(n_estimators=10, - ... max_depth=1).fit(iris.data, iris.target) - >>> features = [3, 2, (3, 2)] - >>> PartialDependenceDisplay.from_estimator(mc_clf, X, features, target=0) - <...> +.. dropdown:: PDPs for multi-class classification + + For multi-class classification, you need to set the class label for which + the PDPs should be created via the ``target`` argument:: -The same parameter ``target`` is used to specify the target in multi-output -regression settings. + >>> from sklearn.datasets import load_iris + >>> iris = load_iris() + >>> mc_clf = GradientBoostingClassifier(n_estimators=10, + ... max_depth=1).fit(iris.data, iris.target) + >>> features = [3, 2, (3, 2)] + >>> PartialDependenceDisplay.from_estimator(mc_clf, X, features, target=0) + <...> -|details-end| + The same parameter ``target`` is used to specify the target in multi-output + regression settings. If you need the raw values of the partial dependence function rather than the plots, you can use the @@ -266,9 +262,9 @@ estimators that support it, and 'brute' is used for the rest. interpreting PDPs is that the features should be independent. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_inspection_plot_partial_dependence.py` +* :ref:`sphx_glr_auto_examples_inspection_plot_partial_dependence.py` .. rubric:: Footnotes @@ -276,21 +272,20 @@ estimators that support it, and 'brute' is used for the rest. class (the positive class for binary classification), or the decision function. -.. topic:: References +.. rubric:: References - .. [H2009] T. Hastie, R. Tibshirani and J. Friedman, - `The Elements of Statistical Learning - `_, - Second Edition, Section 10.13.2, Springer, 2009. +.. [H2009] T. Hastie, R. Tibshirani and J. Friedman, + `The Elements of Statistical Learning + `_, + Second Edition, Section 10.13.2, Springer, 2009. - .. [M2019] C. Molnar, - `Interpretable Machine Learning - `_, - Section 5.1, 2019. +.. [M2019] C. Molnar, + `Interpretable Machine Learning + `_, + Section 5.1, 2019. - .. [G2015] :arxiv:`A. Goldstein, A. Kapelner, J. Bleich, and E. Pitkin, - "Peeking Inside the Black Box: Visualizing Statistical - Learning With Plots of Individual Conditional Expectation" - Journal of Computational and Graphical Statistics, - 24(1): 44-65, Springer, 2015. - <1309.6392>` +.. [G2015] :arxiv:`A. Goldstein, A. Kapelner, J. Bleich, and E. Pitkin, + "Peeking Inside the Black Box: Visualizing Statistical + Learning With Plots of Individual Conditional Expectation" + Journal of Computational and Graphical Statistics, + 24(1): 44-65, Springer, 2015. <1309.6392>` diff --git a/doc/modules/permutation_importance.rst b/doc/modules/permutation_importance.rst index 368c6a6409aa0..12a20a8bcaa6c 100644 --- a/doc/modules/permutation_importance.rst +++ b/doc/modules/permutation_importance.rst @@ -110,48 +110,44 @@ which is more computationally efficient than sequentially calling :func:`permutation_importance` several times with a different scorer, as it reuses model predictions. -|details-start| -**Example of permutation feature importance using multiple scorers** -|details-split| - -In the example below we use a list of metrics, but more input formats are -possible, as documented in :ref:`multimetric_scoring`. - - >>> scoring = ['r2', 'neg_mean_absolute_percentage_error', 'neg_mean_squared_error'] - >>> r_multi = permutation_importance( - ... model, X_val, y_val, n_repeats=30, random_state=0, scoring=scoring) - ... - >>> for metric in r_multi: - ... print(f"{metric}") - ... r = r_multi[metric] - ... for i in r.importances_mean.argsort()[::-1]: - ... if r.importances_mean[i] - 2 * r.importances_std[i] > 0: - ... print(f" {diabetes.feature_names[i]:<8}" - ... f"{r.importances_mean[i]:.3f}" - ... f" +/- {r.importances_std[i]:.3f}") - ... - r2 - s5 0.204 +/- 0.050 - bmi 0.176 +/- 0.048 - bp 0.088 +/- 0.033 - sex 0.056 +/- 0.023 - neg_mean_absolute_percentage_error - s5 0.081 +/- 0.020 - bmi 0.064 +/- 0.015 - bp 0.029 +/- 0.010 - neg_mean_squared_error - s5 1013.866 +/- 246.445 - bmi 872.726 +/- 240.298 - bp 438.663 +/- 163.022 - sex 277.376 +/- 115.123 - -The ranking of the features is approximately the same for different metrics even -if the scales of the importance values are very different. However, this is not -guaranteed and different metrics might lead to significantly different feature -importances, in particular for models trained for imbalanced classification problems, -for which **the choice of the classification metric can be critical**. - -|details-end| +.. dropdown:: Example of permutation feature importance using multiple scorers + + In the example below we use a list of metrics, but more input formats are + possible, as documented in :ref:`multimetric_scoring`. + + >>> scoring = ['r2', 'neg_mean_absolute_percentage_error', 'neg_mean_squared_error'] + >>> r_multi = permutation_importance( + ... model, X_val, y_val, n_repeats=30, random_state=0, scoring=scoring) + ... + >>> for metric in r_multi: + ... print(f"{metric}") + ... r = r_multi[metric] + ... for i in r.importances_mean.argsort()[::-1]: + ... if r.importances_mean[i] - 2 * r.importances_std[i] > 0: + ... print(f" {diabetes.feature_names[i]:<8}" + ... f"{r.importances_mean[i]:.3f}" + ... f" +/- {r.importances_std[i]:.3f}") + ... + r2 + s5 0.204 +/- 0.050 + bmi 0.176 +/- 0.048 + bp 0.088 +/- 0.033 + sex 0.056 +/- 0.023 + neg_mean_absolute_percentage_error + s5 0.081 +/- 0.020 + bmi 0.064 +/- 0.015 + bp 0.029 +/- 0.010 + neg_mean_squared_error + s5 1013.866 +/- 246.445 + bmi 872.726 +/- 240.298 + bp 438.663 +/- 163.022 + sex 277.376 +/- 115.123 + + The ranking of the features is approximately the same for different metrics even + if the scales of the importance values are very different. However, this is not + guaranteed and different metrics might lead to significantly different feature + importances, in particular for models trained for imbalanced classification problems, + for which **the choice of the classification metric can be critical**. Outline of the permutation importance algorithm ----------------------------------------------- @@ -228,12 +224,12 @@ keep one feature from each cluster. For more details on such strategy, see the example :ref:`sphx_glr_auto_examples_inspection_plot_permutation_importance_multicollinear.py`. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_inspection_plot_permutation_importance.py` - * :ref:`sphx_glr_auto_examples_inspection_plot_permutation_importance_multicollinear.py` +* :ref:`sphx_glr_auto_examples_inspection_plot_permutation_importance.py` +* :ref:`sphx_glr_auto_examples_inspection_plot_permutation_importance_multicollinear.py` -.. topic:: References: +.. rubric:: References - .. [1] L. Breiman, :doi:`"Random Forests" <10.1023/A:1010933404324>`, - Machine Learning, 45(1), 5-32, 2001. +.. [1] L. Breiman, :doi:`"Random Forests" <10.1023/A:1010933404324>`, + Machine Learning, 45(1), 5-32, 2001. diff --git a/doc/modules/preprocessing.rst b/doc/modules/preprocessing.rst index 99678f2b3e45b..90889ad5af7e0 100644 --- a/doc/modules/preprocessing.rst +++ b/doc/modules/preprocessing.rst @@ -219,28 +219,22 @@ of the data is likely to not work very well. In these cases, you can use :class:`RobustScaler` as a drop-in replacement instead. It uses more robust estimates for the center and range of your data. -|details-start| -**References** -|details-split| -Further discussion on the importance of centering and scaling data is -available on this FAQ: `Should I normalize/standardize/rescale the data? -`_ +.. dropdown:: References -|details-end| + Further discussion on the importance of centering and scaling data is + available on this FAQ: `Should I normalize/standardize/rescale the data? + `_ -|details-start| -**Scaling vs Whitening** -|details-split| +.. dropdown:: Scaling vs Whitening -It is sometimes not enough to center and scale the features -independently, since a downstream model can further make some assumption -on the linear independence of the features. + It is sometimes not enough to center and scale the features + independently, since a downstream model can further make some assumption + on the linear independence of the features. -To address this issue you can use :class:`~sklearn.decomposition.PCA` with -``whiten=True`` to further remove the linear correlation across features. + To address this issue you can use :class:`~sklearn.decomposition.PCA` with + ``whiten=True`` to further remove the linear correlation across features. -|details-end| .. _kernel_centering: @@ -255,63 +249,59 @@ followed by the removal of the mean in that space. In other words, :class:`KernelCenterer` computes the centered Gram matrix associated to a positive semidefinite kernel :math:`K`. -|details-start| -**Mathematical formulation** -|details-split| +.. dropdown:: Mathematical formulation -We can have a look at the mathematical formulation now that we have the -intuition. Let :math:`K` be a kernel matrix of shape `(n_samples, n_samples)` -computed from :math:`X`, a data matrix of shape `(n_samples, n_features)`, -during the `fit` step. :math:`K` is defined by + We can have a look at the mathematical formulation now that we have the + intuition. Let :math:`K` be a kernel matrix of shape `(n_samples, n_samples)` + computed from :math:`X`, a data matrix of shape `(n_samples, n_features)`, + during the `fit` step. :math:`K` is defined by -.. math:: - K(X, X) = \phi(X) . \phi(X)^{T} + .. math:: + K(X, X) = \phi(X) . \phi(X)^{T} -:math:`\phi(X)` is a function mapping of :math:`X` to a Hilbert space. A -centered kernel :math:`\tilde{K}` is defined as: + :math:`\phi(X)` is a function mapping of :math:`X` to a Hilbert space. A + centered kernel :math:`\tilde{K}` is defined as: -.. math:: - \tilde{K}(X, X) = \tilde{\phi}(X) . \tilde{\phi}(X)^{T} + .. math:: + \tilde{K}(X, X) = \tilde{\phi}(X) . \tilde{\phi}(X)^{T} -where :math:`\tilde{\phi}(X)` results from centering :math:`\phi(X)` in the -Hilbert space. + where :math:`\tilde{\phi}(X)` results from centering :math:`\phi(X)` in the + Hilbert space. -Thus, one could compute :math:`\tilde{K}` by mapping :math:`X` using the -function :math:`\phi(\cdot)` and center the data in this new space. However, -kernels are often used because they allows some algebra calculations that -avoid computing explicitly this mapping using :math:`\phi(\cdot)`. Indeed, one -can implicitly center as shown in Appendix B in [Scholkopf1998]_: + Thus, one could compute :math:`\tilde{K}` by mapping :math:`X` using the + function :math:`\phi(\cdot)` and center the data in this new space. However, + kernels are often used because they allows some algebra calculations that + avoid computing explicitly this mapping using :math:`\phi(\cdot)`. Indeed, one + can implicitly center as shown in Appendix B in [Scholkopf1998]_: -.. math:: - \tilde{K} = K - 1_{\text{n}_{samples}} K - K 1_{\text{n}_{samples}} + 1_{\text{n}_{samples}} K 1_{\text{n}_{samples}} + .. math:: + \tilde{K} = K - 1_{\text{n}_{samples}} K - K 1_{\text{n}_{samples}} + 1_{\text{n}_{samples}} K 1_{\text{n}_{samples}} -:math:`1_{\text{n}_{samples}}` is a matrix of `(n_samples, n_samples)` where -all entries are equal to :math:`\frac{1}{\text{n}_{samples}}`. In the -`transform` step, the kernel becomes :math:`K_{test}(X, Y)` defined as: + :math:`1_{\text{n}_{samples}}` is a matrix of `(n_samples, n_samples)` where + all entries are equal to :math:`\frac{1}{\text{n}_{samples}}`. In the + `transform` step, the kernel becomes :math:`K_{test}(X, Y)` defined as: -.. math:: - K_{test}(X, Y) = \phi(Y) . \phi(X)^{T} + .. math:: + K_{test}(X, Y) = \phi(Y) . \phi(X)^{T} -:math:`Y` is the test dataset of shape `(n_samples_test, n_features)` and thus -:math:`K_{test}` is of shape `(n_samples_test, n_samples)`. In this case, -centering :math:`K_{test}` is done as: + :math:`Y` is the test dataset of shape `(n_samples_test, n_features)` and thus + :math:`K_{test}` is of shape `(n_samples_test, n_samples)`. In this case, + centering :math:`K_{test}` is done as: -.. math:: - \tilde{K}_{test}(X, Y) = K_{test} - 1'_{\text{n}_{samples}} K - K_{test} 1_{\text{n}_{samples}} + 1'_{\text{n}_{samples}} K 1_{\text{n}_{samples}} + .. math:: + \tilde{K}_{test}(X, Y) = K_{test} - 1'_{\text{n}_{samples}} K - K_{test} 1_{\text{n}_{samples}} + 1'_{\text{n}_{samples}} K 1_{\text{n}_{samples}} -:math:`1'_{\text{n}_{samples}}` is a matrix of shape -`(n_samples_test, n_samples)` where all entries are equal to -:math:`\frac{1}{\text{n}_{samples}}`. + :math:`1'_{\text{n}_{samples}}` is a matrix of shape + `(n_samples_test, n_samples)` where all entries are equal to + :math:`\frac{1}{\text{n}_{samples}}`. -.. topic:: References + .. rubric:: References .. [Scholkopf1998] B. Schölkopf, A. Smola, and K.R. Müller, `"Nonlinear component analysis as a kernel eigenvalue problem." `_ Neural computation 10.5 (1998): 1299-1319. -|details-end| - .. _preprocessing_transformer: Non-linear transformation @@ -383,54 +373,46 @@ possible in order to stabilize variance and minimize skewness. :class:`PowerTransformer` currently provides two such power transformations, the Yeo-Johnson transform and the Box-Cox transform. -|details-start| -**Yeo-Johnson transform** -|details-split| - -.. math:: - x_i^{(\lambda)} = - \begin{cases} - [(x_i + 1)^\lambda - 1] / \lambda & \text{if } \lambda \neq 0, x_i \geq 0, \\[8pt] - \ln{(x_i + 1)} & \text{if } \lambda = 0, x_i \geq 0 \\[8pt] - -[(-x_i + 1)^{2 - \lambda} - 1] / (2 - \lambda) & \text{if } \lambda \neq 2, x_i < 0, \\[8pt] - - \ln (- x_i + 1) & \text{if } \lambda = 2, x_i < 0 - \end{cases} - -|details-end| - -|details-start| -**Box-Cox transform** -|details-split| - -.. math:: - x_i^{(\lambda)} = - \begin{cases} - \dfrac{x_i^\lambda - 1}{\lambda} & \text{if } \lambda \neq 0, \\[8pt] - \ln{(x_i)} & \text{if } \lambda = 0, - \end{cases} - - -Box-Cox can only be applied to strictly positive data. In both methods, the -transformation is parameterized by :math:`\lambda`, which is determined through -maximum likelihood estimation. Here is an example of using Box-Cox to map -samples drawn from a lognormal distribution to a normal distribution:: - - >>> pt = preprocessing.PowerTransformer(method='box-cox', standardize=False) - >>> X_lognormal = np.random.RandomState(616).lognormal(size=(3, 3)) - >>> X_lognormal - array([[1.28..., 1.18..., 0.84...], - [0.94..., 1.60..., 0.38...], - [1.35..., 0.21..., 1.09...]]) - >>> pt.fit_transform(X_lognormal) - array([[ 0.49..., 0.17..., -0.15...], - [-0.05..., 0.58..., -0.57...], - [ 0.69..., -0.84..., 0.10...]]) - -While the above example sets the `standardize` option to `False`, -:class:`PowerTransformer` will apply zero-mean, unit-variance normalization -to the transformed output by default. - -|details-end| +.. dropdown:: Yeo-Johnson transform + + .. math:: + x_i^{(\lambda)} = + \begin{cases} + [(x_i + 1)^\lambda - 1] / \lambda & \text{if } \lambda \neq 0, x_i \geq 0, \\[8pt] + \ln{(x_i + 1)} & \text{if } \lambda = 0, x_i \geq 0 \\[8pt] + -[(-x_i + 1)^{2 - \lambda} - 1] / (2 - \lambda) & \text{if } \lambda \neq 2, x_i < 0, \\[8pt] + - \ln (- x_i + 1) & \text{if } \lambda = 2, x_i < 0 + \end{cases} + +.. dropdown:: Box-Cox transform + + .. math:: + x_i^{(\lambda)} = + \begin{cases} + \dfrac{x_i^\lambda - 1}{\lambda} & \text{if } \lambda \neq 0, \\[8pt] + \ln{(x_i)} & \text{if } \lambda = 0, + \end{cases} + + Box-Cox can only be applied to strictly positive data. In both methods, the + transformation is parameterized by :math:`\lambda`, which is determined through + maximum likelihood estimation. Here is an example of using Box-Cox to map + samples drawn from a lognormal distribution to a normal distribution:: + + >>> pt = preprocessing.PowerTransformer(method='box-cox', standardize=False) + >>> X_lognormal = np.random.RandomState(616).lognormal(size=(3, 3)) + >>> X_lognormal + array([[1.28..., 1.18..., 0.84...], + [0.94..., 1.60..., 0.38...], + [1.35..., 0.21..., 1.09...]]) + >>> pt.fit_transform(X_lognormal) + array([[ 0.49..., 0.17..., -0.15...], + [-0.05..., 0.58..., -0.57...], + [ 0.69..., -0.84..., 0.10...]]) + + While the above example sets the `standardize` option to `False`, + :class:`PowerTransformer` will apply zero-mean, unit-variance normalization + to the transformed output by default. + Below are examples of Box-Cox and Yeo-Johnson applied to various probability distributions. Note that when applied to certain distributions, the power @@ -518,9 +500,8 @@ The normalizer instance can then be used on sample vectors as any transformer:: Note: L2 normalization is also known as spatial sign preprocessing. -|details-start| -**Sparse input** -|details-split| +.. dropdown:: Sparse input + :func:`normalize` and :class:`Normalizer` accept **both dense array-like and sparse matrices from scipy.sparse as input**. @@ -529,12 +510,11 @@ Note: L2 normalization is also known as spatial sign preprocessing. efficient Cython routines. To avoid unnecessary memory copies, it is recommended to choose the CSR representation upstream. -|details-end| - .. _preprocessing_categorical_features: Encoding categorical features ============================= + Often features are not given as continuous values but categorical. For example a person could have features ``["male", "female"]``, ``["from Europe", "from US", "from Asia"]``, @@ -721,42 +701,39 @@ not dropped:: >>> drop_enc.inverse_transform(X_trans) array([['female', None, None]], dtype=object) -|details-start| -**Support of categorical features with missing values** -|details-split| +.. dropdown:: Support of categorical features with missing values -:class:`OneHotEncoder` supports categorical features with missing values by -considering the missing values as an additional category:: + :class:`OneHotEncoder` supports categorical features with missing values by + considering the missing values as an additional category:: - >>> X = [['male', 'Safari'], - ... ['female', None], - ... [np.nan, 'Firefox']] - >>> enc = preprocessing.OneHotEncoder(handle_unknown='error').fit(X) - >>> enc.categories_ - [array(['female', 'male', nan], dtype=object), - array(['Firefox', 'Safari', None], dtype=object)] - >>> enc.transform(X).toarray() - array([[0., 1., 0., 0., 1., 0.], - [1., 0., 0., 0., 0., 1.], - [0., 0., 1., 1., 0., 0.]]) - -If a feature contains both `np.nan` and `None`, they will be considered -separate categories:: - - >>> X = [['Safari'], [None], [np.nan], ['Firefox']] - >>> enc = preprocessing.OneHotEncoder(handle_unknown='error').fit(X) - >>> enc.categories_ - [array(['Firefox', 'Safari', None, nan], dtype=object)] - >>> enc.transform(X).toarray() - array([[0., 1., 0., 0.], - [0., 0., 1., 0.], - [0., 0., 0., 1.], - [1., 0., 0., 0.]]) + >>> X = [['male', 'Safari'], + ... ['female', None], + ... [np.nan, 'Firefox']] + >>> enc = preprocessing.OneHotEncoder(handle_unknown='error').fit(X) + >>> enc.categories_ + [array(['female', 'male', nan], dtype=object), + array(['Firefox', 'Safari', None], dtype=object)] + >>> enc.transform(X).toarray() + array([[0., 1., 0., 0., 1., 0.], + [1., 0., 0., 0., 0., 1.], + [0., 0., 1., 1., 0., 0.]]) + + If a feature contains both `np.nan` and `None`, they will be considered + separate categories:: + + >>> X = [['Safari'], [None], [np.nan], ['Firefox']] + >>> enc = preprocessing.OneHotEncoder(handle_unknown='error').fit(X) + >>> enc.categories_ + [array(['Firefox', 'Safari', None, nan], dtype=object)] + >>> enc.transform(X).toarray() + array([[0., 1., 0., 0.], + [0., 0., 1., 0.], + [0., 0., 0., 1.], + [1., 0., 0., 0.]]) -See :ref:`dict_feature_extraction` for categorical features that are -represented as a dict, not as scalars. + See :ref:`dict_feature_extraction` for categorical features that are + represented as a dict, not as scalars. -|details-end| .. _encoder_infrequent_categories: @@ -910,66 +887,55 @@ cardinality, where one-hot encoding would inflate the feature space making it more expensive for a downstream model to process. A classical example of high cardinality categories are location based such as zip code or region. -|details-start| -**Binary classification targets** -|details-split| - -For the binary classification target, the target encoding is given by: - -.. math:: - S_i = \lambda_i\frac{n_{iY}}{n_i} + (1 - \lambda_i)\frac{n_Y}{n} +.. dropdown:: Binary classification targets -where :math:`S_i` is the encoding for category :math:`i`, :math:`n_{iY}` is the -number of observations with :math:`Y=1` and category :math:`i`, :math:`n_i` is -the number of observations with category :math:`i`, :math:`n_Y` is the number of -observations with :math:`Y=1`, :math:`n` is the number of observations, and -:math:`\lambda_i` is a shrinkage factor for category :math:`i`. The shrinkage -factor is given by: + For the binary classification target, the target encoding is given by: -.. math:: - \lambda_i = \frac{n_i}{m + n_i} + .. math:: + S_i = \lambda_i\frac{n_{iY}}{n_i} + (1 - \lambda_i)\frac{n_Y}{n} -where :math:`m` is a smoothing factor, which is controlled with the `smooth` -parameter in :class:`TargetEncoder`. Large smoothing factors will put more -weight on the global mean. When `smooth="auto"`, the smoothing factor is -computed as an empirical Bayes estimate: :math:`m=\sigma_i^2/\tau^2`, where -:math:`\sigma_i^2` is the variance of `y` with category :math:`i` and -:math:`\tau^2` is the global variance of `y`. + where :math:`S_i` is the encoding for category :math:`i`, :math:`n_{iY}` is the + number of observations with :math:`Y=1` and category :math:`i`, :math:`n_i` is + the number of observations with category :math:`i`, :math:`n_Y` is the number of + observations with :math:`Y=1`, :math:`n` is the number of observations, and + :math:`\lambda_i` is a shrinkage factor for category :math:`i`. The shrinkage + factor is given by: -|details-end| + .. math:: + \lambda_i = \frac{n_i}{m + n_i} -|details-start| -**Multiclass classification targets** -|details-split| + where :math:`m` is a smoothing factor, which is controlled with the `smooth` + parameter in :class:`TargetEncoder`. Large smoothing factors will put more + weight on the global mean. When `smooth="auto"`, the smoothing factor is + computed as an empirical Bayes estimate: :math:`m=\sigma_i^2/\tau^2`, where + :math:`\sigma_i^2` is the variance of `y` with category :math:`i` and + :math:`\tau^2` is the global variance of `y`. -For multiclass classification targets, the formulation is similar to binary -classification: +.. dropdown:: Multiclass classification targets -.. math:: - S_{ij} = \lambda_i\frac{n_{iY_j}}{n_i} + (1 - \lambda_i)\frac{n_{Y_j}}{n} + For multiclass classification targets, the formulation is similar to binary + classification: -where :math:`S_{ij}` is the encoding for category :math:`i` and class :math:`j`, -:math:`n_{iY_j}` is the number of observations with :math:`Y=j` and category -:math:`i`, :math:`n_i` is the number of observations with category :math:`i`, -:math:`n_{Y_j}` is the number of observations with :math:`Y=j`, :math:`n` is the -number of observations, and :math:`\lambda_i` is a shrinkage factor for category -:math:`i`. + .. math:: + S_{ij} = \lambda_i\frac{n_{iY_j}}{n_i} + (1 - \lambda_i)\frac{n_{Y_j}}{n} -|details-end| + where :math:`S_{ij}` is the encoding for category :math:`i` and class :math:`j`, + :math:`n_{iY_j}` is the number of observations with :math:`Y=j` and category + :math:`i`, :math:`n_i` is the number of observations with category :math:`i`, + :math:`n_{Y_j}` is the number of observations with :math:`Y=j`, :math:`n` is the + number of observations, and :math:`\lambda_i` is a shrinkage factor for category + :math:`i`. -|details-start| -**Continuous targets** -|details-split| +.. dropdown:: Continuous targets -For continuous targets, the formulation is similar to binary classification: + For continuous targets, the formulation is similar to binary classification: -.. math:: - S_i = \lambda_i\frac{\sum_{k\in L_i}Y_k}{n_i} + (1 - \lambda_i)\frac{\sum_{k=1}^{n}Y_k}{n} + .. math:: + S_i = \lambda_i\frac{\sum_{k\in L_i}Y_k}{n_i} + (1 - \lambda_i)\frac{\sum_{k=1}^{n}Y_k}{n} -where :math:`L_i` is the set of observations with category :math:`i` and -:math:`n_i` is the number of observations with category :math:`i`. + where :math:`L_i` is the set of observations with category :math:`i` and + :math:`n_i` is the number of observations with category :math:`i`. -|details-end| :meth:`~TargetEncoder.fit_transform` internally relies on a :term:`cross fitting` scheme to prevent target information from leaking into the train-time @@ -1005,21 +971,21 @@ encoding learned in :meth:`~TargetEncoder.fit_transform`. that are not seen during `fit` are encoded with the target mean, i.e. `target_mean_`. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_preprocessing_plot_target_encoder.py` - * :ref:`sphx_glr_auto_examples_preprocessing_plot_target_encoder_cross_val.py` +* :ref:`sphx_glr_auto_examples_preprocessing_plot_target_encoder.py` +* :ref:`sphx_glr_auto_examples_preprocessing_plot_target_encoder_cross_val.py` -.. topic:: References +.. rubric:: References - .. [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>` +.. [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>` - .. [PAR] :doi:`Pargent, F., Pfisterer, F., Thomas, J. et al. "Regularized target - encoding outperforms traditional methods in supervised machine learning with - high cardinality features" Comput Stat 37, 2671–2692 (2022) - <10.1007/s00180-022-01207-6>` +.. [PAR] :doi:`Pargent, F., Pfisterer, F., Thomas, J. et al. "Regularized target + encoding outperforms traditional methods in supervised machine learning with + high cardinality features" Comput Stat 37, 2671-2692 (2022) + <10.1007/s00180-022-01207-6>` .. _preprocessing_discretization: @@ -1097,11 +1063,11 @@ For instance, we can use the Pandas function :func:`pandas.cut`:: ['infant', 'kid', 'teen', 'adult', 'senior citizen'] Categories (5, object): ['infant' < 'kid' < 'teen' < 'adult' < 'senior citizen'] -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_preprocessing_plot_discretization.py` - * :ref:`sphx_glr_auto_examples_preprocessing_plot_discretization_classification.py` - * :ref:`sphx_glr_auto_examples_preprocessing_plot_discretization_strategies.py` +* :ref:`sphx_glr_auto_examples_preprocessing_plot_discretization.py` +* :ref:`sphx_glr_auto_examples_preprocessing_plot_discretization_classification.py` +* :ref:`sphx_glr_auto_examples_preprocessing_plot_discretization_strategies.py` .. _preprocessing_binarization: @@ -1294,23 +1260,20 @@ Interestingly, a :class:`SplineTransformer` of ``degree=0`` is the same as ``encode='onehot-dense'`` and ``n_bins = n_knots - 1`` if ``knots = strategy``. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_linear_model_plot_polynomial_interpolation.py` - * :ref:`sphx_glr_auto_examples_applications_plot_cyclical_feature_engineering.py` +* :ref:`sphx_glr_auto_examples_linear_model_plot_polynomial_interpolation.py` +* :ref:`sphx_glr_auto_examples_applications_plot_cyclical_feature_engineering.py` -|details-start| -**References** -|details-split| +.. dropdown:: References - * Eilers, P., & Marx, B. (1996). :doi:`Flexible Smoothing with B-splines and - Penalties <10.1214/ss/1038425655>`. Statist. Sci. 11 (1996), no. 2, 89--121. + * Eilers, P., & Marx, B. (1996). :doi:`Flexible Smoothing with B-splines and + Penalties <10.1214/ss/1038425655>`. Statist. Sci. 11 (1996), no. 2, 89--121. - * Perperoglou, A., Sauerbrei, W., Abrahamowicz, M. et al. :doi:`A review of - spline function procedures in R <10.1186/s12874-019-0666-3>`. - BMC Med Res Methodol 19, 46 (2019). + * Perperoglou, A., Sauerbrei, W., Abrahamowicz, M. et al. :doi:`A review of + spline function procedures in R <10.1186/s12874-019-0666-3>`. + BMC Med Res Methodol 19, 46 (2019). -|details-end| .. _function_transformer: diff --git a/doc/modules/random_projection.rst b/doc/modules/random_projection.rst index 6931feb34ad1d..173aee434576c 100644 --- a/doc/modules/random_projection.rst +++ b/doc/modules/random_projection.rst @@ -19,19 +19,19 @@ samples of the dataset. Thus random projection is a suitable approximation technique for distance based method. -.. topic:: References: +.. rubric:: References - * Sanjoy Dasgupta. 2000. - `Experiments with random projection. `_ - In Proceedings of the Sixteenth conference on Uncertainty in artificial - intelligence (UAI'00), Craig Boutilier and Moisés Goldszmidt (Eds.). Morgan - Kaufmann Publishers Inc., San Francisco, CA, USA, 143-151. +* Sanjoy Dasgupta. 2000. + `Experiments with random projection. `_ + In Proceedings of the Sixteenth conference on Uncertainty in artificial + intelligence (UAI'00), Craig Boutilier and Moisés Goldszmidt (Eds.). Morgan + Kaufmann Publishers Inc., San Francisco, CA, USA, 143-151. - * Ella Bingham and Heikki Mannila. 2001. - `Random projection in dimensionality reduction: applications to image and text data. `_ - In Proceedings of the seventh ACM SIGKDD international conference on - Knowledge discovery and data mining (KDD '01). ACM, New York, NY, USA, - 245-250. +* Ella Bingham and Heikki Mannila. 2001. + `Random projection in dimensionality reduction: applications to image and text data. `_ + In Proceedings of the seventh ACM SIGKDD international conference on + Knowledge discovery and data mining (KDD '01). ACM, New York, NY, USA, + 245-250. .. _johnson_lindenstrauss: @@ -74,17 +74,17 @@ bounded distortion introduced by the random projection:: :scale: 75 :align: center -.. topic:: Example: +.. rubric:: Examples - * See :ref:`sphx_glr_auto_examples_miscellaneous_plot_johnson_lindenstrauss_bound.py` - for a theoretical explication on the Johnson-Lindenstrauss lemma and an - empirical validation using sparse random matrices. +* See :ref:`sphx_glr_auto_examples_miscellaneous_plot_johnson_lindenstrauss_bound.py` + for a theoretical explication on the Johnson-Lindenstrauss lemma and an + empirical validation using sparse random matrices. -.. topic:: References: +.. rubric:: References - * Sanjoy Dasgupta and Anupam Gupta, 1999. - `An elementary proof of the Johnson-Lindenstrauss Lemma. - `_ +* Sanjoy Dasgupta and Anupam Gupta, 1999. + `An elementary proof of the Johnson-Lindenstrauss Lemma. + `_ .. _gaussian_random_matrix: @@ -148,18 +148,17 @@ projection transformer:: (100, 3947) -.. topic:: References: +.. rubric:: References - * D. Achlioptas. 2003. - `Database-friendly random projections: Johnson-Lindenstrauss with binary - coins `_. - Journal of Computer and System Sciences 66 (2003) 671–687 +* D. Achlioptas. 2003. + `Database-friendly random projections: Johnson-Lindenstrauss with binary + coins `_. + Journal of Computer and System Sciences 66 (2003) 671-687. - * Ping Li, Trevor J. Hastie, and Kenneth W. Church. 2006. - `Very sparse random projections. `_ - In Proceedings of the 12th ACM SIGKDD international conference on - Knowledge discovery and data mining (KDD '06). ACM, New York, NY, USA, - 287-296. +* Ping Li, Trevor J. Hastie, and Kenneth W. Church. 2006. + `Very sparse random projections. `_ + In Proceedings of the 12th ACM SIGKDD international conference on + Knowledge discovery and data mining (KDD '06). ACM, New York, NY, USA, 287-296. .. _random_projection_inverse_transform: diff --git a/doc/modules/semi_supervised.rst b/doc/modules/semi_supervised.rst index f8cae0a9ddcdf..8ba33638c6eec 100644 --- a/doc/modules/semi_supervised.rst +++ b/doc/modules/semi_supervised.rst @@ -60,18 +60,18 @@ until all samples have labels or no new samples are selected in that iteration. When using the self-training classifier, the :ref:`calibration ` of the classifier is important. -.. topic:: Examples +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_semi_supervised_plot_self_training_varying_threshold.py` - * :ref:`sphx_glr_auto_examples_semi_supervised_plot_semi_supervised_versus_svm_iris.py` +* :ref:`sphx_glr_auto_examples_semi_supervised_plot_self_training_varying_threshold.py` +* :ref:`sphx_glr_auto_examples_semi_supervised_plot_semi_supervised_versus_svm_iris.py` -.. topic:: References +.. rubric:: References - .. [1] :doi:`"Unsupervised word sense disambiguation rivaling supervised methods" - <10.3115/981658.981684>` - David Yarowsky, Proceedings of the 33rd annual meeting on Association for - Computational Linguistics (ACL '95). Association for Computational Linguistics, - Stroudsburg, PA, USA, 189-196. +.. [1] :doi:`"Unsupervised word sense disambiguation rivaling supervised methods" + <10.3115/981658.981684>` + David Yarowsky, Proceedings of the 33rd annual meeting on Association for + Computational Linguistics (ACL '95). Association for Computational Linguistics, + Stroudsburg, PA, USA, 189-196. .. _label_propagation: @@ -134,18 +134,18 @@ algorithm can lead to prohibitively long running times. On the other hand, the KNN kernel will produce a much more memory-friendly sparse matrix which can drastically reduce running times. -.. topic:: Examples +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_semi_supervised_plot_semi_supervised_versus_svm_iris.py` - * :ref:`sphx_glr_auto_examples_semi_supervised_plot_label_propagation_structure.py` - * :ref:`sphx_glr_auto_examples_semi_supervised_plot_label_propagation_digits.py` - * :ref:`sphx_glr_auto_examples_semi_supervised_plot_label_propagation_digits_active_learning.py` +* :ref:`sphx_glr_auto_examples_semi_supervised_plot_semi_supervised_versus_svm_iris.py` +* :ref:`sphx_glr_auto_examples_semi_supervised_plot_label_propagation_structure.py` +* :ref:`sphx_glr_auto_examples_semi_supervised_plot_label_propagation_digits.py` +* :ref:`sphx_glr_auto_examples_semi_supervised_plot_label_propagation_digits_active_learning.py` -.. topic:: References +.. rubric:: References - [2] Yoshua Bengio, Olivier Delalleau, Nicolas Le Roux. In Semi-Supervised - Learning (2006), pp. 193-216 +[2] Yoshua Bengio, Olivier Delalleau, Nicolas Le Roux. In Semi-Supervised +Learning (2006), pp. 193-216 - [3] Olivier Delalleau, Yoshua Bengio, Nicolas Le Roux. Efficient - Non-Parametric Function Induction in Semi-Supervised Learning. AISTAT 2005 - https://www.gatsby.ucl.ac.uk/aistats/fullpapers/204.pdf +[3] Olivier Delalleau, Yoshua Bengio, Nicolas Le Roux. Efficient +Non-Parametric Function Induction in Semi-Supervised Learning. AISTAT 2005 +https://www.gatsby.ucl.ac.uk/aistats/fullpapers/204.pdf diff --git a/doc/modules/sgd.rst b/doc/modules/sgd.rst index a7981e9d4ec28..73df123b4ed19 100644 --- a/doc/modules/sgd.rst +++ b/doc/modules/sgd.rst @@ -189,14 +189,14 @@ For classification with a logistic loss, another variant of SGD with an averaging strategy is available with Stochastic Average Gradient (SAG) algorithm, available as a solver in :class:`LogisticRegression`. -.. topic:: Examples: +.. rubric:: Examples - - :ref:`sphx_glr_auto_examples_linear_model_plot_sgd_separating_hyperplane.py`, - - :ref:`sphx_glr_auto_examples_linear_model_plot_sgd_iris.py` - - :ref:`sphx_glr_auto_examples_linear_model_plot_sgd_weighted_samples.py` - - :ref:`sphx_glr_auto_examples_linear_model_plot_sgd_comparison.py` - - :ref:`sphx_glr_auto_examples_svm_plot_separating_hyperplane_unbalanced.py` - (See the Note in the example) +- :ref:`sphx_glr_auto_examples_linear_model_plot_sgd_separating_hyperplane.py` +- :ref:`sphx_glr_auto_examples_linear_model_plot_sgd_iris.py` +- :ref:`sphx_glr_auto_examples_linear_model_plot_sgd_weighted_samples.py` +- :ref:`sphx_glr_auto_examples_linear_model_plot_sgd_comparison.py` +- :ref:`sphx_glr_auto_examples_svm_plot_separating_hyperplane_unbalanced.py` + (See the Note in the example) Regression ========== @@ -249,44 +249,40 @@ quadratic in the number of samples. with a large number of training samples (> 10,000) for which the SGD variant can be several orders of magnitude faster. -|details-start| -**Mathematical details** -|details-split| +.. dropdown:: Mathematical details -Its implementation is based on the implementation of the stochastic -gradient descent. Indeed, the original optimization problem of the One-Class -SVM is given by + Its implementation is based on the implementation of the stochastic + gradient descent. Indeed, the original optimization problem of the One-Class + SVM is given by -.. math:: - - \begin{aligned} - \min_{w, \rho, \xi} & \quad \frac{1}{2}\Vert w \Vert^2 - \rho + \frac{1}{\nu n} \sum_{i=1}^n \xi_i \\ - \text{s.t.} & \quad \langle w, x_i \rangle \geq \rho - \xi_i \quad 1 \leq i \leq n \\ - & \quad \xi_i \geq 0 \quad 1 \leq i \leq n - \end{aligned} + .. math:: -where :math:`\nu \in (0, 1]` is the user-specified parameter controlling the -proportion of outliers and the proportion of support vectors. Getting rid of -the slack variables :math:`\xi_i` this problem is equivalent to + \begin{aligned} + \min_{w, \rho, \xi} & \quad \frac{1}{2}\Vert w \Vert^2 - \rho + \frac{1}{\nu n} \sum_{i=1}^n \xi_i \\ + \text{s.t.} & \quad \langle w, x_i \rangle \geq \rho - \xi_i \quad 1 \leq i \leq n \\ + & \quad \xi_i \geq 0 \quad 1 \leq i \leq n + \end{aligned} -.. math:: + where :math:`\nu \in (0, 1]` is the user-specified parameter controlling the + proportion of outliers and the proportion of support vectors. Getting rid of + the slack variables :math:`\xi_i` this problem is equivalent to - \min_{w, \rho} \frac{1}{2}\Vert w \Vert^2 - \rho + \frac{1}{\nu n} \sum_{i=1}^n \max(0, \rho - \langle w, x_i \rangle) \, . + .. math:: -Multiplying by the constant :math:`\nu` and introducing the intercept -:math:`b = 1 - \rho` we obtain the following equivalent optimization problem + \min_{w, \rho} \frac{1}{2}\Vert w \Vert^2 - \rho + \frac{1}{\nu n} \sum_{i=1}^n \max(0, \rho - \langle w, x_i \rangle) \, . -.. math:: + Multiplying by the constant :math:`\nu` and introducing the intercept + :math:`b = 1 - \rho` we obtain the following equivalent optimization problem - \min_{w, b} \frac{\nu}{2}\Vert w \Vert^2 + b\nu + \frac{1}{n} \sum_{i=1}^n \max(0, 1 - (\langle w, x_i \rangle + b)) \, . + .. math:: -This is similar to the optimization problems studied in section -:ref:`sgd_mathematical_formulation` with :math:`y_i = 1, 1 \leq i \leq n` and -:math:`\alpha = \nu/2`, :math:`L` being the hinge loss function and :math:`R` -being the L2 norm. We just need to add the term :math:`b\nu` in the -optimization loop. + \min_{w, b} \frac{\nu}{2}\Vert w \Vert^2 + b\nu + \frac{1}{n} \sum_{i=1}^n \max(0, 1 - (\langle w, x_i \rangle + b)) \, . -|details-end| + This is similar to the optimization problems studied in section + :ref:`sgd_mathematical_formulation` with :math:`y_i = 1, 1 \leq i \leq n` and + :math:`\alpha = \nu/2`, :math:`L` being the hinge loss function and :math:`R` + being the L2 norm. We just need to add the term :math:`b\nu` in the + optimization loop. As :class:`SGDClassifier` and :class:`SGDRegressor`, :class:`SGDOneClassSVM` supports averaged SGD. Averaging can be enabled by setting ``average=True``. @@ -305,9 +301,9 @@ efficiency, however, use the CSR matrix format as defined in `scipy.sparse.csr_matrix `_. -.. topic:: Examples: +.. rubric:: Examples - - :ref:`sphx_glr_auto_examples_text_plot_document_classification_20newsgroups.py` +- :ref:`sphx_glr_auto_examples_text_plot_document_classification_20newsgroups.py` Complexity ========== @@ -385,11 +381,11 @@ Tips on Practical Use * We found that Averaged SGD works best with a larger number of features and a higher eta0. -.. topic:: References: +.. rubric:: References - * `"Efficient BackProp" `_ - Y. LeCun, L. Bottou, G. Orr, K. Müller - In Neural Networks: Tricks - of the Trade 1998. +* `"Efficient BackProp" `_ + Y. LeCun, L. Bottou, G. Orr, K. Müller - In Neural Networks: Tricks + of the Trade 1998. .. _sgd_mathematical_formulation: @@ -416,32 +412,28 @@ where :math:`L` is a loss function that measures model (mis)fit and complexity; :math:`\alpha > 0` is a non-negative hyperparameter that controls the regularization strength. -|details-start| -**Loss functions details** -|details-split| - -Different choices for :math:`L` entail different classifiers or regressors: - -- Hinge (soft-margin): equivalent to Support Vector Classification. - :math:`L(y_i, f(x_i)) = \max(0, 1 - y_i f(x_i))`. -- Perceptron: - :math:`L(y_i, f(x_i)) = \max(0, - y_i f(x_i))`. -- Modified Huber: - :math:`L(y_i, f(x_i)) = \max(0, 1 - y_i f(x_i))^2` if :math:`y_i f(x_i) > - -1`, and :math:`L(y_i, f(x_i)) = -4 y_i f(x_i)` otherwise. -- Log Loss: equivalent to Logistic Regression. - :math:`L(y_i, f(x_i)) = \log(1 + \exp (-y_i f(x_i)))`. -- Squared Error: Linear regression (Ridge or Lasso depending on - :math:`R`). - :math:`L(y_i, f(x_i)) = \frac{1}{2}(y_i - f(x_i))^2`. -- Huber: less sensitive to outliers than least-squares. It is equivalent to - least squares when :math:`|y_i - f(x_i)| \leq \varepsilon`, and - :math:`L(y_i, f(x_i)) = \varepsilon |y_i - f(x_i)| - \frac{1}{2} - \varepsilon^2` otherwise. -- Epsilon-Insensitive: (soft-margin) equivalent to Support Vector Regression. - :math:`L(y_i, f(x_i)) = \max(0, |y_i - f(x_i)| - \varepsilon)`. - -|details-end| +.. dropdown:: Loss functions details + + Different choices for :math:`L` entail different classifiers or regressors: + + - Hinge (soft-margin): equivalent to Support Vector Classification. + :math:`L(y_i, f(x_i)) = \max(0, 1 - y_i f(x_i))`. + - Perceptron: + :math:`L(y_i, f(x_i)) = \max(0, - y_i f(x_i))`. + - Modified Huber: + :math:`L(y_i, f(x_i)) = \max(0, 1 - y_i f(x_i))^2` if :math:`y_i f(x_i) > + -1`, and :math:`L(y_i, f(x_i)) = -4 y_i f(x_i)` otherwise. + - Log Loss: equivalent to Logistic Regression. + :math:`L(y_i, f(x_i)) = \log(1 + \exp (-y_i f(x_i)))`. + - Squared Error: Linear regression (Ridge or Lasso depending on + :math:`R`). + :math:`L(y_i, f(x_i)) = \frac{1}{2}(y_i - f(x_i))^2`. + - Huber: less sensitive to outliers than least-squares. It is equivalent to + least squares when :math:`|y_i - f(x_i)| \leq \varepsilon`, and + :math:`L(y_i, f(x_i)) = \varepsilon |y_i - f(x_i)| - \frac{1}{2} + \varepsilon^2` otherwise. + - Epsilon-Insensitive: (soft-margin) equivalent to Support Vector Regression. + :math:`L(y_i, f(x_i)) = \max(0, |y_i - f(x_i)| - \varepsilon)`. All of the above loss functions can be regarded as an upper bound on the misclassification error (Zero-one loss) as shown in the Figure below. @@ -553,32 +545,29 @@ We use the truncated gradient algorithm proposed in [#3]_ for L1 regularization (and the Elastic Net). The code is written in Cython. -.. topic:: References: +.. rubric:: References - .. [#1] `"Stochastic Gradient Descent" - `_ L. Bottou - Website, 2010. +.. [#1] `"Stochastic Gradient Descent" + `_ L. Bottou - Website, 2010. - .. [#2] :doi:`"Pegasos: Primal estimated sub-gradient solver for svm" - <10.1145/1273496.1273598>` - S. Shalev-Shwartz, Y. Singer, N. Srebro - In Proceedings of ICML '07. +.. [#2] :doi:`"Pegasos: Primal estimated sub-gradient solver for svm" + <10.1145/1273496.1273598>` + S. Shalev-Shwartz, Y. Singer, N. Srebro - In Proceedings of ICML '07. - .. [#3] `"Stochastic gradient descent training for l1-regularized - log-linear models with cumulative penalty" - `_ - Y. Tsuruoka, J. Tsujii, S. Ananiadou - In Proceedings of the AFNLP/ACL - '09. +.. [#3] `"Stochastic gradient descent training for l1-regularized + log-linear models with cumulative penalty" + `_ + Y. Tsuruoka, J. Tsujii, S. Ananiadou - In Proceedings of the AFNLP/ACL'09. - .. [#4] :arxiv:`"Towards Optimal One Pass Large Scale Learning with - Averaged Stochastic Gradient Descent" - <1107.2490v2>` - Xu, Wei (2011) +.. [#4] :arxiv:`"Towards Optimal One Pass Large Scale Learning with + Averaged Stochastic Gradient Descent" + <1107.2490v2>`. Xu, Wei (2011) - .. [#5] :doi:`"Regularization and variable selection via the elastic net" - <10.1111/j.1467-9868.2005.00503.x>` - H. Zou, T. Hastie - Journal of the Royal Statistical Society Series B, - 67 (2), 301-320. +.. [#5] :doi:`"Regularization and variable selection via the elastic net" + <10.1111/j.1467-9868.2005.00503.x>` + H. Zou, T. Hastie - Journal of the Royal Statistical Society Series B, + 67 (2), 301-320. - .. [#6] :doi:`"Solving large scale linear prediction problems using stochastic - gradient descent algorithms" - <10.1145/1015330.1015332>` - T. Zhang - In Proceedings of ICML '04. +.. [#6] :doi:`"Solving large scale linear prediction problems using stochastic + gradient descent algorithms" <10.1145/1015330.1015332>` + T. Zhang - In Proceedings of ICML '04. diff --git a/doc/modules/svm.rst b/doc/modules/svm.rst index e3bc1395819e9..47115e43a89e0 100644 --- a/doc/modules/svm.rst +++ b/doc/modules/svm.rst @@ -108,11 +108,10 @@ properties of these support vectors can be found in attributes >>> clf.n_support_ array([1, 1]...) -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_svm_plot_separating_hyperplane.py`, - * :ref:`sphx_glr_auto_examples_svm_plot_svm_nonlinear.py` - * :ref:`sphx_glr_auto_examples_svm_plot_svm_anova.py`, +* :ref:`sphx_glr_auto_examples_svm_plot_separating_hyperplane.py` +* :ref:`sphx_glr_auto_examples_svm_plot_svm_anova.py` .. _svm_multi_class: @@ -154,65 +153,61 @@ multi-class strategy, thus training `n_classes` models. See :ref:`svm_mathematical_formulation` for a complete description of the decision function. -|details-start| -**Details on multi-class strategies** -|details-split| - -Note that the :class:`LinearSVC` also implements an alternative multi-class -strategy, the so-called multi-class SVM formulated by Crammer and Singer -[#8]_, by using the option ``multi_class='crammer_singer'``. In practice, -one-vs-rest classification is usually preferred, since the results are mostly -similar, but the runtime is significantly less. - -For "one-vs-rest" :class:`LinearSVC` the attributes ``coef_`` and ``intercept_`` -have the shape ``(n_classes, n_features)`` and ``(n_classes,)`` respectively. -Each row of the coefficients corresponds to one of the ``n_classes`` -"one-vs-rest" classifiers and similar for the intercepts, in the -order of the "one" class. - -In the case of "one-vs-one" :class:`SVC` and :class:`NuSVC`, the layout of -the attributes is a little more involved. In the case of a linear -kernel, the attributes ``coef_`` and ``intercept_`` have the shape -``(n_classes * (n_classes - 1) / 2, n_features)`` and ``(n_classes * -(n_classes - 1) / 2)`` respectively. This is similar to the layout for -:class:`LinearSVC` described above, with each row now corresponding -to a binary classifier. The order for classes -0 to n is "0 vs 1", "0 vs 2" , ... "0 vs n", "1 vs 2", "1 vs 3", "1 vs n", . . -. "n-1 vs n". - -The shape of ``dual_coef_`` is ``(n_classes-1, n_SV)`` with -a somewhat hard to grasp layout. -The columns correspond to the support vectors involved in any -of the ``n_classes * (n_classes - 1) / 2`` "one-vs-one" classifiers. -Each support vector ``v`` has a dual coefficient in each of the -``n_classes - 1`` classifiers comparing the class of ``v`` against another class. -Note that some, but not all, of these dual coefficients, may be zero. -The ``n_classes - 1`` entries in each column are these dual coefficients, -ordered by the opposing class. - -This might be clearer with an example: consider a three class problem with -class 0 having three support vectors -:math:`v^{0}_0, v^{1}_0, v^{2}_0` and class 1 and 2 having two support vectors -:math:`v^{0}_1, v^{1}_1` and :math:`v^{0}_2, v^{1}_2` respectively. For each -support vector :math:`v^{j}_i`, there are two dual coefficients. Let's call -the coefficient of support vector :math:`v^{j}_i` in the classifier between -classes :math:`i` and :math:`k` :math:`\alpha^{j}_{i,k}`. -Then ``dual_coef_`` looks like this: - -+------------------------+------------------------+------------------------+------------------------+------------------------+------------------------+------------------------+ -|:math:`\alpha^{0}_{0,1}`|:math:`\alpha^{1}_{0,1}`|:math:`\alpha^{2}_{0,1}`|:math:`\alpha^{0}_{1,0}`|:math:`\alpha^{1}_{1,0}`|:math:`\alpha^{0}_{2,0}`|:math:`\alpha^{1}_{2,0}`| -+------------------------+------------------------+------------------------+------------------------+------------------------+------------------------+------------------------+ -|:math:`\alpha^{0}_{0,2}`|:math:`\alpha^{1}_{0,2}`|:math:`\alpha^{2}_{0,2}`|:math:`\alpha^{0}_{1,2}`|:math:`\alpha^{1}_{1,2}`|:math:`\alpha^{0}_{2,1}`|:math:`\alpha^{1}_{2,1}`| -+------------------------+------------------------+------------------------+------------------------+------------------------+------------------------+------------------------+ -|Coefficients |Coefficients |Coefficients | -|for SVs of class 0 |for SVs of class 1 |for SVs of class 2 | -+--------------------------------------------------------------------------+-------------------------------------------------+-------------------------------------------------+ - -|details-end| - -.. topic:: Examples: - - * :ref:`sphx_glr_auto_examples_svm_plot_iris_svc.py`, +.. dropdown:: Details on multi-class strategies + + Note that the :class:`LinearSVC` also implements an alternative multi-class + strategy, the so-called multi-class SVM formulated by Crammer and Singer + [#8]_, by using the option ``multi_class='crammer_singer'``. In practice, + one-vs-rest classification is usually preferred, since the results are mostly + similar, but the runtime is significantly less. + + For "one-vs-rest" :class:`LinearSVC` the attributes ``coef_`` and ``intercept_`` + have the shape ``(n_classes, n_features)`` and ``(n_classes,)`` respectively. + Each row of the coefficients corresponds to one of the ``n_classes`` + "one-vs-rest" classifiers and similar for the intercepts, in the + order of the "one" class. + + In the case of "one-vs-one" :class:`SVC` and :class:`NuSVC`, the layout of + the attributes is a little more involved. In the case of a linear + kernel, the attributes ``coef_`` and ``intercept_`` have the shape + ``(n_classes * (n_classes - 1) / 2, n_features)`` and ``(n_classes * + (n_classes - 1) / 2)`` respectively. This is similar to the layout for + :class:`LinearSVC` described above, with each row now corresponding + to a binary classifier. The order for classes + 0 to n is "0 vs 1", "0 vs 2" , ... "0 vs n", "1 vs 2", "1 vs 3", "1 vs n", . . + . "n-1 vs n". + + The shape of ``dual_coef_`` is ``(n_classes-1, n_SV)`` with + a somewhat hard to grasp layout. + The columns correspond to the support vectors involved in any + of the ``n_classes * (n_classes - 1) / 2`` "one-vs-one" classifiers. + Each support vector ``v`` has a dual coefficient in each of the + ``n_classes - 1`` classifiers comparing the class of ``v`` against another class. + Note that some, but not all, of these dual coefficients, may be zero. + The ``n_classes - 1`` entries in each column are these dual coefficients, + ordered by the opposing class. + + This might be clearer with an example: consider a three class problem with + class 0 having three support vectors + :math:`v^{0}_0, v^{1}_0, v^{2}_0` and class 1 and 2 having two support vectors + :math:`v^{0}_1, v^{1}_1` and :math:`v^{0}_2, v^{1}_2` respectively. For each + support vector :math:`v^{j}_i`, there are two dual coefficients. Let's call + the coefficient of support vector :math:`v^{j}_i` in the classifier between + classes :math:`i` and :math:`k` :math:`\alpha^{j}_{i,k}`. + Then ``dual_coef_`` looks like this: + + +------------------------+------------------------+------------------------+------------------------+------------------------+------------------------+------------------------+ + |:math:`\alpha^{0}_{0,1}`|:math:`\alpha^{1}_{0,1}`|:math:`\alpha^{2}_{0,1}`|:math:`\alpha^{0}_{1,0}`|:math:`\alpha^{1}_{1,0}`|:math:`\alpha^{0}_{2,0}`|:math:`\alpha^{1}_{2,0}`| + +------------------------+------------------------+------------------------+------------------------+------------------------+------------------------+------------------------+ + |:math:`\alpha^{0}_{0,2}`|:math:`\alpha^{1}_{0,2}`|:math:`\alpha^{2}_{0,2}`|:math:`\alpha^{0}_{1,2}`|:math:`\alpha^{1}_{1,2}`|:math:`\alpha^{0}_{2,1}`|:math:`\alpha^{1}_{2,1}`| + +------------------------+------------------------+------------------------+------------------------+------------------------+------------------------+------------------------+ + |Coefficients |Coefficients |Coefficients | + |for SVs of class 0 |for SVs of class 1 |for SVs of class 2 | + +--------------------------------------------------------------------------+-------------------------------------------------+-------------------------------------------------+ + +.. rubric:: Examples + +* :ref:`sphx_glr_auto_examples_svm_plot_iris_svc.py` .. _scores_probabilities: @@ -295,10 +290,10 @@ to the sample weights: :align: center :scale: 75 -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_svm_plot_separating_hyperplane_unbalanced.py` - * :ref:`sphx_glr_auto_examples_svm_plot_weighted_samples.py`, +* :ref:`sphx_glr_auto_examples_svm_plot_separating_hyperplane_unbalanced.py` +* :ref:`sphx_glr_auto_examples_svm_plot_weighted_samples.py` .. _svm_regression: @@ -343,9 +338,9 @@ floating point values instead of integer values:: array([1.5]) -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_svm_plot_svm_regression.py` +* :ref:`sphx_glr_auto_examples_svm_plot_svm_regression.py` .. _svm_outlier_detection: @@ -516,11 +511,10 @@ Proper choice of ``C`` and ``gamma`` is critical to the SVM's performance. One is advised to use :class:`~sklearn.model_selection.GridSearchCV` with ``C`` and ``gamma`` spaced exponentially far apart to choose good values. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_svm_plot_rbf_parameters.py` - * :ref:`sphx_glr_auto_examples_svm_plot_svm_nonlinear.py` - * :ref:`sphx_glr_auto_examples_svm_plot_svm_scale_c.py` +* :ref:`sphx_glr_auto_examples_svm_plot_rbf_parameters.py` +* :ref:`sphx_glr_auto_examples_svm_plot_svm_scale_c.py` Custom Kernels -------------- @@ -539,60 +533,52 @@ classifiers, except that: use of ``fit()`` and ``predict()`` you will have unexpected results. -|details-start| -**Using Python functions as kernels** -|details-split| +.. dropdown:: Using Python functions as kernels -You can use your own defined kernels by passing a function to the -``kernel`` parameter. + You can use your own defined kernels by passing a function to the + ``kernel`` parameter. -Your kernel must take as arguments two matrices of shape -``(n_samples_1, n_features)``, ``(n_samples_2, n_features)`` -and return a kernel matrix of shape ``(n_samples_1, n_samples_2)``. + Your kernel must take as arguments two matrices of shape + ``(n_samples_1, n_features)``, ``(n_samples_2, n_features)`` + and return a kernel matrix of shape ``(n_samples_1, n_samples_2)``. -The following code defines a linear kernel and creates a classifier -instance that will use that kernel:: + The following code defines a linear kernel and creates a classifier + instance that will use that kernel:: - >>> import numpy as np - >>> from sklearn import svm - >>> def my_kernel(X, Y): - ... return np.dot(X, Y.T) - ... - >>> clf = svm.SVC(kernel=my_kernel) - -|details-end| + >>> import numpy as np + >>> from sklearn import svm + >>> def my_kernel(X, Y): + ... return np.dot(X, Y.T) + ... + >>> clf = svm.SVC(kernel=my_kernel) -|details-start| -**Using the Gram matrix** -|details-split| +.. dropdown:: Using the Gram matrix -You can pass pre-computed kernels by using the ``kernel='precomputed'`` -option. You should then pass Gram matrix instead of X to the `fit` and -`predict` methods. The kernel values between *all* training vectors and the -test vectors must be provided: + You can pass pre-computed kernels by using the ``kernel='precomputed'`` + option. You should then pass Gram matrix instead of X to the `fit` and + `predict` methods. The kernel values between *all* training vectors and the + test vectors must be provided: - >>> import numpy as np - >>> from sklearn.datasets import make_classification - >>> from sklearn.model_selection import train_test_split - >>> from sklearn import svm - >>> X, y = make_classification(n_samples=10, random_state=0) - >>> X_train , X_test , y_train, y_test = train_test_split(X, y, random_state=0) - >>> clf = svm.SVC(kernel='precomputed') - >>> # linear kernel computation - >>> gram_train = np.dot(X_train, X_train.T) - >>> clf.fit(gram_train, y_train) - SVC(kernel='precomputed') - >>> # predict on training examples - >>> gram_test = np.dot(X_test, X_train.T) - >>> clf.predict(gram_test) - array([0, 1, 0]) + >>> import numpy as np + >>> from sklearn.datasets import make_classification + >>> from sklearn.model_selection import train_test_split + >>> from sklearn import svm + >>> X, y = make_classification(n_samples=10, random_state=0) + >>> X_train , X_test , y_train, y_test = train_test_split(X, y, random_state=0) + >>> clf = svm.SVC(kernel='precomputed') + >>> # linear kernel computation + >>> gram_train = np.dot(X_train, X_train.T) + >>> clf.fit(gram_train, y_train) + SVC(kernel='precomputed') + >>> # predict on training examples + >>> gram_test = np.dot(X_test, X_train.T) + >>> clf.predict(gram_test) + array([0, 1, 0]) -|details-end| +.. rubric:: Examples -.. topic:: Examples: - - * :ref:`sphx_glr_auto_examples_svm_plot_custom_kernel.py`. +* :ref:`sphx_glr_auto_examples_svm_plot_custom_kernel.py` .. _svm_mathematical_formulation: @@ -689,43 +675,35 @@ term :math:`b` estimator used is :class:`~sklearn.linear_model.Ridge` regression, the relation between them is given as :math:`C = \frac{1}{alpha}`. -|details-start| -**LinearSVC** -|details-split| +.. dropdown:: LinearSVC -The primal problem can be equivalently formulated as + The primal problem can be equivalently formulated as -.. math:: + .. math:: - \min_ {w, b} \frac{1}{2} w^T w + C \sum_{i=1}^{n}\max(0, 1 - y_i (w^T \phi(x_i) + b)), + \min_ {w, b} \frac{1}{2} w^T w + C \sum_{i=1}^{n}\max(0, 1 - y_i (w^T \phi(x_i) + b)), -where we make use of the `hinge loss -`_. This is the form that is -directly optimized by :class:`LinearSVC`, but unlike the dual form, this one -does not involve inner products between samples, so the famous kernel trick -cannot be applied. This is why only the linear kernel is supported by -:class:`LinearSVC` (:math:`\phi` is the identity function). - -|details-end| + where we make use of the `hinge loss + `_. This is the form that is + directly optimized by :class:`LinearSVC`, but unlike the dual form, this one + does not involve inner products between samples, so the famous kernel trick + cannot be applied. This is why only the linear kernel is supported by + :class:`LinearSVC` (:math:`\phi` is the identity function). .. _nu_svc: -|details-start| -**NuSVC** -|details-split| - -The :math:`\nu`-SVC formulation [#7]_ is a reparameterization of the -:math:`C`-SVC and therefore mathematically equivalent. +.. dropdown:: NuSVC -We introduce a new parameter :math:`\nu` (instead of :math:`C`) which -controls the number of support vectors and *margin errors*: -:math:`\nu \in (0, 1]` is an upper bound on the fraction of margin errors and -a lower bound of the fraction of support vectors. A margin error corresponds -to a sample that lies on the wrong side of its margin boundary: it is either -misclassified, or it is correctly classified but does not lie beyond the -margin. + The :math:`\nu`-SVC formulation [#7]_ is a reparameterization of the + :math:`C`-SVC and therefore mathematically equivalent. -|details-end| + We introduce a new parameter :math:`\nu` (instead of :math:`C`) which + controls the number of support vectors and *margin errors*: + :math:`\nu \in (0, 1]` is an upper bound on the fraction of margin errors and + a lower bound of the fraction of support vectors. A margin error corresponds + to a sample that lies on the wrong side of its margin boundary: it is either + misclassified, or it is correctly classified but does not lie beyond the + margin. SVR --- @@ -774,21 +752,17 @@ which holds the difference :math:`\alpha_i - \alpha_i^*`, ``support_vectors_`` w holds the support vectors, and ``intercept_`` which holds the independent term :math:`b` -|details-start| -**LinearSVR** -|details-split| +.. dropdown:: LinearSVR -The primal problem can be equivalently formulated as - -.. math:: + The primal problem can be equivalently formulated as - \min_ {w, b} \frac{1}{2} w^T w + C \sum_{i=1}^{n}\max(0, |y_i - (w^T \phi(x_i) + b)| - \varepsilon), + .. math:: -where we make use of the epsilon-insensitive loss, i.e. errors of less than -:math:`\varepsilon` are ignored. This is the form that is directly optimized -by :class:`LinearSVR`. + \min_ {w, b} \frac{1}{2} w^T w + C \sum_{i=1}^{n}\max(0, |y_i - (w^T \phi(x_i) + b)| - \varepsilon), -|details-end| + where we make use of the epsilon-insensitive loss, i.e. errors of less than + :math:`\varepsilon` are ignored. This is the form that is directly optimized + by :class:`LinearSVR`. .. _svm_implementation_details: @@ -804,38 +778,37 @@ used, please refer to their respective papers. .. _`libsvm`: https://www.csie.ntu.edu.tw/~cjlin/libsvm/ .. _`liblinear`: https://www.csie.ntu.edu.tw/~cjlin/liblinear/ -.. topic:: References: +.. rubric:: References - .. [#1] Platt `"Probabilistic outputs for SVMs and comparisons to - regularized likelihood methods" - `_. +.. [#1] Platt `"Probabilistic outputs for SVMs and comparisons to + regularized likelihood methods" + `_. - .. [#2] Wu, Lin and Weng, `"Probability estimates for multi-class - classification by pairwise coupling" - `_, JMLR - 5:975-1005, 2004. +.. [#2] Wu, Lin and Weng, `"Probability estimates for multi-class + classification by pairwise coupling" + `_, + JMLR 5:975-1005, 2004. - .. [#3] Fan, Rong-En, et al., - `"LIBLINEAR: A library for large linear classification." - `_, - Journal of machine learning research 9.Aug (2008): 1871-1874. +.. [#3] Fan, Rong-En, et al., + `"LIBLINEAR: A library for large linear classification." + `_, + Journal of machine learning research 9.Aug (2008): 1871-1874. - .. [#4] Chang and Lin, `LIBSVM: A Library for Support Vector Machines - `_. +.. [#4] Chang and Lin, `LIBSVM: A Library for Support Vector Machines + `_. - .. [#5] Bishop, `Pattern recognition and machine learning - `_, - chapter 7 Sparse Kernel Machines +.. [#5] Bishop, `Pattern recognition and machine learning + `_, + chapter 7 Sparse Kernel Machines - .. [#6] :doi:`"A Tutorial on Support Vector Regression" - <10.1023/B:STCO.0000035301.49549.88>` - Alex J. Smola, Bernhard Schölkopf - Statistics and Computing archive - Volume 14 Issue 3, August 2004, p. 199-222. +.. [#6] :doi:`"A Tutorial on Support Vector Regression" + <10.1023/B:STCO.0000035301.49549.88>` + Alex J. Smola, Bernhard Schölkopf - Statistics and Computing archive + Volume 14 Issue 3, August 2004, p. 199-222. - .. [#7] Schölkopf et. al `New Support Vector Algorithms - `_ +.. [#7] Schölkopf et. al `New Support Vector Algorithms + `_ - .. [#8] Crammer and Singer `On the Algorithmic Implementation ofMulticlass - Kernel-based Vector Machines - `_, - JMLR 2001. +.. [#8] Crammer and Singer `On the Algorithmic Implementation ofMulticlass + Kernel-based Vector Machines + `_, JMLR 2001. diff --git a/doc/modules/tree.rst b/doc/modules/tree.rst index b54b913573a34..9b475d6c09f5f 100644 --- a/doc/modules/tree.rst +++ b/doc/modules/tree.rst @@ -146,82 +146,78 @@ Once trained, you can plot the tree with the :func:`plot_tree` function:: :scale: 75 :align: center -|details-start| -**Alternative ways to export trees** -|details-split| - -We can also export the tree in `Graphviz -`_ format using the :func:`export_graphviz` -exporter. If you use the `conda `_ package manager, the graphviz binaries -and the python package can be installed with `conda install python-graphviz`. - -Alternatively binaries for graphviz can be downloaded from the graphviz project homepage, -and the Python wrapper installed from pypi with `pip install graphviz`. - -Below is an example graphviz export of the above tree trained on the entire -iris dataset; the results are saved in an output file `iris.pdf`:: - - - >>> import graphviz # doctest: +SKIP - >>> dot_data = tree.export_graphviz(clf, out_file=None) # doctest: +SKIP - >>> graph = graphviz.Source(dot_data) # doctest: +SKIP - >>> graph.render("iris") # doctest: +SKIP - -The :func:`export_graphviz` exporter also supports a variety of aesthetic -options, including coloring nodes by their class (or value for regression) and -using explicit variable and class names if desired. Jupyter notebooks also -render these plots inline automatically:: - - >>> dot_data = tree.export_graphviz(clf, out_file=None, # doctest: +SKIP - ... feature_names=iris.feature_names, # doctest: +SKIP - ... class_names=iris.target_names, # doctest: +SKIP - ... filled=True, rounded=True, # doctest: +SKIP - ... special_characters=True) # doctest: +SKIP - >>> graph = graphviz.Source(dot_data) # doctest: +SKIP - >>> graph # doctest: +SKIP - -.. only:: html - - .. figure:: ../images/iris.svg - :align: center - -.. only:: latex - - .. figure:: ../images/iris.pdf - :align: center - -.. figure:: ../auto_examples/tree/images/sphx_glr_plot_iris_dtc_001.png - :target: ../auto_examples/tree/plot_iris_dtc.html - :align: center - :scale: 75 - -Alternatively, the tree can also be exported in textual format with the -function :func:`export_text`. This method doesn't require the installation -of external libraries and is more compact: - - >>> from sklearn.datasets import load_iris - >>> from sklearn.tree import DecisionTreeClassifier - >>> from sklearn.tree import export_text - >>> iris = load_iris() - >>> decision_tree = DecisionTreeClassifier(random_state=0, max_depth=2) - >>> decision_tree = decision_tree.fit(iris.data, iris.target) - >>> 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 - - -|details-end| - -.. topic:: Examples: - - * :ref:`sphx_glr_auto_examples_tree_plot_iris_dtc.py` - * :ref:`sphx_glr_auto_examples_tree_plot_unveil_tree_structure.py` +.. dropdown:: Alternative ways to export trees + + We can also export the tree in `Graphviz + `_ format using the :func:`export_graphviz` + exporter. If you use the `conda `_ package manager, the graphviz binaries + and the python package can be installed with `conda install python-graphviz`. + + Alternatively binaries for graphviz can be downloaded from the graphviz project homepage, + and the Python wrapper installed from pypi with `pip install graphviz`. + + Below is an example graphviz export of the above tree trained on the entire + iris dataset; the results are saved in an output file `iris.pdf`:: + + + >>> import graphviz # doctest: +SKIP + >>> dot_data = tree.export_graphviz(clf, out_file=None) # doctest: +SKIP + >>> graph = graphviz.Source(dot_data) # doctest: +SKIP + >>> graph.render("iris") # doctest: +SKIP + + The :func:`export_graphviz` exporter also supports a variety of aesthetic + options, including coloring nodes by their class (or value for regression) and + using explicit variable and class names if desired. Jupyter notebooks also + render these plots inline automatically:: + + >>> dot_data = tree.export_graphviz(clf, out_file=None, # doctest: +SKIP + ... feature_names=iris.feature_names, # doctest: +SKIP + ... class_names=iris.target_names, # doctest: +SKIP + ... filled=True, rounded=True, # doctest: +SKIP + ... special_characters=True) # doctest: +SKIP + >>> graph = graphviz.Source(dot_data) # doctest: +SKIP + >>> graph # doctest: +SKIP + + .. only:: html + + .. figure:: ../images/iris.svg + :align: center + + .. only:: latex + + .. figure:: ../images/iris.pdf + :align: center + + .. figure:: ../auto_examples/tree/images/sphx_glr_plot_iris_dtc_001.png + :target: ../auto_examples/tree/plot_iris_dtc.html + :align: center + :scale: 75 + + Alternatively, the tree can also be exported in textual format with the + function :func:`export_text`. This method doesn't require the installation + of external libraries and is more compact: + + >>> from sklearn.datasets import load_iris + >>> from sklearn.tree import DecisionTreeClassifier + >>> from sklearn.tree import export_text + >>> iris = load_iris() + >>> decision_tree = DecisionTreeClassifier(random_state=0, max_depth=2) + >>> decision_tree = decision_tree.fit(iris.data, iris.target) + >>> 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 + + +.. rubric:: Examples + +* :ref:`sphx_glr_auto_examples_tree_plot_iris_dtc.py` +* :ref:`sphx_glr_auto_examples_tree_plot_unveil_tree_structure.py` .. _tree_regression: @@ -248,9 +244,9 @@ instead of integer values:: >>> clf.predict([[1, 1]]) array([0.5]) -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_tree_plot_tree_regression.py` +* :ref:`sphx_glr_auto_examples_tree_plot_tree_regression.py` .. _tree_multioutput: @@ -306,21 +302,17 @@ the lower half of those faces. :scale: 75 :align: center -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_tree_plot_tree_regression_multioutput.py` - * :ref:`sphx_glr_auto_examples_miscellaneous_plot_multioutput_face_completion.py` +* :ref:`sphx_glr_auto_examples_tree_plot_tree_regression_multioutput.py` +* :ref:`sphx_glr_auto_examples_miscellaneous_plot_multioutput_face_completion.py` -|details-start| -**References** -|details-split| +.. rubric:: References * M. Dumont et al, `Fast multi-class image annotation with random subwindows and multiple output randomized trees - `_, International Conference on - Computer Vision Theory and Applications 2009 - -|details-end| + `_, + International Conference on Computer Vision Theory and Applications 2009 .. _tree_complexity: @@ -412,36 +404,32 @@ Tree algorithms: ID3, C4.5, C5.0 and CART What are all the various decision tree algorithms and how do they differ from each other? Which one is implemented in scikit-learn? -|details-start| -**Various decision tree algorithms** -|details-split| - -ID3_ (Iterative Dichotomiser 3) was developed in 1986 by Ross Quinlan. -The algorithm creates a multiway tree, finding for each node (i.e. in -a greedy manner) the categorical feature that will yield the largest -information gain for categorical targets. Trees are grown to their -maximum size and then a pruning step is usually applied to improve the -ability of the tree to generalize to unseen data. - -C4.5 is the successor to ID3 and removed the restriction that features -must be categorical by dynamically defining a discrete attribute (based -on numerical variables) that partitions the continuous attribute value -into a discrete set of intervals. C4.5 converts the trained trees -(i.e. the output of the ID3 algorithm) into sets of if-then rules. -The accuracy of each rule is then evaluated to determine the order -in which they should be applied. Pruning is done by removing a rule's -precondition if the accuracy of the rule improves without it. - -C5.0 is Quinlan's latest version release under a proprietary license. -It uses less memory and builds smaller rulesets than C4.5 while being -more accurate. - -CART (Classification and Regression Trees) is very similar to C4.5, but -it differs in that it supports numerical target variables (regression) and -does not compute rule sets. CART constructs binary trees using the feature -and threshold that yield the largest information gain at each node. - -|details-end| +.. dropdown:: Various decision tree algorithms + + ID3_ (Iterative Dichotomiser 3) was developed in 1986 by Ross Quinlan. + The algorithm creates a multiway tree, finding for each node (i.e. in + a greedy manner) the categorical feature that will yield the largest + information gain for categorical targets. Trees are grown to their + maximum size and then a pruning step is usually applied to improve the + ability of the tree to generalize to unseen data. + + C4.5 is the successor to ID3 and removed the restriction that features + must be categorical by dynamically defining a discrete attribute (based + on numerical variables) that partitions the continuous attribute value + into a discrete set of intervals. C4.5 converts the trained trees + (i.e. the output of the ID3 algorithm) into sets of if-then rules. + The accuracy of each rule is then evaluated to determine the order + in which they should be applied. Pruning is done by removing a rule's + precondition if the accuracy of the rule improves without it. + + C5.0 is Quinlan's latest version release under a proprietary license. + It uses less memory and builds smaller rulesets than C4.5 while being + more accurate. + + CART (Classification and Regression Trees) is very similar to C4.5, but + it differs in that it supports numerical target variables (regression) and + does not compute rule sets. CART constructs binary trees using the feature + and threshold that yield the largest information gain at each node. scikit-learn uses an optimized version of the CART algorithm; however, the scikit-learn implementation does not support categorical variables for now. @@ -515,39 +503,35 @@ Log Loss or Entropy: H(Q_m) = - \sum_k p_{mk} \log(p_{mk}) -|details-start| -**Shannon entropy** -|details-split| - -The entropy criterion computes the Shannon entropy of the possible classes. It -takes the class frequencies of the training data points that reached a given -leaf :math:`m` as their probability. Using the **Shannon entropy as tree node -splitting criterion is equivalent to minimizing the log loss** (also known as -cross-entropy and multinomial deviance) between the true labels :math:`y_i` -and the probabilistic predictions :math:`T_k(x_i)` of the tree model :math:`T` for class :math:`k`. +.. dropdown:: Shannon entropy -To see this, first recall that the log loss of a tree model :math:`T` -computed on a dataset :math:`D` is defined as follows: + The entropy criterion computes the Shannon entropy of the possible classes. It + takes the class frequencies of the training data points that reached a given + leaf :math:`m` as their probability. Using the **Shannon entropy as tree node + splitting criterion is equivalent to minimizing the log loss** (also known as + cross-entropy and multinomial deviance) between the true labels :math:`y_i` + and the probabilistic predictions :math:`T_k(x_i)` of the tree model :math:`T` for class :math:`k`. -.. math:: + To see this, first recall that the log loss of a tree model :math:`T` + computed on a dataset :math:`D` is defined as follows: - \mathrm{LL}(D, T) = -\frac{1}{n} \sum_{(x_i, y_i) \in D} \sum_k I(y_i = k) \log(T_k(x_i)) + .. math:: -where :math:`D` is a training dataset of :math:`n` pairs :math:`(x_i, y_i)`. + \mathrm{LL}(D, T) = -\frac{1}{n} \sum_{(x_i, y_i) \in D} \sum_k I(y_i = k) \log(T_k(x_i)) -In a classification tree, the predicted class probabilities within leaf nodes -are constant, that is: for all :math:`(x_i, y_i) \in Q_m`, one has: -:math:`T_k(x_i) = p_{mk}` for each class :math:`k`. + where :math:`D` is a training dataset of :math:`n` pairs :math:`(x_i, y_i)`. -This property makes it possible to rewrite :math:`\mathrm{LL}(D, T)` as the -sum of the Shannon entropies computed for each leaf of :math:`T` weighted by -the number of training data points that reached each leaf: + In a classification tree, the predicted class probabilities within leaf nodes + are constant, that is: for all :math:`(x_i, y_i) \in Q_m`, one has: + :math:`T_k(x_i) = p_{mk}` for each class :math:`k`. -.. math:: + This property makes it possible to rewrite :math:`\mathrm{LL}(D, T)` as the + sum of the Shannon entropies computed for each leaf of :math:`T` weighted by + the number of training data points that reached each leaf: - \mathrm{LL}(D, T) = \sum_{m \in T} \frac{n_m}{n} H(Q_m) + .. math:: -|details-end| + \mathrm{LL}(D, T) = \sum_{m \in T} \frac{n_m}{n} H(Q_m) Regression criteria ------------------- @@ -685,13 +669,11 @@ with the smallest value of :math:`\alpha_{eff}` is the weakest link and will be pruned. This process stops when the pruned tree's minimal :math:`\alpha_{eff}` is greater than the ``ccp_alpha`` parameter. -.. topic:: Examples: +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_tree_plot_cost_complexity_pruning.py` +* :ref:`sphx_glr_auto_examples_tree_plot_cost_complexity_pruning.py` -|details-start| -**References** -|details-split| +.. rubric:: References .. [BRE] L. Breiman, J. Friedman, R. Olshen, and C. Stone. Classification and Regression Trees. Wadsworth, Belmont, CA, 1984. @@ -705,5 +687,3 @@ be pruned. This process stops when the pruned tree's minimal * T. Hastie, R. Tibshirani and J. Friedman. Elements of Statistical Learning, Springer, 2009. - -|details-end| diff --git a/doc/modules/unsupervised_reduction.rst b/doc/modules/unsupervised_reduction.rst index 90c80714c3131..f94d6ac301e47 100644 --- a/doc/modules/unsupervised_reduction.rst +++ b/doc/modules/unsupervised_reduction.rst @@ -24,9 +24,9 @@ PCA: principal component analysis :class:`decomposition.PCA` looks for a combination of features that capture well the variance of the original features. See :ref:`decompositions`. -.. topic:: **Examples** +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_applications_plot_face_recognition.py` +* :ref:`sphx_glr_auto_examples_applications_plot_face_recognition.py` Random projections ------------------- @@ -35,9 +35,9 @@ The module: :mod:`~sklearn.random_projection` provides several tools for data reduction by random projections. See the relevant section of the documentation: :ref:`random_projection`. -.. topic:: **Examples** +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_miscellaneous_plot_johnson_lindenstrauss_bound.py` +* :ref:`sphx_glr_auto_examples_miscellaneous_plot_johnson_lindenstrauss_bound.py` Feature agglomeration ------------------------ @@ -46,10 +46,10 @@ Feature agglomeration :ref:`hierarchical_clustering` to group together features that behave similarly. -.. topic:: **Examples** +.. rubric:: Examples - * :ref:`sphx_glr_auto_examples_cluster_plot_feature_agglomeration_vs_univariate_selection.py` - * :ref:`sphx_glr_auto_examples_cluster_plot_digits_agglomeration.py` +* :ref:`sphx_glr_auto_examples_cluster_plot_feature_agglomeration_vs_univariate_selection.py` +* :ref:`sphx_glr_auto_examples_cluster_plot_digits_agglomeration.py` .. topic:: **Feature scaling** diff --git a/doc/preface.rst b/doc/preface.rst deleted file mode 100644 index 447083a3a8136..0000000000000 --- a/doc/preface.rst +++ /dev/null @@ -1,32 +0,0 @@ -.. This helps define the TOC ordering for "about us" sections. Particularly - useful for PDF output as this section is not linked from elsewhere. - -.. Places global toc into the sidebar - -:globalsidebartoc: True - -.. _preface_menu: - -.. include:: includes/big_toc_css.rst -.. include:: tune_toc.rst - -======================= -Welcome to scikit-learn -======================= - -| - -.. toctree:: - :maxdepth: 2 - - install - faq - support - related_projects - about - testimonials/testimonials - whats_new - roadmap - governance - -| diff --git a/doc/scss/api-search.scss b/doc/scss/api-search.scss new file mode 100644 index 0000000000000..5e9bbfdcf27ba --- /dev/null +++ b/doc/scss/api-search.scss @@ -0,0 +1,114 @@ +/** + * This is the styling for the API index page (`api/index`), in particular for the API + * search table. It involves overriding the style sheet of DataTables which does not + * fit well into the theme, especially in dark theme; see https://datatables.net/ + */ + +.dt-container { + margin-bottom: 2rem; + + // Fix the selection box for entries per page + select.dt-input { + padding: 0 !important; + margin-right: 0.4rem !important; + + > option { + color: var(--pst-color-text-base); + background-color: var(--pst-color-background); + } + } + + // Fix the search box + input.dt-input { + width: 50%; + line-height: normal; + padding: 0.1rem 0.3rem !important; + margin-left: 0.4rem !important; + } + + table.dataTable { + th { + // Fix border color of the header + border-color: inherit; + + // Disabled the bottom margin of

    in table cells to avoid making it too tall + p { + margin-bottom: 0; + } + + // Fix the ascending/descending order buttons in the header + span.dt-column-order { + &::before, + &::after { + color: var(--pst-color-text-base); + line-height: 0.7rem !important; + } + } + } + + td { + // Fix color of text warning no records found + &.dt-empty { + color: var(--pst-color-text-base) !important; + } + } + + // Fix border color of the last row + tr:last-child > * { + border-bottom-color: var(--bs-table-border-color) !important; + } + } + + div.dt-paging button.dt-paging-button { + padding: 0 0.5rem; + + &.disabled { + color: var(--pst-color-border) !important; + + // Overwrite the !important color assigned by DataTables because we must keep + // the color of disabled buttons consistent with and without hovering + &:hover { + color: var(--pst-color-border) !important; + } + } + + // Fix colors of paging buttons + &.current, + &:not(.disabled):not(.current):hover { + color: var(--pst-color-on-surface) !important; + border-color: var(--pst-color-surface) !important; + background: var(--pst-color-surface) !important; + } + + // Highlight the border of the current selected paging button + &.current { + border-color: var(--pst-color-text-base) !important; + } + } +} + +// Styling the object description cells in the table +div.sk-apisearch-desc { + p { + margin-bottom: 0; + } + + div.caption > p { + a, + code { + color: var(--pst-color-text-muted); + } + + code { + padding: 0; + font-size: 0.7rem; + font-weight: var(--pst-font-weight-caption); + background-color: transparent; + } + + .sd-badge { + font-size: 0.7rem; + margin-left: 0.3rem; + } + } +} diff --git a/doc/scss/api.scss b/doc/scss/api.scss new file mode 100644 index 0000000000000..d7110def4ac09 --- /dev/null +++ b/doc/scss/api.scss @@ -0,0 +1,52 @@ +/** + * This is the styling for API reference pages, currently under `modules/generated`. + * Note that it should be applied *ONLY* to API reference pages, as the selectors are + * designed based on how `autodoc` and `autosummary` generate the stuff. + */ + +// Make the admonitions more compact +div.versionadded, +div.versionchanged, +div.deprecated { + margin: 1rem auto; + + > p { + margin: 0.3rem auto; + } +} + +// Make docstrings more compact +dd { + p:not(table *) { + margin-bottom: 0.5rem !important; + } + + ul { + margin-bottom: 0.5rem !important; + padding-left: 2rem !important; + } +} + +// The first method is too close the the docstring above +dl.py.method:first-of-type { + margin-top: 2rem; +} + +// https://github.com/pydata/pydata-sphinx-theme/blob/8cf45f835bfdafc5f3821014a18f3b7e0fc2d44b/src/pydata_sphinx_theme/assets/styles/content/_api.scss +dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) { + margin-bottom: 1.5rem; + + dd { + margin-left: 1.2rem; + } + + // "Parameters", "Returns", etc. in the docstring + dt.field-odd, + dt.field-even { + margin: 0.5rem 0; + + + dd > dl { + margin-bottom: 0.5rem; + } + } +} diff --git a/doc/scss/colors.scss b/doc/scss/colors.scss new file mode 100644 index 0000000000000..bbc6aa6c2a3d6 --- /dev/null +++ b/doc/scss/colors.scss @@ -0,0 +1,51 @@ +/** + * This is the style sheet for customized colors of scikit-learn. + * Tints and shades are generated by https://colorkit.co/color-shades-generator/ + * + * This file is compiled into styles/colors.css by sphinxcontrib.sass, see: + * https://sass-lang.com/guide/ + */ + +:root { + /* scikit-learn cyan */ + --sk-cyan-tint-9: #edf7fd; + --sk-cyan-tint-8: #daeffa; + --sk-cyan-tint-7: #c8e6f8; + --sk-cyan-tint-6: #b5def5; + --sk-cyan-tint-5: #a2d6f2; + --sk-cyan-tint-4: #8fcdef; + --sk-cyan-tint-3: #7ac5ec; + --sk-cyan-tint-2: #64bce9; + --sk-cyan-tint-1: #4bb4e5; + --sk-cyan: #29abe2; + --sk-cyan-shades-1: #2294c4; + --sk-cyan-shades-2: #1c7ea8; + --sk-cyan-shades-3: #15688c; + --sk-cyan-shades-4: #0f5471; + --sk-cyan-shades-5: #094057; + --sk-cyan-shades-6: #052d3e; + --sk-cyan-shades-7: #021b27; + --sk-cyan-shades-8: #010b12; + --sk-cyan-shades-9: #000103; + + /* scikit-learn orange */ + --sk-orange-tint-9: #fff5ec; + --sk-orange-tint-8: #ffead9; + --sk-orange-tint-7: #ffe0c5; + --sk-orange-tint-6: #ffd5b2; + --sk-orange-tint-5: #fecb9e; + --sk-orange-tint-4: #fdc08a; + --sk-orange-tint-3: #fcb575; + --sk-orange-tint-2: #fbaa5e; + --sk-orange-tint-1: #f99f44; + --sk-orange: #f7931e; + --sk-orange-shades-1: #d77f19; + --sk-orange-shades-2: #b76c13; + --sk-orange-shades-3: #99590e; + --sk-orange-shades-4: #7c4709; + --sk-orange-shades-5: #603605; + --sk-orange-shades-6: #452503; + --sk-orange-shades-7: #2c1601; + --sk-orange-shades-8: #150800; + --sk-orange-shades-9: #030100; +} diff --git a/doc/scss/custom.scss b/doc/scss/custom.scss new file mode 100644 index 0000000000000..ce4451fce4467 --- /dev/null +++ b/doc/scss/custom.scss @@ -0,0 +1,192 @@ +/** + * This is a general styling sheet. + * It should be used for customizations that affect multiple pages. + * + * This file is compiled into styles/custom.css by sphinxcontrib.sass, see: + * https://sass-lang.com/guide/ + */ + +/* Global */ + +code.literal { + border: 0; +} + +/* Version switcher */ + +.version-switcher__menu a.list-group-item.sk-avail-docs-link { + display: flex; + align-items: center; + + &:after { + content: var(--pst-icon-external-link); + font: var(--fa-font-solid); + font-size: 0.75rem; + margin-left: 0.5rem; + } +} + +/* Primary sidebar */ + +.bd-sidebar-primary { + width: 22.5%; + min-width: 16rem; + + // The version switcher button in the sidebar is ill-styled + button.version-switcher__button { + margin-bottom: unset; + margin-left: 0.3rem; + font-size: 1rem; + } + + // The section navigation part is to close to the right boundary (originally an even + // larger negative right margin was used) + nav.bd-links { + margin-right: -0.5rem; + } +} + +/* Article content */ + +.bd-article { + h1 { + font-weight: 500; + margin-bottom: 2rem; + } + + h2 { + font-weight: 500; + margin-bottom: 1.5rem; + } + + // Avoid changing the aspect ratio of images; add some padding so that at least + // there is some space between image and background in dark mode + img { + height: unset !important; + padding: 1%; + } + + // Resize table of contents to make the top few levels of headings more visible + li.toctree-l1 { + padding-bottom: 0.5em; + + > a { + font-size: 150%; + font-weight: bold; + } + } + + li.toctree-l2, + li.toctree-l3, + li.toctree-l4 { + margin-left: 15px; + } +} + +/* Dropdowns (sphinx-design) */ + +details.sd-dropdown { + &:hover > summary.sd-summary-title > a.headerlink { + visibility: visible; + } + + > summary.sd-summary-title { + > a.headerlink { + font-size: 1rem; + } + + // See `js/scripts/dropdown.js`: this is styling the "expand/collapse all" button + > button.sk-toggle-all { + color: var(--pst-sd-dropdown-color); + top: 0.9rem !important; + right: 3rem !important; + pointer-events: auto !important; + display: none; + border: none; + background: transparent; + } + } + + &[open] > summary.sd-summary-title:hover > .sd-summary-up.sk-toggle-all, + &:not([open]) + > summary.sd-summary-title:hover + > .sd-summary-down.sk-toggle-all { + display: block; + } +} + +/* scikit-learn buttons */ + +a.btn { + &.sk-btn-orange { + background-color: var(--sk-orange-tint-1); + color: black !important; + + &:hover { + background-color: var(--sk-orange-tint-3); + } + } + + &.sk-btn-cyan { + background-color: var(--sk-cyan-shades-2); + color: white !important; + + &:hover { + background-color: var(--sk-cyan-shades-1); + } + } +} + +/* scikit-learn avatar grid, see build_tools/generate_authors_table.py */ + +div.sk-authors-container { + display: flex; + flex-wrap: wrap; + justify-content: center; + + > div { + width: 6rem; + margin: 0.5rem; + font-size: 0.9rem; + } +} + +/* scikit-learn text-image grid, used in testimonials and sponsors pages */ + +@mixin sk-text-image-grid($img-max-height) { + display: flex; + align-items: center; + flex-wrap: wrap; + + div.text-box, + div.image-box { + width: 50%; + + @media screen and (max-width: 500px) { + width: 100%; + } + } + + div.text-box .annotation { + font-size: 0.9rem; + font-style: italic; + color: var(--pst-color-text-muted); + } + + div.image-box { + text-align: center; + + img { + max-height: $img-max-height; + max-width: 50%; + } + } +} + +div.sk-text-image-grid-small { + @include sk-text-image-grid(60px); +} + +div.sk-text-image-grid-large { + @include sk-text-image-grid(100px); +} diff --git a/doc/scss/index.scss b/doc/scss/index.scss new file mode 100644 index 0000000000000..4e3f371f236d4 --- /dev/null +++ b/doc/scss/index.scss @@ -0,0 +1,175 @@ +/** + * Styling sheet for the scikit-learn landing page. This should be loaded only for the + * landing page. + * + * This file is compiled into styles/index.css by sphinxcontrib.sass, see: + * https://sass-lang.com/guide/ + */ + +/* Theme-aware colors for the landing page */ + +html { + &[data-theme="light"] { + --sk-landing-bg-1: var(--sk-cyan-shades-3); + --sk-landing-bg-2: var(--sk-cyan); + --sk-landing-bg-3: var(--sk-orange-tint-8); + --sk-landing-bg-4: var(--sk-orange-tint-3); + } + + &[data-theme="dark"] { + --sk-landing-bg-1: var(--sk-cyan-shades-5); + --sk-landing-bg-2: var(--sk-cyan-shades-2); + --sk-landing-bg-3: var(--sk-orange-tint-4); + --sk-landing-bg-4: var(--sk-orange-tint-1); + } +} + +/* General */ + +div.sk-landing-container { + max-width: 1400px; +} + +/* Top bar */ + +div.sk-landing-top-bar { + background-image: linear-gradient( + 160deg, + var(--sk-landing-bg-1) 0%, + var(--sk-landing-bg-2) 17%, + var(--sk-landing-bg-3) 59%, + var(--sk-landing-bg-4) 100% + ); + + .sk-landing-header, + .sk-landing-subheader { + color: white; + text-shadow: 0px 0px 8px var(--sk-landing-bg-1); + } + + .sk-landing-header { + font-size: 3.2rem; + margin-bottom: 0.5rem; + } + + .sk-landing-subheader { + letter-spacing: 0.17rem; + margin-top: 0; + font-weight: 500; + } + + a.sk-btn-orange { + font-size: 1.1rem; + font-weight: 500; + } + + ul.sk-landing-header-body { + margin-top: auto; + margin-bottom: auto; + font-size: 1.2rem; + font-weight: 500; + color: black; + } +} + +/* Body */ + +div.sk-landing-body { + div.card { + background-color: var(--pst-color-background); + border-color: var(--pst-color-border); + } + + .sk-px-xl-4 { + @media screen and (min-width: 1200px) { + padding-left: 1.3rem !important; + padding-right: 1.3rem !important; + } + } + + .card-body { + p { + margin-bottom: 0.8rem; + } + + .sk-card-title { + font-weight: 700; + margin: 0 0 1rem 0; + } + } + + .sk-card-img-container { + display: flex; + justify-content: center; + align-items: end; + margin-bottom: 1rem; + + img { + max-width: unset; + height: 15rem; + } + } +} + +/* More info */ + +div.sk-landing-more-info { + font-size: 0.96rem; + background-color: var(--pst-color-surface); + + .sk-landing-call-header { + font-weight: 700; + margin-top: 0; + + html[data-theme="light"] & { + color: var(--sk-orange-shades-1); + } + + html[data-theme="dark"] & { + color: var(--sk-orange); + } + } + + ul.sk-landing-call-list > li { + margin-bottom: 0.25rem; + } + + .sk-who-uses-carousel { + min-height: 200px; + + .carousel-item img { + max-height: 100px; + max-width: 50%; + margin: 0.5rem; + } + } + + .sk-more-testimonials { + text-align: right !important; + } +} + +/* Footer */ + +div.sk-landing-footer { + a.sk-footer-funding-link { + text-decoration: none; + + p.sk-footer-funding-text { + color: var(--pst-color-link); + + &:hover { + color: var(--pst-color-secondary); + } + } + + div.sk-footer-funding-logos > img { + max-height: 40px; + max-width: 85px; + margin: 0 8px 8px 8px; + padding: 5px; + border-radius: 3px; + background-color: white; + } + } +} diff --git a/doc/scss/install.scss b/doc/scss/install.scss new file mode 100644 index 0000000000000..965b3d589e86d --- /dev/null +++ b/doc/scss/install.scss @@ -0,0 +1,33 @@ +/** + * Styling for the installation page, including overriding some default styling of + * sphinx-design. This style sheet should be included only for the install page. + * + * This file is compiled into styles/install.css by sphinxcontrib.sass, see: + * https://sass-lang.com/guide/ + */ + +.install-instructions .sd-tab-set { + .sd-tab-content { + box-shadow: 0 -0.0625rem var(--pst-color-border); + padding: 0.5rem 0 0 0; + + > p:first-child { + margin-top: 1.25rem !important; + } + } + + > label.sd-tab-label { + border-top: none !important; + padding: 0 0 0.1rem 0; + margin: 0; + text-align: center; + + &.tab-6 { + width: 50% !important; + } + + &.tab-4 { + width: calc(100% / 3) !important; + } + } +} diff --git a/doc/sphinxext/add_toctree_functions.py b/doc/sphinxext/add_toctree_functions.py deleted file mode 100644 index 4459ab971f4c4..0000000000000 --- a/doc/sphinxext/add_toctree_functions.py +++ /dev/null @@ -1,160 +0,0 @@ -"""Inspired by https://github.com/pandas-dev/pydata-sphinx-theme - -BSD 3-Clause License - -Copyright (c) 2018, pandas -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -""" - -import docutils - - -def add_toctree_functions(app, pagename, templatename, context, doctree): - """Add functions so Jinja templates can add toctree objects. - - This converts the docutils nodes into a nested dictionary that Jinja can - use in our templating. - """ - from sphinx.environment.adapters.toctree import TocTree - - def get_nav_object(maxdepth=None, collapse=True, numbered=False, **kwargs): - """Return a list of nav links that can be accessed from Jinja. - - Parameters - ---------- - maxdepth: int - How many layers of TocTree will be returned - collapse: bool - Whether to only include sub-pages of the currently-active page, - instead of sub-pages of all top-level pages of the site. - numbered: bool - Whether to add section number to title - kwargs: key/val pairs - Passed to the `TocTree.get_toctree_for` Sphinx method - """ - # The TocTree will contain the full site TocTree including sub-pages. - # "collapse=True" collapses sub-pages of non-active TOC pages. - # maxdepth controls how many TOC levels are returned - toctree = TocTree(app.env).get_toctree_for( - pagename, app.builder, collapse=collapse, maxdepth=maxdepth, **kwargs - ) - # If no toctree is defined (AKA a single-page site), skip this - if toctree is None: - return [] - - # toctree has this structure - # - # - # - # - # `list_item`s are the actual TOC links and are the only thing we want - toc_items = [ - item - for child in toctree.children - for item in child - if isinstance(item, docutils.nodes.list_item) - ] - - # Now convert our docutils nodes into dicts that Jinja can use - nav = [ - docutils_node_to_jinja(child, only_pages=True, numbered=numbered) - for child in toc_items - ] - - return nav - - context["get_nav_object"] = get_nav_object - - -def docutils_node_to_jinja(list_item, only_pages=False, numbered=False): - """Convert a docutils node to a structure that can be read by Jinja. - - Parameters - ---------- - list_item : docutils list_item node - A parent item, potentially with children, corresponding to the level - of a TocTree. - only_pages : bool - Only include items for full pages in the output dictionary. Exclude - anchor links (TOC items with a URL that starts with #) - numbered: bool - Whether to add section number to title - - Returns - ------- - nav : dict - The TocTree, converted into a dictionary with key/values that work - within Jinja. - """ - if not list_item.children: - return None - - # We assume this structure of a list item: - # - # - # <-- the thing we want - reference = list_item.children[0].children[0] - title = reference.astext() - url = reference.attributes["refuri"] - active = "current" in list_item.attributes["classes"] - - secnumber = reference.attributes.get("secnumber", None) - if numbered and secnumber is not None: - secnumber = ".".join(str(n) for n in secnumber) - title = f"{secnumber}. {title}" - - # If we've got an anchor link, skip it if we wish - if only_pages and "#" in url: - return None - - # Converting the docutils attributes into jinja-friendly objects - nav = {} - nav["title"] = title - nav["url"] = url - nav["active"] = active - - # Recursively convert children as well - # If there are sub-pages for this list_item, there should be two children: - # a paragraph, and a bullet_list. - nav["children"] = [] - if len(list_item.children) > 1: - # The `.children` of the bullet_list has the nodes of the sub-pages. - subpage_list = list_item.children[1].children - for sub_page in subpage_list: - child_nav = docutils_node_to_jinja( - sub_page, only_pages=only_pages, numbered=numbered - ) - if child_nav is not None: - nav["children"].append(child_nav) - return nav - - -def setup(app): - app.connect("html-page-context", add_toctree_functions) - - return {"parallel_read_safe": True, "parallel_write_safe": True} diff --git a/doc/sphinxext/autoshortsummary.py b/doc/sphinxext/autoshortsummary.py new file mode 100644 index 0000000000000..8451f3133d05b --- /dev/null +++ b/doc/sphinxext/autoshortsummary.py @@ -0,0 +1,53 @@ +from sphinx.ext.autodoc import ModuleLevelDocumenter + + +class ShortSummaryDocumenter(ModuleLevelDocumenter): + """An autodocumenter that only renders the short summary of the object.""" + + # Defines the usage: .. autoshortsummary:: {{ object }} + objtype = "shortsummary" + + # Disable content indentation + content_indent = "" + + # Avoid being selected as the default documenter for some objects, because we are + # returning `can_document_member` as True for all objects + priority = -99 + + @classmethod + def can_document_member(cls, member, membername, isattr, parent): + """Allow documenting any object.""" + return True + + def get_object_members(self, want_all): + """Document no members.""" + return (False, []) + + def add_directive_header(self, sig): + """Override default behavior to add no directive header or options.""" + pass + + def add_content(self, more_content): + """Override default behavior to add only the first line of the docstring. + + Modified based on the part of processing docstrings in the original + implementation of this method. + + https://github.com/sphinx-doc/sphinx/blob/faa33a53a389f6f8bc1f6ae97d6015fa92393c4a/sphinx/ext/autodoc/__init__.py#L609-L622 + """ + sourcename = self.get_sourcename() + docstrings = self.get_doc() + + if docstrings is not None: + if not docstrings: + docstrings.append([]) + # Get the first non-empty line of the processed docstring; this could lead + # to unexpected results if the object does not have a short summary line. + short_summary = next( + (s for s in self.process_doc(docstrings) if s), "" + ) + self.add_line(short_summary, sourcename, 0) + + +def setup(app): + app.add_autodocumenter(ShortSummaryDocumenter) diff --git a/doc/sphinxext/dropdown_anchors.py b/doc/sphinxext/dropdown_anchors.py new file mode 100644 index 0000000000000..eb0b414de6ae8 --- /dev/null +++ b/doc/sphinxext/dropdown_anchors.py @@ -0,0 +1,78 @@ +import re + +from docutils import nodes +from sphinx.transforms.post_transforms import SphinxPostTransform +from sphinx_design.dropdown import dropdown_main, dropdown_title + + +class DropdownAnchorAdder(SphinxPostTransform): + """Insert anchor links to the sphinx-design dropdowns. + + Some of the dropdowns were originally headers that had automatic anchors, so we + need to make sure that the old anchors still work. See the original implementation + (in JS): https://github.com/scikit-learn/scikit-learn/pull/27409 + + The structure of each sphinx-design dropdown node is expected to be: + + + + ...icon <-- This exists if the "icon" option of the sphinx-design + dropdown is set; we do not use it in our documentation + + ...title <-- This may contain multiple nodes, e.g. literal nodes if + there are inline codes; we use the concatenated text of + all these nodes to generate the anchor ID + + Here we insert the anchor link! + + <-- The "dropdown closed" marker + <-- The "dropdown open" marker + + + ...main contents + + + """ + + default_priority = 9999 # Apply later than everything else + formats = ["html"] + + def run(self): + """Run the post transformation.""" + # Counter to store the duplicated summary text to add it as a suffix in the + # anchor ID + anchor_id_counters = {} + + for sd_dropdown in self.document.findall(dropdown_main): + # Grab the dropdown title + sd_dropdown_title = sd_dropdown.next_node(dropdown_title) + + # Concatenate the text of relevant nodes as the title text + # Since we do not have the prefix icon, the relevant nodes are the very + # first child node until the third last node (last two are markers) + title_text = "".join( + node.astext() for node in sd_dropdown_title.children[:-2] + ) + + # The ID uses the first line, lowercased, with spaces replaced by dashes; + # suffix the anchor ID with a counter if it already exists + anchor_id = re.sub(r"\s+", "-", title_text.strip().split("\n")[0]).lower() + if anchor_id in anchor_id_counters: + anchor_id_counters[anchor_id] += 1 + anchor_id = f"{anchor_id}-{anchor_id_counters[anchor_id]}" + else: + anchor_id_counters[anchor_id] = 1 + sd_dropdown["ids"].append(anchor_id) + + # Create the anchor element and insert after the title text; we do this + # directly with raw HTML + anchor_html = ( + f'#' + ) + anchor_node = nodes.raw("", anchor_html, format="html") + sd_dropdown_title.insert(-2, anchor_node) # before the two markers + + +def setup(app): + app.add_post_transform(DropdownAnchorAdder) diff --git a/doc/sphinxext/move_gallery_links.py b/doc/sphinxext/move_gallery_links.py new file mode 100644 index 0000000000000..dff27f7358c7f --- /dev/null +++ b/doc/sphinxext/move_gallery_links.py @@ -0,0 +1,193 @@ +""" +This script intends to better integrate sphinx-gallery into pydata-sphinx-theme. In +particular, it moves the download links and badge links in the footer of each generated +example page into the secondary sidebar, then removes the footer and the top note +pointing to the footer. + +The download links are for Python source code and Jupyter notebook respectively, and +the badge links are for JupyterLite and Binder. + +Currently this is achieved via post-processing the HTML generated by sphinx-gallery. +This hack can be removed if the following upstream issue is resolved: +https://github.com/sphinx-gallery/sphinx-gallery/issues/1258 +""" + +from pathlib import Path + +from bs4 import BeautifulSoup +from sphinx.util.display import status_iterator +from sphinx.util.logging import getLogger + +logger = getLogger(__name__) + + +def move_gallery_links(app, exception): + if exception is not None: + return + + for gallery_dir in app.config.sphinx_gallery_conf["gallery_dirs"]: + html_gallery_dir = Path(app.builder.outdir, gallery_dir) + + # Get all gallery example files to be tweaked; tuples (file, docname) + flat = [] + for file in html_gallery_dir.rglob("*.html"): + if file.name in ("index.html", "sg_execution_times.html"): + # These are not gallery example pages, skip + continue + + # Extract the documentation name from the path + docname = file.relative_to(app.builder.outdir).with_suffix("").as_posix() + if docname in app.config.html_context["redirects"]: + # This is a redirected page, skip + continue + if docname not in app.project.docnames: + # This should not happen, warn + logger.warning(f"Document {docname} not found but {file} exists") + continue + flat.append((file, docname)) + + for html_file, _ in status_iterator( + flat, + length=len(flat), + summary="Tweaking gallery links... ", + verbosity=app.verbosity, + stringify_func=lambda x: x[1], # display docname + ): + with html_file.open("r", encoding="utf-8") as f: + html = f.read() + soup = BeautifulSoup(html, "html.parser") + + # Find the secondary sidebar; it should exist in all gallery example pages + secondary_sidebar = soup.find("div", class_="sidebar-secondary-items") + if secondary_sidebar is None: + logger.warning(f"Secondary sidebar not found in {html_file}") + continue + + def _create_secondary_sidebar_component(items): + """Create a new component in the secondary sidebar. + + `items` should be a list of dictionaries with "element" being the bs4 + tag of the component and "title" being the title (None if not needed). + """ + component = soup.new_tag("div", **{"class": "sidebar-secondary-item"}) + for item in items: + item_wrapper = soup.new_tag("div") + item_wrapper.append(item["element"]) + if item["title"]: + item_wrapper["title"] = item["title"] + component.append(item_wrapper) + secondary_sidebar.append(component) + + def _create_download_link(link, is_jupyter=False): + """Create a download link to be appended to a component. + + `link` should be the bs4 tag of the original download link, either for + the Python source code (is_jupyter=False) of for the Jupyter notebook + (is_jupyter=True). `link` will not be removed; instead the whole + footnote would be removed where `link` is located. + + This returns a dictionary with "element" being the bs4 tag of the new + download link and "title" being the name of the file to download. + """ + new_link = soup.new_tag("a", href=link["href"], download="") + + # Place a download icon at the beginning of the new link + download_icon = soup.new_tag("i", **{"class": "fa-solid fa-download"}) + new_link.append(download_icon) + + # Create the text of the new link; it is shortend to fit better into + # the secondary sidebar. The leading space before "Download ..." is + # intentional to create a small gap between the icon and the text, + # being consistent with the other pydata-sphinx-theme components + link_type = "Jupyter notebook" if is_jupyter else "source code" + new_text = soup.new_string(f" Download {link_type}") + new_link.append(new_text) + + # Get the file name to download and use it as the title of the new link + # which will show up when hovering over the link; the file name is + # expected to be in the last span of `link` + link_spans = link.find_all("span") + title = link_spans[-1].text if link_spans else None + + return {"element": new_link, "title": title} + + def _create_badge_link(link): + """Create a badge link to be appended to a component. + + `link` should be the bs4 tag of the original badge link, either for + binder or JupyterLite. `link` will not be removed; instead the whole + footnote would be removed where `link` is located. + + This returns a dictionary with "element" being the bs4 tag of the new + download link and "title" being `None` (no need). + """ + new_link = soup.new_tag("a", href=link["href"]) + + # The link would essentially be an anchor wrapper outside the image of + # the badge; we get the src and alt attributes by finding the original + # image and limit the height to 20px (fixed) so that the secondary + # sidebar will appear neater + badge_img = link.find("img") + new_img = soup.new_tag( + "img", src=badge_img["src"], alt=badge_img["alt"], height=20 + ) + new_link.append(new_img) + + return {"element": new_link, "title": None} + + try: + # `sg_note` is the "go to the end" note at the top of the page + # `sg_footer` is the footer with the download links and badge links + # These will be removed at the end if new links are successfully created + sg_note = soup.find("div", class_="sphx-glr-download-link-note") + sg_footer = soup.find("div", class_="sphx-glr-footer") + + # If any one of these two is not found, we directly give up tweaking + if sg_note is None or sg_footer is None: + continue + + # Move the download links into the secondary sidebar + py_link_div = sg_footer.find("div", class_="sphx-glr-download-python") + ipy_link_div = sg_footer.find("div", class_="sphx-glr-download-jupyter") + _create_secondary_sidebar_component( + [ + _create_download_link(py_link_div.a, is_jupyter=False), + _create_download_link(ipy_link_div.a, is_jupyter=True), + ] + ) + + # Move the badge links into the secondary sidebar + lite_link_div = sg_footer.find("div", class_="lite-badge") + binder_link_div = sg_footer.find("div", class_="binder-badge") + _create_secondary_sidebar_component( + [ + _create_badge_link(lite_link_div.a), + _create_badge_link(binder_link_div.a), + ] + ) + + # Remove the sourcelink component from the secondary sidebar; the reason + # we do not remove it by configuration is that we need the secondary + # sidebar to be present for this script to work, while in-page toc alone + # could have been empty + sourcelink = secondary_sidebar.find("div", class_="sourcelink") + if sourcelink is not None: + sourcelink.parent.extract() # because sourcelink has a wrapper div + + # Remove the the top note and the whole footer + sg_note.extract() + sg_footer.extract() + + except Exception: + # If any step fails we directly skip the file + continue + + # Write the modified file back + with html_file.open("w", encoding="utf-8") as f: + f.write(str(soup)) + + +def setup(app): + # Default priority is 500 which sphinx-gallery uses for its build-finished events; + # we need a larger priority to run after sphinx-gallery (larger is later) + app.connect("build-finished", move_gallery_links, priority=900) diff --git a/doc/sphinxext/override_pst_pagetoc.py b/doc/sphinxext/override_pst_pagetoc.py new file mode 100644 index 0000000000000..f5697de8ef155 --- /dev/null +++ b/doc/sphinxext/override_pst_pagetoc.py @@ -0,0 +1,84 @@ +from functools import cache + +from sphinx.util.logging import getLogger + +logger = getLogger(__name__) + + +def override_pst_pagetoc(app, pagename, templatename, context, doctree): + """Overrides the `generate_toc_html` function of pydata-sphinx-theme for API.""" + + @cache + def generate_api_toc_html(kind="html"): + """Generate the in-page toc for an API page. + + This relies on the `generate_toc_html` function added by pydata-sphinx-theme + into the context. We save the original function into `pst_generate_toc_html` + and override `generate_toc_html` with this function for generated API pages. + + The pagetoc of an API page would look like the following: + +

      <-- Unwrap +
    • <-- Unwrap + {{obj}} <-- Decompose + +
        +
      • + ...object +
          <-- Set visible if exists +
        • ...method 1
        • <-- Shorten +
        • ...method 2
        • <-- Shorten + ...more methods <-- Shorten +
        +
      • +
      • ...gallery examples
      • +
      + +
    • <-- Unwrapped +
    <-- Unwrapped + """ + soup = context["pst_generate_toc_html"](kind="soup") + + try: + # Unwrap the outermost level + soup.ul.unwrap() + soup.li.unwrap() + soup.a.decompose() + + # Get all toc-h2 level entries, where the first one should be the function + # or class, and the second one, if exists, should be the examples; there + # should be no more than two entries at this level for generated API pages + lis = soup.ul.select("li.toc-h2") + main_li = lis[0] + meth_list = main_li.ul + + if meth_list is not None: + # This is a class API page, we remove the class name from the method + # names to make them better fit into the secondary sidebar; also we + # make the toc-h3 level entries always visible to more easily navigate + # through the methods + meth_list["class"].append("visible") + for meth in meth_list.find_all("li", {"class": "toc-h3"}): + target = meth.a.code.span + target.string = target.string.split(".", 1)[1] + + # This corresponds to the behavior of `generate_toc_html` + return str(soup) if kind == "html" else soup + + except Exception as e: + # Upon any failure we return the original pagetoc + logger.warning( + f"Failed to generate API pagetoc for {pagename}: {e}; falling back" + ) + return context["pst_generate_toc_html"](kind=kind) + + # Override the pydata-sphinx-theme implementation for generate API pages + if pagename.startswith("modules/generated/"): + context["pst_generate_toc_html"] = context["generate_toc_html"] + context["generate_toc_html"] = generate_api_toc_html + + +def setup(app): + # Need to be triggered after `pydata_sphinx_theme.toctree.add_toctree_functions`, + # and since default priority is 500 we set 900 for safety + app.connect("html-page-context", override_pst_pagetoc, priority=900) diff --git a/doc/supervised_learning.rst b/doc/supervised_learning.rst index 71fb3007c2e3c..ba24e8ee23c6f 100644 --- a/doc/supervised_learning.rst +++ b/doc/supervised_learning.rst @@ -1,9 +1,3 @@ -.. Places parent toc into the sidebar - -:parenttoc: True - -.. include:: includes/big_toc_css.rst - .. _supervised-learning: Supervised learning diff --git a/doc/templates/base.rst b/doc/templates/base.rst new file mode 100644 index 0000000000000..ee86bd8a18dbe --- /dev/null +++ b/doc/templates/base.rst @@ -0,0 +1,36 @@ +{{ objname | escape | underline(line="=") }} + +{% if objtype == "module" -%} + +.. automodule:: {{ fullname }} + +{%- elif objtype == "function" -%} + +.. currentmodule:: {{ module }} + +.. autofunction:: {{ objname }} + +.. minigallery:: {{ module }}.{{ objname }} + :add-heading: Gallery examples + :heading-level: - + +{%- elif objtype == "class" -%} + +.. currentmodule:: {{ module }} + +.. autoclass:: {{ objname }} + :members: + :inherited-members: + :special-members: __call__ + +.. minigallery:: {{ module }}.{{ objname }} {% for meth in methods %}{{ module }}.{{ objname }}.{{ meth }} {% endfor %} + :add-heading: Gallery examples + :heading-level: - + +{%- else -%} + +.. currentmodule:: {{ module }} + +.. auto{{ objtype }}:: {{ objname }} + +{%- endif -%} diff --git a/doc/templates/class.rst b/doc/templates/class.rst deleted file mode 100644 index 1e98be4099b73..0000000000000 --- a/doc/templates/class.rst +++ /dev/null @@ -1,17 +0,0 @@ -.. - The empty line below should not be removed. It is added such that the `rst_prolog` - is added before the :mod: directive. Otherwise, the rendering will show as a - paragraph instead of a header. - -:mod:`{{module}}`.{{objname}} -{{ underline }}============== - -.. currentmodule:: {{ module }} - -.. autoclass:: {{ objname }} - -.. include:: {{module}}.{{objname}}.examples - -.. raw:: html - -
    diff --git a/doc/templates/class_with_call.rst b/doc/templates/class_with_call.rst deleted file mode 100644 index bc1567709c9d3..0000000000000 --- a/doc/templates/class_with_call.rst +++ /dev/null @@ -1,21 +0,0 @@ -.. - The empty line below should not be removed. It is added such that the `rst_prolog` - is added before the :mod: directive. Otherwise, the rendering will show as a - paragraph instead of a header. - -:mod:`{{module}}`.{{objname}} -{{ underline }}=============== - -.. currentmodule:: {{ module }} - -.. autoclass:: {{ objname }} - - {% block methods %} - .. automethod:: __call__ - {% endblock %} - -.. include:: {{module}}.{{objname}}.examples - -.. raw:: html - -
    diff --git a/doc/templates/deprecated_class.rst b/doc/templates/deprecated_class.rst deleted file mode 100644 index 5c31936f6fc36..0000000000000 --- a/doc/templates/deprecated_class.rst +++ /dev/null @@ -1,28 +0,0 @@ -.. - The empty line below should not be removed. It is added such that the `rst_prolog` - is added before the :mod: directive. Otherwise, the rendering will show as a - paragraph instead of a header. - -:mod:`{{module}}`.{{objname}} -{{ underline }}============== - -.. meta:: - :robots: noindex - -.. warning:: - **DEPRECATED** - - -.. currentmodule:: {{ module }} - -.. autoclass:: {{ objname }} - - {% block methods %} - .. automethod:: __init__ - {% endblock %} - -.. include:: {{module}}.{{objname}}.examples - -.. raw:: html - -
    diff --git a/doc/templates/deprecated_class_with_call.rst b/doc/templates/deprecated_class_with_call.rst deleted file mode 100644 index 072a31112be50..0000000000000 --- a/doc/templates/deprecated_class_with_call.rst +++ /dev/null @@ -1,29 +0,0 @@ -.. - The empty line below should not be removed. It is added such that the `rst_prolog` - is added before the :mod: directive. Otherwise, the rendering will show as a - paragraph instead of a header. - -:mod:`{{module}}`.{{objname}} -{{ underline }}=============== - -.. meta:: - :robots: noindex - -.. warning:: - **DEPRECATED** - - -.. currentmodule:: {{ module }} - -.. autoclass:: {{ objname }} - - {% block methods %} - .. automethod:: __init__ - .. automethod:: __call__ - {% endblock %} - -.. include:: {{module}}.{{objname}}.examples - -.. raw:: html - -
    diff --git a/doc/templates/deprecated_class_without_init.rst b/doc/templates/deprecated_class_without_init.rst deleted file mode 100644 index a26afbead5451..0000000000000 --- a/doc/templates/deprecated_class_without_init.rst +++ /dev/null @@ -1,24 +0,0 @@ -.. - The empty line below should not be removed. It is added such that the `rst_prolog` - is added before the :mod: directive. Otherwise, the rendering will show as a - paragraph instead of a header. - -:mod:`{{module}}`.{{objname}} -{{ underline }}============== - -.. meta:: - :robots: noindex - -.. warning:: - **DEPRECATED** - - -.. currentmodule:: {{ module }} - -.. autoclass:: {{ objname }} - -.. include:: {{module}}.{{objname}}.examples - -.. raw:: html - -
    diff --git a/doc/templates/deprecated_function.rst b/doc/templates/deprecated_function.rst deleted file mode 100644 index ead5abec27076..0000000000000 --- a/doc/templates/deprecated_function.rst +++ /dev/null @@ -1,24 +0,0 @@ -.. - The empty line below should not be removed. It is added such that the `rst_prolog` - is added before the :mod: directive. Otherwise, the rendering will show as a - paragraph instead of a header. - -:mod:`{{module}}`.{{objname}} -{{ underline }}==================== - -.. meta:: - :robots: noindex - -.. warning:: - **DEPRECATED** - - -.. currentmodule:: {{ module }} - -.. autofunction:: {{ objname }} - -.. include:: {{module}}.{{objname}}.examples - -.. raw:: html - -
    diff --git a/doc/templates/display_all_class_methods.rst b/doc/templates/display_all_class_methods.rst deleted file mode 100644 index b179473cf841e..0000000000000 --- a/doc/templates/display_all_class_methods.rst +++ /dev/null @@ -1,19 +0,0 @@ -.. - The empty line below should not be removed. It is added such that the `rst_prolog` - is added before the :mod: directive. Otherwise, the rendering will show as a - paragraph instead of a header. - -:mod:`{{module}}`.{{objname}} -{{ underline }}============== - -.. currentmodule:: {{ module }} - -.. autoclass:: {{ objname }} - -.. include:: {{module}}.{{objname}}.examples -.. include:: {{module}}.{{objname}}.from_estimator.examples -.. include:: {{module}}.{{objname}}.from_predictions.examples - -.. raw:: html - -
    diff --git a/doc/templates/display_only_from_estimator.rst b/doc/templates/display_only_from_estimator.rst deleted file mode 100644 index 9981910dc8be7..0000000000000 --- a/doc/templates/display_only_from_estimator.rst +++ /dev/null @@ -1,18 +0,0 @@ -.. - The empty line below should not be removed. It is added such that the `rst_prolog` - is added before the :mod: directive. Otherwise, the rendering will show as a - paragraph instead of a header. - -:mod:`{{module}}`.{{objname}} -{{ underline }}============== - -.. currentmodule:: {{ module }} - -.. autoclass:: {{ objname }} - -.. include:: {{module}}.{{objname}}.examples -.. include:: {{module}}.{{objname}}.from_estimator.examples - -.. raw:: html - -
    diff --git a/doc/templates/function.rst b/doc/templates/function.rst deleted file mode 100644 index 93d368ecfe6d5..0000000000000 --- a/doc/templates/function.rst +++ /dev/null @@ -1,17 +0,0 @@ -.. - The empty line below should not be removed. It is added such that the `rst_prolog` - is added before the :mod: directive. Otherwise, the rendering will show as a - paragraph instead of a header. - -:mod:`{{module}}`.{{objname}} -{{ underline }}==================== - -.. currentmodule:: {{ module }} - -.. autofunction:: {{ objname }} - -.. include:: {{module}}.{{objname}}.examples - -.. raw:: html - -
    diff --git a/doc/templates/generate_deprecated.sh b/doc/templates/generate_deprecated.sh deleted file mode 100755 index a7301fb5dc419..0000000000000 --- a/doc/templates/generate_deprecated.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -for f in [^d]*; do (head -n2 < $f; echo ' -.. meta:: - :robots: noindex - -.. warning:: - **DEPRECATED** -'; tail -n+3 $f) > deprecated_$f; done diff --git a/doc/templates/index.html b/doc/templates/index.html index 74816a4b473d3..61457be2494ea 100644 --- a/doc/templates/index.html +++ b/doc/templates/index.html @@ -1,25 +1,27 @@ {% extends "layout.html" %} {% set title = 'scikit-learn: machine learning in Python' %} -{% if theme_link_to_live_contributing_page|tobool %} +{% if is_devrelease|tobool %} + {%- set contributing_link = pathto("developers/contributing") %} + {%- set contributing_attrs = "" %} +{%- else %} {%- set contributing_link = "https://scikit-learn.org/dev/developers/contributing.html" %} {%- set contributing_attrs = 'target="_blank" rel="noopener noreferrer"' %} -{%- else %} - {%- set contributing_link = pathto('developers/contributing') %} - {%- set contributing_attrs = '' %} {%- endif %} +{%- import "static/webpack-macros.html" as _webpack with context %} -{% block content %} -
    +{% block docs_navbar %} +{{ super() }} + +
    -

    scikit-learn

    -

    Machine Learning in Python

    - Getting Started - Release Highlights for {{ release_highlights_version }} - GitHub +

    scikit-learn

    +

    Machine Learning in Python

    + Getting Started + Release Highlights for {{ release_highlights_version }}
      @@ -33,236 +35,279 @@

      Machine Learning in

    -
    +{% endblock docs_navbar %} + +{% block docs_main %} + +
    +
    -
    +
    -

    Classification

    -

    Identifying which category an object belongs to.

    -

    Applications: Spam detection, image recognition.
    - Algorithms: - Gradient boosting, - nearest neighbors, - random forest, - logistic regression, - and more...

    +

    + Classification +

    +

    Identifying which category an object belongs to.

    +

    + Applications: Spam detection, image recognition.
    + Algorithms: + Gradient boosting, + nearest neighbors, + random forest, + logistic regression, + and more... +

    -
    +
    -
    +
    -

    Regression

    -

    Predicting a continuous-valued attribute associated with an object.

    -

    Applications: Drug response, Stock prices.
    - Algorithms: - Gradient boosting, - nearest neighbors, - random forest, - ridge, - and more...

    +

    + Regression +

    +

    Predicting a continuous-valued attribute associated with an object.

    +

    + Applications: Drug response, stock prices.
    + Algorithms: + Gradient boosting, + nearest neighbors, + random forest, + ridge, + and more... +

    -
    +
    -
    +
    -

    Clustering

    -

    Automatic grouping of similar objects into sets.

    -

    Applications: Customer segmentation, Grouping experiment outcomes
    - Algorithms: - k-Means, - HDBSCAN, - hierarchical - clustering, - and more...

    +

    + Clustering +

    +

    Automatic grouping of similar objects into sets.

    +

    + Applications: Customer segmentation, grouping experiment outcomes.
    + Algorithms: + k-Means, + HDBSCAN, + hierarchical clustering, + and more... +

    -
    +
    -
    +
    -

    Dimensionality reduction

    -

    Reducing the number of random variables to consider.

    -

    Applications: Visualization, Increased efficiency
    - Algorithms: - PCA, - feature selection, - non-negative matrix factorization, - and more...

    +

    + Dimensionality reduction +

    +

    Reducing the number of random variables to consider.

    +

    + Applications: Visualization, increased efficiency.
    + Algorithms: + PCA, + feature selection, + non-negative matrix factorization, + and more... +

    -
    +
    -
    +
    -

    Model selection

    -

    Comparing, validating and choosing parameters and models.

    -

    Applications: Improved accuracy via parameter tuning
    - Algorithms: - grid search, - cross validation, - metrics, - and more...

    +

    + Model selection +

    +

    Comparing, validating and choosing parameters and models.

    +

    + Applications: Improved accuracy via parameter tuning.
    + Algorithms: + Grid search, + cross validation, + metrics, + and more... +

    -
    +
    -
    +
    -

    Preprocessing

    -

    Feature extraction and normalization.

    -

    Applications: Transforming input data such as text for use with machine learning algorithms.
    - Algorithms: - preprocessing, - feature extraction, - and more...

    +

    + Preprocessing +

    +

    Feature extraction and normalization.

    +

    + Applications: Transforming input data such as text for use with machine learning algorithms.
    + Algorithms: + Preprocessing, + feature extraction, + and more... +

    -
    -
    -
    +{% endblock docs_main %} + +{% block footer %} + +
    +
    +

    News

      -
    • On-going development: - scikit-learn 1.6 (Changelog) -
    • -
    • May 2024. scikit-learn 1.5.0 is available for download (Changelog). -
    • -
    • April 2024. scikit-learn 1.4.2 is available for download (Changelog). -
    • -
    • February 2024. scikit-learn 1.4.1.post1 is available for download (Changelog). -
    • -
    • January 2024. scikit-learn 1.4.0 is available for download (Changelog). -
    • -
    • All releases: - What's new (Changelog) -
    • +
    • On-going development: scikit-learn 1.6 (Changelog).
    • +
    • May 2024. scikit-learn 1.5.0 is available for download (Changelog).
    • +
    • April 2024. scikit-learn 1.4.2 is available for download (Changelog).
    • +
    • February 2024. scikit-learn 1.4.1.post1 is available for download (Changelog).
    • +
    • January 2024. scikit-learn 1.4.0 is available for download (Changelog).
    • +
    • October 2023. scikit-learn 1.3.2 is available for download (Changelog).
    • +
    • September 2023. scikit-learn 1.3.1 is available for download (Changelog).
    • +
    • June 2023. scikit-learn 1.3.0 is available for download (Changelog).
    • +
    • All releases: What's new (Changelog).
    +

    Community

    - - Help us, donate! - Cite us! +

    + Help us, donate! + Cite us! +

    +

    Who uses scikit-learn?

    -
    -
    + +