CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
_function_transformer.py450 linesDownload Raw Back to preprocessing
1# Authors: The scikit-learn developers
2# SPDX-License-Identifier: BSD-3-Clause
3
4import warnings
5from functools import partial
6
7import numpy as np
8
9from ..base import BaseEstimator, TransformerMixin, _fit_context
10from ..utils._param_validation import StrOptions
11from ..utils._repr_html.estimator import _VisualBlock
12from ..utils._set_output import (
13    _get_adapter_from_container,
14    _get_output_config,
15)
16from ..utils.metaestimators import available_if
17from ..utils.validation import (
18    _allclose_dense_sparse,
19    _check_feature_names,
20    _check_feature_names_in,
21    _check_n_features,
22    _get_feature_names,
23    _is_pandas_df,
24    _is_polars_df,
25    check_array,
26    validate_data,
27)
28
29
30def _identity(X):
31    """The identity function."""
32    return X
33
34
35class FunctionTransformer(TransformerMixin, BaseEstimator):
36    """Constructs a transformer from an arbitrary callable.
37
38    A FunctionTransformer forwards its X (and optionally y) arguments to a
39    user-defined function or function object and returns the result of this
40    function. This is useful for stateless transformations such as taking the
41    log of frequencies, doing custom scaling, etc.
42
43    Note: If a lambda is used as the function, then the resulting
44    transformer will not be pickleable.
45
46    .. versionadded:: 0.17
47
48    Read more in the :ref:`User Guide <function_transformer>`.
49
50    Parameters
51    ----------
52    func : callable, default=None
53        The callable to use for the transformation. This will be passed
54        the same arguments as transform, with args and kwargs forwarded.
55        If func is None, then func will be the identity function.
56
57    inverse_func : callable, default=None
58        The callable to use for the inverse transformation. This will be
59        passed the same arguments as inverse transform, with args and
60        kwargs forwarded. If inverse_func is None, then inverse_func
61        will be the identity function.
62
63    validate : bool, default=False
64        Indicate that the input X array should be checked before calling
65        ``func``. The possibilities are:
66
67        - If False, there is no input validation.
68        - If True, then X will be converted to a 2-dimensional NumPy array or
69          sparse matrix. If the conversion is not possible an exception is
70          raised.
71
72        .. versionchanged:: 0.22
73           The default of ``validate`` changed from True to False.
74
75    accept_sparse : bool, default=False
76        Indicate that func accepts a sparse matrix as input. If validate is
77        False, this has no effect. Otherwise, if accept_sparse is false,
78        sparse matrix inputs will cause an exception to be raised.
79
80    check_inverse : bool, default=True
81       Whether to check that or ``func`` followed by ``inverse_func`` leads to
82       the original inputs. It can be used for a sanity check, raising a
83       warning when the condition is not fulfilled.
84
85       .. versionadded:: 0.20
86
87    feature_names_out : callable, 'one-to-one' or None, default=None
88        Determines the list of feature names that will be returned by the
89        `get_feature_names_out` method. If it is 'one-to-one', then the output
90        feature names will be equal to the input feature names. If it is a
91        callable, then it must take two positional arguments: this
92        `FunctionTransformer` (`self`) and an array-like of input feature names
93        (`input_features`). It must return an array-like of output feature
94        names. The `get_feature_names_out` method is only defined if
95        `feature_names_out` is not None.
96
97        See ``get_feature_names_out`` for more details.
98
99        .. versionadded:: 1.1
100
101    kw_args : dict, default=None
102        Dictionary of additional keyword arguments to pass to func.
103
104        .. versionadded:: 0.18
105
106    inv_kw_args : dict, default=None
107        Dictionary of additional keyword arguments to pass to inverse_func.
108
109        .. versionadded:: 0.18
110
111    Attributes
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` has feature
120        names that are all strings.
121
122        .. versionadded:: 1.0
123
124    See Also
125    --------
126    MaxAbsScaler : Scale each feature by its maximum absolute value.
127    StandardScaler : Standardize features by removing the mean and
128        scaling to unit variance.
129    LabelBinarizer : Binarize labels in a one-vs-all fashion.
130    MultiLabelBinarizer : Transform between iterable of iterables
131        and a multilabel format.
132
133    Notes
134    -----
135    If `func` returns an output with a `columns` attribute, then the columns is enforced
136    to be consistent with the output of `get_feature_names_out`.
137
138    Examples
139    --------
140    >>> import numpy as np
141    >>> from sklearn.preprocessing import FunctionTransformer
142    >>> transformer = FunctionTransformer(np.log1p)
143    >>> X = np.array([[0, 1], [2, 3]])
144    >>> transformer.transform(X)
145    array([[0.       , 0.6931],
146           [1.0986, 1.3862]])
147    """
148
149    _parameter_constraints: dict = {
150        "func": [callable, None],
151        "inverse_func": [callable, None],
152        "validate": ["boolean"],
153        "accept_sparse": ["boolean"],
154        "check_inverse": ["boolean"],
155        "feature_names_out": [callable, StrOptions({"one-to-one"}), None],
156        "kw_args": [dict, None],
157        "inv_kw_args": [dict, None],
158    }
159
160    def __init__(
161        self,
162        func=None,
163        inverse_func=None,
164        *,
165        validate=False,
166        accept_sparse=False,
167        check_inverse=True,
168        feature_names_out=None,
169        kw_args=None,
170        inv_kw_args=None,
171    ):
172        self.func = func
173        self.inverse_func = inverse_func
174        self.validate = validate
175        self.accept_sparse = accept_sparse
176        self.check_inverse = check_inverse
177        self.feature_names_out = feature_names_out
178        self.kw_args = kw_args
179        self.inv_kw_args = inv_kw_args
180
181    def _check_input(self, X, *, reset):
182        if self.validate:
183            return validate_data(self, X, accept_sparse=self.accept_sparse, reset=reset)
184        elif reset:
185            # Set feature_names_in_ and n_features_in_ even if validate=False
186            # We run this only when reset==True to store the attributes but not
187            # validate them, because validate=False
188            _check_n_features(self, X, reset=reset)
189            _check_feature_names(self, X, reset=reset)
190        return X
191
192    def _check_inverse_transform(self, X):
193        """Check that func and inverse_func are the inverse."""
194        idx_selected = slice(None, None, max(1, X.shape[0] // 100))
195        X_round_trip = self.inverse_transform(self.transform(X[idx_selected]))
196
197        if hasattr(X, "dtype"):
198            dtypes = [X.dtype]
199        elif hasattr(X, "dtypes"):
200            # Dataframes can have multiple dtypes
201            dtypes = X.dtypes
202
203        # Not all dtypes are numpy dtypes, they can be pandas dtypes as well
204        if not all(
205            isinstance(d, np.dtype) and np.issubdtype(d, np.number) for d in dtypes
206        ):
207            raise ValueError(
208                "'check_inverse' is only supported when all the elements in `X` is"
209                " numerical."
210            )
211
212        if not _allclose_dense_sparse(X[idx_selected], X_round_trip):
213            warnings.warn(
214                (
215                    "The provided functions are not strictly"
216                    " inverse of each other. If you are sure you"
217                    " want to proceed regardless, set"
218                    " 'check_inverse=False'."
219                ),
220                UserWarning,
221            )
222
223    @_fit_context(prefer_skip_nested_validation=True)
224    def fit(self, X, y=None):
225        """Fit transformer by checking X.
226
227        If ``validate`` is ``True``, ``X`` will be checked.
228
229        Parameters
230        ----------
231        X : {array-like, sparse-matrix} of shape (n_samples, n_features) \
232                if `validate=True` else any object that `func` can handle
233            Input array.
234
235        y : Ignored
236            Not used, present here for API consistency by convention.
237
238        Returns
239        -------
240        self : object
241            FunctionTransformer class instance.
242        """
243        X = self._check_input(X, reset=True)
244        if self.check_inverse and not (self.func is None or self.inverse_func is None):
245            self._check_inverse_transform(X)
246        return self
247
248    def transform(self, X):
249        """Transform X using the forward function.
250
251        Parameters
252        ----------
253        X : {array-like, sparse-matrix} of shape (n_samples, n_features) \
254                if `validate=True` else any object that `func` can handle
255            Input array.
256
257        Returns
258        -------
259        X_out : array-like, shape (n_samples, n_features)
260            Transformed input.
261        """
262        X = self._check_input(X, reset=False)
263        out = self._transform(X, func=self.func, kw_args=self.kw_args)
264        output_config = _get_output_config("transform", self)["dense"]
265
266        if hasattr(out, "columns") and self.feature_names_out is not None:
267            # check the consistency between the column provided by `transform` and
268            # the column names provided by `get_feature_names_out`.
269            feature_names_out = self.get_feature_names_out()
270            if list(out.columns) != list(feature_names_out):
271                # we can override the column names of the output if it is inconsistent
272                # with the column names provided by `get_feature_names_out` in the
273                # following cases:
274                # * `func` preserved the column names between the input and the output
275                # * the input column names are all numbers
276                # * the output is requested to be a DataFrame (pandas or polars)
277                feature_names_in = getattr(
278                    X, "feature_names_in_", _get_feature_names(X)
279                )
280                same_feature_names_in_out = feature_names_in is not None and list(
281                    feature_names_in
282                ) == list(out.columns)
283                not_all_str_columns = not all(
284                    isinstance(col, str) for col in out.columns
285                )
286                if same_feature_names_in_out or not_all_str_columns:
287                    adapter = _get_adapter_from_container(out)
288                    out = adapter.create_container(
289                        X_output=out,
290                        X_original=out,
291                        columns=feature_names_out,
292                        inplace=False,
293                    )
294                else:
295                    raise ValueError(
296                        "The output generated by `func` have different column names "
297                        "than the ones provided by `get_feature_names_out`. "
298                        f"Got output with columns names: {list(out.columns)} and "
299                        "`get_feature_names_out` returned: "
300                        f"{list(self.get_feature_names_out())}. "
301                        "The column names can be overridden by setting "
302                        "`set_output(transform='pandas')` or "
303                        "`set_output(transform='polars')` such that the column names "
304                        "are set to the names provided by `get_feature_names_out`."
305                    )
306
307        if self.feature_names_out is None:
308            warn_msg = (
309                "When `set_output` is configured to be '{0}', `func` should return "
310                "a {0} DataFrame to follow the `set_output` API  or `feature_names_out`"
311                " should be defined."
312            )
313            if output_config == "pandas" and not _is_pandas_df(out):
314                warnings.warn(warn_msg.format("pandas"))
315            elif output_config == "polars" and not _is_polars_df(out):
316                warnings.warn(warn_msg.format("polars"))
317
318        return out
319
320    def inverse_transform(self, X):
321        """Transform X using the inverse function.
322
323        Parameters
324        ----------
325        X : {array-like, sparse-matrix} of shape (n_samples, n_features) \
326                if `validate=True` else any object that `inverse_func` can handle
327            Input array.
328
329        Returns
330        -------
331        X_original : array-like, shape (n_samples, n_features)
332            Transformed input.
333        """
334        if self.validate:
335            X = check_array(X, accept_sparse=self.accept_sparse)
336        return self._transform(X, func=self.inverse_func, kw_args=self.inv_kw_args)
337
338    @available_if(lambda self: self.feature_names_out is not None)
339    def get_feature_names_out(self, input_features=None):
340        """Get output feature names for transformation.
341
342        This method is only defined if `feature_names_out` is not None.
343
344        Parameters
345        ----------
346        input_features : array-like of str or None, default=None
347            Input feature names.
348
349            - If `input_features` is None, then `feature_names_in_` is
350              used as the input feature names. If `feature_names_in_` is not
351              defined, then names are generated:
352              `[x0, x1, ..., x(n_features_in_ - 1)]`.
353            - If `input_features` is array-like, then `input_features` must
354              match `feature_names_in_` if `feature_names_in_` is defined.
355
356        Returns
357        -------
358        feature_names_out : ndarray of str objects
359            Transformed feature names.
360
361            - If `feature_names_out` is 'one-to-one', the input feature names
362              are returned (see `input_features` above). This requires
363              `feature_names_in_` and/or `n_features_in_` to be defined, which
364              is done automatically if `validate=True`. Alternatively, you can
365              set them in `func`.
366            - If `feature_names_out` is a callable, then it is called with two
367              arguments, `self` and `input_features`, and its return value is
368              returned by this method.
369        """
370        if hasattr(self, "n_features_in_") or input_features is not None:
371            input_features = _check_feature_names_in(self, input_features)
372        if self.feature_names_out == "one-to-one":
373            names_out = input_features
374        elif callable(self.feature_names_out):
375            names_out = self.feature_names_out(self, input_features)
376        else:
377            raise ValueError(
378                f"feature_names_out={self.feature_names_out!r} is invalid. "
379                'It must either be "one-to-one" or a callable with two '
380                "arguments: the function transformer and an array-like of "
381                "input feature names. The callable must return an array-like "
382                "of output feature names."
383            )
384        return np.asarray(names_out, dtype=object)
385
386    def _transform(self, X, func=None, kw_args=None):
387        if func is None:
388            func = _identity
389
390        return func(X, **(kw_args if kw_args else {}))
391
392    def __sklearn_is_fitted__(self):
393        """Return True since FunctionTransfomer is stateless."""
394        return True
395
396    def __sklearn_tags__(self):
397        tags = super().__sklearn_tags__()
398        tags.no_validation = not self.validate
399        tags.requires_fit = False
400        tags.input_tags.sparse = not self.validate or self.accept_sparse
401        return tags
402
403    def set_output(self, *, transform=None):
404        """Set output container.
405
406        See :ref:`sphx_glr_auto_examples_miscellaneous_plot_set_output.py`
407        for an example on how to use the API.
408
409        Parameters
410        ----------
411        transform : {"default", "pandas", "polars"}, default=None
412            Configure output of `transform` and `fit_transform`.
413
414            - `"default"`: Default output format of a transformer
415            - `"pandas"`: DataFrame output
416            - `"polars"`: Polars output
417            - `None`: Transform configuration is unchanged
418
419            .. versionadded:: 1.4
420                `"polars"` option was added.
421
422        Returns
423        -------
424        self : estimator instance
425            Estimator instance.
426        """
427        if not hasattr(self, "_sklearn_output_config"):
428            self._sklearn_output_config = {}
429
430        self._sklearn_output_config["transform"] = transform
431        return self
432
433    def _get_function_name(self):
434        """Get the name display of the `func` used in HTML representation."""
435        if hasattr(self.func, "__name__"):
436            return self.func.__name__
437        if isinstance(self.func, partial):
438            return self.func.func.__name__
439        return f"{self.func.__class__.__name__}(...)"
440
441    def _sk_visual_block_(self):
442        return _VisualBlock(
443            "single",
444            self,
445            names=self._get_function_name(),
446            name_details=str(self),
447            name_caption="FunctionTransformer",
448            doc_link_label="FunctionTransformer",
449        )
450 
Aluode/PerceptionLabPortable · CoolFace