4  Applying transformers to columns

4.1 Introduction

Often, transformers need to be applied only to a subset of columns, rather than the entire dataframe.

As an example, it does not make sense to apply a StandardScaler to a column that contains strings, and indeed doing so would raise an exception. Conversely, a OneHotEncoder should be appled only to categorical columns. Similarly, it would not make sense to try and extract time-based features (year, month, hour etc.) from anything but datetime columns.

Scikit-learn provides the ColumnTransformer to deal with this:

import pandas as pd
from sklearn.compose import make_column_selector as selector
from sklearn.compose import make_column_transformer
from sklearn.preprocessing import StandardScaler, OrdinalEncoder

df = pd.DataFrame({
    "user_id": [0, 1, 2],
    "date": ["03 January 2023", "04 February 2023","14 April 2023" ],
    "city": ["Paris", "London", "Rome"],
    "metric_1": [10, 20, 30],
    "metric_2": [3, 22, 45]
})

categorical_columns = selector(dtype_include=object)(df)
numerical_columns = selector(dtype_exclude=object)(df)

ct = make_column_transformer(
      (StandardScaler(),
       numerical_columns),
      (OrdinalEncoder(),
       categorical_columns))
transformed = ct.fit_transform(df)
transformed
array([[-1.22474487, -1.22474487, -1.18407545,  0.        ,  1.        ],
       [ 0.        ,  0.        , -0.07764429,  1.        ,  0.        ],
       [ 1.22474487,  1.22474487,  1.26171974,  2.        ,  2.        ]])

make_column_selector allows to choose columns based on their datatype, or by using regex to filter column names. In some cases, this degree of control is not sufficient.

To address such situations, skrub implements different transformers that allow to modify columns from within scikit-learn pipelines. Additionally, the selectors API allows to implement powerful, custom-made column selection filters.

SelectCols and DropCols are transformers that can be used as part of a pipeline to filter columns according to the selectors API, while ApplyToCols and ApplyToFrame replicate the ColumnTransformer behavior with a different syntax and access to the selectors.

4.2 Applying transformers to columns with ApplyToCols

Pre-processing pipelines are intended to transform specific columns in specific ways. To make this process easier, skrub provides the ApplyToCols transformer.

ApplyToCols applies the given transformer to a subset of columns that can be selected by name, or by using filters.

In this snippet, the OrdinalEncoder is applied only to column city, which is selected by the parameter cols.

from skrub import ApplyToCols
import skrub.selectors as s
from sklearn.preprocessing import OrdinalEncoder

ordinal = ApplyToCols(OrdinalEncoder(), cols="city")
transformed = ordinal.fit_transform(df)
transformed
user_id date metric_1 metric_2 city
0 0 03 January 2023 10 3 1.0
1 1 04 February 2023 20 22 0.0
2 2 14 April 2023 30 45 2.0

4.2.1 Single column transformers

Depending on the transformer, the output of the transformation may need to be treated in particular ways.

Most skrub transformers are designed so that they take a single column as input, and return a dataframe that contains one or more columns. This allows to extract produce multiple features from a single column, for example by creating a column for each date part in a datetime.

In this example, columns “Name” and “Desc” have been encoded by a categorical encoder, so that they are represented by three components each rather than a single column.

Any other transformer based on scikit-learn’s design is instead designed to take one or multiple columns at once, and return a number of columns that depends on the specific transformer. A StandardScaler will take N columns as input, and return N as output, while a PCA would instead take N columns as input and return n_components as output, like in the following example:

ApplyToCols deals with this automatically under the hood: it detects the transformer type, then feeds it the set of columns that was selected, while passing the other columns through unchanged.

By passing through unselected columns without changes it is possible to chain several ApplyToCols together by putting them in a scikit-learn pipeline.

4.2.2 Excluding specific columns

It’s possible to exclude one or more columns with the exclude_cols parameter. The parameter can also be combined with cols for finer grained control. Here, for example, the StandardScaler is applied only to numeric columns whose name is different from “user_id”.

from sklearn.preprocessing import StandardScaler

scaler = ApplyToCols(StandardScaler(), cols=s.numeric(), exclude_cols="user_id")
scaler.fit_transform(df)
user_id date city metric_1 metric_2
0 0 03 January 2023 Paris -1.224745 -1.184075
1 1 04 February 2023 London 0.000000 -0.077644
2 2 14 April 2023 Rome 1.224745 1.261720

4.2.3 Example: applying a PCA only to columns whose name starts with “metric”

from skrub import ApplyToCols
from sklearn.decomposition import PCA

reduce = ApplyToCols(PCA(n_components=2), cols=s.glob("metric_*"))

df_reduced = reduce.fit_transform(df)
df_reduced.head()
user_id date city pca0 pca1
0 0 03 January 2023 Paris -22.657216 -0.308264
1 1 04 February 2023 London -1.204361 0.572095
2 2 14 April 2023 Rome 23.861577 -0.263830

4.3 Concatenating the skrub column transformers

Skrub column transformers can be concatenated by using scikit-learn pipelines. In the following example, we first select only the column patient_id, then encode it using OneHotEncoder and finally use PCA to reduce the number of dimensions.

This is done by wrapping the transformers ApplyToCols, and then putting all transformers in order in a scikit-learn pipeline using make_pipeline.

from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import OneHotEncoder
from skrub import SelectCols
import numpy as np

n_patients = 5 

df = pd.DataFrame({
    "patient_id": [f"P{i:03d}" for i in range(n_patients)],
    "age": np.random.randint(18, 80, size=n_patients),
    "sex": np.random.choice(["M", "F"], size=n_patients),
})

select = SelectCols("patient_id")
encode = ApplyToCols(OneHotEncoder(sparse_output=False))
reduce = ApplyToCols(PCA(n_components=2))

transform = make_pipeline(select, encode, reduce)
dft= transform.fit_transform(df)
dft.head(5)
pca0 pca1
0 -7.404359e-17 1.802674e-16
1 -4.389383e-16 8.660254e-01
2 8.164966e-01 -2.886751e-01
3 -4.082483e-01 -2.886751e-01
4 -4.082483e-01 -2.886751e-01
Important

ApplyToCols is intended to work on dataframes, which are dense. As a result, transformers that produce sparse outputs (like the OneHotEncoder) must be set so that their output is dense.

4.4 Example: convert to datetime and encode

from skrub import ToDatetime, DatetimeEncoder
from sklearn.pipeline import make_pipeline

df = pd.DataFrame({
    "date": ["03 January 2023", "04 February 2023"],
    "city": ["Paris", "London"],
    "values": [10, 20]
})

encode_datetime = make_pipeline(
    ApplyToCols(ToDatetime(), cols="date"),
    ApplyToCols(DatetimeEncoder(), cols="date"),
)
encode_datetime.fit_transform(df)
date_year date_month date_day date_total_seconds city values
0 2023.0 1.0 3.0 1.672704e+09 Paris 10
1 2023.0 2.0 4.0 1.675469e+09 London 20

4.4.1 The order of column transformations is important

Some care must be taken when concatenating columnn transformers, in particular when selection is done on datatypes. Consider this case:

encode = ApplyToCols(OneHotEncoder(sparse_output=False), cols=s.string())
scale = ApplyToCols(StandardScaler(), cols=s.numeric())

In the first case, we encode and then scale, in the second case we instead scale first and then encode.

transform_1 = make_pipeline(encode, scale)
dft = transform_1.fit_transform(df)
dft.head(5)
values date_03 January 2023 date_04 February 2023 city_London city_Paris
0 -1.0 1.0 -1.0 -1.0 1.0
1 1.0 -1.0 1.0 1.0 -1.0
transform_2 = make_pipeline(scale, encode)
dft = transform_2.fit_transform(df)
dft.head(5)
values date_03 January 2023 date_04 February 2023 city_London city_Paris
0 -1.0 1.0 0.0 0.0 1.0
1 1.0 0.0 1.0 1.0 0.0

The result of transform_1 is that the features that have been generated by the OneHotEncoder are then scaled by the StandardScaler, because the new features are numeric and are therefore selected in the next step.

In many cases, this behavior is not desired: while some model types may not be affected by the different ordering (such as tree-based models), linear models and NN-based models may produce worse results.

4.4.2 The allow_reject parameter

When ApplyToCols is using a skrub transformer, it can use the allow_reject parameter for more flexibility. By setting allow_reject to True, columns that cannot be treated by the current transformer will be ignored rather than raising an exception.

Consider this example. By default, ToDatetime raises a RejectColumn exception when it finds a column it cannot convert to datetime.

from skrub import ToDatetime
df = pd.DataFrame({
    "date": ["03 January 2023", "04 February 2023", "05 March 2023"],
    "values": [10, 20, 30]
})
df
date values
0 03 January 2023 10
1 04 February 2023 20
2 05 March 2023 30
from skrub import ApplyToCols, ToDatetime

with_reject = ApplyToCols(ToDatetime(), allow_reject=False)
result = with_reject.fit_transform(df)
---------------------------------------------------------------------------
RejectColumn                              Traceback (most recent call last)
Cell In[11], line 4
      1 from skrub import ApplyToCols, ToDatetime
      2 
      3 with_reject = ApplyToCols(ToDatetime(), allow_reject=False)
----> 4 result = with_reject.fit_transform(df)

File ~/work/skrub-tutorials/.pixi/envs/doc/lib/python3.14/site-packages/sklearn/utils/_set_output.py:319, in _wrap_method_output.<locals>.wrapped(self, X, *args, **kwargs)
    317 @wraps(f)
    318 def wrapped(self, X, *args, **kwargs):
--> 319     data_to_wrap = f(self, X, *args, **kwargs)
    320     if isinstance(data_to_wrap, tuple):
    321         # only wrap the first output for cross decomposition
    322         return_tuple = (
    323             _wrap_data_with_container(method, data_to_wrap[0], X, self),
    324             *data_to_wrap[1:],
    325         )

File ~/work/skrub-tutorials/.pixi/envs/doc/lib/python3.14/site-packages/skrub/_apply_to_cols.py:380, in ApplyToCols.fit_transform(self, X, y, **kwargs)
    365     raise TypeError(
    366         f"Invalid value for 'keep_original': {self.keep_original}. "
    367         "Expected a boolean."
    368     )
    370 self._wrapped_transformer = wrap_transformer(
    371     self.transformer,
    372     cols=self.cols,
   (...)    378     columnwise="auto",
    379 )
--> 380 X_transformed = self._wrapped_transformer.fit_transform(X, y, **kwargs)
    382 self.all_inputs_ = self._wrapped_transformer.all_inputs_
    383 self.used_inputs_ = self._wrapped_transformer.used_inputs_

File ~/work/skrub-tutorials/.pixi/envs/doc/lib/python3.14/site-packages/sklearn/utils/_set_output.py:319, in _wrap_method_output.<locals>.wrapped(self, X, *args, **kwargs)
    317 @wraps(f)
    318 def wrapped(self, X, *args, **kwargs):
--> 319     data_to_wrap = f(self, X, *args, **kwargs)
    320     if isinstance(data_to_wrap, tuple):
    321         # only wrap the first output for cross decomposition
    322         return_tuple = (
    323             _wrap_data_with_container(method, data_to_wrap[0], X, self),
    324             *data_to_wrap[1:],
    325         )

File ~/work/skrub-tutorials/.pixi/envs/doc/lib/python3.14/site-packages/skrub/_apply_to_each_col.py:319, in ApplyToEachCol.fit_transform(self, X, y, **kwargs)
    317 parallel = Parallel(n_jobs=self.n_jobs)
    318 func = delayed(_fit_transform_column)
--> 319 results = parallel(
    320     func(
    321         sbd.col(X, col_name),
    322         y,
    323         self._columns,
    324         self.transformer,
    325         self.allow_reject,
    326         kwargs,
    327     )
    328     for col_name in all_columns
    329 )
    330 return self._process_fit_transform_results(results, X)

File ~/work/skrub-tutorials/.pixi/envs/doc/lib/python3.14/site-packages/joblib/parallel.py:1986, in Parallel.__call__(self, iterable)
   1984     output = self._get_sequential_output(iterable)
   1985     next(output)
-> 1986     return output if self.return_generator else list(output)
   1988 # Let's create an ID that uniquely identifies the current call. If the
   1989 # call is interrupted early and that the same instance is immediately
   1990 # reused, this id will be used to prevent workers that were
   1991 # concurrently finalizing a task from the previous call to run the
   1992 # callback.
   1993 with self._lock:

File ~/work/skrub-tutorials/.pixi/envs/doc/lib/python3.14/site-packages/joblib/parallel.py:1914, in Parallel._get_sequential_output(self, iterable)
   1912 self.n_dispatched_batches += 1
   1913 self.n_dispatched_tasks += 1
-> 1914 res = func(*args, **kwargs)
   1915 self.n_completed_tasks += 1
   1916 self.print_progress()

File ~/work/skrub-tutorials/.pixi/envs/doc/lib/python3.14/site-packages/skrub/_apply_to_each_col.py:441, in _fit_transform_column(column, y, columns_to_handle, transformer, allow_reject, kwargs)
    439 allowed = (RejectColumn,) if allow_reject else ()
    440 try:
--> 441     output = transformer.fit_transform(transformer_input, y=y, **kwargs)
    442 except allowed:
    443     return col_name, [column], None

File ~/work/skrub-tutorials/.pixi/envs/doc/lib/python3.14/site-packages/skrub/_single_column_transformer.py:273, in _wrap_add_check_single_column.<locals>.fit_transform(self, X, y, **kwargs)
    270 @functools.wraps(f)
    271 def fit_transform(self, X, y=None, **kwargs):
    272     X = self._check_single_column(X, f.__name__)
--> 273     return f(self, X, y=y, **kwargs)

File ~/work/skrub-tutorials/.pixi/envs/doc/lib/python3.14/site-packages/skrub/_to_datetime.py:399, in ToDatetime.fit_transform(***failed resolving arguments***)
    397     return column
    398 if not (sbd.is_pandas_object(column) or sbd.is_string(column)):
--> 399     raise RejectColumn(f"Column {sbd.name(column)!r} does not contain strings.")
    401 datetime_format = self._get_datetime_format(column)
    402 if datetime_format is None:

RejectColumn: Column 'values' does not contain strings.
Transformer ToDatetime.fit_transform failed on column 'values'. See above for the full traceback.

By setting allow_reject=True, the datetime column is converted properly and the other column is passed through without issues.

with_reject = ApplyToCols(ToDatetime(), allow_reject=True)
with_reject.fit_transform(df)
date values
0 2023-01-03 10
1 2023-02-04 20
2 2023-03-05 30

4.5 Selection operations in a scikit-learn pipeline

SelectCols and DropCols allow selecting or removing specific columns in a dataframe according to user-provided rules. For example, to remove columns that include null values, or to select only columns that have a specific dtype.

SelectCols and DropCols take a cols parameter to choose which columns to select or drop respectively.

from skrub import ToDatetime
df = pd.DataFrame({
    "date": ["03 January 2023", "04 February 2023", "05 March 2023"],
    "values": [10, 20, 30]
})
df
date values
0 03 January 2023 10
1 04 February 2023 20
2 05 March 2023 30

We can selectively choose or drop columns based on names, or more complex rules (see the next chapter).

from skrub import SelectCols
SelectCols("date").fit_transform(df)
date
0 03 January 2023
1 04 February 2023
2 05 March 2023
from skrub import DropCols
DropCols("date").fit_transform(df)
values
0 10
1 20
2 30

4.6 What we have seen in this chapter

In this chapter we covered how skrub can simplify applying transformers to a subset of columns by using ApplyToCols. This can be done by leveraging the cols and exclude_cols parameters. We also saw how to combine and concatenate transformers by making use of the fact that unselected columns are passed through without changes. allow_reject lets the transformers “reject” columns they cannot deal with, rather than raising exceptions. Finally, we looked into how SelectCols and DropCols can be used to select or drop columns based on conditions.

In the next chapter we will look into more advanced column selection methods and how they can be combined with the meta-transformers we have explained here.