Skip to content

MNT/DOC Raise for **fit_params in fit_transform if not expected by fit #31830

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

Open
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

StefanieSenger
Copy link
Contributor

For transformers that inherit from TransformerMixin, the docs always display fit_params as passable into fit_transform even if their fit method doesn't accept anything other than X (and y=None).

That is for instance the case in Normalizer, LinearDiscriminantAnalysis and FeatureHasher:

fit_transform(X, y=None, **fit_params)

A python TypeError is raised in this case:

from sklearn.preprocessing import Normalizer

X = [[4, 1, 2, 2],
[1, 3, 9, 3],
[5, 7, 5, 1]]

Normalizer().fit_transform(X, sample_weight=np.array([[1,1,1]]))

TypeError: Normalizer.fit() got an unexpected keyword argument 'sample_weight'

This PR documents this on the method and adds a more intuitive error message that scikit-learn can raise directly.

This doesn't prevent fit_params to be displayed in wrong places in the docs, but deals with the problems that users can encounter in such a case.

Another solution to this problem that would remove fit_params from the docs in wrong places would be to over-write more of the fit_transform methods in the classes of routers (as it is already the case in many places), as for instance in Stacking* and RFE that still use fit_transform from TransformerMixin, and thus totally de-coubple the metadata routing-logic from TransformerMixin's fit_transform. Or, alternatively, add an additional RoutingTransformerMixin for the routing logic.
But both breakages of inheritance structure seem a bit too much for handling this little thing.

CC: @adrinjalali

Copy link

github-actions bot commented Jul 24, 2025

✔️ Linting Passed

All linting checks passed. Your pull request is in excellent shape! ☀️

Generated for commit: bc64ce0. Link to the linter CI: here

sklearn/base.py Outdated
Comment on lines 893 to 903
for key, _ in fit_params.items():
if (
key not in inspect.signature(self.fit).parameters
and "fit_params" not in inspect.signature(self.fit).parameters
):
message = (
f"`{self.__class__.__name__}.fit` got an unexpected keyword "
f"argument `{key}`. Only pass {key} into `fit_transform` if `fit` "
"expects them or can handle `**fit_params`."
)
raise TypeError(message)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think instead of doing this manually, you can rely on python's machinery, and put a try/except around call to fit and catch TypeError, and then do a raise ... from e with a TypeError and a nicer error message.

Copy link
Member

@jeremiedbb jeremiedbb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm unsure about this PR. It improves the error message but doesn't fix the root of the problem which is a doc issue. And I'm not even convinced that there's a doc issue.

The doc says **fit_params are parameters passed to fit. We have several choices on the way we pass them

    1. pass them directly and fit raises if they're not in the signature.
    1. check the signature of fit before passing them. Then pass only the ones that are in the signature and warn that the other are ignored.
    1. check the signature of fit before passing them. Then pass only the ones that are in the signature silently.

So far in scikit-learn we do the first one. And it's not at all present only with fit_transform, it already happens with fit on meta_estimators. Take for example

from sklearn.datasets import make_regression
from sklearn.compose import TransformedTargetRegressor
from sklearn.linear_model import Ridge
from sklearn.preprocessing import StandardScaler

X, y = make_regression()
TransformedTargetRegressor(regressor=Ridge()).fit(X, y, not_a_fit_param=1)
TypeError: Ridge.fit() got an unexpected keyword argument 'not_a_fit_param'

I don't find that surprising and if I have a doubt I got check the signature of Ridge.fit

This PR adds a 1b) option that is raise but check the signature to improve the error message. It don't think it's necessary. However if we chose to do this (which should first be done in a RFC issue imo), I think it should be done consistently across the code base.

sklearn/base.py Outdated
Comment on lines 893 to 894
if y is None:
# fit method of arity 1 (unsupervised transformation)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is it only done in the y is None branch ? should be in both.

from sklearn.preprocessing import Normalizer

X = [[4, 1, 2, 2],
[1, 3, 9, 3],
[5, 7, 5, 1]]

Normalizer().fit_transform(X, y=[1,2,3], ]sample_weight=np.array([[1,1,1]]))
TypeError: Normalizer.fit() got an unexpected keyword argument 'sample_weight'

Copy link
Contributor Author

@StefanieSenger StefanieSenger Jul 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh true. I push a fix for now. This is irrespective of whether we want ti keep this PR or not.

@StefanieSenger
Copy link
Contributor Author

StefanieSenger commented Jul 25, 2025

Thank you, @jeremiedbb.

I feel that I didn't state well the intention and scope of this PR, that is supposed to be much smaller.

The issue that I had tried to solve was actually only a documentation issue, that was newly introduced with metadata routing and only in transformers that do inherit from TransformerMixin AND do not use metadata in their fit method. Here, unfortunately we now display **fit_params as passable into the fit_transform method, which is incorrect.

For instance the docs for Normalizer display

fit(X, y=None)
...
fit_transform(X, y=None, **fit_params)
...

The PR tries to even out the fact that we display wrong documentation, by giving the users a service by re-raising with a clearer error message. I don't think that's necessary in general, because the Python ValueError is fine otherwise.

I agree it's not an ideal solution.
And you are right, that it happens in other places as well, that I hadn't seen or noted before.

Another way of dealing with this would be to remove the metadata routing logic from TransformerMixin.fit_transform() and only use it in routers, by overriding the method or completely de-coupeling the inheritance.
(This is to my current knowledge less invasive than to leave the metadata routing logic there and instead override the method in transformers that cannot route metadata, because it seems to apply to less cases, but I would need to check.)

Or, the added line to the docs could be enough.

@jeremiedbb
Copy link
Member

The PR tries to even out the fact that we display wrong documentation,

The doc is not technically wrong. fit_transform does have **fit_params in its signature and does pass them to fit. It's just that fit doesn't accept any additional param.

scope of this PR, that is supposed to be much smaller.

It's really not that different from the example I showed. Yes here there are no meta estimators in between, but the idea is the same. We pass additional params to fit and if doesn't have these additional params, it raises.

Another way of dealing with this would be to remove the metadata routing logic from TransformerMixin.fit_transform() and only use it in routers, by overriding the method or completely de-coupeling the inheritance.

If you really don't want to see **fit_params in the signature of Normalizer.fit_transform, I prefer the approach of overriding the fit_transform method, or even maybe have a TransformerMixin and a MetaTransformerMixin.

@StefanieSenger
Copy link
Contributor Author

It's really not that different from the example I showed. Yes here there are no meta estimators in between, but the idea is the same. We pass additional params to fit and if doesn't have these additional params, it raises.

That's true. Let's only keep the documentation change, if that's alright.
I have removed the re-raising logic and the added test.

If you really don't want to see **fit_params in the signature of Normalizer.fit_transform, I prefer the approach of overriding the fit_transform method, or even maybe have a TransformerMixin and a MetaTransformerMixin.

I think it might not be worth it for now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants