Skip to content

Commit 743f643

Browse files
jeromedockesrcap107
andcommitted
rename values for .skb.apply(how=...) (skrub-data#1628)
Co-authored-by: Riccardo Cappuzzo <7548232+rcap107@users.noreply.github.com>
1 parent 25dfaf5 commit 743f643

5 files changed

Lines changed: 108 additions & 40 deletions

File tree

CHANGES.rst

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,15 @@ Changes
2020
:func:`datasets.get_ken_table_aliases`, and :func:`datasets.get_ken_types` will be
2121
removed in the next release of skrub.
2222
:pr:`1546` by :user:`Vincent Maladiere <Vincent-Maladiere>`.
23-
2423
- Improved error messages when a DataOp is being sent to dispatched functions.
2524
:pr:`1607` by :user:`Riccardo Cappuzzo<rcap107>`.
25+
- The accepted values for the parameter ``how`` of :meth:`DataOp.skb.apply` have
26+
changed. The new values are ``"auto"`` (unchanged), ``"cols"`` to wrap the
27+
transformer in :class:`ApplyToCols`, ``"frame"`` to wrap the transformer in
28+
:class:`ApplyToFrame`, or ``"no_wrap"`` for no wrapping. The old values are
29+
deprecated and will result in an error in a future release.
30+
:pr:`1628` by :user:`Jérôme Dockès <jeromedockes>`.
31+
2632

2733
Bugfixes
2834
--------

skrub/_data_ops/_data_ops.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -722,8 +722,8 @@ def _check_wrap_params(cols, how, allow_reject, reason):
722722
msg = None
723723
if not isinstance(cols, type(s.all())):
724724
msg = f"`cols` must be `all()` (the default) when {reason}"
725-
elif how not in ["auto", "full_frame"]:
726-
msg = f"`how` must be 'auto' (the default) or 'full_frame' when {reason}"
725+
elif how not in ["auto", "no_wrap"]:
726+
msg = f"`how` must be 'auto' (the default) or 'no_wrap' when {reason}"
727727
elif allow_reject:
728728
msg = f"`allow_reject` must be False (the default) when {reason}"
729729
if msg is not None:
@@ -754,11 +754,34 @@ def _check_estimator_type(estimator):
754754
)
755755

756756

757+
def _check_apply_how(how):
758+
valid = ["auto", "cols", "frame", "no_wrap"]
759+
if how in valid:
760+
return how
761+
762+
# TODO remove when the old names are completely dropped in 0.7.0
763+
translate = {"columnwise": "cols", "sub_frame": "frame", "full_frame": "no_wrap"}
764+
if how in translate:
765+
new = translate[how]
766+
warnings.warn(
767+
(
768+
f"{how!r} has been renamed to {new!r}: use .skb.apply(how={new!r})"
769+
" instead."
770+
),
771+
FutureWarning,
772+
)
773+
return new
774+
775+
raise ValueError(f"`how` must be one of {valid}. Got: {how!r}")
776+
777+
757778
def _wrap_estimator(estimator, cols, how, allow_reject, X):
758779
"""
759780
Wrap the estimator passed to .skb.apply in ApplyToCols or ApplyToFrame if
760781
needed.
761782
"""
783+
how = _check_apply_how(how)
784+
762785
if estimator in [None, "passthrough"]:
763786
estimator = PassThrough()
764787

@@ -767,16 +790,16 @@ def _wrap_estimator(estimator, cols, how, allow_reject, X):
767790
def _check(reason):
768791
_check_wrap_params(cols, how, allow_reject, reason)
769792

770-
if how == "full_frame":
771-
_check("`how` is 'full_frame'")
793+
if how == "no_wrap":
794+
_check("`how` is 'no_wrap'")
772795
return estimator
773796
if hasattr(estimator, "predict") or not hasattr(estimator, "transform"):
774797
_check("`estimator` is a predictor (not a transformer)")
775798
return estimator
776799
if not sbd.is_dataframe(X):
777800
_check("the input is not a DataFrame")
778801
return estimator
779-
columnwise = {"auto": "auto", "columnwise": True, "sub_frame": False}[how]
802+
columnwise = {"auto": "auto", "cols": True, "frame": False}[how]
780803
return wrap_transformer(
781804
estimator, cols, allow_reject=allow_reject, columnwise=columnwise
782805
)
@@ -1186,7 +1209,7 @@ def eval(self, *, mode, environment):
11861209

11871210

11881211
def _check_column_names(X):
1189-
# NOTE: could allow int column names when how='full_frame', prob. not worth
1212+
# NOTE: could allow int column names when how='no_wrap', prob. not worth
11901213
# the added complexity.
11911214
#
11921215
# TODO: maybe also forbid duplicates? use a reduced version of

skrub/_data_ops/_estimator.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -249,15 +249,15 @@ def find_fitted_estimator(self, name):
249249
>>> learner.find_fitted_estimator("classifier")
250250
DummyClassifier()
251251
252-
Depending on the parameters passed to ``skb.apply()``, the estimator we provide
253-
can be wrapped in a skrub transformer that applies it to several columns in the
254-
input, or to a subset of the columns in a dataframe. In other cases it may be
255-
applied without any wrapping. We provide examples for those 3 different cases
256-
below.
252+
Depending on the parameters passed to :meth:`DataOp.skb.apply`, the
253+
estimator we provide can be wrapped in a skrub transformer that applies
254+
it to several columns in the input, or to a subset of the columns in a
255+
dataframe. In other cases it may be applied without any wrapping. We
256+
provide examples for those 3 different cases below.
257257
258258
Case 1: the ``StringEncoder`` is a skrub single-column transformer: it
259259
transforms a single column. In the learner it gets wrapped in a
260-
``skrub.ApplyToCols`` which independently fits a separate instance of the
260+
:class:`ApplyToCols` which independently fits a separate instance of the
261261
``StringEncoder`` to each of the columns it transforms (in this case there is
262262
only one column, ``'product'``). The individual transformers can be found in the
263263
fitted attribute ``transformers_`` which maps column names to the corresponding
@@ -269,13 +269,13 @@ def find_fitted_estimator(self, name):
269269
>>> encoder.transformers_['product'].vectorizer_.vocabulary_
270270
{' pe': 2, 'pen': 12, 'en ': 8, ' pen': 3, 'pen ': 13, ' cu': 0, 'cup': 6, 'up ': 18, ' cup': 1, 'cup ': 7, ' sp': 4, 'spo': 16, 'poo': 14, 'oon': 10, 'on ': 9, ' spo': 5, 'spoo': 17, 'poon': 15, 'oon ': 11}
271271
272-
This case (wrapping in ``ApplyToCols``) happens when the estimator is a skrub
272+
This case (wrapping in :class:`ApplyToCols`) happens when the estimator is a skrub
273273
single-column transformer (it has a ``__single_column_transformer__``
274-
attribute), we pass ``.skb.apply(how='columnwise')`` or we pass
274+
attribute), we pass ``.skb.apply(how='cols')`` or we pass
275275
``.skb.apply(allow_reject=True)``.
276276
277277
Case 2: the ``PCA`` is a regular scikit-learn transformer. In the learner it
278-
gets wrapped in a ``skrub.ApplyToFrame`` which applies it to the subset of columns
278+
gets wrapped in a :class:`ApplyToFrame` which applies it to the subset of columns
279279
in the dataframe selected by the ``cols`` argument passed to ``.skb.apply()``.
280280
The fitted ``PCA`` can be found in the fitted attribute ``transformer_``.
281281
@@ -287,8 +287,9 @@ def find_fitted_estimator(self, name):
287287
>>> pca.transformer_.mean_
288288
array([2020., 4., 4.], dtype=float32)
289289
290-
This case (wrapping in ``ApplyToFrame``) happens when the estimator is a
291-
scikit-learn transformer but not a single-column transformer.
290+
This case (wrapping in :class:`ApplyToFrame`) happens when the estimator is a
291+
scikit-learn transformer but not a single-column transformer, or we
292+
pass ``.skb.apply(how='frame')``.
292293
293294
The ``DummyRegressor`` is a scikit-learn predictor. In the learner it gets
294295
applied directly to the input dataframe without any wrapping.
@@ -301,7 +302,7 @@ def find_fitted_estimator(self, name):
301302
302303
This case (no wrapping) happens when the estimator is a scikit-learn predictor
303304
(not a transformer), the input is not a dataframe (e.g. it is a numpy array), or
304-
we pass ``.skb.apply(how='full_frame')``.
305+
we pass ``.skb.apply(how='no_wrap')``.
305306
""" # noqa: E501
306307
node = find_node_by_name(self.data_op, name)
307308
if node is None:

skrub/_data_ops/_skrub_namespace.py

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -143,13 +143,24 @@ def apply(
143143
_not_ be applied. The columns that are matched by ``cols`` AND not
144144
matched by ``exclude_cols`` are transformed.
145145
146-
how : "auto", "columnwise", "subframe" or "full_frame", optional
147-
The mode in which it is applied. In the vast majority of cases the
148-
default "auto" is appropriate. "columnwise" means a separate clone
149-
of the transformer is applied to each column. "subframe" means it
150-
is applied to a subset of the columns, passed as a single
151-
dataframe. "full_frame" means the whole input dataframe is passed
152-
directly to the provided ``estimator``.
146+
how : "auto", "cols", "frame" or "no_wrap", optional
147+
How the estimator is applied. In most cases the default "auto"
148+
is appropriate.
149+
- "cols" means `estimator` is wrapped in a :class:`ApplyToCols`
150+
transformer, which fits a separate clone of `estimator` each
151+
column in `cols`. `estimator` must be a transformer (have a
152+
``fit_transform`` method).
153+
- "frame" means `estimator` is wrapped in a :class:`ApplyToFrame`
154+
transformer, which fits a single clone of `estimator` to the
155+
selected part of the input dataframe. `estimator` must be a
156+
transformer.
157+
- "no_wrap" means no wrapping, `estimator` is applied directly to
158+
the unmodified input.
159+
- "auto" chooses the wrapping depending on the input and estimator.
160+
If the input is not a dataframe or the estimator is not a
161+
transformer, the "no_wrap" strategy is chosen. Otherwise if the
162+
estimator has a ``__single_column_transformer__`` attribute,
163+
"cols" is chosen. Otherwise "frame" is chosen.
153164
154165
allow_reject : bool, optional
155166
Whether the transformer can refuse to transform columns for which
@@ -181,6 +192,12 @@ def apply(
181192
--------
182193
skrub.DataOp.skb.make_learner :
183194
Get a skrub learner for this DataOp.
195+
skrub.ApplyToCols :
196+
Transformer that applies a given transformer separately to each
197+
selected column.
198+
skrub.ApplyToFrame:
199+
Transformer that applies a given transformer to part of a
200+
dataframe.
184201
185202
Examples
186203
--------
@@ -554,7 +571,7 @@ def select(self, cols):
554571
2 cup 2020-04-04
555572
3 spoon 2020-04-05
556573
"""
557-
return self._apply(SelectCols(cols), how="full_frame")
574+
return self._apply(SelectCols(cols), how="no_wrap")
558575

559576
@check_data_op
560577
def drop(self, cols):
@@ -607,7 +624,7 @@ def drop(self, cols):
607624
2 3 5
608625
3 4 1
609626
"""
610-
return self._apply(DropCols(cols), how="full_frame")
627+
return self._apply(DropCols(cols), how="no_wrap")
611628

612629
@check_data_op
613630
def concat(self, others, axis=0):

skrub/_data_ops/tests/test_data_ops.py

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from sklearn.model_selection import train_test_split
1313

1414
import skrub
15+
from skrub import ApplyToCols
1516
from skrub import selectors as s
1617
from skrub._data_ops import _data_ops
1718
from skrub._utils import PassThrough
@@ -329,45 +330,65 @@ class A(_data_ops.DataOpImpl):
329330
a.skb.eval()
330331

331332

332-
@pytest.mark.parametrize("why_full_frame", ["numpy", "predictor", "how"])
333+
@pytest.mark.parametrize("why_no_wrap", ["numpy", "predictor", "how"])
333334
@pytest.mark.parametrize("bad_param", ["cols", "how", "allow_reject"])
334-
def test_apply_bad_params(why_full_frame, bad_param):
335+
def test_apply_bad_params(why_no_wrap, bad_param):
335336
# When the estimator is a predictor or the input is a numpy array (not a
336-
# dataframe) (or how='full_frame') the estimator can only be applied to the
337+
# dataframe) (or how='no_wrap') the estimator can only be applied to the
337338
# full input without wrapping in ApplyToCols or ApplyToFrame. In this case
338339
# if the user passed a parameter that would require wrapping, such as
339340
# passing a value for `cols` that is not `all()`, or passing
340-
# how='columnwise' or allow_reject=True, we get an error.
341+
# how='cols' or allow_reject=True, we get an error.
341342

342-
if why_full_frame == bad_param == "how":
343+
if why_no_wrap == bad_param == "how":
343344
return
344345
X_a, y_a = make_classification(random_state=0)
345346
X_df = pd.DataFrame(X_a, columns=[f"col_{i}" for i in range(X_a.shape[1])])
346347

347-
X = skrub.X(X_a) if why_full_frame == "numpy" else skrub.X(X_df)
348-
if why_full_frame == "predictor":
348+
X = skrub.X(X_a) if why_no_wrap == "numpy" else skrub.X(X_df)
349+
if why_no_wrap == "predictor":
349350
estimator = LogisticRegression()
350351
y = skrub.y(y_a)
351352
else:
352353
estimator = PassThrough()
353354
y = None
354-
how = "full_frame" if why_full_frame == "how" else "auto"
355-
# X is a numpy array: how must be full_frame and selecting columns is not
355+
how = "no_wrap" if why_no_wrap == "how" else "auto"
356+
# X is a numpy array: how must be no_wrap and selecting columns is not
356357
# allowed.
357358
if bad_param == "cols":
358-
if why_full_frame == "numpy":
359+
if why_no_wrap == "numpy":
359360
cols = [0, 1]
360361
else:
361362
cols = ["col_0", "col_1"]
362363
else:
363364
cols = s.all()
364-
how = "columnwise" if bad_param == "how" else how
365+
how = "cols" if bad_param == "how" else how
365366
allow_reject = True if bad_param == "allow_reject" else False
366367

367-
with pytest.raises((ValueError, RuntimeError), match=""):
368+
with pytest.raises(
369+
(ValueError, RuntimeError),
370+
match=(
371+
r"(`cols` must be `all\(\)`|`how` must be 'auto'|`allow_reject` must be"
372+
r" False)"
373+
),
374+
):
368375
X.skb.apply(estimator, y=y, how=how, allow_reject=allow_reject, cols=cols)
369376

370377

378+
def test_apply_invalid_how():
379+
df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
380+
X = skrub.var("X", df)
381+
t = PassThrough()
382+
for how in ["auto", "cols", "frame", "no_wrap"]:
383+
assert list(X.skb.apply(t, how=how).skb.eval().columns) == ["a", "b"]
384+
with pytest.raises(RuntimeError, match="`how` must be one of"):
385+
X.skb.apply(t, how="bad value")
386+
# TODO: remove when old names are dropped in 0.7.0
387+
with pytest.warns(FutureWarning, match="'columnwise' has been renamed to 'cols'"):
388+
wrapper = X.skb.apply(t, how="columnwise").skb.applied_estimator.skb.eval()
389+
assert isinstance(wrapper, ApplyToCols)
390+
391+
371392
class Mul(BaseEstimator):
372393
def __init__(self, factor=1):
373394
self.factor = factor

0 commit comments

Comments
 (0)