Skip to content

fix Sklearnwrapper output for OneHotEncoder and PolynomialFeatures to avoid duplicated features - #491

Merged
solegalli merged 4 commits into
feature-engine:mainfrom
datacubeR:creators_wrapper_fix
Aug 30, 2022
Merged

fix Sklearnwrapper output for OneHotEncoder and PolynomialFeatures to avoid duplicated features#491
solegalli merged 4 commits into
feature-engine:mainfrom
datacubeR:creators_wrapper_fix

Conversation

@datacubeR

Copy link
Copy Markdown
Contributor

Hi @solegalli,
This is my shot for fixing #489.
After checking the code in detail I think the issue affects not only PolynomialFeatures but also Sklearn OneHotEncoder.
When using SklearnTransformerWrapper + OneHotEncoder I get this:

image

Which is totally unexpected, since the idea is to replace Categorical Features for OneHotEncoded ones.

After the fix I'm proposing I get this for OneHotEncoder
image

Which I think is the expected behavior.

For PolynomialFeatures I get the following:
image

Please note that an OrdinalEncoder was applied to Categorical Variables before applying PolynomialFeatures, otherwise it throws an error since they are not numerical features.

I think these cases could be added as test cases if you think it's OK. But before I would love to get your feedback.

Best,

Alfonso

@solegalli

Copy link
Copy Markdown
Collaborator

Hi @datacubeR

Thanks a lot for the changes.

I do agree that the idea is to replace the original categorical variables by the one hot encoded ones. So I guess, it is OK to have them removed from the dataset as you did.

Originally I thought, if that is the intended functionality, then why not use the OHE from feature engine instead of the one from sklearn? that is why I decided not to drop them. But I think it makes sense to go ahead with your suggestion.

I think it would be great to have those code snippets as tests, and also maybe in the user guide: https://feature-engine.readthedocs.io/en/latest/user_guide/wrappers/Wrapper.html#sklearn-wrapper (I mean examples of wrapping the OHE and the PolynomialFeatures)

would you be able to do that?

thanks a lot!

@solegalli

solegalli commented Aug 6, 2022

Copy link
Copy Markdown
Collaborator

PS: I fixed the failing tests in the main branch last week. It was something related to the boxcox with the latest version of scipy. If you sync your main branch and then rebase it to this feature branch, it should get those sorted @datacubeR

thank you!

@datacubeR

Copy link
Copy Markdown
Contributor Author

@solegalli I think my fork is up to date, but the failing tests is because they considering adding Features rather than replacing the existing feature for the transformed ones. I will fix those and I'll get back to you.

@datacubeR
datacubeR force-pushed the creators_wrapper_fix branch from 0d94afc to e96d54c Compare August 7, 2022 00:41
@datacubeR

Copy link
Copy Markdown
Contributor Author

@solegalli do I need to create a separate PR for the examples in the User Guide or I just include those in this one?

@solegalli

Copy link
Copy Markdown
Collaborator

parate PR for the exampl

Here would be good :)

Thank you!

Comment thread tests/test_wrappers/test_sklearn_wrapper.py Outdated
@solegalli

Copy link
Copy Markdown
Collaborator

It's looking good.

I think we need to add a test to corroborate that the method get_feature_names_out returns the original features, without the ones used in the transformation, plus the new ones, when it is called without passing the input_feature parameters.

so tr_wrap.get_feature_names_out() should return = feature_names_in - self.variables_ + new features

Could you add a test for that please? Or is it already there? I mean just for the Poly and OHE

@datacubeR
datacubeR force-pushed the creators_wrapper_fix branch from 2a3b89d to 5c3cca8 Compare August 13, 2022 03:44

@datacubeR datacubeR left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Would you please review the wording of the new examples added?

Comment on lines +193 to +203
def test_wrap_polynomial_features_get_features_name_out():
X = fetch_california_housing(as_frame=True).frame

tr = PolynomialFeatures()
tr_wrap = SklearnTransformerWrapper(transformer=PolynomialFeatures())
varlist = ["MedInc", "HouseAge", "AveRooms", "AveBedrms"]

tr.fit(X[varlist])
tr_wrap.fit(X[varlist])

assert (tr.get_feature_names_out() == tr_wrap.get_feature_names_out()).all()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is the test for Polynomial Features

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

the test that is missing is something like this:

def test_wrap_polynomial_features_get_features_name_out():
    X = fetch_california_housing(as_frame=True).frame

    varlist = ["MedInc", "HouseAge", "AveRooms", "AveBedrms"]
    tr_wrap = SklearnTransformerWrapper(transformer=PolynomialFeatures(), variables=varlist)
   
    tr_wrap.fit(X)

    assert tr_wrap.get_feature_names_out() == The expected list (all features in the original + new features)
    assert tr_wrap.get_feature_names_out(varlist) == All the new polynomial features
    assert tr_wrap.get_feature_names_out(["Medinc") == the features expected from Medinc

The thing is, given that the logic in get_feature_names_out was changed slightly, I would like to make sure that it still returns what it is expected to return given the input to input_features

Would you be able to change it?

@datacubeR datacubeR Aug 17, 2022

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@solegalli, I was checking this one, and just to make sure I didn't touch get_feature_names_out. The thing is I'm not sure what is the expected behavior for this one: assert tr_wrap.get_feature_names_out(["Medinc") == the features expected from Medinc.
This one should work even if it was trained on a different pool of features? And the expected output is: Medinc, Medinc^2 and all the interactions that includes Medinc?

Currently, tr_wrap.get_feature_names_out(["Medinc"]) outputs: ValueError: input_features is not equal to feature_names_in_

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Assuming that the transformer was trained as in my example:

    varlist = ["MedInc", "HouseAge", "AveRooms", "AveBedrms"]
    tr_wrap = SklearnTransformerWrapper(transformer=PolynomialFeatures(), variables=varlist)

Then the output of tr_wrap.get_feature_names_out(["Medinc") would be as you say the polynomial combinations that involve MedInc. And this, should come out of the box from the Polynomial Features.

If the transformer was trained like this instead:

    varlist = ["HouseAge", "AveRooms", "AveBedrms"]
    tr_wrap = SklearnTransformerWrapper(transformer=PolynomialFeatures(), variables=varlist)

where MedInc was not part of variables, then the outcome of tr_wrap.get_feature_names_out(["Medinc") should be an error, and I think this should be handled by our class method get_feature_names_out.

@datacubeR datacubeR Aug 18, 2022

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@solegalli, This issue was a bit more complicated than I thought. Actually I noticed the following things:

  • First, the current implementation throws an error because as per Sklearn Documentation input_features needs to be equals to name_features_in when using get_feature_names_out in PolynomialFeatures and OneHotEncoder. See this:

image

  • So adding something like tr_wrap.get_feature_names_out(["Medinc"]) is not valid when trained with more features.

  • This means current implementation does not work as expected and cannot be obtained out of the box as mentioned previously.

I implemented a solution when input_features is not None, but I find it not very elegant:

# Get the names of all the new features
added_features = self.transformer_.get_feature_names_out(
                    self.variables_
                )
# Get all the features related to input_features... Sorry for the double for loop
feature_names = []
for feature in added_features:
    for _in in input_features:
        if _in in feature and feature not in feature_names:
            feature_names.append(feature)

# In case of PolynomialFeatures and include_bias is True I need to also retrieve the Bias called '1'
if (
    self.transformer_.__class__.__name__ == "PolynomialFeatures"
    and self.transformer_.include_bias
):
    feature_names = ["1"] + feature_names

The reason why I'm using this double for loop (specially for PolynomialFeatures) is because I need to check all the created features (squared ones, interactions, etc.) related to an input features are included and not repeated. Adittionally in case of include bias, I'm adding '1' as part of the output feature names, I did this because it was the expected behavior for test_get_feature_names_out_polynomialfeature. After a lot of trial an error (breaking a lot of tests) I came up to this solution, but I'm totally open to make it better. Didn't find a better way to implement this with lists. I tried sets, but they don't preserve the order of features, so order of features is a bit messy and difficult to test.

The thing is, if an input_variable is not part of the training variables not error is thrown, so what error should I raise if this happens?

PS: I'm currently passing all the tests locally and I'm getting the results as expected in our discussion, if you accept a solution like this I can push my changes.

Thanks!!

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hi @datacubeR

Thank you so much for so much detail.

I wonder what the point was in offering input_features as parameter in the PolynomialFeatures , if then you can only pass feature_names_in :/

In this case, I think we do not need to modify the code further or add the additional test.

Sorry, that was my bad. For some reason I thought that you could pass one variable and obtain the derived features.

Comment on lines +421 to +427
def test_wrap_one_hot_encoder_get_features_name_out(df_vartypes):
ohe = OneHotEncoder()
ohe_wrap = SklearnTransformerWrapper(transformer=OneHotEncoder(sparse=False))
ohe.fit(df_vartypes)
ohe_wrap.fit(df_vartypes)

assert (ohe.get_feature_names_out() == ohe_wrap.get_feature_names_out()).all()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is the test for OneHotEncoder

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same as per previous comment, the idea is to just test the functionality of the method, and not in comparison to that of sklearn's.

Comment thread tests/test_wrappers/test_sklearn_wrapper.py
Comment thread docs/user_guide/wrappers/Wrapper.rst Outdated
X_train_t = selector.transform(X_train.fillna(0))
X_test_t = selector.transform(X_test.fillna(0))

Even though Feature Engine has its own implementation of OneHotEncoder, you may want to use Scikit-Learn Implementation in order to access different options, such as drop first Category.

@solegalli solegalli Aug 13, 2022

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Could you replace this sentence by the below:

Even though Feature-engine has its own implementation of OneHotEncoder, you may want to use Scikit-Learn'stransformer in order to access different options, such as drop first Category. In the following example, we show you how to apply Scikit-learn's OneHotEncoder to a subset of categories using the :class:SklearnTransformerWrapper().

Comment thread docs/user_guide/wrappers/Wrapper.rst Outdated
y = df.survived
X_train, X_test, y_train, y_test= train_test_split(X, y, test_size=0.2, random_state=42)

ohe = SklearnTransformerWrapper(OneHotEncoder(sparse=False, drop='first'))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The point of the sklearntransformerwrapper is to apply sklearn transformer only to a subset of variables. In this case, the df has only one variable, so you could just apply the OHE directly.

I think a more relevant example would be to use the entire titanic data, and apply the ohe to just a subset for example pclass and sex.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ok!!


X_train_transformed = ohe.transform(X_train)
X_test_transformed = ohe.transform(X_test)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It would be good to add an image or a printout of the result, for example execute X_train_transform.head() and after that show an image of the final df, or instead print(X_train_transform.head()) and then copy the code output in a code block.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sure

Comment thread docs/user_guide/wrappers/Wrapper.rst Outdated
X_test_transformed = ohe.transform(X_test)


Let's say you want to use :class:`SklearnTransformerWrapper()` in a more complex context. As you may note there are `?` signs to denote unknown values. Due to the complexity of the transformations needed we'll use a Pipeline to impute missing values, encode categorical features and create interactions for specific variables using Scikit-Learn PolynomialFeatures.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It would be great if you could shorten the length of the sentence to 88 char (that is, break the sentence over 2 lines).

It reads well, the last 2 words should be: Scikit-Learn's PolynomialFeatures().

])
pipeline.fit(X_train)
X_train_transformed = pipeline.transform(X_train)
X_test_transformed = pipeline.transform(X_test)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

great example, thank you!!

Could you add a 5 row display of the resulting dataframe? like in my previous comment.

@solegalli

Copy link
Copy Markdown
Collaborator

Hey @datacubeR

This is almost ready. Some small changes here and there. Would you be able to have a look?

Thank you!

@solegalli

Copy link
Copy Markdown
Collaborator

Hi @datacubeR

Thank you for looking into the get_feature_names_out() issue. I think we can ignore that request then.

Let me know when this is good to go.

Thanks a lot

@datacubeR
datacubeR force-pushed the creators_wrapper_fix branch from 5c3cca8 to c204083 Compare August 25, 2022 00:57
@datacubeR
datacubeR force-pushed the creators_wrapper_fix branch from c348a26 to ec12e8c Compare August 25, 2022 01:30
@datacubeR

Copy link
Copy Markdown
Contributor Author

Hi @solegalli, sorry for the delay. I updated my OS and had to reinstall lot of things. I just pushed all the last observations for your review. Please let me know if everything looks ok.

Best,

Alfonso

@solegalli

Copy link
Copy Markdown
Collaborator

Thank you @datacubeR !!

Great fix.

@solegalli solegalli linked an issue Aug 30, 2022 that may be closed by this pull request
@solegalli solegalli changed the title Fix repeated features for OneHotEncoder and PolynomialFeatures fix Sklearnwrapper output for OneHotEncoder and PolynomialFeatures to avoid duplicated features Aug 30, 2022
@solegalli
solegalli merged commit 8c21dfe into feature-engine:main Aug 30, 2022
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.

Polynomial Feaatures + SklearnWrapper weird behavior

2 participants