Skip to content

Commit 6454baa

Browse files
jeromedockesdierickxsimon
authored andcommitted
allow passing kwargs in .skb.apply() (skrub-data#1642)
1 parent 1b8508c commit 6454baa

6 files changed

Lines changed: 313 additions & 32 deletions

File tree

CHANGES.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ Ongoing Development
1111

1212
New features
1313
------------
14+
- :meth:`DataOp.skb.apply` now allows passing extra named arguments to the
15+
estimator's methods through the parameters ``fit_kwargs``, ``predict_kwargs``
16+
etc. :pr:`1642` by :user:`Jérôme Dockès <jeromedockes>`.
1417
- TableReport now displays the mean statistic for boolean columns.
1518
:pr:`1647` by :user:`Abdelhakim Benechehab <abenechehab>`.
1619
- :meth:`DataOp.skb.iter_cv_splits` iterates over the training and testing

skrub/_apply_to_cols.py

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,12 @@ class SingleColumnTransformer(BaseEstimator):
7373

7474
__single_column_transformer__ = True
7575

76-
def fit(self, column, y=None):
76+
def fit(self, column, y=None, **kwargs):
7777
"""Fit the transformer.
7878
79+
This default implementation simply calls ``fit_transform()`` and
80+
returns ``self``.
81+
7982
Subclasses should implement ``fit_transform`` and ``transform``.
8083
8184
Parameters
@@ -87,12 +90,15 @@ def fit(self, column, y=None):
8790
y : column or dataframe
8891
Prediction targets.
8992
93+
**kwargs
94+
Extra named arguments are passed to ``self.fit_transform()``.
95+
9096
Returns
9197
-------
9298
self
9399
The fitted transformer.
94100
"""
95-
self.fit_transform(column, y=y)
101+
self.fit_transform(column, y=y, **kwargs)
96102
return self
97103

98104
def _check_single_column(self, column, function_name):
@@ -132,35 +138,35 @@ def _wrap_add_check_single_column(f):
132138
if f.__name__ == "fit":
133139

134140
@functools.wraps(f)
135-
def fit(self, X, y=None):
141+
def fit(self, X, y=None, **kwargs):
136142
self._check_single_column(X, f.__name__)
137-
return f(self, X, y=y)
143+
return f(self, X, y=y, **kwargs)
138144

139145
return fit
140146
elif f.__name__ == "partial_fit":
141147

142148
@functools.wraps(f)
143-
def partial_fit(self, X, y=None):
149+
def partial_fit(self, X, y=None, **kwargs):
144150
self._check_single_column(X, f.__name__)
145-
return f(self, X, y=y)
151+
return f(self, X, y=y, **kwargs)
146152

147153
return partial_fit
148154

149155
elif f.__name__ == "fit_transform":
150156

151157
@functools.wraps(f)
152-
def fit_transform(self, X, y=None):
158+
def fit_transform(self, X, y=None, **kwargs):
153159
self._check_single_column(X, f.__name__)
154-
return f(self, X, y=y)
160+
return f(self, X, y=y, **kwargs)
155161

156162
return fit_transform
157163
else:
158164
assert f.__name__ == "transform", f.__name__
159165

160166
@functools.wraps(f)
161-
def transform(self, X):
167+
def transform(self, X, **kwargs):
162168
self._check_single_column(X, f.__name__)
163-
return f(self, X)
169+
return f(self, X, **kwargs)
164170

165171
return transform
166172

@@ -438,7 +444,7 @@ def __init__(
438444
self.rename_columns = rename_columns
439445
self.n_jobs = n_jobs
440446

441-
def fit(self, X, y=None):
447+
def fit(self, X, y=None, **kwargs):
442448
"""Fit the transformer on each column independently.
443449
444450
Parameters
@@ -449,15 +455,19 @@ def fit(self, X, y=None):
449455
y : Pandas or Polars Series or DataFrame, default=None
450456
The target data.
451457
458+
**kwargs
459+
Extra named arguments are passed to the ``fit_transform()`` method of
460+
the individual column transformers (the clones of ``self.transformer``).
461+
452462
Returns
453463
-------
454464
ApplyToCols
455465
The transformer itself.
456466
"""
457-
self.fit_transform(X, y)
467+
self.fit_transform(X, y, **kwargs)
458468
return self
459469

460-
def fit_transform(self, X, y=None):
470+
def fit_transform(self, X, y=None, **kwargs):
461471
"""Fit the transformer on each column independently and transform X.
462472
463473
Parameters
@@ -468,6 +478,10 @@ def fit_transform(self, X, y=None):
468478
y : Pandas or Polars Series or DataFrame, default=None
469479
The target data.
470480
481+
**kwargs
482+
Extra named arguments are passed to the ``fit_transform()`` method of
483+
the individual column transformers (the clones of ``self.transformer``).
484+
471485
Returns
472486
-------
473487
result : Pandas or Polars DataFrame
@@ -485,19 +499,25 @@ def fit_transform(self, X, y=None):
485499
self._columns,
486500
self.transformer,
487501
self.allow_reject,
502+
kwargs,
488503
)
489504
for col_name in all_columns
490505
)
491506
return self._process_fit_transform_results(results, X)
492507

493-
def transform(self, X):
508+
def transform(self, X, **kwargs):
494509
"""Transform a dataframe.
495510
496511
Parameters
497512
----------
498513
X : Pandas or Polars DataFrame
499514
The column to transform.
500515
516+
**kwargs
517+
Extra named arguments are passed to the ``transform()`` method of
518+
the fitted individual column transformers (the values of
519+
``self.transformers_``, which are clones of ``self.transformer``).
520+
501521
Returns
502522
-------
503523
result : Pandas or Polars DataFrame
@@ -507,10 +527,7 @@ def transform(self, X):
507527
parallel = Parallel(n_jobs=self.n_jobs)
508528
func = delayed(_transform_column)
509529
outputs = parallel(
510-
func(
511-
sbd.col(X, col_name),
512-
self.transformers_.get(col_name),
513-
)
530+
func(sbd.col(X, col_name), self.transformers_.get(col_name), kwargs)
514531
for col_name in sbd.column_names(X)
515532
)
516533
transformed_columns = []
@@ -586,7 +603,9 @@ def _prepare_transformer_input(transformer, column):
586603
return sbd.make_dataframe_like(column, [column])
587604

588605

589-
def _fit_transform_column(column, y, columns_to_handle, transformer, allow_reject):
606+
def _fit_transform_column(
607+
column, y, columns_to_handle, transformer, allow_reject, kwargs
608+
):
590609
col_name = sbd.name(column)
591610
if col_name not in columns_to_handle:
592611
return col_name, [column], None
@@ -595,7 +614,7 @@ def _fit_transform_column(column, y, columns_to_handle, transformer, allow_rejec
595614
transformer_input = _prepare_transformer_input(transformer, column)
596615
allowed = (RejectColumn,) if allow_reject else ()
597616
try:
598-
output = transformer.fit_transform(transformer_input, y=y)
617+
output = transformer.fit_transform(transformer_input, y=y, **kwargs)
599618
except allowed:
600619
return col_name, [column], None
601620
except Exception as e:
@@ -608,12 +627,12 @@ def _fit_transform_column(column, y, columns_to_handle, transformer, allow_rejec
608627
return col_name, output_cols, transformer
609628

610629

611-
def _transform_column(column, transformer):
630+
def _transform_column(column, transformer, kwargs):
612631
if transformer is None:
613632
return [column]
614633
transformer_input = _prepare_transformer_input(transformer, column)
615634
try:
616-
output = transformer.transform(transformer_input)
635+
output = transformer.transform(transformer_input, **kwargs)
617636
except Exception as e:
618637
raise ValueError(
619638
f"Transformer {transformer.__class__.__name__}.transform "

skrub/_apply_to_frame.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ def __init__(
137137
self.keep_original = keep_original
138138
self.rename_columns = rename_columns
139139

140-
def fit(self, X, y=None):
140+
def fit(self, X, y=None, **kwargs):
141141
"""Fit the transformer on all columns jointly.
142142
143143
Parameters
@@ -148,15 +148,19 @@ def fit(self, X, y=None):
148148
y : Pandas or Polars Series or DataFrame, default=None
149149
The target data.
150150
151+
**kwargs
152+
Extra named arguments are passed to the ``fit_transform()`` method
153+
of ``self.transformer``.
154+
151155
Returns
152156
-------
153157
ApplyToFrame
154158
The transformer itself.
155159
"""
156-
self.fit_transform(X, y)
160+
self.fit_transform(X, y, **kwargs)
157161
return self
158162

159-
def fit_transform(self, X, y=None):
163+
def fit_transform(self, X, y=None, **kwargs):
160164
"""Fit the transformer on all columns jointly and transform X.
161165
162166
Parameters
@@ -167,6 +171,10 @@ def fit_transform(self, X, y=None):
167171
y : Pandas or Polars Series or DataFrame, default=None
168172
The target data.
169173
174+
**kwargs
175+
Extra named arguments are passed to the ``fit_transform()`` method
176+
of ``self.transformer``.
177+
170178
Returns
171179
-------
172180
result : Pandas or Polars DataFrame
@@ -183,7 +191,7 @@ def fit_transform(self, X, y=None):
183191
if self._columns:
184192
self.transformer_ = clone(self.transformer)
185193
_utils.set_output(self.transformer_, X)
186-
transformed = self.transformer_.fit_transform(to_transform, y)
194+
transformed = self.transformer_.fit_transform(to_transform, y, **kwargs)
187195
transformed = _utils.check_output(
188196
self.transformer_, to_transform, transformed, allow_column_list=False
189197
)
@@ -212,14 +220,18 @@ def fit_transform(self, X, y=None):
212220
result = sbd.copy_index(X, result)
213221
return result
214222

215-
def transform(self, X):
223+
def transform(self, X, **kwargs):
216224
"""Transform a dataframe.
217225
218226
Parameters
219227
----------
220228
X : Pandas or Polars DataFrame
221229
The column to transform.
222230
231+
**kwargs
232+
Extra named arguments are passed to the ``transform()`` method
233+
of ``self.transformer_``.
234+
223235
Returns
224236
-------
225237
result : Pandas or Polars DataFrame
@@ -236,7 +248,7 @@ def transform(self, X):
236248
passthrough = selectors.select(X, selectors.inv(self._columns))
237249
if not self._columns:
238250
return passthrough
239-
transformed = self.transformer_.transform(to_transform)
251+
transformed = self.transformer_.transform(to_transform, **kwargs)
240252
# we do not call `_utils.check_output` here, assuming that if the output
241253
# had a correct type (e.g. polars dataframe) in `fit_transform` it will
242254
# have the same (correct) type in `transform`.

skrub/_data_ops/_data_ops.py

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1253,7 +1253,16 @@ def check_subsampled_X_y_shape(X_op, y_op, X_value, y_value, mode, environment,
12531253
class Apply(DataOpImpl):
12541254
""".skb.apply() nodes."""
12551255

1256-
_fields = ["X", "estimator", "y", "cols", "how", "allow_reject", "unsupervised"]
1256+
_fields = [
1257+
"X",
1258+
"estimator",
1259+
"y",
1260+
"cols",
1261+
"how",
1262+
"allow_reject",
1263+
"unsupervised",
1264+
"kwargs",
1265+
]
12571266

12581267
# We define `eval()` rather than `compute` because some children may not
12591268
# need to be evaluated depending on the mode. For example in "predict" mode
@@ -1299,8 +1308,10 @@ def eval(self, *, mode, environment):
12991308
# `.skb.preview()` or `.skb.eval()`). We replace `.transform()`
13001309
# with `.predict()`
13011310
if method_name == "fit_transform":
1302-
self.estimator_.fit(X, y)
1303-
pred = self.estimator_.predict(X)
1311+
fit_kwargs = yield from self._eval_kwargs("fit")
1312+
self.estimator_.fit(X, y, **fit_kwargs)
1313+
predict_kwargs = yield from self._eval_kwargs("predict")
1314+
pred = self.estimator_.predict(X, **predict_kwargs)
13041315
# In `(fit_)transform` mode only, format the predictions as a
13051316
# dataframe or column if y was one during `fit()`
13061317
return self._format_predictions(X, pred)
@@ -1316,7 +1327,8 @@ def eval(self, *, mode, environment):
13161327
y_arg = (y,)
13171328
else:
13181329
y_arg = ()
1319-
return getattr(self.estimator_, method_name)(X, *y_arg)
1330+
method_kwargs = yield from self._eval_kwargs(method_name)
1331+
return getattr(self.estimator_, method_name)(X, *y_arg, **method_kwargs)
13201332

13211333
def _store_y_format(self, y):
13221334
if sbd.is_dataframe(y):
@@ -1355,6 +1367,26 @@ def _format_predictions(self, X, pred):
13551367
return sbd.copy_index(X, pred)
13561368
return pred
13571369

1370+
def _eval_kwargs(self, method_name):
1371+
"""
1372+
Evaluate the kwargs we need to pass to the given method.
1373+
1374+
The values in ``self.kwargs`` can be (or contain) DataOps or choices.
1375+
This looks up the kwargs for ``method_name``, yields it for evaluation,
1376+
and checks that the result is actually a dictionary before returning it.
1377+
"""
1378+
kwargs = yield self.kwargs.get(method_name, None)
1379+
if kwargs is None:
1380+
# We check if kwargs is None _after_ evaluation
1381+
kwargs = {}
1382+
if not isinstance(kwargs, dict):
1383+
raise TypeError(
1384+
f"The `{method_name}_kwargs` passed to `.skb.apply()` should be a dict"
1385+
" of named arguments. Got an object of type"
1386+
f" {type(kwargs).__name__!r} instead: {kwargs!r}"
1387+
)
1388+
return kwargs
1389+
13581390
def supported_modes(self):
13591391
"""
13601392
Used by SkrubLearner and param search to decide if they have the

0 commit comments

Comments
 (0)