Skip to content

Added DropNullColumn transformer to remove columns that contain only nulls #1115

Merged
jeromedockes merged 64 commits into
skrub-data:mainfrom
rcap107:drop_null_columns
Nov 18, 2024
Merged

Added DropNullColumn transformer to remove columns that contain only nulls #1115
jeromedockes merged 64 commits into
skrub-data:mainfrom
rcap107:drop_null_columns

Conversation

@rcap107

@rcap107 rcap107 commented Oct 17, 2024

Copy link
Copy Markdown
Member

fixes #1110

DropNullColumn (provisional name) takes as input a column, and drops it if all the values are nulls or nans. TableVectorizer was also updated with a drop_null_columns flag set to False by default; if the flag is set to True, the DropNullColumn is added as a processing step for all columns.

I've also added drop and is_all_null to _common.py, though I am not sure if they should go there. Maybe is_all_null can stay in the DropNullColumn file.

The test I wrote passes, but I'm not sure if it's good enough.

The documentation is still missing.

@TheooJ TheooJ left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hi @rcap107 ! I made a first pass and have a few comments :

  • Personnally I like the name DropNullColumn, I think it’s clear what it does !
  • I would rename the file _drop_null.py
  • Make sure you pre-commit run --all-files before pushing, it seems to be what’s breaking the CI for you here
  • I think is_all_null could be placed in the DropNullColumn file if it’s only used there for now, but I could also see it being in _common.py

Comment thread skrub/tests/test_dropnulls.py Outdated
Comment thread skrub/tests/test_dropnulls.py Outdated
Comment thread skrub/_dataframe/_common.py
Comment thread skrub/_dataframe/_common.py Outdated
Comment thread skrub/_dataframe/_common.py Outdated
return make_dataframe_like(df, cols)

@dispatch
def drop(obj, col):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don’t know if drop is necessary, you could directly use skrub selectors:
df = s.select(df, ~s.cols(col))

Comment thread skrub/_table_vectorizer.py Outdated
similar functionality to what is offered by scikit-learn's
:class:`~sklearn.compose.ColumnTransformer`.

drop_null_columns : bool, default=False

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we want it to be True by default ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

That should be discussed with others I think

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.

I vote for true by default -- there's nothing we can learn from a completely empty column.

if it is False by default, I think it should be set to True in the tabular_learner

Comment thread skrub/tests/test_dropnulls.py Outdated
Comment thread skrub/tests/test_dropnulls.py Outdated
main_table_dropped = ns.drop(main_table_dropped, "value_nan")

# Don't drop null columns
tv = TableVectorizer(drop_null_columns=False)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This test needs to go in the TV test file IMO

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I can move it 👍

@rcap107

rcap107 commented Oct 21, 2024

Copy link
Copy Markdown
Member Author

Hi @TheooJ, thanks a lot for the comments! I'll address them and update the PR 👍

Comment thread skrub/tests/test_drop_nulls.py Outdated

# assert_array_equal(
# sbd.to_numpy(sbd.col(drop_null_table, "value_almost_null")),
# np.array(["almost", None, None]),

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Not sure how to write this check so that it works with either pandas or polars

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You could use df_module as a fixture in the test by adding it to the arguments, then comparing series instead of numpy arrays:

df_module.assert_column_equal(
    sbd.col(drop_null_table, "value_almost_null"),
    df_module.make_column("value_almost_null", ["almost", None, None]),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Test would look like

def test_single_column(drop_null_table, df_module):
    """Check that null columns are dropped and non-null columns are kept."""
    dn = DropNullColumn()
    assert dn.fit_transform(drop_null_table["value_nan"]) == []
    assert dn.fit_transform(drop_null_table["value_null"]) == []

    df_module.assert_column_equal(
        sbd.col(drop_null_table, "idx"), df_module.make_column("idx", [1, 2, 3])
    )

    df_module.assert_column_equal(
        sbd.col(drop_null_table, "value_almost_nan"),
        df_module.make_column("value_almost_nan", [2.5, np.nan, np.nan]),
    )

    df_module.assert_column_equal(
        sbd.col(drop_null_table, "value_almost_null"),
        df_module.make_column("value_almost_null", ["almost", None, None]),
    )

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This also circumvents that depending on the version of pandas, null values are not treated the same

@jeromedockes

Copy link
Copy Markdown
Member

the failure in the min-deps environment is not related to this pr; the fix is in #1122

Comment thread skrub/_dataframe/_common.py Outdated
Comment thread skrub/_dataframe/_common.py
Comment thread skrub/_drop_null.py Outdated
Comment thread skrub/_drop_null.py
Comment thread skrub/_table_vectorizer.py Outdated

self._preprocessors = [CheckInputDataFrame()]
if self.drop_null_columns:
add_step(self._preprocessors, DropNullColumn(), cols, allow_reject=True)

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.

  • we may want to insert it after CleanNullStrings? so that if the column becomes full of nulls after converting "N/A" to null it will be dropped. also it's not important but your transformer never raises a RejectColumn exception so allow_reject has no effect you don't need it here and can leave the default

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I added it after CleanNullStrings, but I think I did it in an ugly way, maybe it can be fixed

Comment thread skrub/_dataframe/_common.py Outdated
Comment thread skrub/_drop_null.py
@rcap107

rcap107 commented Nov 8, 2024

Copy link
Copy Markdown
Member Author

[...] when in debug mode?

I'm not sure to understand what debug mode is?

Nevermind, I'll just raise the warning by default. I was thinking that maybe it would be possible to only raise a warning in verbose mode (if it's even a thing), but in the end I just went with a different solution.

  • DropNull then takes the same argument and default

Also, "warn" looks like a good default, but that's up for discussion.

We also need to decide whether it's "warn and drop", or "warn and keep", and explain the behavior in the documentation.

Comment thread skrub/tests/test_table_vectorizer.py Outdated
@@ -778,11 +777,7 @@ def test_drop_null_column():

# Raise exception if a null column is found
with pytest.raises(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This test is still failing because the TableVectorizer is not raising the correct exception and I don't know how to make it do that.

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.

here raise a ValueError instead of RejectColumn

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.

rejectcolumn is a way to signify to the tablevectorizer "I'm not the right transformer for this column, don't apply me here".

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Gotcha, fixed

@rcap107

rcap107 commented Nov 8, 2024

Copy link
Copy Markdown
Member Author

I have updated the code to have "warn and keep" as the default behavior, I think it's the version that makes the most sense.

At the moment the only problem I have is that I don't know how to raise the proper exception from TableVectorizer. In DropNull I am raising RejectColumn, but then I don't know how to propagate it correctly,

@jeromedockes

Copy link
Copy Markdown
Member

I find it a bit weird to have a DropColumnIfNull that does not drop the column and just raises an exception. maybe it should be named something like CheckNulls or something?

@jeromedockes

Copy link
Copy Markdown
Member

Consider a situation where you have a TableVectorizer in a scikit-learn pipeline, trained in a production environment every week. If you built this pipeline assuming you would have access to some column "col_1", but for some reason, the data production system now only produces NaN values for this column

I'm not sure I understand -- in any case the tablevectorizer chooses the estimators during fit so the schema of the output can change every week in this scenario. eg if a column has one more unique value than the previous week it can change from a one-hot encoding to a gap encoding. or for example if you had high_cardinality="drop" those columns would also be dropped or kept depending on their content

@jeromedockes

Copy link
Copy Markdown
Member

so if you want consistent output schema (number of columns, names and types) across training the tablevetorizer is not what you want anyway

@Vincent-Maladiere

Copy link
Copy Markdown
Member

I find it a bit weird to have a DropColumnIfNull that does not drop the column and just raises an exception. maybe it should be named something like CheckNulls or something?

Yes, I agree this sounds weird. I recognize dropping NaN columns –which are usually useless– looks good. What about having the transformer drop by default, but allowing the user to pass other options as arguments in the TableVectorizer?

I'm not sure I understand -- in any case the tablevectorizer chooses the estimators during fit [...]

Right, I was pointing out that even outside of the TableVectorizer (e.g. in the ColumnTransformer), the DropColumnIfNull could raise surprising mistakes.

It's true though that this issue broadly applies to the TableVectorizer, hence we should encourage people to perform checks in production (with dedicated check functions? There might be some potential here for production usage).

@rcap107

rcap107 commented Nov 18, 2024

Copy link
Copy Markdown
Member Author

Following today's discussion with @Vincent-Maladiere and @jeromedockes, I reverted the changes back to the old version (with the simple flag)

@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.

LGTM! thanks again @rcap107

(the codecov report is bogus)

I'll let @Vincent-Maladiere do a final review & merge

@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.

In our IRL discussion, haven't we agreed on a threshold for the null ratio in the column (which is 1.0 by default)?

Apart from that, and to keep in mind for later, the items we discussed were:

  1. The ability to freeze TableVectorizer column-to-transformer mapping. It would help to obtain consistent results for retraining in an automated environment, and having more sensible errors to debug.
  2. Decoupling the check/cleaning part of the TableVectorizer (which comes before the vectorizing part) so that it can be used as a standalone object wherever.

@jeromedockes

Copy link
Copy Markdown
Member

In our IRL discussion, haven't we agreed on a threshold for the null ratio in the column (which is 1.0 by default)?

we definitely did; I had understood it would be tackled in a separate PR

+1 for points 1. and 2. -- let's open a separate issue for 1. and there is #925 for 2.

@rcap107

rcap107 commented Nov 18, 2024

Copy link
Copy Markdown
Member Author

I also understood that the threshold would be added in a separate issue

@rcap107 rcap107 closed this Nov 18, 2024
@rcap107 rcap107 reopened this Nov 18, 2024

@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.

LGTM then, thanks @rcap107 :)

@jeromedockes jeromedockes merged commit 2cdf8ad into skrub-data:main Nov 18, 2024
@jeromedockes

Copy link
Copy Markdown
Member

yay 🎉 !! thanks @rcap107 !

@rcap107

rcap107 commented Nov 18, 2024

Copy link
Copy Markdown
Member Author

🎉

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.

Add a DropEmpty flag to tabular_learner

4 participants