From cb0916c44068874b5518238fba1a740c4e1a04f6 Mon Sep 17 00:00:00 2001 From: Joe Jevnik Date: Mon, 1 Jun 2015 16:08:31 -0400 Subject: [PATCH 1/5] ENH: Adds CallableTransformer CallableTransformer allows a user to convert a standard python callable into a transformer for use in a Pipeline. --- doc/modules/classes.rst | 1 + doc/modules/preprocessing.rst | 20 +++++ .../plot_callable_transformer.py | 69 ++++++++++++++++ sklearn/preprocessing/__init__.py | 4 + sklearn/preprocessing/callable_transformer.py | 44 +++++++++++ .../tests/test_callable_transformer.py | 79 +++++++++++++++++++ sklearn/utils/estimator_checks.py | 3 +- 7 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 examples/preprocessing/plot_callable_transformer.py create mode 100644 sklearn/preprocessing/callable_transformer.py create mode 100644 sklearn/preprocessing/tests/test_callable_transformer.py diff --git a/doc/modules/classes.rst b/doc/modules/classes.rst index ff5708efeca7c..0784deb6cea41 100644 --- a/doc/modules/classes.rst +++ b/doc/modules/classes.rst @@ -1104,6 +1104,7 @@ See the :ref:`metrics` section of the user guide for further details. :template: class.rst preprocessing.Binarizer + preprocessing.CallableTransformer preprocessing.Imputer preprocessing.KernelCenterer preprocessing.LabelBinarizer diff --git a/doc/modules/preprocessing.rst b/doc/modules/preprocessing.rst index 469726dedbea7..88b43af566398 100644 --- a/doc/modules/preprocessing.rst +++ b/doc/modules/preprocessing.rst @@ -508,3 +508,23 @@ The features of X have been transformed from :math:`(X_1, X_2, X_3)` to :math:`( Note that polynomial features are used implicitily in `kernel methods `_ (e.g., :class:`sklearn.svm.SVC`, :class:`sklearn.decomposition.KernelPCA`) when using polynomial :ref:`svm_kernels`. See :ref:`example_linear_model_plot_polynomial_interpolation.py` for Ridge regression using created polynomial features. + +Custom Transformers +=================== + +Often, you will want to convert an existing python function into a transformer +to assist in data cleaning or processing. Users may implement a transformer from +an arbitrary callable with :class:`CallableTransformer`. For example, one could +apply a log transformation in a pipeline like:: + + >>> import numpy as np + >>> from sklearn.preprocessing import CallableTransformer + >>> transformer = CallableTransformer(np.log) + >>> X = np.array([[1, 2], [3, 4]]) + >>> transformer.transform(X) + array([[ 0. , 0.69314718], + [ 1.09861229, 1.38629436]]) + +For a full code example that demonstrates using a :class:`CallableTransformer` +to do column selection, +see :ref:`example_preprocessing_plot_callable_transformer.py` diff --git a/examples/preprocessing/plot_callable_transformer.py b/examples/preprocessing/plot_callable_transformer.py new file mode 100644 index 0000000000000..98746541d36bb --- /dev/null +++ b/examples/preprocessing/plot_callable_transformer.py @@ -0,0 +1,69 @@ +""" +========================================================= +Using CallableTransformer to select columns +========================================================= + +Shows how to use a callable transformer in a pipeline. If you know your +dataset's first principle component is irrelevant for a classification task, +you can use the CallableTransformer to select all but the first column of the +PCA transformed data. +""" +import matplotlib.pyplot as plt +import numpy as np + +from sklearn.cross_validation import train_test_split +from sklearn.decomposition import PCA +from sklearn.pipeline import make_pipeline +from sklearn.preprocessing import CallableTransformer + + +def _generate_vector(shift=0.5, noise=15): + return np.arange(1000) + (np.random.rand(1000) - shift) * noise + + +def generate_dataset(): + """ + This dataset is two lines with a slope ~ 1, where one has + a y offset of ~100 + """ + return np.vstack(( + np.vstack(( + _generate_vector(), + _generate_vector() + 100, + )).T, + np.vstack(( + _generate_vector(), + _generate_vector(), + )).T, + )), np.hstack((np.zeros(1000), np.ones(1000))) + + +def all_but_first_column(X, y): + return X[:, 1:] + + +def drop_first_component(X, y): + """ + Create a pipeline with PCA and the column selector and use it to + transform the dataset. + """ + pipeline = make_pipeline( + PCA(), CallableTransformer(all_but_first_column), + ) + X_train, X_test, y_train, y_test = train_test_split(X, y) + pipeline.fit(X_train, y_train) + return pipeline.transform(X_test), y_test + + +if __name__ == '__main__': + X, y = generate_dataset() + plt.scatter(X[:, 0], X[:, 1], c=y, s=50) + plt.show() + X_transformed, y_transformed = drop_first_component(*generate_dataset()) + plt.scatter( + X_transformed[:, 0], + np.zeros(len(X_transformed)), + c=y_transformed, + s=50, + ) + plt.show() diff --git a/sklearn/preprocessing/__init__.py b/sklearn/preprocessing/__init__.py index 380f5704a963e..254bc35750488 100644 --- a/sklearn/preprocessing/__init__.py +++ b/sklearn/preprocessing/__init__.py @@ -3,6 +3,8 @@ normalization, binarization and imputation methods. """ +from .callable_transformer import CallableTransformer + from .data import Binarizer from .data import KernelCenterer from .data import MinMaxScaler @@ -28,8 +30,10 @@ from .imputation import Imputer + __all__ = [ 'Binarizer', + 'CallableTransformer', 'Imputer', 'KernelCenterer', 'LabelBinarizer', diff --git a/sklearn/preprocessing/callable_transformer.py b/sklearn/preprocessing/callable_transformer.py new file mode 100644 index 0000000000000..cd7097bb6de94 --- /dev/null +++ b/sklearn/preprocessing/callable_transformer.py @@ -0,0 +1,44 @@ +from ..base import BaseEstimator, TransformerMixin +from ..utils import check_array + + +class CallableTransformer(BaseEstimator, TransformerMixin): + """Allows the construction of a transformer from an arbitrary callable. + + Parameters + ---------- + func : callable, optional default=None + The callable to use for the transformation. This will be passed + the same arguments as transform, with args and kwargs forwarded. + If func is None, then func will be the identity function. + validate : bool, optional default=True + Indicate that the input X array should be checked before calling + func. If validate is false, there will be no input validation. + accept_sparse : boolean, optional + Indicate that func accepts a sparse matrix as input. + args : tuple, optional + A tuple of positional arguments to be passed to func. These will + be passed after X and y. + kwargs : dict, optional + A dictionary of keyword arguments to be passed to func. + + """ + def __init__(self, func=None, validate=True, accept_sparse=False, + args=None, kwargs=None): + self.func = func + self.validate = validate + self.accept_sparse = accept_sparse + self.args = args + self.kwargs = kwargs + + def fit(self, X, y=None): + if self.validate: + check_array(X, self.accept_sparse) + return self + + def transform(self, X, y=None): + if self.validate: + X = check_array(X, self.accept_sparse) + return (self.func or (lambda X, y, *args, **kwargs: X))( + X, y, *(self.args or ()), **(self.kwargs or {}) + ) diff --git a/sklearn/preprocessing/tests/test_callable_transformer.py b/sklearn/preprocessing/tests/test_callable_transformer.py new file mode 100644 index 0000000000000..474451200285e --- /dev/null +++ b/sklearn/preprocessing/tests/test_callable_transformer.py @@ -0,0 +1,79 @@ +from nose.tools import assert_equal +import numpy as np + +from ..callable_transformer import CallableTransformer + + +def _make_func(args_store, kwargs_store, func=lambda X, *a, **k: X): + def _func(X, *args, **kwargs): + args_store.append(X) + args_store.extend(args) + kwargs_store.update(kwargs) + return func(X) + + return _func + + +def test_delegate_to_func(): + # (args|kwargs)_store will hold the positional and keyword arguments + # passed to the function inside the CallableTransformer. + args_store = [] + kwargs_store = {} + X = np.arange(10).reshape((5, 2)) + np.testing.assert_array_equal( + CallableTransformer(_make_func(args_store, kwargs_store)).transform(X), + X, + 'transform should have returned X unchanged', + ) + + # The function should only have recieved X and y, where y is None. + assert_equal( + args_store, + [X, None], + 'Incorrect positional arguments passed to func: {args}'.format( + args=args_store, + ), + ) + assert_equal( + kwargs_store, + {}, + 'Unexpected keyword arguments passed to func: {args}'.format( + args=kwargs_store, + ), + ) + + +def test_argument_closure(): + # (args|kwargs)_store will hold the positional and keyword arguments + # passed to the function inside the CallableTransformer. + args_store = [] + kwargs_store = {} + args = (object(), object()) + kwargs = {'a': object(), 'b': object()} + X = np.arange(10).reshape((5, 2)) + np.testing.assert_array_equal( + CallableTransformer( + _make_func(args_store, kwargs_store), + args=args, + kwargs=kwargs, + ).transform(X), + X, + 'transform should have returned X unchanged', + ) + + # The function should have been passed X, y (None), and the args + # that were passed to the CallableTransformer. + assert_equal( + args_store, + [X, None] + list(args), + 'Incorrect positional arguments passed to func: {args}'.format( + args=args_store, + ), + ) + assert_equal( + kwargs_store, + kwargs, + 'Incorrect keyword arguments passed to func: {args}'.format( + args=kwargs_store, + ), + ) diff --git a/sklearn/utils/estimator_checks.py b/sklearn/utils/estimator_checks.py index 63c396203106a..ad10deea15c83 100644 --- a/sklearn/utils/estimator_checks.py +++ b/sklearn/utils/estimator_checks.py @@ -138,7 +138,8 @@ def _yield_transformer_checks(name, Transformer): 'PLSCanonical', 'PLSRegression', 'CCA', 'PLSSVD']: yield check_transformer_data_not_an_array # these don't actually fit the data, so don't raise errors - if name not in ['AdditiveChi2Sampler', 'Binarizer', 'Normalizer']: + if name not in ['AdditiveChi2Sampler', 'Binarizer', + 'Normalizer', 'CallableTransformer']: # basic tests yield check_transformer_general yield check_transformers_unfitted From 9d14f7352e6579b39512d5ce9545e070aafd5ab3 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Thu, 30 Jul 2015 12:48:04 -0400 Subject: [PATCH 2/5] ENH: Renames CallableTransformer -> FunctionTransformer. Makes `pass_y` an argument to FunctionTransformer to indicate that the labels should be passed to the wrapped function. --- doc/modules/classes.rst | 2 +- doc/modules/preprocessing.rst | 10 ++-- ...former.py => plot_function_transformer.py} | 12 ++--- sklearn/preprocessing/__init__.py | 4 +- ...transformer.py => function_transformer.py} | 36 ++++++++------ ...former.py => test_function_transformer.py} | 48 ++++++++++--------- sklearn/utils/estimator_checks.py | 2 +- 7 files changed, 63 insertions(+), 51 deletions(-) rename examples/preprocessing/{plot_callable_transformer.py => plot_function_transformer.py} (83%) rename sklearn/preprocessing/{callable_transformer.py => function_transformer.py} (60%) rename sklearn/preprocessing/tests/{test_callable_transformer.py => test_function_transformer.py} (60%) diff --git a/doc/modules/classes.rst b/doc/modules/classes.rst index 0784deb6cea41..b310ba3ea3dbf 100644 --- a/doc/modules/classes.rst +++ b/doc/modules/classes.rst @@ -1104,7 +1104,7 @@ See the :ref:`metrics` section of the user guide for further details. :template: class.rst preprocessing.Binarizer - preprocessing.CallableTransformer + preprocessing.FunctionTransformer preprocessing.Imputer preprocessing.KernelCenterer preprocessing.LabelBinarizer diff --git a/doc/modules/preprocessing.rst b/doc/modules/preprocessing.rst index 88b43af566398..842f071185830 100644 --- a/doc/modules/preprocessing.rst +++ b/doc/modules/preprocessing.rst @@ -514,17 +514,17 @@ Custom Transformers Often, you will want to convert an existing python function into a transformer to assist in data cleaning or processing. Users may implement a transformer from -an arbitrary callable with :class:`CallableTransformer`. For example, one could +an arbitrary function with :class:`FunctionTransformer`. For example, one could apply a log transformation in a pipeline like:: >>> import numpy as np - >>> from sklearn.preprocessing import CallableTransformer - >>> transformer = CallableTransformer(np.log) + >>> from sklearn.preprocessing import FunctionTransformer + >>> transformer = FunctionTransformer(np.log) >>> X = np.array([[1, 2], [3, 4]]) >>> transformer.transform(X) array([[ 0. , 0.69314718], [ 1.09861229, 1.38629436]]) -For a full code example that demonstrates using a :class:`CallableTransformer` +For a full code example that demonstrates using a :class:`FunctionTransformer` to do column selection, -see :ref:`example_preprocessing_plot_callable_transformer.py` +see :ref:`example_preprocessing_plot_function_transformer.py` diff --git a/examples/preprocessing/plot_callable_transformer.py b/examples/preprocessing/plot_function_transformer.py similarity index 83% rename from examples/preprocessing/plot_callable_transformer.py rename to examples/preprocessing/plot_function_transformer.py index 98746541d36bb..46cff6f5a784f 100644 --- a/examples/preprocessing/plot_callable_transformer.py +++ b/examples/preprocessing/plot_function_transformer.py @@ -1,11 +1,11 @@ """ ========================================================= -Using CallableTransformer to select columns +Using FunctionTransformer to select columns ========================================================= -Shows how to use a callable transformer in a pipeline. If you know your +Shows how to use a function transformer in a pipeline. If you know your dataset's first principle component is irrelevant for a classification task, -you can use the CallableTransformer to select all but the first column of the +you can use the FunctionTransformer to select all but the first column of the PCA transformed data. """ import matplotlib.pyplot as plt @@ -14,7 +14,7 @@ from sklearn.cross_validation import train_test_split from sklearn.decomposition import PCA from sklearn.pipeline import make_pipeline -from sklearn.preprocessing import CallableTransformer +from sklearn.preprocessing import FunctionTransformer def _generate_vector(shift=0.5, noise=15): @@ -38,7 +38,7 @@ def generate_dataset(): )), np.hstack((np.zeros(1000), np.ones(1000))) -def all_but_first_column(X, y): +def all_but_first_column(X): return X[:, 1:] @@ -48,7 +48,7 @@ def drop_first_component(X, y): transform the dataset. """ pipeline = make_pipeline( - PCA(), CallableTransformer(all_but_first_column), + PCA(), FunctionTransformer(all_but_first_column), ) X_train, X_test, y_train, y_test = train_test_split(X, y) pipeline.fit(X_train, y_train) diff --git a/sklearn/preprocessing/__init__.py b/sklearn/preprocessing/__init__.py index 254bc35750488..2ffc645857c52 100644 --- a/sklearn/preprocessing/__init__.py +++ b/sklearn/preprocessing/__init__.py @@ -3,7 +3,7 @@ normalization, binarization and imputation methods. """ -from .callable_transformer import CallableTransformer +from .function_transformer import FunctionTransformer from .data import Binarizer from .data import KernelCenterer @@ -33,7 +33,7 @@ __all__ = [ 'Binarizer', - 'CallableTransformer', + 'FunctionTransformer', 'Imputer', 'KernelCenterer', 'LabelBinarizer', diff --git a/sklearn/preprocessing/callable_transformer.py b/sklearn/preprocessing/function_transformer.py similarity index 60% rename from sklearn/preprocessing/callable_transformer.py rename to sklearn/preprocessing/function_transformer.py index cd7097bb6de94..d3c432afac7a3 100644 --- a/sklearn/preprocessing/callable_transformer.py +++ b/sklearn/preprocessing/function_transformer.py @@ -2,8 +2,17 @@ from ..utils import check_array -class CallableTransformer(BaseEstimator, TransformerMixin): - """Allows the construction of a transformer from an arbitrary callable. +def _identity(X): + """The identity function. + """ + return X + + +class FunctionTransformer(BaseEstimator, TransformerMixin): + """Constructs a transformer from an arbitrary callable. + + Note: If a lambda is used as the function, then the resulting + transformer will not be pickleable. Parameters ---------- @@ -11,25 +20,25 @@ class CallableTransformer(BaseEstimator, TransformerMixin): The callable to use for the transformation. This will be passed the same arguments as transform, with args and kwargs forwarded. If func is None, then func will be the identity function. + validate : bool, optional default=True Indicate that the input X array should be checked before calling func. If validate is false, there will be no input validation. + accept_sparse : boolean, optional Indicate that func accepts a sparse matrix as input. - args : tuple, optional - A tuple of positional arguments to be passed to func. These will - be passed after X and y. - kwargs : dict, optional - A dictionary of keyword arguments to be passed to func. + + pass_y: bool, optional default=False + Indicate that transform should forward the y argument to the + inner callable. """ - def __init__(self, func=None, validate=True, accept_sparse=False, - args=None, kwargs=None): + def __init__(self, func=None, validate=True, + accept_sparse=False, pass_y=False): self.func = func self.validate = validate self.accept_sparse = accept_sparse - self.args = args - self.kwargs = kwargs + self.pass_y = pass_y def fit(self, X, y=None): if self.validate: @@ -39,6 +48,5 @@ def fit(self, X, y=None): def transform(self, X, y=None): if self.validate: X = check_array(X, self.accept_sparse) - return (self.func or (lambda X, y, *args, **kwargs: X))( - X, y, *(self.args or ()), **(self.kwargs or {}) - ) + + return (self.func or _identity)(X, *((y,) if self.pass_y else ())) diff --git a/sklearn/preprocessing/tests/test_callable_transformer.py b/sklearn/preprocessing/tests/test_function_transformer.py similarity index 60% rename from sklearn/preprocessing/tests/test_callable_transformer.py rename to sklearn/preprocessing/tests/test_function_transformer.py index 474451200285e..c0ae702259790 100644 --- a/sklearn/preprocessing/tests/test_callable_transformer.py +++ b/sklearn/preprocessing/tests/test_function_transformer.py @@ -1,7 +1,7 @@ from nose.tools import assert_equal import numpy as np -from ..callable_transformer import CallableTransformer +from ..function_transformer import FunctionTransformer def _make_func(args_store, kwargs_store, func=lambda X, *a, **k: X): @@ -16,20 +16,20 @@ def _func(X, *args, **kwargs): def test_delegate_to_func(): # (args|kwargs)_store will hold the positional and keyword arguments - # passed to the function inside the CallableTransformer. + # passed to the function inside the FunctionTransformer. args_store = [] kwargs_store = {} X = np.arange(10).reshape((5, 2)) np.testing.assert_array_equal( - CallableTransformer(_make_func(args_store, kwargs_store)).transform(X), + FunctionTransformer(_make_func(args_store, kwargs_store)).transform(X), X, 'transform should have returned X unchanged', ) - # The function should only have recieved X and y, where y is None. + # The function should only have recieved X. assert_equal( args_store, - [X, None], + [X], 'Incorrect positional arguments passed to func: {args}'.format( args=args_store, ), @@ -42,38 +42,42 @@ def test_delegate_to_func(): ), ) + # reset the argument stores. + args_store.clear() + kwargs_store.clear() + y = object() -def test_argument_closure(): - # (args|kwargs)_store will hold the positional and keyword arguments - # passed to the function inside the CallableTransformer. - args_store = [] - kwargs_store = {} - args = (object(), object()) - kwargs = {'a': object(), 'b': object()} - X = np.arange(10).reshape((5, 2)) np.testing.assert_array_equal( - CallableTransformer( + FunctionTransformer( _make_func(args_store, kwargs_store), - args=args, - kwargs=kwargs, - ).transform(X), + pass_y=True, + ).transform(X, y), X, 'transform should have returned X unchanged', ) - # The function should have been passed X, y (None), and the args - # that were passed to the CallableTransformer. + # The function should have recieved X and y. assert_equal( args_store, - [X, None] + list(args), + [X, y], 'Incorrect positional arguments passed to func: {args}'.format( args=args_store, ), ) assert_equal( kwargs_store, - kwargs, - 'Incorrect keyword arguments passed to func: {args}'.format( + {}, + 'Unexpected keyword arguments passed to func: {args}'.format( args=kwargs_store, ), ) + + +def test_np_log(): + X = np.arange(10).reshape((5, 2)) + + # Test that the numpy.log example still works. + np.testing.assert_array_equal( + FunctionTransformer(np.log).transform(X), + np.log(X), + ) diff --git a/sklearn/utils/estimator_checks.py b/sklearn/utils/estimator_checks.py index ad10deea15c83..3255d9c58790a 100644 --- a/sklearn/utils/estimator_checks.py +++ b/sklearn/utils/estimator_checks.py @@ -139,7 +139,7 @@ def _yield_transformer_checks(name, Transformer): yield check_transformer_data_not_an_array # these don't actually fit the data, so don't raise errors if name not in ['AdditiveChi2Sampler', 'Binarizer', - 'Normalizer', 'CallableTransformer']: + 'FunctionTransformer', 'Normalizer']: # basic tests yield check_transformer_general yield check_transformers_unfitted From ba44e7bcc3810ca9e63b15e3db084543205b5c22 Mon Sep 17 00:00:00 2001 From: Joe Jevnik Date: Mon, 8 Jun 2015 12:53:25 -0400 Subject: [PATCH 3/5] COMPAT: Makes test_function_transformer py2 compatible. --- sklearn/preprocessing/tests/test_function_transformer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sklearn/preprocessing/tests/test_function_transformer.py b/sklearn/preprocessing/tests/test_function_transformer.py index c0ae702259790..e02e7580ce5eb 100644 --- a/sklearn/preprocessing/tests/test_function_transformer.py +++ b/sklearn/preprocessing/tests/test_function_transformer.py @@ -43,7 +43,7 @@ def test_delegate_to_func(): ) # reset the argument stores. - args_store.clear() + args_store[:] = [] # python2 compatible inplace list clear. kwargs_store.clear() y = object() From bf8b91f9103f0ac3faa80a4d357c6f6e884cbdde Mon Sep 17 00:00:00 2001 From: Lars Buitinck Date: Fri, 3 Jul 2015 20:36:47 +0200 Subject: [PATCH 4/5] DOC expand FunctionTransform docstring --- sklearn/preprocessing/function_transformer.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/sklearn/preprocessing/function_transformer.py b/sklearn/preprocessing/function_transformer.py index d3c432afac7a3..eae76d9ba9ead 100644 --- a/sklearn/preprocessing/function_transformer.py +++ b/sklearn/preprocessing/function_transformer.py @@ -11,6 +11,13 @@ def _identity(X): class FunctionTransformer(BaseEstimator, TransformerMixin): """Constructs a transformer from an arbitrary callable. + A FunctionTransformer forwards its X (and optionally y) arguments to a + user-defined function or function object and returns the result of this + function. This is useful for stateless transformations such as taking the + log of frequencies, doing custom scaling, etc. + + A FunctionTransformer will not do any checks on its function's output. + Note: If a lambda is used as the function, then the resulting transformer will not be pickleable. @@ -24,9 +31,14 @@ class FunctionTransformer(BaseEstimator, TransformerMixin): validate : bool, optional default=True Indicate that the input X array should be checked before calling func. If validate is false, there will be no input validation. + If it is true, then X will be converted to a 2-dimensional NumPy + array or sparse matrix. If this conversion is not possible or X + contains NaN or infinity, an exception is raised. accept_sparse : boolean, optional - Indicate that func accepts a sparse matrix as input. + Indicate that func accepts a sparse matrix as input. If validate is + False, this has no effect. Otherwise, if accept_sparse is false, + sparse matrix inputs will cause an exception to be raised. pass_y: bool, optional default=False Indicate that transform should forward the y argument to the From a2a2d83be4999ab1596bff00f4ef075f182eae99 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Mon, 3 Aug 2015 12:59:41 -0400 Subject: [PATCH 5/5] FIX be robust to obscure callables that are false. --- sklearn/preprocessing/function_transformer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sklearn/preprocessing/function_transformer.py b/sklearn/preprocessing/function_transformer.py index eae76d9ba9ead..c814b14bf377e 100644 --- a/sklearn/preprocessing/function_transformer.py +++ b/sklearn/preprocessing/function_transformer.py @@ -60,5 +60,7 @@ def fit(self, X, y=None): def transform(self, X, y=None): if self.validate: X = check_array(X, self.accept_sparse) + func = self.func if self.func is not None else _identity - return (self.func or _identity)(X, *((y,) if self.pass_y else ())) + + return func(X, *((y,) if self.pass_y else ()))