New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[MRG+2] TransformedTargetRegressor #9041
Changes from 25 commits
21a2ff3
7b1f7e8
b306fa5
7d9badf
97da7a3
61a543a
de8dbb4
254fac2
53c7c81
693de84
3dafc8f
27f1c43
73bbcaf
e6a4e7d
503a985
63dbe9a
9feafda
32a85a6
af51cf8
dcae366
d8310ad
4c3ab11
49ea3c4
f1a7289
ffe6892
2a868ee
18c66c6
85a8865
7a10796
086fba0
44ea999
6c4734e
3ecde9f
5e7d6c9
db4bf57
0fe1622
8b94056
01d94e2
a0b84c4
437dfaa
36968ba
d253fcd
451dfd3
19a6f94
9e07197
0ddfee0
8392cc5
a0bf0b0
18bcec0
f3e151f
85cc14c
075bf92
51583c2
9853552
a1998fa
7c8c0ca
97330b0
7f13b9a
ae973f8
129373d
9064f24
3d80728
5f9db73
58c5506
d0f83fa
4e61395
687703b
500a77c
35cb75d
9a939f3
00e6d78
04dc4a7
f757c10
214fde6
68c5b7e
9976ace
3c99cde
0b364f6
64f5d52
5929f81
d637038
790c86a
bbee2be
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,3 @@ | ||
|
||
.. currentmodule:: sklearn.preprocessing | ||
|
||
.. _preprocessing_targets: | ||
|
@@ -7,6 +6,72 @@ | |
Transforming the prediction target (``y``) | ||
========================================== | ||
|
||
Transforming target in regression | ||
--------------------------------- | ||
|
||
:class:`TransformTargetRegressor` transforms the target before fitting a | ||
regression model and inverting back the prediction to the original space. It | ||
takes as an argument the regressor that will be used for prediction, and the | ||
transformer that will be applied to the target variable:: | ||
|
||
>>> import numpy as np | ||
>>> from sklearn.datasets import load_boston | ||
>>> from sklearn import preprocessing | ||
>>> from sklearn.linear_model import LinearRegression | ||
>>> from sklearn.model_selection import train_test_split | ||
>>> boston = load_boston() | ||
>>> X = boston.data | ||
>>> y = boston.target | ||
>>> transformer = preprocessing.StandardScaler() | ||
>>> regressor = LinearRegression() | ||
>>> regr = preprocessing.TransformTargetRegressor(regressor=regressor, | ||
... transformer=transformer) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. indentation |
||
>>> X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0) | ||
>>> regr.fit(X_train, y_train) # doctest: +ELLIPSIS | ||
TransformTargetRegressor(...) | ||
>>> print('R2 score:', regr.score(X_test, y_test)) # doctest : +ELLIPSIS | ||
R2 score: 0.63... | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe as a comparison you could add the score of the linear model on the original target variable. I get the following:
|
||
The transformer can also be replaced by a function and an inverse function. We | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rephrase: "For simple transformations, instead of a because it's not clear to me what it means "the transformer can be replaced". Plus, it can't always: not when it's stateful. |
||
can define the following two functions:: | ||
|
||
>>> from __future__ import division | ||
>>> def func(x): | ||
... return np.log(x) | ||
>>> def inverse_func(x): | ||
... return np.exp(x) | ||
|
||
Subsequently, the object is created as:: | ||
|
||
>>> regr = preprocessing.TransformTargetRegressor(regressor=regressor, | ||
... func=func, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. indentation |
||
... inverse_func=inverse_func) | ||
>>> regr.fit(X_train, y_train) # doctest: +ELLIPSIS | ||
TransformTargetRegressor(...) | ||
>>> print('R2 score:', regr.score(X_test, y_test)) # doctest: +ELLIPSIS | ||
R2 score: 0.64... | ||
|
||
By default, the provided function are checked at each fit to be the inverse of | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. *functions |
||
each other. However, it is possible to bypass this checking by setting | ||
``check_inverse`` to ``False``:: | ||
|
||
>>> def inverse_func(x): | ||
... return x | ||
>>> regr = preprocessing.TransformTargetRegressor(regressor=regressor, | ||
... func=func, | ||
... inverse_func=inverse_func, | ||
... check_inverse=False) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unrelated, but I wonder if we should have this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would not be against it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like the idea of moving |
||
>>> regr.fit(X_train, y_train) # doctest: +ELLIPSIS | ||
TransformTargetRegressor(...) | ||
>>> print('R2 score:', regr.score(X_test, y_test)) # doctest: +ELLIPSIS | ||
R2 score: -4.50... | ||
|
||
.. note:: | ||
|
||
The transformation can be triggered by setting either ``transformer`` or the | ||
functions ``func`` and ``inverse_func``. However, setting both options | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the pair of functions --- clearer to me |
||
will raise an error. | ||
|
||
Label binarization | ||
------------------ | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -31,6 +31,15 @@ Changelog | |
New features | ||
............ | ||
|
||
- Added the :class:`sklearn.preprocessing.TransformTargetRegressor` wraps | ||
a regressor and applies a transformation to the target before fitting, | ||
finally transforming the regressor's predictions back to the original | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. possibly we can reuse the exact phrasing from my comment 5 min ago |
||
space. :issue:`9041` by `Andreas Müller`_ and | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Guillaume is missing. EDIT: actually the entry is duplicated but in slightly different version |
||
- Added the :class:`sklearn.preprocessing.TransformedTargetRegressor` which | ||
is a meta-estimator to regress on a modified ``y`` for example, to perform | ||
regression in log-space. :issue:`9041` by `Andreas Müller`_ and | ||
:user:`Guillaume Lemaitre <glemaitre>`. | ||
|
||
- Validation that input data contains no NaN or inf can now be suppressed | ||
using :func:`config_context`, at your own risk. This will save on runtime, | ||
and may be particularly useful for prediction time. :issue:`7548` by | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,6 +28,8 @@ | |
from .label import LabelEncoder | ||
from .label import MultiLabelBinarizer | ||
|
||
from .target import TransformTargetRegressor | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. unhelpful whitespace |
||
from .imputation import Imputer | ||
|
||
|
||
|
@@ -45,6 +47,7 @@ | |
'OneHotEncoder', | ||
'RobustScaler', | ||
'StandardScaler', | ||
'TransformTargetRegressor', | ||
'add_dummy_feature', | ||
'PolynomialFeatures', | ||
'binarize', | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,203 @@ | ||
# Authors: Andreas Mueller < andreas.mueller@columbia.edu> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. extra space before andreas There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do like me some extra space There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. here's some for you
|
||
# Guillaume Lemaitre <guillaume.lemaitre@inria.fr> | ||
# License: BSD 3 clause | ||
|
||
import numpy as np | ||
|
||
from ..base import BaseEstimator, RegressorMixin, clone | ||
from ..linear_model import LinearRegression | ||
from ..utils.fixes import signature | ||
from ..utils.validation import check_is_fitted, check_array | ||
from ._function_transformer import FunctionTransformer | ||
|
||
__all__ = ['TransformTargetRegressor'] | ||
|
||
|
||
class TransformTargetRegressor(BaseEstimator, RegressorMixin): | ||
"""Meta-estimator to regress on a transformed target. | ||
|
||
Useful for applying a non-linear transformation in regression | ||
problems. This transformation can be given as a Transformer such as the | ||
QuantileTransformer or as a function and its inverse such as ``np.log`` and | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. perhaps just call these log and exp, leaving out the np |
||
``np.exp``. | ||
|
||
The computation during ``fit`` is:: | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we remove this blank line? |
||
regressor.fit(X, func(y)) | ||
|
||
or:: | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. and this etc? |
||
regressor.fit(X, transformer.transform(y)) | ||
|
||
The computation during ``predict`` is:: | ||
|
||
inverse_func(regressor.predict(X)) | ||
|
||
or:: | ||
|
||
transformer.inverse_transform(regressor.predict(X)) | ||
|
||
Parameters | ||
---------- | ||
regressor : object, (default=LinearRegression()) | ||
Regressor object such as derived from ``RegressorMixin``. This | ||
regressor will be cloned during fitting. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "This regressor will automatically be cloned each time prior to fitting." |
||
|
||
transformer : object, (default=None) | ||
Estimator object such as derived from ``TransformerMixin``. Cannot be | ||
set at the same time as ``func`` and ``inverse_func``. If ``None`` and | ||
``func`` and ``inverse_func`` are ``None`` as well, the transformer | ||
will be an identity transformer. The transformer will be cloned during | ||
fitting. | ||
|
||
func : function, optional | ||
Function to apply to ``y`` before passing to ``fit``. Cannot be set at | ||
the same time than ``transformer``. If ``None`` and ``transformer`` is | ||
``None`` as well, the function used will be the identity function. | ||
|
||
inverse_func : function, optional | ||
Function to apply to the prediction of the regressor. Cannot be set at | ||
the same time than ``transformer``. If ``None`` and ``transformer`` as | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. at the same time as (and the same for func) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. and |
||
well, the function used will be the identity function. The inverse | ||
function is used to return to the same space of the original training | ||
labels during prediction. | ||
|
||
check_inverse : bool, (default=True) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
Whether to check that ``transform`` followed by ``inverse_transform`` | ||
or ``func`` followed by ``inverse_func`` leads to the original data. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nitpick: data -> targets. |
||
|
||
Attributes | ||
---------- | ||
regressor_ : object | ||
Fitted regressor. | ||
|
||
transformer_ : object | ||
Used transformer in ``fit`` and ``predict``. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "Transformer used" |
||
|
||
y_ndim_ : int | ||
Number of targets. | ||
|
||
Examples | ||
-------- | ||
>>> import numpy as np | ||
>>> from sklearn.linear_model import LinearRegression | ||
>>> from sklearn.preprocessing import TransformTargetRegressor | ||
>>> tt = TransformTargetRegressor(regressor=LinearRegression(), | ||
... func=np.log, inverse_func=np.exp) | ||
>>> X = np.arange(4).reshape(-1, 1) | ||
>>> y = np.exp(2 * X).ravel() | ||
>>> tt.fit(X, y) | ||
... #doctest: +NORMALIZE_WHITESPACE | ||
TransformTargetRegressor(check_inverse=True, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it useful to show this? |
||
func=<ufunc 'log'>, | ||
inverse_func=<ufunc 'exp'>, | ||
regressor=LinearRegression(copy_X=True, | ||
fit_intercept=True, | ||
n_jobs=1, | ||
normalize=False), | ||
transformer=None) | ||
>>> tt.score(X, y) | ||
1.0 | ||
>>> tt.regressor_.coef_ | ||
array([ 2.]) | ||
|
||
""" | ||
def __init__(self, regressor=None, transformer=None, | ||
func=None, inverse_func=None, check_inverse=True): | ||
self.regressor = regressor | ||
self.transformer = transformer | ||
self.func = func | ||
self.inverse_func = inverse_func | ||
self.check_inverse = check_inverse | ||
|
||
def _fit_transformer(self, y, sample_weight): | ||
if (self.transformer is not None and | ||
(self.func is not None or self.inverse_func is not None)): | ||
raise ValueError("Both 'transformer' and functions 'func'/" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. drop "Both" or move it to become "cannot both be set" (without "at the same time"). |
||
"'inverse_func' cannot be set at the same time.") | ||
elif self.transformer is not None: | ||
self.transformer_ = clone(self.transformer) | ||
else: | ||
self.transformer_ = FunctionTransformer( | ||
func=self.func, inverse_func=self.inverse_func, validate=False) | ||
fit_parameters = signature(self.transformer_.fit).parameters | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should really have a helper for that but whatever ... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. good point! does this break if the transformer's fit comes from a @if_delegate_has_method ?? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should not, I think.... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (see discussion here ) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can a fit ever come from an "@if_delegate_has_method"? I would understand for a predict or a transform, but a fit? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. good point, makes no sense to have it There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you are looking for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is the wrong strategy. We should pass sample_weight iff it is not None, or we should never pass in weights until we have a prop routing API. Passing on the basis of the signature is brittle (it is silent when the weights are not passed even if the used passed in weights explicitly), and would make the code a mess if more properties were supported. Unless I've missed some motivation for this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In fact, I could not find a transformer with support_sample_weight = has_fit_parameter(self.regressor_, 'sample_weight')
if support_sample_weight:
if sample_weight is None:
current_sample_weight = np.ones((y.shape[0],))
else:
current_sample_weight = sample_weight In case that transformers should handle |
||
if "sample_weight" in fit_parameters: | ||
self.transformer_.fit(y, sample_weight=sample_weight) | ||
else: | ||
self.transformer_.fit(y) | ||
if self.check_inverse: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. does it make sense to move this check inside There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So this will remain here regardless of its being present in FunctionTransformer? And will remain an error rather than a warning? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I change in the FunctionTransformer but I forgot to change it here. |
||
n_subsample = min(10, y.shape[0]) | ||
subsample_idx = np.random.choice(range(y.shape[0]), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unprotected np.random: we need to use a random_state |
||
size=n_subsample, replace=False) | ||
if not np.allclose( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what's the default tolerance? maybe we want to lower that a little bit? |
||
y[subsample_idx], | ||
self.transformer_.inverse_transform( | ||
self.transformer_.transform(y[subsample_idx])), | ||
atol=1e-4): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is surprisingly high tolerance to me There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 1e-7 seems better to you? |
||
raise ValueError("The provided functions or transformer are" | ||
" not strictly inverse of each other. If" | ||
" you are sure you want to proceed regardless" | ||
", set 'check_inverse=False'") | ||
|
||
def fit(self, X, y, sample_weight=None): | ||
"""Fit the model according to the given training data. | ||
|
||
Parameters | ||
---------- | ||
X : {array-like, sparse matrix}, shape (n_samples, n_features) | ||
Training vector, where n_samples is the number of samples and | ||
n_features is the number of features. | ||
|
||
y : array-like, shape (n_samples,) | ||
Target values. | ||
|
||
sample_weight : array-like, shape (n_samples,) optional | ||
Array of weights that are assigned to individual samples. | ||
If not provided, then each sample is given unit weight. | ||
|
||
Returns | ||
------- | ||
self : object | ||
Returns self. | ||
""" | ||
y = check_array(y, ensure_2d=False) | ||
self.y_ndim_ = y.ndim | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Need to Should probably document this attribute. |
||
if y.ndim == 1 and self.func is None: | ||
y_2d = y.reshape(-1, 1) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suspect we don't want to do this when There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I come back on this point. I am not sure this is a great idea since that it changes the behaviour between if you pass a function or a transformer. We could still make the transform to 2d and build the FunctionTransformer with I don't see a case in which the user would define a function which working on a 1D array but would failed on a 2D array |
||
else: | ||
y_2d = y | ||
self._fit_transformer(y_2d, sample_weight) | ||
if self.regressor is None: | ||
self.regressor_ = LinearRegression() | ||
else: | ||
self.regressor_ = clone(self.regressor) | ||
if sample_weight is not None: | ||
self.regressor_.fit(X, self.transformer_.fit_transform(y_2d), | ||
sample_weight=sample_weight) | ||
else: | ||
self.regressor_.fit(X, self.transformer_.fit_transform(y_2d)) | ||
return self | ||
|
||
def predict(self, X): | ||
"""Predict using the base regressor, applying inverse. | ||
|
||
The regressor is used to predict and the ``inverse_func`` or | ||
``inverse_transform`` is applied before returning the prediction. | ||
|
||
Parameters | ||
---------- | ||
X : {array-like, sparse matrix}, shape = (n_samples, n_features) | ||
Samples. | ||
|
||
Returns | ||
------- | ||
y_hat : array, shape = (n_samples,) | ||
Predicted values. | ||
|
||
""" | ||
check_is_fitted(self, "regressor_") | ||
pred = self.transformer_.inverse_transform(self.regressor_.predict(X)) | ||
if self.y_ndim_ == 1 and self.func is None: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why the second condition? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To avoid the useless reshaping for |
||
return pred.ravel() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I prefer ".squeeze" rather than ".ravel": ravel is too flexible and can hide errors |
||
else: | ||
return pred |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion:
transforms the target
y
before fitting a regression model. The predictions are mapped back to the original space via an inverse transform. It takes...