Skip to content

Implemented adaptive squashing#1310

Merged
GaelVaroquaux merged 28 commits into
skrub-data:mainfrom
dholzmueller:squashing
Jul 23, 2025
Merged

Implemented adaptive squashing#1310
GaelVaroquaux merged 28 commits into
skrub-data:mainfrom
dholzmueller:squashing

Conversation

@dholzmueller

Copy link
Copy Markdown
Contributor

WIP

@dholzmueller

Copy link
Copy Markdown
Contributor Author

Here is an implementation of the robust scaling + smooth clipping from my paper (https://arxiv.org/abs/2407.04491). It could be used as a robust numerical preprocessing methods for neural network based methods.

Some considerations:

  • which name do we want to use for the class? Currently it is AdaptiveSquashingTransformer.
  • I did not check the rendering of the documentation. Do we need to link the class from somewhere in the documentation?
  • If the column passed in transform() has a different name than in fit_transform(), the output column still gets the name of the column passed in fit_transform(). I copied this behavior from StringEncoder, is that good? (See also the todo in the corresponding tests.)

@dholzmueller
dholzmueller marked this pull request as ready for review April 11, 2025 16:21

@Vincent-Maladiere Vincent-Maladiere left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Hey @dholzmueller thank you for this PR!

If you want to make your class public, you should add an entry in doc/reference/index.rst, maybe in the "cleaning a dataframe" section?

Also, I think having a smallish example that shows when and where to use your class would be great. WDYT?

Comment thread skrub/_adaptive_squashing.py Outdated
Comment thread skrub/_adaptive_squashing.py Outdated
Comment thread skrub/_adaptive_squashing.py Outdated
Comment thread skrub/_adaptive_squashing.py Outdated
Comment thread skrub/_adaptive_squashing.py Outdated
Comment thread skrub/_adaptive_squashing.py Outdated
Co-authored-by: Vincent M <maladiere.vincent@yahoo.fr>
@dholzmueller

Copy link
Copy Markdown
Contributor Author

Also, I think having a smallish example that shows when and where to use your class would be great. WDYT?

Sounds good, where would you put it?

@dholzmueller

Copy link
Copy Markdown
Contributor Author

I addressed everything except from the example now.

@dholzmueller

dholzmueller commented Apr 14, 2025

Copy link
Copy Markdown
Contributor Author

I removed the test that tested if a column with name None after transformation still had name None, since it failed for polars. I assume this is not relevant? (The test is not necessary anymore to reach 100% coverage.)

@jeromedockes jeromedockes left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

a first few comments, I'll add more. thanks this is great!! 🚀

Comment thread CHANGES.rst Outdated
Comment thread doc/reference/index.rst Outdated
Comment thread skrub/_adaptive_squashing.py Outdated
Comment thread skrub/_adaptive_squashing.py Outdated
Comment thread skrub/_adaptive_squashing.py Outdated
Comment thread skrub/_adaptive_squashing.py Outdated
Comment thread skrub/_adaptive_squashing.py Outdated
Comment thread skrub/_adaptive_squashing.py Outdated
@Vincent-Maladiere

Copy link
Copy Markdown
Member

Sounds good, where would you put it?

@dholzmueller, good question, I'm not sure. What would be the best way to illustrate its usefulness? Do you have a small benchmark or experiment that could fit into an example? Otherwise we might need to be creative

@dholzmueller

dholzmueller commented Apr 15, 2025

Copy link
Copy Markdown
Contributor Author

We could try it on a dataset with an MLP and see if it improves something (compared to StandardScaler? or QuantileTransformer?). Don't know what would be the best one to do this.

@Vincent-Maladiere

Copy link
Copy Markdown
Member

In what setup (dataset + model) have you observed a sharp improvement in your experiments? Otherwise, we could try on any skrub dataset with an MLP as you suggest

@dholzmueller

Copy link
Copy Markdown
Contributor Author

Updates:

  • renamed the class to SquashingScaler
  • made it work for dataframes and not only series by using OnEachColumn with the old class
  • renamed the args to lower_quantile and upper_quantile
  • incorporated minor suggestions
  • added an example using the SquashingScaler

@jeromedockes jeromedockes left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Great, thanks a lot @dholzmueller . I just have a small comment on the fitted attributes but LGTM otherwise :)

Comment thread skrub/_squashing_scaler.py Outdated
return result


class SquashingScaler(BaseEstimator, TransformerMixin):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

even though it wouldn't make a difference here normally we would put the transformermixin first because we want methods to be looked up there before looking in baseestimator

Suggested change
class SquashingScaler(BaseEstimator, TransformerMixin):
class SquashingScaler(TransformerMixin, BaseEstimator):

Comment thread skrub/_squashing_scaler.py Outdated
self: SquashingScaler
The current object.
"""
self.tfm_ = SingleColumnSquashingScaler(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

maybe we could make the transformer private by renaming it to self._transformer and exposing its learned attributes directly by copying them to the outer object's dict with something like

if isinstance(self._transformer, OnEachColumn):
    self.median_ = [t.median_ for t in self._transformer.transformers_.values()]
    self.scale_ = [t.median_ for t in self._transformer.transformers_.values()]
else:
    self.median_ = self._transformer.median_
    self.scale_ = self._transformer.scale_

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(in this case we could add a section like

    Attributes
    ----------
    median_ : float or list of float
        median of the training data

    scale_ : ...

just below the 'parameters' section in the docstring )

@dholzmueller

Copy link
Copy Markdown
Contributor Author

@GaelVaroquaux was suggesting to implement SquashingScaler directly instead of with the indirection through SingleColumnSquashingScaler. I'll need to check how easy that is...

Comment thread skrub/_squashing_scaler.py
Comment thread skrub/tests/test_squashing_scaler.py

@rcap107 rcap107 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Overall, I think that the code in this PR is not documented well enough, as there are a lot of parts that "do stuff" in a non-transparent way, and that would benefit from being commented more.

Besides being a problem to maintain (I had to try and reverse engineer most of the code and the tests to figure out how it worked), I think that the scaling could have some unexpected results that may surprise users, and that are not documented or explained properly in the docstrings or in the code itself.

@Vincent-Maladiere

Vincent-Maladiere commented Jul 15, 2025

Copy link
Copy Markdown
Member

Hey @rcap107 thanks for reviewing. I iterated on the documentation; let me know what you think

Comment thread skrub/_squashing_scaler.py Outdated

@rcap107 rcap107 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for adding all the comments @Vincent-Maladiere, now the code is much clearer 👍

@rcap107

rcap107 commented Jul 15, 2025

Copy link
Copy Markdown
Member

Hey @rcap107 thanks for reviewing. I iterated on the documentation; let me know what you think

It's much better, thanks a lot! I fixed a small typo, but aside from that I think we can merge this

@Vincent-Maladiere

Copy link
Copy Markdown
Member

Great! Waiting for other people to give their feedback

Co-authored-by: Riccardo Cappuzzo <7548232+rcap107@users.noreply.github.com>
@dholzmueller

Copy link
Copy Markdown
Contributor Author

Interface looks good to me! Did you test what happens if you pass in an all-nan column?

@GaelVaroquaux GaelVaroquaux left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM. Very good!!

@GaelVaroquaux

Copy link
Copy Markdown
Member

I'm waiting for @Vincent-Maladiere 's reply to @dholzmueller 's above, and then merge

@Vincent-Maladiere

Copy link
Copy Markdown
Member

Interface looks good to me! Did you test what happens if you pass in an all-nan column?

Good question. We get a column full of nans, since they are left untouched. Is that what you would expect?

Reproducer:

import numpy as np
import skrub
import pandas as pd

skrub.SquashingScaler().fit_transform(
    pd.DataFrame(dict(a=[np.nan, np.nan]))
)

@dholzmueller

Copy link
Copy Markdown
Contributor Author

Yes, that is what I would expect. Just wasn't sure if it's going to work with the nanmax/nanmin operations.

@dholzmueller

Copy link
Copy Markdown
Contributor Author

Just a thought, having quantile_range as a tuple instead of two separate values might make it harder for people to tune it in some hyperparameter optimization. I think it's probably not an important hyperparameter to tune, though.

@rcap107

rcap107 commented Jul 23, 2025

Copy link
Copy Markdown
Member

Just a thought, having quantile_range as a tuple instead of two separate values might make it harder for people to tune it in some hyperparameter optimization. I think it's probably not an important hyperparameter to tune, though.

This can be addressed in another PR

@rcap107 rcap107 added this to the 0.6.0 milestone Jul 23, 2025
@dholzmueller

dholzmueller commented Jul 23, 2025

Copy link
Copy Markdown
Contributor Author

This can be addressed in another PR

you mean, another one for the same release? because it's not as convenient to change the parametes after release, right?

@rcap107

rcap107 commented Jul 23, 2025

Copy link
Copy Markdown
Member

This can be addressed in another PR

you mean, another one for the same release? because it's not as convenient to change the parametes after release, right?

It's not a very big deal, and it should not be a big change, up to you @Vincent-Maladiere

(I'd rather have this PR in the release and open a new one later than risk not having it)

@Vincent-Maladiere

Copy link
Copy Markdown
Member

That's a good point, both sklearn's RobustScaler and MinMaxScaler have a tuple range as a hyperparameter, so I think this is fine.

@dholzmueller

Copy link
Copy Markdown
Contributor Author

okay, then let's leave it like it is.

@GaelVaroquaux

GaelVaroquaux commented Jul 23, 2025 via email

Copy link
Copy Markdown
Member

@dholzmueller

Copy link
Copy Markdown
Contributor Author

I think the NaN thing is not an issue, since Vincent tested it and it worked.

@GaelVaroquaux
GaelVaroquaux merged commit 1484cba into skrub-data:main Jul 23, 2025
25 of 26 checks passed
@GaelVaroquaux

Copy link
Copy Markdown
Member

Merged!

Thanks everyone

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants