Skip to content
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ Release 0.10.0

New Features
------------
- The :class:`Cleaner` and :class:`TableVectorizer` classes now have a
:method:`list_transformations` method that outputs a human-readable
summary of the columns transformed by each of its steps.
:pr:`2122` by :user:`Eloi Massoulié <emassoulie>`.

Changes
-------
Expand Down
96 changes: 96 additions & 0 deletions skrub/_table_vectorizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,42 @@ def _get_preprocessors(
return steps


def _list_transformations(estimator):
message = ""

# if isinstance(estimator, TableVectorizer):
Comment thread
emassoulie marked this conversation as resolved.
Outdated
# 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"
Comment thread
emassoulie marked this conversation as resolved.
Outdated
# case ToFloat():
Comment thread
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.

Expand Down Expand Up @@ -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):

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.

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.
Expand Down Expand Up @@ -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):
Comment thread
emassoulie marked this conversation as resolved.
Outdated
"""Returns a string reporting the transformations applied by the table
Comment thread
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

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.

numpy formatting requires a single line

Suggested change
"""Returns a string reporting the transformations applied by the table
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.
"""Returns a string reporting the transformations applied by the TableVectorizer \
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.

"""
preprocessing_transformations = (
"Preprocessors\n=============\n\n" + _list_transformations(self)
)
vectorize_transformations = "\n\nProcessors by type\n==================\n\n"
Comment thread
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:
Comment thread
emassoulie marked this conversation as resolved.
vectorize_transformations += (
f"{getattr(self, transformer_type).__class__.__name__} "
Comment thread
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
Comment thread
emassoulie marked this conversation as resolved.
Outdated
)


def limit_cols(col_names, max_cols=10):
Comment thread
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
Comment thread
emassoulie marked this conversation as resolved.
63 changes: 63 additions & 0 deletions skrub/tests/test_table_vectorizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 = [

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.

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
Comment thread
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 == (
Comment thread
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"
Comment thread
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)

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.

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, list_category should be moved outside of the test

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.

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"
)