Aluode/PerceptionLabPortable
0
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 