-
Notifications
You must be signed in to change notification settings - Fork 273
Exposing transformations applied by Cleaner and TableVectorizer
#2122
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
base: main
Are you sure you want to change the base?
Changes from 19 commits
0a22b31
e93c7ec
3e1832c
36a6e71
1270c71
054ddbf
dffb2d2
4b59efd
11b6318
d2c1ef1
26abe8b
35195e2
0bb9b57
7d891c1
bcf72cc
2c698bf
4b34cd5
6d3f6c5
4bbcb87
b3580c3
2b411e0
5b3df56
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 | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -184,6 +184,42 @@ def _get_preprocessors( | |||||||||||||||||||
| return steps | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| def _list_transformations(estimator): | ||||||||||||||||||||
| message = "" | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # if isinstance(estimator, TableVectorizer): | ||||||||||||||||||||
| # post = estimator._postprocessors | ||||||||||||||||||||
| # else: | ||||||||||||||||||||
| # post = [] | ||||||||||||||||||||
|
|
||||||||||||||||||||
| for step in estimator._pipeline.named_steps: | ||||||||||||||||||||
| if step == "checkinputdataframe": | ||||||||||||||||||||
| continue | ||||||||||||||||||||
| transformer = estimator._pipeline.named_steps[step] | ||||||||||||||||||||
| match transformer.transformer: | ||||||||||||||||||||
| case DropUninformative(): | ||||||||||||||||||||
| dropped = set(transformer.all_inputs_) - set(transformer.all_outputs_) | ||||||||||||||||||||
| if dropped: | ||||||||||||||||||||
| message += f"DropUninformative ({len(dropped)} columns):" + "\n\t- " | ||||||||||||||||||||
| message += "\n\t- ".join(limit_cols(dropped)) + "\n\n" | ||||||||||||||||||||
|
emassoulie marked this conversation as resolved.
Outdated
|
||||||||||||||||||||
| # case ToFloat(): | ||||||||||||||||||||
|
emassoulie marked this conversation as resolved.
Outdated
|
||||||||||||||||||||
| # if transformer not in post: | ||||||||||||||||||||
| # message += "Columns transformed to float:" + "\n" | ||||||||||||||||||||
| # message += "\n\t".join(transformer.used_inputs_) | ||||||||||||||||||||
| case ToDatetime(): | ||||||||||||||||||||
| message += ( | ||||||||||||||||||||
| f"Datetime ({len(transformer.used_inputs_)} columns):" + "\n\t- " | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| message += "\n\t- ".join(limit_cols(transformer.used_inputs_)) + "\n\n" | ||||||||||||||||||||
| case CleanNullStrings(): | ||||||||||||||||||||
| message += ( | ||||||||||||||||||||
| f"Null values cleaned ({len(transformer.used_inputs_)} columns):" | ||||||||||||||||||||
| + "\n\t- " | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| message += "\n\t- ".join(limit_cols(transformer.used_inputs_)) + "\n\n" | ||||||||||||||||||||
| return message | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| class Cleaner(TransformerMixin, SkrubBaseEstimator): | ||||||||||||||||||||
| """Column-wise consistency checks and sanitization of dtypes, null values and dates. | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
@@ -541,6 +577,9 @@ def get_feature_names_out(self, input_features=None): | |||||||||||||||||||
| check_is_fitted(self, "all_outputs_") | ||||||||||||||||||||
| return np.asarray(self.all_outputs_) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| def list_transformations(self): | ||||||||||||||||||||
|
Member
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 a public method so it should have a docstring |
||||||||||||||||||||
| return _list_transformations(self) | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| class TableVectorizer(TransformerMixin, SkrubBaseEstimator): | ||||||||||||||||||||
| """Transform a dataframe to a numeric (vectorized) representation. | ||||||||||||||||||||
|
|
@@ -1166,3 +1205,60 @@ def get_feature_names_out(self, input_features=None): | |||||||||||||||||||
| """ | ||||||||||||||||||||
| check_is_fitted(self, "all_outputs_") | ||||||||||||||||||||
| return np.asarray(self.all_outputs_) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| def list_transformations(self): | ||||||||||||||||||||
|
emassoulie marked this conversation as resolved.
Outdated
|
||||||||||||||||||||
| """Returns a string reporting the transformations applied by the table | ||||||||||||||||||||
|
emassoulie marked this conversation as resolved.
|
||||||||||||||||||||
| vectorizer, and the columns they are each applied to. This covers every | ||||||||||||||||||||
| preprocessing step, each of the `numeric`, `datetime`, `low cardinality` | ||||||||||||||||||||
| and `high cardinality` transformations and any specific transformer. | ||||||||||||||||||||
|
Comment on lines
+1216
to
+1219
Member
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. numpy formatting requires a single line
Suggested change
|
||||||||||||||||||||
| """ | ||||||||||||||||||||
| preprocessing_transformations = ( | ||||||||||||||||||||
| "Preprocessors\n=============\n\n" + _list_transformations(self) | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| vectorize_transformations = "\n\nProcessors by type\n==================\n\n" | ||||||||||||||||||||
|
emassoulie marked this conversation as resolved.
Outdated
|
||||||||||||||||||||
| specific_transformations = "" | ||||||||||||||||||||
|
|
||||||||||||||||||||
| all_transformers = self.kind_to_columns_.copy() | ||||||||||||||||||||
| specific = all_transformers.pop("specific") | ||||||||||||||||||||
|
|
||||||||||||||||||||
| for transformer_type, transformer_cols in all_transformers.items(): | ||||||||||||||||||||
| if transformer_cols: | ||||||||||||||||||||
|
emassoulie marked this conversation as resolved.
|
||||||||||||||||||||
| vectorize_transformations += ( | ||||||||||||||||||||
| f"{getattr(self, transformer_type).__class__.__name__} " | ||||||||||||||||||||
|
emassoulie marked this conversation as resolved.
|
||||||||||||||||||||
| f"({transformer_type} - {len(transformer_cols)} columns): " | ||||||||||||||||||||
| + "\n\t- " | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| vectorize_transformations += ( | ||||||||||||||||||||
| "\n\t- ".join(limit_cols(transformer_cols)) + "\n\n" | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| else: | ||||||||||||||||||||
| vectorize_transformations += ( | ||||||||||||||||||||
| f"No {transformer_type} columns have been detected." + "\n\n" | ||||||||||||||||||||
| ) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if self.specific_transformers: | ||||||||||||||||||||
| specific_transformations = ( | ||||||||||||||||||||
| "\n\nSpecific transformers\n=====================\n\n" | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| for t in self.specific_transformers: | ||||||||||||||||||||
| specific_transformations += ( | ||||||||||||||||||||
| f"{t[0].__class__.__name__} (specific, {len(specific)} columns):" | ||||||||||||||||||||
| + "\n\t- " | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| specific_transformations += "\n\t- ".join(limit_cols(specific)) + "\n\n" | ||||||||||||||||||||
|
|
||||||||||||||||||||
| return ( | ||||||||||||||||||||
| preprocessing_transformations | ||||||||||||||||||||
| + "\n\n" | ||||||||||||||||||||
| + vectorize_transformations | ||||||||||||||||||||
| + "\n\n" | ||||||||||||||||||||
| + specific_transformations | ||||||||||||||||||||
|
emassoulie marked this conversation as resolved.
Outdated
|
||||||||||||||||||||
| ) | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| def limit_cols(col_names, max_cols=10): | ||||||||||||||||||||
|
emassoulie marked this conversation as resolved.
Outdated
|
||||||||||||||||||||
| if len(col_names) > max_cols: | ||||||||||||||||||||
| list_cols = list(col_names)[:max_cols] + ["..."] | ||||||||||||||||||||
| else: | ||||||||||||||||||||
| list_cols = list(col_names) | ||||||||||||||||||||
| return list_cols | ||||||||||||||||||||
|
emassoulie marked this conversation as resolved.
|
||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,6 +27,7 @@ | |
| ) | ||
| from skrub._to_float import ToFloat | ||
| from skrub._to_str import ToStr | ||
| from skrub._utils import PassThrough | ||
| from skrub.conftest import _POLARS_INSTALLED | ||
|
|
||
| MSG_PANDAS_DEPRECATED_WARNING = "Skip deprecation warning" | ||
|
|
@@ -1277,3 +1278,65 @@ def test_duration_to_float(df_module): | |
| vectorizer = Cleaner() | ||
| transformed = vectorizer.fit_transform(df) | ||
| df_module.assert_column_equal(transformed["duration"], df["duration"]) | ||
|
|
||
|
|
||
| def test_list_transformations(df_module): | ||
| passthrough_line = [ | ||
|
Member
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. please add a small comment explaining what the test dataset looks like |
||
| "red", | ||
| "orange", | ||
| "yellow", | ||
| "green", | ||
| "blue", | ||
| "indigo", | ||
| "violet", | ||
| ] | ||
| df_dict = { | ||
| "numbers": [1, 2, 3, 4, 5, 6, None], | ||
| "low_card": ["up", "up", "up", "down", "down", "up", "down"], | ||
| "datetime": [ | ||
| "2026-06-01", | ||
| "2026-06-04", | ||
| "2026-07-03", | ||
| "2026-05-29", | ||
| "2026-01-08", | ||
| "2026-06-20", | ||
| None, | ||
| ], | ||
| "uninformative": [False, False, False, False, False, False, False], | ||
| } | ||
| for i in range(1, 12): | ||
| df_dict[f"passthrough_{i}"] = passthrough_line | ||
|
emassoulie marked this conversation as resolved.
|
||
|
|
||
| df = df_module.make_dataframe(df_dict) | ||
|
|
||
| vectorizer = TableVectorizer( | ||
| specific_transformers=[ | ||
| (PassThrough(), [f"passthrough_{i}" for i in range(1, 12)]) | ||
| ] | ||
| ) | ||
| _ = vectorizer.fit_transform(df) | ||
| vectorizer_output = vectorizer.list_transformations() | ||
| assert vectorizer_output == ( | ||
|
emassoulie marked this conversation as resolved.
Outdated
|
||
| "Preprocessors\n=============\n\nNull values cleaned (2 columns):" | ||
| "\n\t- low_card\n\t- datetime\n\nDatetime (1 columns):\n\t- datetime" | ||
| "\n\n\n\n\n\nProcessors by type\n==================\n\nPassThrough " | ||
| "(numeric - 2 columns): \n\t- numbers\n\t- uninformative\n\n" | ||
| "DatetimeEncoder (datetime - 1 columns): \n\t- datetime\n\nOneHotEncoder" | ||
| " (low_cardinality - 1 columns): \n\t- low_card\n\nNo high_cardinality" | ||
| " columns have been detected.\n\n\n\n\n\nSpecific transformers\n" | ||
|
emassoulie marked this conversation as resolved.
Outdated
|
||
| "=====================\n\nPassThrough (specific, 11 columns):\n\t- " | ||
| "passthrough_1\n\t- passthrough_2\n\t- passthrough_3\n\t- " | ||
| "passthrough_4\n\t- passthrough_5\n\t- passthrough_6\n\t- passthrough_7" | ||
| "\n\t- passthrough_8\n\t- passthrough_9\n\t- passthrough_10\n\t- ...\n\n" | ||
| ) | ||
|
|
||
| vectorizer = Cleaner(drop_if_constant=True) | ||
|
Member
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 cleaner should be moved to a separate test, or the test should be parametrized to have both the cleaner and the tablevectorizer in this case it may be simpler to have two separate tests, though that means repeating a lot of the code either way,
Member
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 reason I prefer having two separate tests (or a parametrized test) is that I was debugging this, and the test was failing on the Cleaner part I did not notice that was the case because the diff was very long, so I was looking for the failure in the part about the TableVectorizer when it was in the Cleaner |
||
| _ = vectorizer.fit_transform(df) | ||
| cleaner_output = vectorizer.list_transformations() | ||
| assert ( | ||
| cleaner_output == "Null values cleaned (13 columns):\n\t- low_card\n\t" | ||
| "- datetime\n\t- passthrough_1\n\t- passthrough_2\n\t- passthrough_3\n\t" | ||
| "- passthrough_4\n\t- passthrough_5\n\t- passthrough_6\n\t- passthrough_7" | ||
| "\n\t- passthrough_8\n\t- ...\n\nDropUninformative (1 columns):\n\t- " | ||
| "uninformative\n\nDatetime (1 columns):\n\t- datetime\n\n" | ||
| ) | ||
Uh oh!
There was an error while loading. Please reload this page.