CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
_polynomial.py1154 linesDownload Raw Back to preprocessing
1"""
2This file contains preprocessing tools based on polynomials.
3"""
4
5# Authors: The scikit-learn developers
6# SPDX-License-Identifier: BSD-3-Clause
7
8import collections
9from itertools import chain, combinations
10from itertools import combinations_with_replacement as combinations_w_r
11from numbers import Integral
12
13import numpy as np
14from scipy import sparse
15from scipy.interpolate import BSpline
16from scipy.special import comb
17
18from ..base import BaseEstimator, TransformerMixin, _fit_context
19from ..utils import check_array
20from ..utils._param_validation import Interval, StrOptions
21from ..utils.fixes import parse_version, sp_version
22from ..utils.stats import _weighted_percentile
23from ..utils.validation import (
24    FLOAT_DTYPES,
25    _check_feature_names_in,
26    _check_sample_weight,
27    check_is_fitted,
28    validate_data,
29)
30from ._csr_polynomial_expansion import (
31    _calc_expanded_nnz,
32    _calc_total_nnz,
33    _csr_polynomial_expansion,
34)
35
36__all__ = [
37    "PolynomialFeatures",
38    "SplineTransformer",
39]
40
41
42def _create_expansion(X, interaction_only, deg, n_features, cumulative_size=0):
43    """Helper function for creating and appending sparse expansion matrices"""
44
45    total_nnz = _calc_total_nnz(X.indptr, interaction_only, deg)
46    expanded_col = _calc_expanded_nnz(n_features, interaction_only, deg)
47
48    if expanded_col == 0:
49        return None
50    # This only checks whether each block needs 64bit integers upon
51    # expansion. We prefer to keep int32 indexing where we can,
52    # since currently SciPy's CSR construction downcasts when possible,
53    # so we prefer to avoid an unnecessary cast. The dtype may still
54    # change in the concatenation process if needed.
55    # See: https://github.com/scipy/scipy/issues/16569
56    max_indices = expanded_col - 1
57    max_indptr = total_nnz
58    max_int32 = np.iinfo(np.int32).max
59    needs_int64 = max(max_indices, max_indptr) > max_int32
60    index_dtype = np.int64 if needs_int64 else np.int32
61
62    # Result of the expansion, modified in place by the
63    # `_csr_polynomial_expansion` routine.
64    expanded_data = np.empty(shape=total_nnz, dtype=X.data.dtype)
65    expanded_indices = np.empty(shape=total_nnz, dtype=index_dtype)
66    expanded_indptr = np.empty(shape=X.indptr.shape[0], dtype=index_dtype)
67    _csr_polynomial_expansion(
68        X.data,
69        X.indices,
70        X.indptr,
71        X.shape[1],
72        expanded_data,
73        expanded_indices,
74        expanded_indptr,
75        interaction_only,
76        deg,
77    )
78    return sparse.csr_matrix(
79        (expanded_data, expanded_indices, expanded_indptr),
80        shape=(X.indptr.shape[0] - 1, expanded_col),
81        dtype=X.dtype,
82    )
83
84
85class PolynomialFeatures(TransformerMixin, BaseEstimator):
86    """Generate polynomial and interaction features.
87
88    Generate a new feature matrix consisting of all polynomial combinations
89    of the features with degree less than or equal to the specified degree.
90    For example, if an input sample is two dimensional and of the form
91    [a, b], the degree-2 polynomial features are [1, a, b, a^2, ab, b^2].
92
93    Read more in the :ref:`User Guide <polynomial_features>`.
94
95    Parameters
96    ----------
97    degree : int or tuple (min_degree, max_degree), default=2
98        If a single int is given, it specifies the maximal degree of the
99        polynomial features. If a tuple `(min_degree, max_degree)` is passed,
100        then `min_degree` is the minimum and `max_degree` is the maximum
101        polynomial degree of the generated features. Note that `min_degree=0`
102        and `min_degree=1` are equivalent as outputting the degree zero term is
103        determined by `include_bias`.
104
105    interaction_only : bool, default=False
106        If `True`, only interaction features are produced: features that are
107        products of at most `degree` *distinct* input features, i.e. terms with
108        power of 2 or higher of the same input feature are excluded:
109
110        - included: `x[0]`, `x[1]`, `x[0] * x[1]`, etc.
111        - excluded: `x[0] ** 2`, `x[0] ** 2 * x[1]`, etc.
112
113    include_bias : bool, default=True
114        If `True` (default), then include a bias column, the feature in which
115        all polynomial powers are zero (i.e. a column of ones - acts as an
116        intercept term in a linear model).
117
118    order : {'C', 'F'}, default='C'
119        Order of output array in the dense case. `'F'` order is faster to
120        compute, but may slow down subsequent estimators.
121
122        .. versionadded:: 0.21
123
124    Attributes
125    ----------
126    powers_ : ndarray of shape (`n_output_features_`, `n_features_in_`)
127        `powers_[i, j]` is the exponent of the jth input in the ith output.
128
129    n_features_in_ : int
130        Number of features seen during :term:`fit`.
131
132        .. versionadded:: 0.24
133
134    feature_names_in_ : ndarray of shape (`n_features_in_`,)
135        Names of features seen during :term:`fit`. Defined only when `X`
136        has feature names that are all strings.
137
138        .. versionadded:: 1.0
139
140    n_output_features_ : int
141        The total number of polynomial output features. The number of output
142        features is computed by iterating over all suitably sized combinations
143        of input features.
144
145    See Also
146    --------
147    SplineTransformer : Transformer that generates univariate B-spline bases
148        for features.
149
150    Notes
151    -----
152    Be aware that the number of features in the output array scales
153    polynomially in the number of features of the input array, and
154    exponentially in the degree. High degrees can cause overfitting.
155
156    See :ref:`examples/linear_model/plot_polynomial_interpolation.py
157    <sphx_glr_auto_examples_linear_model_plot_polynomial_interpolation.py>`
158
159    Examples
160    --------
161    >>> import numpy as np
162    >>> from sklearn.preprocessing import PolynomialFeatures
163    >>> X = np.arange(6).reshape(3, 2)
164    >>> X
165    array([[0, 1],
166           [2, 3],
167           [4, 5]])
168    >>> poly = PolynomialFeatures(2)
169    >>> poly.fit_transform(X)
170    array([[ 1.,  0.,  1.,  0.,  0.,  1.],
171           [ 1.,  2.,  3.,  4.,  6.,  9.],
172           [ 1.,  4.,  5., 16., 20., 25.]])
173    >>> poly = PolynomialFeatures(interaction_only=True)
174    >>> poly.fit_transform(X)
175    array([[ 1.,  0.,  1.,  0.],
176           [ 1.,  2.,  3.,  6.],
177           [ 1.,  4.,  5., 20.]])
178    """
179
180    _parameter_constraints: dict = {
181        "degree": [Interval(Integral, 0, None, closed="left"), "array-like"],
182        "interaction_only": ["boolean"],
183        "include_bias": ["boolean"],
184        "order": [StrOptions({"C", "F"})],
185    }
186
187    def __init__(
188        self, degree=2, *, interaction_only=False, include_bias=True, order="C"
189    ):
190        self.degree = degree
191        self.interaction_only = interaction_only
192        self.include_bias = include_bias
193        self.order = order
194
195    @staticmethod
196    def _combinations(
197        n_features, min_degree, max_degree, interaction_only, include_bias
198    ):
199        comb = combinations if interaction_only else combinations_w_r
200        start = max(1, min_degree)
201        iter = chain.from_iterable(
202            comb(range(n_features), i) for i in range(start, max_degree + 1)
203        )
204        if include_bias:
205            iter = chain(comb(range(n_features), 0), iter)
206        return iter
207
208    @staticmethod
209    def _num_combinations(
210        n_features, min_degree, max_degree, interaction_only, include_bias
211    ):
212        """Calculate number of terms in polynomial expansion
213
214        This should be equivalent to counting the number of terms returned by
215        _combinations(...) but much faster.
216        """
217
218        if interaction_only:
219            combinations = sum(
220                [
221                    comb(n_features, i, exact=True)
222                    for i in range(max(1, min_degree), min(max_degree, n_features) + 1)
223                ]
224            )
225        else:
226            combinations = comb(n_features + max_degree, max_degree, exact=True) - 1
227            if min_degree > 0:
228                d = min_degree - 1
229                combinations -= comb(n_features + d, d, exact=True) - 1
230
231        if include_bias:
232            combinations += 1
233
234        return combinations
235
236    @property
237    def powers_(self):
238        """Exponent for each of the inputs in the output."""
239        check_is_fitted(self)
240
241        combinations = self._combinations(
242            n_features=self.n_features_in_,
243            min_degree=self._min_degree,
244            max_degree=self._max_degree,
245            interaction_only=self.interaction_only,
246            include_bias=self.include_bias,
247        )
248        return np.vstack(
249            [np.bincount(c, minlength=self.n_features_in_) for c in combinations]
250        )
251
252    def get_feature_names_out(self, input_features=None):
253        """Get output feature names for transformation.
254
255        Parameters
256        ----------
257        input_features : array-like of str or None, default=None
258            Input features.
259
260            - If `input_features is None`, then `feature_names_in_` is
261              used as feature names in. If `feature_names_in_` is not defined,
262              then the following input feature names are generated:
263              `["x0", "x1", ..., "x(n_features_in_ - 1)"]`.
264            - If `input_features` is an array-like, then `input_features` must
265              match `feature_names_in_` if `feature_names_in_` is defined.
266
267        Returns
268        -------
269        feature_names_out : ndarray of str objects
270            Transformed feature names.
271        """
272        powers = self.powers_
273        input_features = _check_feature_names_in(self, input_features)
274        feature_names = []
275        for row in powers:
276            inds = np.where(row)[0]
277            if len(inds):
278                name = " ".join(
279                    (
280                        "%s^%d" % (input_features[ind], exp)
281                        if exp != 1
282                        else input_features[ind]
283                    )
284                    for ind, exp in zip(inds, row[inds])
285                )
286            else:
287                name = "1"
288            feature_names.append(name)
289        return np.asarray(feature_names, dtype=object)
290
291    @_fit_context(prefer_skip_nested_validation=True)
292    def fit(self, X, y=None):
293        """
294        Compute number of output features.
295
296        Parameters
297        ----------
298        X : {array-like, sparse matrix} of shape (n_samples, n_features)
299            The data.
300
301        y : Ignored
302            Not used, present here for API consistency by convention.
303
304        Returns
305        -------
306        self : object
307            Fitted transformer.
308        """
309        _, n_features = validate_data(self, X, accept_sparse=True).shape
310
311        if isinstance(self.degree, Integral):
312            if self.degree == 0 and not self.include_bias:
313                raise ValueError(
314                    "Setting degree to zero and include_bias to False would result in"
315                    " an empty output array."
316                )
317
318            self._min_degree = 0
319            self._max_degree = self.degree
320        elif (
321            isinstance(self.degree, collections.abc.Iterable) and len(self.degree) == 2
322        ):
323            self._min_degree, self._max_degree = self.degree
324            if not (
325                isinstance(self._min_degree, Integral)
326                and isinstance(self._max_degree, Integral)
327                and self._min_degree >= 0
328                and self._min_degree <= self._max_degree
329            ):
330                raise ValueError(
331                    "degree=(min_degree, max_degree) must "
332                    "be non-negative integers that fulfil "
333                    "min_degree <= max_degree, got "
334                    f"{self.degree}."
335                )
336            elif self._max_degree == 0 and not self.include_bias:
337                raise ValueError(
338                    "Setting both min_degree and max_degree to zero and include_bias to"
339                    " False would result in an empty output array."
340                )
341        else:
342            raise ValueError(
343                "degree must be a non-negative int or tuple "
344                "(min_degree, max_degree), got "
345                f"{self.degree}."
346            )
347
348        self.n_output_features_ = self._num_combinations(
349            n_features=n_features,
350            min_degree=self._min_degree,
351            max_degree=self._max_degree,
352            interaction_only=self.interaction_only,
353            include_bias=self.include_bias,
354        )
355        if self.n_output_features_ > np.iinfo(np.intp).max:
356            msg = (
357                "The output that would result from the current configuration would"
358                f" have {self.n_output_features_} features which is too large to be"
359                f" indexed by {np.intp().dtype.name}. Please change some or all of the"
360                " following:\n- The number of features in the input, currently"
361                f" {n_features=}\n- The range of degrees to calculate, currently"
362                f" [{self._min_degree}, {self._max_degree}]\n- Whether to include only"
363                f" interaction terms, currently {self.interaction_only}\n- Whether to"
364                f" include a bias term, currently {self.include_bias}."
365            )
366            if (
367                np.intp == np.int32
368                and self.n_output_features_ <= np.iinfo(np.int64).max
369            ):  # pragma: nocover
370                msg += (
371                    "\nNote that the current Python runtime has a limited 32 bit "
372                    "address space and that this configuration would have been "
373                    "admissible if run on a 64 bit Python runtime."
374                )
375            raise ValueError(msg)
376        # We also record the number of output features for
377        # _min_degree = 0
378        self._n_out_full = self._num_combinations(
379            n_features=n_features,
380            min_degree=0,
381            max_degree=self._max_degree,
382            interaction_only=self.interaction_only,
383            include_bias=self.include_bias,
384        )
385
386        return self
387
388    def transform(self, X):
389        """Transform data to polynomial features.
390
391        Parameters
392        ----------
393        X : {array-like, sparse matrix} of shape (n_samples, n_features)
394            The data to transform, row by row.
395
396            Prefer CSR over CSC for sparse input (for speed), but CSC is
397            required if the degree is 4 or higher. If the degree is less than
398            4 and the input format is CSC, it will be converted to CSR, have
399            its polynomial features generated, then converted back to CSC.
400
401            If the degree is 2 or 3, the method described in "Leveraging
402            Sparsity to Speed Up Polynomial Feature Expansions of CSR Matrices
403            Using K-Simplex Numbers" by Andrew Nystrom and John Hughes is
404            used, which is much faster than the method used on CSC input. For
405            this reason, a CSC input will be converted to CSR, and the output
406            will be converted back to CSC prior to being returned, hence the
407            preference of CSR.
408
409        Returns
410        -------
411        XP : {ndarray, sparse matrix} of shape (n_samples, NP)
412            The matrix of features, where `NP` is the number of polynomial
413            features generated from the combination of inputs. If a sparse
414            matrix is provided, it will be converted into a sparse
415            `csr_matrix`.
416        """
417        check_is_fitted(self)
418
419        X = validate_data(
420            self,
421            X,
422            order="F",
423            dtype=FLOAT_DTYPES,
424            reset=False,
425            accept_sparse=("csr", "csc"),
426        )
427
428        n_samples, n_features = X.shape
429        max_int32 = np.iinfo(np.int32).max
430        if sparse.issparse(X) and X.format == "csr":
431            if self._max_degree > 3:
432                return self.transform(X.tocsc()).tocsr()
433            to_stack = []
434            if self.include_bias:
435                to_stack.append(
436                    sparse.csr_matrix(np.ones(shape=(n_samples, 1), dtype=X.dtype))
437                )
438            if self._min_degree <= 1 and self._max_degree > 0:
439                to_stack.append(X)
440
441            cumulative_size = sum(mat.shape[1] for mat in to_stack)
442            for deg in range(max(2, self._min_degree), self._max_degree + 1):
443                expanded = _create_expansion(
444                    X=X,
445                    interaction_only=self.interaction_only,
446                    deg=deg,
447                    n_features=n_features,
448                    cumulative_size=cumulative_size,
449                )
450                if expanded is not None:
451                    to_stack.append(expanded)
452                    cumulative_size += expanded.shape[1]
453            if len(to_stack) == 0:
454                # edge case: deal with empty matrix
455                XP = sparse.csr_matrix((n_samples, 0), dtype=X.dtype)
456            else:
457                # `scipy.sparse.hstack` breaks in scipy<1.9.2
458                # when `n_output_features_ > max_int32`
459                all_int32 = all(mat.indices.dtype == np.int32 for mat in to_stack)
460                if (
461                    sp_version < parse_version("1.9.2")
462                    and self.n_output_features_ > max_int32
463                    and all_int32
464                ):
465                    raise ValueError(  # pragma: no cover
466                        "In scipy versions `<1.9.2`, the function `scipy.sparse.hstack`"
467                        " produces negative columns when:\n1. The output shape contains"
468                        " `n_cols` too large to be represented by a 32bit signed"
469                        " integer.\n2. All sub-matrices to be stacked have indices of"
470                        " dtype `np.int32`.\nTo avoid this error, either use a version"
471                        " of scipy `>=1.9.2` or alter the `PolynomialFeatures`"
472                        " transformer to produce fewer than 2^31 output features"
473                    )
474                XP = sparse.hstack(to_stack, dtype=X.dtype, format="csr")
475        elif sparse.issparse(X) and X.format == "csc" and self._max_degree < 4:
476            return self.transform(X.tocsr()).tocsc()
477        elif sparse.issparse(X):
478            combinations = self._combinations(
479                n_features=n_features,
480                min_degree=self._min_degree,
481                max_degree=self._max_degree,
482                interaction_only=self.interaction_only,
483                include_bias=self.include_bias,
484            )
485            columns = []
486            for combi in combinations:
487                if combi:
488                    out_col = 1
489                    for col_idx in combi:
490                        out_col = X[:, [col_idx]].multiply(out_col)
491                    columns.append(out_col)
492                else:
493                    bias = sparse.csc_matrix(np.ones((X.shape[0], 1)))
494                    columns.append(bias)
495            XP = sparse.hstack(columns, dtype=X.dtype).tocsc()
496        else:
497            # Do as if _min_degree = 0 and cut down array after the
498            # computation, i.e. use _n_out_full instead of n_output_features_.
499            XP = np.empty(
500                shape=(n_samples, self._n_out_full), dtype=X.dtype, order=self.order
501            )
502
503            # What follows is a faster implementation of:
504            # for i, comb in enumerate(combinations):
505            #     XP[:, i] = X[:, comb].prod(1)
506            # This implementation uses two optimisations.
507            # First one is broadcasting,
508            # multiply ([X1, ..., Xn], X1) -> [X1 X1, ..., Xn X1]
509            # multiply ([X2, ..., Xn], X2) -> [X2 X2, ..., Xn X2]
510            # ...
511            # multiply ([X[:, start:end], X[:, start]) -> ...
512            # Second optimisation happens for degrees >= 3.
513            # Xi^3 is computed reusing previous computation:
514            # Xi^3 = Xi^2 * Xi.
515
516            # degree 0 term
517            if self.include_bias:
518                XP[:, 0] = 1
519                current_col = 1
520            else:
521                current_col = 0
522
523            if self._max_degree == 0:
524                return XP
525
526            # degree 1 term
527            XP[:, current_col : current_col + n_features] = X
528            index = list(range(current_col, current_col + n_features))
529            current_col += n_features
530            index.append(current_col)
531
532            # loop over degree >= 2 terms
533            for _ in range(2, self._max_degree + 1):
534                new_index = []
535                end = index[-1]
536                for feature_idx in range(n_features):
537                    start = index[feature_idx]
538                    new_index.append(current_col)
539                    if self.interaction_only:
540                        start += index[feature_idx + 1] - index[feature_idx]
541                    next_col = current_col + end - start
542                    if next_col <= current_col:
543                        break
544                    # XP[:, start:end] are terms of degree d - 1
545                    # that exclude feature #feature_idx.
546                    np.multiply(
547                        XP[:, start:end],
548                        X[:, feature_idx : feature_idx + 1],
549                        out=XP[:, current_col:next_col],
550                        casting="no",
551                    )
552                    current_col = next_col
553
554                new_index.append(current_col)
555                index = new_index
556
557            if self._min_degree > 1:
558                n_XP, n_Xout = self._n_out_full, self.n_output_features_
559                if self.include_bias:
560                    Xout = np.empty(
561                        shape=(n_samples, n_Xout), dtype=XP.dtype, order=self.order
562                    )
563                    Xout[:, 0] = 1
564                    Xout[:, 1:] = XP[:, n_XP - n_Xout + 1 :]
565                else:
566                    Xout = XP[:, n_XP - n_Xout :].copy()
567                XP = Xout
568        return XP
569
570    def __sklearn_tags__(self):
571        tags = super().__sklearn_tags__()
572        tags.input_tags.sparse = True
573        return tags
574
575
576class SplineTransformer(TransformerMixin, BaseEstimator):
577    """Generate univariate B-spline bases for features.
578
579    Generate a new feature matrix consisting of
580    `n_splines=n_knots + degree - 1` (`n_knots - 1` for
581    `extrapolation="periodic"`) spline basis functions
582    (B-splines) of polynomial order=`degree` for each feature.
583
584    In order to learn more about the SplineTransformer class go to:
585    :ref:`sphx_glr_auto_examples_applications_plot_cyclical_feature_engineering.py`
586
587    Read more in the :ref:`User Guide <spline_transformer>`.
588
589    .. versionadded:: 1.0
590
591    Parameters
592    ----------
593    n_knots : int, default=5
594        Number of knots of the splines if `knots` equals one of
595        {'uniform', 'quantile'}. Must be larger or equal 2. Ignored if `knots`
596        is array-like.
597
598    degree : int, default=3
599        The polynomial degree of the spline basis. Must be a non-negative
600        integer.
601
602    knots : {'uniform', 'quantile'} or array-like of shape \
603        (n_knots, n_features), default='uniform'
604        Set knot positions such that first knot <= features <= last knot.
605
606        - If 'uniform', `n_knots` number of knots are distributed uniformly
607          from min to max values of the features.
608        - If 'quantile', they are distributed uniformly along the quantiles of
609          the features.
610        - If an array-like is given, it directly specifies the sorted knot
611          positions including the boundary knots. Note that, internally,
612          `degree` number of knots are added before the first knot, the same
613          after the last knot.
614
615    extrapolation : {'error', 'constant', 'linear', 'continue', 'periodic'}, \
616        default='constant'
617        If 'error', values outside the min and max values of the training
618        features raises a `ValueError`. If 'constant', the value of the
619        splines at minimum and maximum value of the features is used as
620        constant extrapolation. If 'linear', a linear extrapolation is used.
621        If 'continue', the splines are extrapolated as is, i.e. option
622        `extrapolate=True` in :class:`scipy.interpolate.BSpline`. If
623        'periodic', periodic splines with a periodicity equal to the distance
624        between the first and last knot are used. Periodic splines enforce
625        equal function values and derivatives at the first and last knot.
626        For example, this makes it possible to avoid introducing an arbitrary
627        jump between Dec 31st and Jan 1st in spline features derived from a
628        naturally periodic "day-of-year" input feature. In this case it is
629        recommended to manually set the knot values to control the period.
630
631    include_bias : bool, default=True
632        If False, then the last spline element inside the data range
633        of a feature is dropped. As B-splines sum to one over the spline basis
634        functions for each data point, they implicitly include a bias term,
635        i.e. a column of ones. It acts as an intercept term in a linear models.
636
637    order : {'C', 'F'}, default='C'
638        Order of output array in the dense case. `'F'` order is faster to compute, but
639        may slow down subsequent estimators.
640
641    sparse_output : bool, default=False
642        Will return sparse CSR matrix if set True else will return an array.
643
644        .. versionadded:: 1.2
645
646    Attributes
647    ----------
648    bsplines_ : list of shape (n_features,)
649        List of BSplines objects, one for each feature.
650
651    n_features_in_ : int
652        The total number of input features.
653
654    feature_names_in_ : ndarray of shape (`n_features_in_`,)
655        Names of features seen during :term:`fit`. Defined only when `X`
656        has feature names that are all strings.
657
658        .. versionadded:: 1.0
659
660    n_features_out_ : int
661        The total number of output features, which is computed as
662        `n_features * n_splines`, where `n_splines` is
663        the number of bases elements of the B-splines,
664        `n_knots + degree - 1` for non-periodic splines and
665        `n_knots - 1` for periodic ones.
666        If `include_bias=False`, then it is only
667        `n_features * (n_splines - 1)`.
668
669    See Also
670    --------
671    KBinsDiscretizer : Transformer that bins continuous data into intervals.
672
673    PolynomialFeatures : Transformer that generates polynomial and interaction
674        features.
675
676    Notes
677    -----
678    High degrees and a high number of knots can cause overfitting.
679
680    See :ref:`examples/linear_model/plot_polynomial_interpolation.py
681    <sphx_glr_auto_examples_linear_model_plot_polynomial_interpolation.py>`.
682
683    Examples
684    --------
685    >>> import numpy as np
686    >>> from sklearn.preprocessing import SplineTransformer
687    >>> X = np.arange(6).reshape(6, 1)
688    >>> spline = SplineTransformer(degree=2, n_knots=3)
689    >>> spline.fit_transform(X)
690    array([[0.5 , 0.5 , 0.  , 0.  ],
691           [0.18, 0.74, 0.08, 0.  ],
692           [0.02, 0.66, 0.32, 0.  ],
693           [0.  , 0.32, 0.66, 0.02],
694           [0.  , 0.08, 0.74, 0.18],
695           [0.  , 0.  , 0.5 , 0.5 ]])
696    """
697
698    _parameter_constraints: dict = {
699        "n_knots": [Interval(Integral, 2, None, closed="left")],
700        "degree": [Interval(Integral, 0, None, closed="left")],
701        "knots": [StrOptions({"uniform", "quantile"}), "array-like"],
702        "extrapolation": [
703            StrOptions({"error", "constant", "linear", "continue", "periodic"})
704        ],
705        "include_bias": ["boolean"],
706        "order": [StrOptions({"C", "F"})],
707        "sparse_output": ["boolean"],
708    }
709
710    def __init__(
711        self,
712        n_knots=5,
713        degree=3,
714        *,
715        knots="uniform",
716        extrapolation="constant",
717        include_bias=True,
718        order="C",
719        sparse_output=False,
720    ):
721        self.n_knots = n_knots
722        self.degree = degree
723        self.knots = knots
724        self.extrapolation = extrapolation
725        self.include_bias = include_bias
726        self.order = order
727        self.sparse_output = sparse_output
728
729    @staticmethod
730    def _get_base_knot_positions(X, n_knots=10, knots="uniform", sample_weight=None):
731        """Calculate base knot positions.
732
733        Base knots such that first knot <= feature <= last knot. For the
734        B-spline construction with scipy.interpolate.BSpline, 2*degree knots
735        beyond the base interval are added.
736
737        Returns
738        -------
739        knots : ndarray of shape (n_knots, n_features), dtype=np.float64
740            Knot positions (points) of base interval.
741        """
742        if knots == "quantile":
743            percentile_ranks = 100 * np.linspace(
744                start=0, stop=1, num=n_knots, dtype=np.float64
745            )
746
747            if sample_weight is None:
748                knots = np.percentile(X, percentile_ranks, axis=0)
749            else:
750                knots = np.array(
751                    [
752                        _weighted_percentile(X, sample_weight, percentile_rank)
753                        for percentile_rank in percentile_ranks
754                    ]
755                )
756
757        else:
758            # knots == 'uniform':
759            # Note that the variable `knots` has already been validated and
760            # `else` is therefore safe.
761            # Disregard observations with zero weight.
762            mask = slice(None, None, 1) if sample_weight is None else sample_weight > 0
763            x_min = np.amin(X[mask], axis=0)
764            x_max = np.amax(X[mask], axis=0)
765
766            knots = np.linspace(
767                start=x_min,
768                stop=x_max,
769                num=n_knots,
770                endpoint=True,
771                dtype=np.float64,
772            )
773
774        return knots
775
776    def get_feature_names_out(self, input_features=None):
777        """Get output feature names for transformation.
778
779        Parameters
780        ----------
781        input_features : array-like of str or None, default=None
782            Input features.
783
784            - If `input_features` is `None`, then `feature_names_in_` is
785              used as feature names in. If `feature_names_in_` is not defined,
786              then the following input feature names are generated:
787              `["x0", "x1", ..., "x(n_features_in_ - 1)"]`.
788            - If `input_features` is an array-like, then `input_features` must
789              match `feature_names_in_` if `feature_names_in_` is defined.
790
791        Returns
792        -------
793        feature_names_out : ndarray of str objects
794            Transformed feature names.
795        """
796        check_is_fitted(self, "n_features_in_")
797        n_splines = self.bsplines_[0].c.shape[1]
798
799        input_features = _check_feature_names_in(self, input_features)
800        feature_names = []
801        for i in range(self.n_features_in_):
802            for j in range(n_splines - 1 + self.include_bias):
803                feature_names.append(f"{input_features[i]}_sp_{j}")
804        return np.asarray(feature_names, dtype=object)
805
806    @_fit_context(prefer_skip_nested_validation=True)
807    def fit(self, X, y=None, sample_weight=None):
808        """Compute knot positions of splines.
809
810        Parameters
811        ----------
812        X : array-like of shape (n_samples, n_features)
813            The data.
814
815        y : None
816            Ignored.
817
818        sample_weight : array-like of shape (n_samples,), default = None
819            Individual weights for each sample. Used to calculate quantiles if
820            `knots="quantile"`. For `knots="uniform"`, zero weighted
821            observations are ignored for finding the min and max of `X`.
822
823        Returns
824        -------
825        self : object
826            Fitted transformer.
827        """
828        X = validate_data(
829            self,
830            X,
831            reset=True,
832            accept_sparse=False,
833            ensure_min_samples=2,
834            ensure_2d=True,
835        )
836        if sample_weight is not None:
837            sample_weight = _check_sample_weight(sample_weight, X, dtype=X.dtype)
838
839        _, n_features = X.shape
840
841        if isinstance(self.knots, str):
842            base_knots = self._get_base_knot_positions(
843                X, n_knots=self.n_knots, knots=self.knots, sample_weight=sample_weight
844            )
845        else:
846            base_knots = check_array(self.knots, dtype=np.float64)
847            if base_knots.shape[0] < 2:
848                raise ValueError("Number of knots, knots.shape[0], must be >= 2.")
849            elif base_knots.shape[1] != n_features:
850                raise ValueError("knots.shape[1] == n_features is violated.")
851            elif not np.all(np.diff(base_knots, axis=0) > 0):
852                raise ValueError("knots must be sorted without duplicates.")
853
854        # number of knots for base interval
855        n_knots = base_knots.shape[0]
856
857        if self.extrapolation == "periodic" and n_knots <= self.degree:
858            raise ValueError(
859                "Periodic splines require degree < n_knots. Got n_knots="
860                f"{n_knots} and degree={self.degree}."
861            )
862
863        # number of splines basis functions
864        if self.extrapolation != "periodic":
865            n_splines = n_knots + self.degree - 1
866        else:
867            # periodic splines have self.degree less degrees of freedom
868            n_splines = n_knots - 1
869
870        degree = self.degree
871        n_out = n_features * n_splines
872        # We have to add degree number of knots below, and degree number knots
873        # above the base knots in order to make the spline basis complete.
874        if self.extrapolation == "periodic":
875            # For periodic splines the spacing of the first / last degree knots
876            # needs to be a continuation of the spacing of the last / first
877            # base knots.
878            period = base_knots[-1] - base_knots[0]
879            knots = np.r_[
880                base_knots[-(degree + 1) : -1] - period,
881                base_knots,
882                base_knots[1 : (degree + 1)] + period,
883            ]
884
885        else:
886            # Eilers & Marx in "Flexible smoothing with B-splines and
887            # penalties" https://doi.org/10.1214/ss/1038425655 advice
888            # against repeating first and last knot several times, which
889            # would have inferior behaviour at boundaries if combined with
890            # a penalty (hence P-Spline). We follow this advice even if our
891            # splines are unpenalized. Meaning we do not:
892            # knots = np.r_[
893            #     np.tile(base_knots.min(axis=0), reps=[degree, 1]),
894            #     base_knots,
895            #     np.tile(base_knots.max(axis=0), reps=[degree, 1])
896            # ]
897            # Instead, we reuse the distance of the 2 fist/last knots.
898            dist_min = base_knots[1] - base_knots[0]
899            dist_max = base_knots[-1] - base_knots[-2]
900
901            knots = np.r_[
902                np.linspace(
903                    base_knots[0] - degree * dist_min,
904                    base_knots[0] - dist_min,
905                    num=degree,
906                ),
907                base_knots,
908                np.linspace(
909                    base_knots[-1] + dist_max,
910                    base_knots[-1] + degree * dist_max,
911                    num=degree,
912                ),
913            ]
914
915        # With a diagonal coefficient matrix, we get back the spline basis
916        # elements, i.e. the design matrix of the spline.
917        # Note, BSpline appreciates C-contiguous float64 arrays as c=coef.
918        coef = np.eye(n_splines, dtype=np.float64)
919        if self.extrapolation == "periodic":
920            coef = np.concatenate((coef, coef[:degree, :]))
921
922        extrapolate = self.extrapolation in ["periodic", "continue"]
923
924        bsplines = [
925            BSpline.construct_fast(
926                knots[:, i], coef, self.degree, extrapolate=extrapolate
927            )
928            for i in range(n_features)
929        ]
930        self.bsplines_ = bsplines
931
932        self.n_features_out_ = n_out - n_features * (1 - self.include_bias)
933        return self
934
935    def transform(self, X):
936        """Transform each feature data to B-splines.
937
938        Parameters
939        ----------
940        X : array-like of shape (n_samples, n_features)
941            The data to transform.
942
943        Returns
944        -------
945        XBS : {ndarray, sparse matrix} of shape (n_samples, n_features * n_splines)
946            The matrix of features, where n_splines is the number of bases
947            elements of the B-splines, n_knots + degree - 1.
948        """
949        check_is_fitted(self)
950
951        X = validate_data(self, X, reset=False, accept_sparse=False, ensure_2d=True)
952
953        n_samples, n_features = X.shape
954        n_splines = self.bsplines_[0].c.shape[1]
955        degree = self.degree
956
957        # TODO: Remove this condition, once scipy 1.10 is the minimum version.
958        #       Only scipy => 1.10 supports design_matrix(.., extrapolate=..).
959        #       The default (implicit in scipy < 1.10) is extrapolate=False.
960        scipy_1_10 = sp_version >= parse_version("1.10.0")
961        # Note: self.bsplines_[0].extrapolate is True for extrapolation in
962        # ["periodic", "continue"]
963        if scipy_1_10:
964            use_sparse = self.sparse_output
965            kwargs_extrapolate = {"extrapolate": self.bsplines_[0].extrapolate}
966        else:
967            use_sparse = self.sparse_output and not self.bsplines_[0].extrapolate
968            kwargs_extrapolate = dict()
969
970        # Note that scipy BSpline returns float64 arrays and converts input
971        # x=X[:, i] to c-contiguous float64.
972        n_out = self.n_features_out_ + n_features * (1 - self.include_bias)
973        if X.dtype in FLOAT_DTYPES:
974            dtype = X.dtype
975        else:
976            dtype = np.float64
977        if use_sparse:
978            output_list = []
979        else:
980            XBS = np.zeros((n_samples, n_out), dtype=dtype, order=self.order)
981
982        for i in range(n_features):
983            spl = self.bsplines_[i]
984
985            if self.extrapolation in ("continue", "error", "periodic"):
986                if self.extrapolation == "periodic":
987                    # With periodic extrapolation we map x to the segment
988                    # [spl.t[k], spl.t[n]].
989                    # This is equivalent to BSpline(.., extrapolate="periodic")
990                    # for scipy>=1.0.0.
991                    n = spl.t.size - spl.k - 1
992                    # Assign to new array to avoid inplace operation
993                    x = spl.t[spl.k] + (X[:, i] - spl.t[spl.k]) % (
994                        spl.t[n] - spl.t[spl.k]
995                    )
996                else:
997                    x = X[:, i]
998
999                if use_sparse:
1000                    XBS_sparse = BSpline.design_matrix(
1001                        x, spl.t, spl.k, **kwargs_extrapolate
1002                    )
1003                    if self.extrapolation == "periodic":
1004                        # See the construction of coef in fit. We need to add the last
1005                        # degree spline basis function to the first degree ones and
1006                        # then drop the last ones.
1007                        # Note: See comment about SparseEfficiencyWarning below.
1008                        XBS_sparse = XBS_sparse.tolil()
1009                        XBS_sparse[:, :degree] += XBS_sparse[:, -degree:]
1010                        XBS_sparse = XBS_sparse[:, :-degree]
1011                else:
1012                    XBS[:, (i * n_splines) : ((i + 1) * n_splines)] = spl(x)
1013            else:  # extrapolation in ("constant", "linear")
1014                xmin, xmax = spl.t[degree], spl.t[-degree - 1]
1015                # spline values at boundaries
1016                f_min, f_max = spl(xmin), spl(xmax)
1017                mask = (xmin <= X[:, i]) & (X[:, i] <= xmax)
1018                if use_sparse:
1019                    mask_inv = ~mask
1020                    x = X[:, i].copy()
1021                    # Set some arbitrary values outside boundary that will be reassigned
1022                    # later.
1023                    x[mask_inv] = spl.t[self.degree]
1024                    XBS_sparse = BSpline.design_matrix(x, spl.t, spl.k)
1025                    # Note: Without converting to lil_matrix we would get:
1026                    # scipy.sparse._base.SparseEfficiencyWarning: Changing the sparsity
1027                    # structure of a csr_matrix is expensive. lil_matrix is more
1028                    # efficient.
1029                    if np.any(mask_inv):
1030                        XBS_sparse = XBS_sparse.tolil()
1031                        XBS_sparse[mask_inv, :] = 0
1032                else:
1033                    XBS[mask, (i * n_splines) : ((i + 1) * n_splines)] = spl(X[mask, i])
1034
1035            # Note for extrapolation:
1036            # 'continue' is already returned as is by scipy BSplines
1037            if self.extrapolation == "error":
1038                # BSpline with extrapolate=False does not raise an error, but
1039                # outputs np.nan.
1040                if (use_sparse and np.any(np.isnan(XBS_sparse.data))) or (
1041                    not use_sparse
1042                    and np.any(
1043                        np.isnan(XBS[:, (i * n_splines) : ((i + 1) * n_splines)])
1044                    )
1045                ):
1046                    raise ValueError(
1047                        "X contains values beyond the limits of the knots."
1048                    )
1049            elif self.extrapolation == "constant":
1050                # Set all values beyond xmin and xmax to the value of the
1051                # spline basis functions at those two positions.
1052                # Only the first degree and last degree number of splines
1053                # have non-zero values at the boundaries.
1054
1055                mask = X[:, i] < xmin
1056                if np.any(mask):
1057                    if use_sparse:
1058                        # Note: See comment about SparseEfficiencyWarning above.
1059                        XBS_sparse = XBS_sparse.tolil()
1060                        XBS_sparse[mask, :degree] = f_min[:degree]
1061
1062                    else:
1063                        XBS[mask, (i * n_splines) : (i * n_splines + degree)] = f_min[
1064                            :degree
1065                        ]
1066
1067                mask = X[:, i] > xmax
1068                if np.any(mask):
1069                    if use_sparse:
1070                        # Note: See comment about SparseEfficiencyWarning above.
1071                        XBS_sparse = XBS_sparse.tolil()
1072                        XBS_sparse[mask, -degree:] = f_max[-degree:]
1073                    else:
1074                        XBS[
1075                            mask,
1076                            ((i + 1) * n_splines - degree) : ((i + 1) * n_splines),
1077                        ] = f_max[-degree:]
1078
1079            elif self.extrapolation == "linear":
1080                # Continue the degree first and degree last spline bases
1081                # linearly beyond the boundaries, with slope = derivative at
1082                # the boundary.
1083                # Note that all others have derivative = value = 0 at the
1084                # boundaries.
1085
1086                # spline derivatives = slopes at boundaries
1087                fp_min, fp_max = spl(xmin, nu=1), spl(xmax, nu=1)
1088                # Compute the linear continuation.
1089                if degree <= 1:
1090                    # For degree=1, the derivative of 2nd spline is not zero at
1091                    # boundary. For degree=0 it is the same as 'constant'.
1092                    degree += 1
1093                for j in range(degree):
1094                    mask = X[:, i] < xmin
1095                    if np.any(mask):
1096                        linear_extr = f_min[j] + (X[mask, i] - xmin) * fp_min[j]
1097                        if use_sparse:
1098                            # Note: See comment about SparseEfficiencyWarning above.
1099                            XBS_sparse = XBS_sparse.tolil()
1100                            XBS_sparse[mask, j] = linear_extr
1101                        else:
1102                            XBS[mask, i * n_splines + j] = linear_extr
1103
1104                    mask = X[:, i] > xmax
1105                    if np.any(mask):
1106                        k = n_splines - 1 - j
1107                        linear_extr = f_max[k] + (X[mask, i] - xmax) * fp_max[k]
1108                        if use_sparse:
1109                            # Note: See comment about SparseEfficiencyWarning above.
1110                            XBS_sparse = XBS_sparse.tolil()
1111                            XBS_sparse[mask, k : k + 1] = linear_extr[:, None]
1112                        else:
1113                            XBS[mask, i * n_splines + k] = linear_extr
1114
1115            if use_sparse:
1116                XBS_sparse = XBS_sparse.tocsr()
1117                output_list.append(XBS_sparse)
1118
1119        if use_sparse:
1120            # TODO: Remove this conditional error when the minimum supported version of
1121            # SciPy is 1.9.2
1122            # `scipy.sparse.hstack` breaks in scipy<1.9.2
1123            # when `n_features_out_ > max_int32`
1124            max_int32 = np.iinfo(np.int32).max
1125            all_int32 = True
1126            for mat in output_list:
1127                all_int32 &= mat.indices.dtype == np.int32
1128            if (
1129                sp_version < parse_version("1.9.2")
1130                and self.n_features_out_ > max_int32
1131                and all_int32
1132            ):
1133                raise ValueError(
1134                    "In scipy versions `<1.9.2`, the function `scipy.sparse.hstack`"
1135                    " produces negative columns when:\n1. The output shape contains"
1136                    " `n_cols` too large to be represented by a 32bit signed"
1137                    " integer.\n. All sub-matrices to be stacked have indices of"
1138                    " dtype `np.int32`.\nTo avoid this error, either use a version"
1139                    " of scipy `>=1.9.2` or alter the `SplineTransformer`"
1140                    " transformer to produce fewer than 2^31 output features"
1141                )
1142            XBS = sparse.hstack(output_list, format="csr")
1143        elif self.sparse_output:
1144            # TODO: Remove ones scipy 1.10 is the minimum version. See comments above.
1145            XBS = sparse.csr_matrix(XBS)
1146
1147        if self.include_bias:
1148            return XBS
1149        else:
1150            # We throw away one spline basis per feature.
1151            # We chose the last one.
1152            indices = [j for j in range(XBS.shape[1]) if (j + 1) % n_splines != 0]
1153            return XBS[:, indices]
1154 
Aluode/PerceptionLabPortable · CoolFace