CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
_discretization.py549 linesDownload Raw Back to preprocessing
1# Authors: The scikit-learn developers
2# SPDX-License-Identifier: BSD-3-Clause
3
4
5import warnings
6from numbers import Integral
7
8import numpy as np
9
10from ..base import BaseEstimator, TransformerMixin, _fit_context
11from ..utils import resample
12from ..utils._param_validation import Interval, Options, StrOptions
13from ..utils.stats import _averaged_weighted_percentile, _weighted_percentile
14from ..utils.validation import (
15    _check_feature_names_in,
16    _check_sample_weight,
17    check_array,
18    check_is_fitted,
19    validate_data,
20)
21from ._encoders import OneHotEncoder
22
23
24class KBinsDiscretizer(TransformerMixin, BaseEstimator):
25    """
26    Bin continuous data into intervals.
27
28    Read more in the :ref:`User Guide <preprocessing_discretization>`.
29
30    .. versionadded:: 0.20
31
32    Parameters
33    ----------
34    n_bins : int or array-like of shape (n_features,), default=5
35        The number of bins to produce. Raises ValueError if ``n_bins < 2``.
36
37    encode : {'onehot', 'onehot-dense', 'ordinal'}, default='onehot'
38        Method used to encode the transformed result.
39
40        - 'onehot': Encode the transformed result with one-hot encoding
41          and return a sparse matrix. Ignored features are always
42          stacked to the right.
43        - 'onehot-dense': Encode the transformed result with one-hot encoding
44          and return a dense array. Ignored features are always
45          stacked to the right.
46        - 'ordinal': Return the bin identifier encoded as an integer value.
47
48    strategy : {'uniform', 'quantile', 'kmeans'}, default='quantile'
49        Strategy used to define the widths of the bins.
50
51        - 'uniform': All bins in each feature have identical widths.
52        - 'quantile': All bins in each feature have the same number of points.
53        - 'kmeans': Values in each bin have the same nearest center of a 1D
54          k-means cluster.
55
56        For an example of the different strategies see:
57        :ref:`sphx_glr_auto_examples_preprocessing_plot_discretization_strategies.py`.
58
59    quantile_method : {"inverted_cdf", "averaged_inverted_cdf",
60            "closest_observation", "interpolated_inverted_cdf", "hazen",
61            "weibull", "linear", "median_unbiased", "normal_unbiased"},
62            default="linear"
63            Method to pass on to np.percentile calculation when using
64            strategy="quantile". Only `averaged_inverted_cdf` and `inverted_cdf`
65            support the use of `sample_weight != None` when subsampling is not
66            active.
67
68            .. versionadded:: 1.7
69
70    dtype : {np.float32, np.float64}, default=None
71        The desired data-type for the output. If None, output dtype is
72        consistent with input dtype. Only np.float32 and np.float64 are
73        supported.
74
75        .. versionadded:: 0.24
76
77    subsample : int or None, default=200_000
78        Maximum number of samples, used to fit the model, for computational
79        efficiency.
80        `subsample=None` means that all the training samples are used when
81        computing the quantiles that determine the binning thresholds.
82        Since quantile computation relies on sorting each column of `X` and
83        that sorting has an `n log(n)` time complexity,
84        it is recommended to use subsampling on datasets with a
85        very large number of samples.
86
87        .. versionchanged:: 1.3
88            The default value of `subsample` changed from `None` to `200_000` when
89            `strategy="quantile"`.
90
91        .. versionchanged:: 1.5
92            The default value of `subsample` changed from `None` to `200_000` when
93            `strategy="uniform"` or `strategy="kmeans"`.
94
95    random_state : int, RandomState instance or None, default=None
96        Determines random number generation for subsampling.
97        Pass an int for reproducible results across multiple function calls.
98        See the `subsample` parameter for more details.
99        See :term:`Glossary <random_state>`.
100
101        .. versionadded:: 1.1
102
103    Attributes
104    ----------
105    bin_edges_ : ndarray of ndarray of shape (n_features,)
106        The edges of each bin. Contain arrays of varying shapes ``(n_bins_, )``
107        Ignored features will have empty arrays.
108
109    n_bins_ : ndarray of shape (n_features,), dtype=np.int64
110        Number of bins per feature. Bins whose width are too small
111        (i.e., <= 1e-8) are removed with a warning.
112
113    n_features_in_ : int
114        Number of features seen during :term:`fit`.
115
116        .. versionadded:: 0.24
117
118    feature_names_in_ : ndarray of shape (`n_features_in_`,)
119        Names of features seen during :term:`fit`. Defined only when `X`
120        has feature names that are all strings.
121
122        .. versionadded:: 1.0
123
124    See Also
125    --------
126    Binarizer : Class used to bin values as ``0`` or
127        ``1`` based on a parameter ``threshold``.
128
129    Notes
130    -----
131
132    For a visualization of discretization on different datasets refer to
133    :ref:`sphx_glr_auto_examples_preprocessing_plot_discretization_classification.py`.
134    On the effect of discretization on linear models see:
135    :ref:`sphx_glr_auto_examples_preprocessing_plot_discretization.py`.
136
137    In bin edges for feature ``i``, the first and last values are used only for
138    ``inverse_transform``. During transform, bin edges are extended to::
139
140      np.concatenate([-np.inf, bin_edges_[i][1:-1], np.inf])
141
142    You can combine ``KBinsDiscretizer`` with
143    :class:`~sklearn.compose.ColumnTransformer` if you only want to preprocess
144    part of the features.
145
146    ``KBinsDiscretizer`` might produce constant features (e.g., when
147    ``encode = 'onehot'`` and certain bins do not contain any data).
148    These features can be removed with feature selection algorithms
149    (e.g., :class:`~sklearn.feature_selection.VarianceThreshold`).
150
151    Examples
152    --------
153    >>> from sklearn.preprocessing import KBinsDiscretizer
154    >>> X = [[-2, 1, -4,   -1],
155    ...      [-1, 2, -3, -0.5],
156    ...      [ 0, 3, -2,  0.5],
157    ...      [ 1, 4, -1,    2]]
158    >>> est = KBinsDiscretizer(
159    ...     n_bins=3, encode='ordinal', strategy='uniform'
160    ... )
161    >>> est.fit(X)
162    KBinsDiscretizer(...)
163    >>> Xt = est.transform(X)
164    >>> Xt  # doctest: +SKIP
165    array([[ 0., 0., 0., 0.],
166           [ 1., 1., 1., 0.],
167           [ 2., 2., 2., 1.],
168           [ 2., 2., 2., 2.]])
169
170    Sometimes it may be useful to convert the data back into the original
171    feature space. The ``inverse_transform`` function converts the binned
172    data into the original feature space. Each value will be equal to the mean
173    of the two bin edges.
174
175    >>> est.bin_edges_[0]
176    array([-2., -1.,  0.,  1.])
177    >>> est.inverse_transform(Xt)
178    array([[-1.5,  1.5, -3.5, -0.5],
179           [-0.5,  2.5, -2.5, -0.5],
180           [ 0.5,  3.5, -1.5,  0.5],
181           [ 0.5,  3.5, -1.5,  1.5]])
182    """
183
184    _parameter_constraints: dict = {
185        "n_bins": [Interval(Integral, 2, None, closed="left"), "array-like"],
186        "encode": [StrOptions({"onehot", "onehot-dense", "ordinal"})],
187        "strategy": [StrOptions({"uniform", "quantile", "kmeans"})],
188        "quantile_method": [
189            StrOptions(
190                {
191                    "warn",
192                    "inverted_cdf",
193                    "averaged_inverted_cdf",
194                    "closest_observation",
195                    "interpolated_inverted_cdf",
196                    "hazen",
197                    "weibull",
198                    "linear",
199                    "median_unbiased",
200                    "normal_unbiased",
201                }
202            )
203        ],
204        "dtype": [Options(type, {np.float64, np.float32}), None],
205        "subsample": [Interval(Integral, 1, None, closed="left"), None],
206        "random_state": ["random_state"],
207    }
208
209    def __init__(
210        self,
211        n_bins=5,
212        *,
213        encode="onehot",
214        strategy="quantile",
215        quantile_method="warn",
216        dtype=None,
217        subsample=200_000,
218        random_state=None,
219    ):
220        self.n_bins = n_bins
221        self.encode = encode
222        self.strategy = strategy
223        self.quantile_method = quantile_method
224        self.dtype = dtype
225        self.subsample = subsample
226        self.random_state = random_state
227
228    @_fit_context(prefer_skip_nested_validation=True)
229    def fit(self, X, y=None, sample_weight=None):
230        """
231        Fit the estimator.
232
233        Parameters
234        ----------
235        X : array-like of shape (n_samples, n_features)
236            Data to be discretized.
237
238        y : None
239            Ignored. This parameter exists only for compatibility with
240            :class:`~sklearn.pipeline.Pipeline`.
241
242        sample_weight : ndarray of shape (n_samples,)
243            Contains weight values to be associated with each sample.
244
245            .. versionadded:: 1.3
246
247            .. versionchanged:: 1.7
248               Added support for strategy="uniform".
249
250        Returns
251        -------
252        self : object
253            Returns the instance itself.
254        """
255        X = validate_data(self, X, dtype="numeric")
256
257        if self.dtype in (np.float64, np.float32):
258            output_dtype = self.dtype
259        else:  # self.dtype is None
260            output_dtype = X.dtype
261
262        n_samples, n_features = X.shape
263
264        if sample_weight is not None:
265            sample_weight = _check_sample_weight(sample_weight, X, dtype=X.dtype)
266
267        if self.subsample is not None and n_samples > self.subsample:
268            # Take a subsample of `X`
269            # When resampling, it is important to subsample **with replacement** to
270            # preserve the distribution, in particular in the presence of a few data
271            # points with large weights. You can check this by setting `replace=False`
272            # in sklearn.utils.test.test_indexing.test_resample_weighted and check that
273            # it fails as a justification for this claim.
274            X = resample(
275                X,
276                replace=True,
277                n_samples=self.subsample,
278                random_state=self.random_state,
279                sample_weight=sample_weight,
280            )
281            # Since we already used the weights when resampling when provided,
282            # we set them back to `None` to avoid accounting for the weights twice
283            # in subsequent operations to compute weight-aware bin edges with
284            # quantiles or k-means.
285            sample_weight = None
286
287        n_features = X.shape[1]
288        n_bins = self._validate_n_bins(n_features)
289
290        bin_edges = np.zeros(n_features, dtype=object)
291
292        # TODO(1.9): remove and switch to quantile_method="averaged_inverted_cdf"
293        # by default.
294        quantile_method = self.quantile_method
295        if self.strategy == "quantile" and quantile_method == "warn":
296            warnings.warn(
297                "The current default behavior, quantile_method='linear', will be "
298                "changed to quantile_method='averaged_inverted_cdf' in "
299                "scikit-learn version 1.9 to naturally support sample weight "
300                "equivalence properties by default. Pass "
301                "quantile_method='averaged_inverted_cdf' explicitly to silence this "
302                "warning.",
303                FutureWarning,
304            )
305            quantile_method = "linear"
306
307        if (
308            self.strategy == "quantile"
309            and quantile_method not in ["inverted_cdf", "averaged_inverted_cdf"]
310            and sample_weight is not None
311        ):
312            raise ValueError(
313                "When fitting with strategy='quantile' and sample weights, "
314                "quantile_method should either be set to 'averaged_inverted_cdf' or "
315                f"'inverted_cdf', got quantile_method='{quantile_method}' instead."
316            )
317
318        if self.strategy != "quantile" and sample_weight is not None:
319            # Prepare a mask to filter out zero-weight samples when extracting
320            # the min and max values of each columns which are needed for the
321            # "uniform" and "kmeans" strategies.
322            nnz_weight_mask = sample_weight != 0
323        else:
324            # Otherwise, all samples are used. Use a slice to avoid creating a
325            # new array.
326            nnz_weight_mask = slice(None)
327
328        for jj in range(n_features):
329            column = X[:, jj]
330            col_min = column[nnz_weight_mask].min()
331            col_max = column[nnz_weight_mask].max()
332
333            if col_min == col_max:
334                warnings.warn(
335                    "Feature %d is constant and will be replaced with 0." % jj
336                )
337                n_bins[jj] = 1
338                bin_edges[jj] = np.array([-np.inf, np.inf])
339                continue
340
341            if self.strategy == "uniform":
342                bin_edges[jj] = np.linspace(col_min, col_max, n_bins[jj] + 1)
343
344            elif self.strategy == "quantile":
345                percentile_levels = np.linspace(0, 100, n_bins[jj] + 1)
346
347                # method="linear" is the implicit default for any numpy
348                # version. So we keep it version independent in that case by
349                # using an empty param dict.
350                percentile_kwargs = {}
351                if quantile_method != "linear" and sample_weight is None:
352                    percentile_kwargs["method"] = quantile_method
353
354                if sample_weight is None:
355                    bin_edges[jj] = np.asarray(
356                        np.percentile(column, percentile_levels, **percentile_kwargs),
357                        dtype=np.float64,
358                    )
359                else:
360                    # TODO: make _weighted_percentile and
361                    # _averaged_weighted_percentile accept an array of
362                    # quantiles instead of calling it multiple times and
363                    # sorting the column multiple times as a result.
364                    percentile_func = {
365                        "inverted_cdf": _weighted_percentile,
366                        "averaged_inverted_cdf": _averaged_weighted_percentile,
367                    }[quantile_method]
368                    bin_edges[jj] = np.asarray(
369                        [
370                            percentile_func(column, sample_weight, percentile_rank=p)
371                            for p in percentile_levels
372                        ],
373                        dtype=np.float64,
374                    )
375            elif self.strategy == "kmeans":
376                from ..cluster import KMeans  # fixes import loops
377
378                # Deterministic initialization with uniform spacing
379                uniform_edges = np.linspace(col_min, col_max, n_bins[jj] + 1)
380                init = (uniform_edges[1:] + uniform_edges[:-1])[:, None] * 0.5
381
382                # 1D k-means procedure
383                km = KMeans(n_clusters=n_bins[jj], init=init, n_init=1)
384                centers = km.fit(
385                    column[:, None], sample_weight=sample_weight
386                ).cluster_centers_[:, 0]
387                # Must sort, centers may be unsorted even with sorted init
388                centers.sort()
389                bin_edges[jj] = (centers[1:] + centers[:-1]) * 0.5
390                bin_edges[jj] = np.r_[col_min, bin_edges[jj], col_max]
391
392            # Remove bins whose width are too small (i.e., <= 1e-8)
393            if self.strategy in ("quantile", "kmeans"):
394                mask = np.ediff1d(bin_edges[jj], to_begin=np.inf) > 1e-8
395                bin_edges[jj] = bin_edges[jj][mask]
396                if len(bin_edges[jj]) - 1 != n_bins[jj]:
397                    warnings.warn(
398                        "Bins whose width are too small (i.e., <= "
399                        "1e-8) in feature %d are removed. Consider "
400                        "decreasing the number of bins." % jj
401                    )
402                    n_bins[jj] = len(bin_edges[jj]) - 1
403
404        self.bin_edges_ = bin_edges
405        self.n_bins_ = n_bins
406
407        if "onehot" in self.encode:
408            self._encoder = OneHotEncoder(
409                categories=[np.arange(i) for i in self.n_bins_],
410                sparse_output=self.encode == "onehot",
411                dtype=output_dtype,
412            )
413            # Fit the OneHotEncoder with toy datasets
414            # so that it's ready for use after the KBinsDiscretizer is fitted
415            self._encoder.fit(np.zeros((1, len(self.n_bins_))))
416
417        return self
418
419    def _validate_n_bins(self, n_features):
420        """Returns n_bins_, the number of bins per feature."""
421        orig_bins = self.n_bins
422        if isinstance(orig_bins, Integral):
423            return np.full(n_features, orig_bins, dtype=int)
424
425        n_bins = check_array(orig_bins, dtype=int, copy=True, ensure_2d=False)
426
427        if n_bins.ndim > 1 or n_bins.shape[0] != n_features:
428            raise ValueError("n_bins must be a scalar or array of shape (n_features,).")
429
430        bad_nbins_value = (n_bins < 2) | (n_bins != orig_bins)
431
432        violating_indices = np.where(bad_nbins_value)[0]
433        if violating_indices.shape[0] > 0:
434            indices = ", ".join(str(i) for i in violating_indices)
435            raise ValueError(
436                "{} received an invalid number "
437                "of bins at indices {}. Number of bins "
438                "must be at least 2, and must be an int.".format(
439                    KBinsDiscretizer.__name__, indices
440                )
441            )
442        return n_bins
443
444    def transform(self, X):
445        """
446        Discretize the data.
447
448        Parameters
449        ----------
450        X : array-like of shape (n_samples, n_features)
451            Data to be discretized.
452
453        Returns
454        -------
455        Xt : {ndarray, sparse matrix}, dtype={np.float32, np.float64}
456            Data in the binned space. Will be a sparse matrix if
457            `self.encode='onehot'` and ndarray otherwise.
458        """
459        check_is_fitted(self)
460
461        # check input and attribute dtypes
462        dtype = (np.float64, np.float32) if self.dtype is None else self.dtype
463        Xt = validate_data(self, X, copy=True, dtype=dtype, reset=False)
464
465        bin_edges = self.bin_edges_
466        for jj in range(Xt.shape[1]):
467            Xt[:, jj] = np.searchsorted(bin_edges[jj][1:-1], Xt[:, jj], side="right")
468
469        if self.encode == "ordinal":
470            return Xt
471
472        dtype_init = None
473        if "onehot" in self.encode:
474            dtype_init = self._encoder.dtype
475            self._encoder.dtype = Xt.dtype
476        try:
477            Xt_enc = self._encoder.transform(Xt)
478        finally:
479            # revert the initial dtype to avoid modifying self.
480            self._encoder.dtype = dtype_init
481        return Xt_enc
482
483    def inverse_transform(self, X):
484        """
485        Transform discretized data back to original feature space.
486
487        Note that this function does not regenerate the original data
488        due to discretization rounding.
489
490        Parameters
491        ----------
492        X : array-like of shape (n_samples, n_features)
493            Transformed data in the binned space.
494
495        Returns
496        -------
497        X_original : ndarray, dtype={np.float32, np.float64}
498            Data in the original feature space.
499        """
500
501        check_is_fitted(self)
502
503        if "onehot" in self.encode:
504            X = self._encoder.inverse_transform(X)
505
506        Xinv = check_array(X, copy=True, dtype=(np.float64, np.float32))
507        n_features = self.n_bins_.shape[0]
508        if Xinv.shape[1] != n_features:
509            raise ValueError(
510                "Incorrect number of features. Expecting {}, received {}.".format(
511                    n_features, Xinv.shape[1]
512                )
513            )
514
515        for jj in range(n_features):
516            bin_edges = self.bin_edges_[jj]
517            bin_centers = (bin_edges[1:] + bin_edges[:-1]) * 0.5
518            Xinv[:, jj] = bin_centers[(Xinv[:, jj]).astype(np.int64)]
519
520        return Xinv
521
522    def get_feature_names_out(self, input_features=None):
523        """Get output feature names.
524
525        Parameters
526        ----------
527        input_features : array-like of str or None, default=None
528            Input features.
529
530            - If `input_features` is `None`, then `feature_names_in_` is
531              used as feature names in. If `feature_names_in_` is not defined,
532              then the following input feature names are generated:
533              `["x0", "x1", ..., "x(n_features_in_ - 1)"]`.
534            - If `input_features` is an array-like, then `input_features` must
535              match `feature_names_in_` if `feature_names_in_` is defined.
536
537        Returns
538        -------
539        feature_names_out : ndarray of str objects
540            Transformed feature names.
541        """
542        check_is_fitted(self, "n_features_in_")
543        input_features = _check_feature_names_in(self, input_features)
544        if hasattr(self, "_encoder"):
545            return self._encoder.get_feature_names_out(input_features)
546
547        # ordinal encoding
548        return input_features
549 
Aluode/PerceptionLabPortable · CoolFace