Skip to content

Commit f0939a0

Browse files
committed
doc & tests
1 parent 6a13d10 commit f0939a0

7 files changed

Lines changed: 140 additions & 9 deletions

File tree

doc/api_reference.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@
237237
"DataOp.skb.make_grid_search",
238238
"DataOp.skb.make_randomized_search",
239239
"DataOp.skb.if_else",
240+
"DataOp.skb.iter_cv_splits",
240241
"DataOp.skb.iter_learners_grid",
241242
"DataOp.skb.iter_learners_randomized",
242243
"DataOp.skb.mark_as_X",

skrub/_data_ops/_estimator.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
from sklearn import model_selection
55
from sklearn.base import BaseEstimator, TransformerMixin, clone
66
from sklearn.exceptions import NotFittedError
7-
from sklearn.model_selection import check_cv
7+
from sklearn.model_selection import KFold, check_cv
88
from sklearn.utils.validation import check_is_fitted
99

10+
from .. import _dataframe as sbd
1011
from .. import _join_utils
1112
from ._choosing import BaseNumericChoice, get_default
1213
from ._data_ops import Apply, check_subsampled_X_y_shape
@@ -26,7 +27,7 @@
2627
from ._inspection import describe_params
2728
from ._parallel_coord import DEFAULT_COLORSCALE, plot_parallel_coord
2829
from ._subsampling import env_with_subsampling
29-
from ._utils import X_NAME, Y_NAME, _CloudPickle, attribute_error
30+
from ._utils import KFOLD_5, X_NAME, Y_NAME, _CloudPickle, attribute_error
3031

3132
_FITTING_METHODS = ["fit", "fit_transform"]
3233
_SKLEARN_SEARCH_FITTED_ATTRIBUTES_TO_COPY = [
@@ -676,12 +677,20 @@ def train_test_split(
676677
return result
677678

678679

679-
def iter_cv_splits(data_op, environment, *, keep_subsampling=False, cv=5):
680+
def iter_cv_splits(data_op, environment, *, keep_subsampling=False, cv=KFOLD_5):
681+
"""Yield splits of an environment into training an testing environments.
682+
683+
This functionality is exposed to users through the
684+
``DataOp.skb.iter_cv_splits()`` method. See the corresponding docstring for
685+
details and examples.
686+
"""
687+
if cv is KFOLD_5:
688+
cv = KFold(5)
680689
cv = check_cv(cv)
681690
environment = env_with_subsampling(data_op, environment, keep_subsampling)
682691
X, y = _compute_Xy(data_op, environment)
683692
for train_idx, test_idx in cv.split(X, y):
684-
X_train, X_test = X[train_idx], X[test_idx]
693+
X_train, X_test = sbd.select_rows(X, train_idx), sbd.select_rows(X, test_idx)
685694
train_env = {**environment, X_NAME: X_train}
686695
test_env = {**environment, X_NAME: X_test}
687696
split_info = {
@@ -691,7 +700,9 @@ def iter_cv_splits(data_op, environment, *, keep_subsampling=False, cv=5):
691700
"X_test": X_test,
692701
}
693702
if y is not None:
694-
y_train, y_test = y[train_idx], y[test_idx]
703+
y_train, y_test = sbd.select_rows(y, train_idx), sbd.select_rows(
704+
y, test_idx
705+
)
695706
train_env[Y_NAME] = y_train
696707
test_env[Y_NAME] = y_test
697708
split_info["y_train"] = y_train

skrub/_data_ops/_skrub_namespace.py

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
full_report,
4040
)
4141
from ._subsampling import SubsamplePreviews, env_with_subsampling
42-
from ._utils import NULL, attribute_error
42+
from ._utils import KFOLD_5, NULL, attribute_error
4343

4444

4545
def _var_values_provided(data_op, environment):
@@ -1534,9 +1534,9 @@ def train_test_split(
15341534
15351535
- train: a dictionary containing the training environment
15361536
- test: a dictionary containing the test environment
1537-
- X_train: the value of the variable marked with ``skb.mark_as_x()`` in
1537+
- X_train: the value of the variable marked with ``skb.mark_as_X()`` in
15381538
the train environment
1539-
- X_test: the value of the variable marked with ``skb.mark_as_x()`` in
1539+
- X_test: the value of the variable marked with ``skb.mark_as_X()`` in
15401540
the test environment
15411541
- y_train: the value of the variable marked with ``skb.mark_as_y()`` in
15421542
the train environment, if there is one (may not be the case for
@@ -1590,7 +1590,65 @@ def train_test_split(
15901590
**split_func_kwargs,
15911591
)
15921592

1593-
def iter_cv_splits(self, environment=None, *, keep_subsampling=False, cv=5):
1593+
def iter_cv_splits(self, environment=None, *, keep_subsampling=False, cv=KFOLD_5):
1594+
"""Yield splits of an environment into training and testing environments.
1595+
1596+
Parameters
1597+
----------
1598+
environment : dict, optional
1599+
The environment (dict mapping variable names to values) containing the
1600+
full data. If ``None`` (the default), the data is retrieved from the
1601+
DataOp.
1602+
1603+
keep_subsampling : bool, default=False
1604+
If True, and if subsampling has been configured (see
1605+
:meth:`DataOp.skb.subsample`), use a subsample of the data. By
1606+
default subsampling is not applied and all the data is used.
1607+
1608+
cv : int, cross-validation generator or iterable, default=KFold(5)
1609+
The default is 5-fold without shuffling. Can be a cross-validation
1610+
splitter, an iterable yielding pairs of (train, test) indices, or an
1611+
int to specify the number of folds for KFold splitting.
1612+
1613+
Yields
1614+
------
1615+
dict
1616+
For each split, a dict is produced, containing the following keys:
1617+
1618+
- train: a dictionary containing the training environment
1619+
- test: a dictionary containing the test environment
1620+
- X_train: the value of the variable marked with ``skb.mark_as_X()`` in
1621+
the train environment
1622+
- X_test: the value of the variable marked with ``skb.mark_as_X()`` in
1623+
the test environment
1624+
- y_train: the value of the variable marked with ``skb.mark_as_y()`` in
1625+
the train environment, if there is one (may not be the case for
1626+
unsupervised learning).
1627+
- y_test: the value of the variable marked with ``skb.mark_as_y()`` in
1628+
the test environment, if there is one (may not be the case for
1629+
unsupervised learning).
1630+
1631+
Examples
1632+
--------
1633+
>>> import skrub
1634+
>>> from sklearn.dummy import DummyClassifier
1635+
>>> from sklearn.metrics import accuracy_score
1636+
1637+
>>> orders = skrub.var("orders")
1638+
>>> X = orders.skb.drop("delayed").skb.mark_as_X()
1639+
>>> y = orders["delayed"].skb.mark_as_y()
1640+
>>> delayed = X.skb.apply(skrub.TableVectorizer()).skb.apply(
1641+
... DummyClassifier(), y=y
1642+
... )
1643+
>>> df = skrub.datasets.toy_orders().orders
1644+
>>> accuracies = []
1645+
>>> for split in delayed.skb.iter_cv_splits({"orders": df}, cv=3):
1646+
... learner = delayed.skb.make_learner().fit(split["train"])
1647+
... prediction = learner.predict(split["test"])
1648+
... accuracies.append(accuracy_score(split["y_test"], prediction))
1649+
>>> accuracies
1650+
[1.0, 0.0, 1.0]
1651+
"""
15941652
if environment is None:
15951653
environment = self.get_data()
15961654
yield from iter_cv_splits(

skrub/_data_ops/_utils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
class Sentinels(enum.Enum):
1313
NULL = "NULL"
1414
OPTIONAL_VALUE = "value"
15+
KFOLD_5 = "KFold(5)"
1516

1617
def __repr__(self):
1718
return self.value
@@ -22,6 +23,7 @@ def __str__(self):
2223

2324
NULL = Sentinels.NULL
2425
OPTIONAL_VALUE = Sentinels.OPTIONAL_VALUE
26+
KFOLD_5 = Sentinels.KFOLD_5
2527

2628

2729
def simple_repr(data_op):

skrub/_data_ops/tests/test_estimators.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,31 @@ def test_train_test_split(with_y):
422422
assert e.skb.eval() == [7, 6, 5, 4, 3, 2, 1, 0]
423423

424424

425+
def test_iter_cv_splits():
426+
X = skrub.X(np.arange(5) * 10)
427+
splits = X.skb.iter_cv_splits()
428+
s = next(splits)
429+
assert list(s["X_train"]) == list(s["train"]["_skrub_X"]) == [10, 20, 30, 40]
430+
assert list(s["X_test"]) == list(s["test"]["_skrub_X"]) == [0]
431+
s = next(splits)
432+
assert list(s["X_train"]) == list(s["train"]["_skrub_X"]) == [0, 20, 30, 40]
433+
assert list(s["X_test"]) == list(s["test"]["_skrub_X"]) == [10]
434+
435+
X = skrub.X(np.arange(4) * 10)
436+
y = skrub.y(np.arange(4) * -10)
437+
splits = skrub.as_data_op((X, y)).skb.iter_cv_splits(cv=4)
438+
s = next(splits)
439+
assert list(s["X_train"]) == list(s["train"]["_skrub_X"]) == [10, 20, 30]
440+
assert list(s["X_test"]) == list(s["test"]["_skrub_X"]) == [0]
441+
assert list(s["y_train"]) == list(s["train"]["_skrub_y"]) == [-10, -20, -30]
442+
assert list(s["y_test"]) == list(s["test"]["_skrub_y"]) == [0]
443+
s = next(splits)
444+
assert list(s["X_train"]) == list(s["train"]["_skrub_X"]) == [0, 20, 30]
445+
assert list(s["X_test"]) == list(s["test"]["_skrub_X"]) == [10]
446+
assert list(s["y_train"]) == list(s["train"]["_skrub_y"]) == [0, -20, -30]
447+
assert list(s["y_test"]) == list(s["test"]["_skrub_y"]) == [-10]
448+
449+
425450
def test_train_test_split_splitter_renaming():
426451
# TODO remove when `splitter` is removed in 0.7.0
427452
X = skrub.X(list(range(10)))

skrub/_dataframe/_common.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@
103103
"sample",
104104
"head",
105105
"slice",
106+
"select_rows",
106107
"replace",
107108
"with_columns",
108109
"abs",
@@ -1374,6 +1375,21 @@ def _slice_polars(obj, *start_stop):
13741375
return obj.slice(start, stop - start)
13751376

13761377

1378+
@dispatch
1379+
def select_rows(obj, idx):
1380+
return np.asarray(obj)[list(idx)]
1381+
1382+
1383+
@select_rows.specialize("pandas")
1384+
def _select_rows_pandas(obj, idx):
1385+
return obj.iloc[list(idx)]
1386+
1387+
1388+
@select_rows.specialize("polars")
1389+
def _select_rows_polars(obj, idx):
1390+
return obj[list(idx)]
1391+
1392+
13771393
@dispatch
13781394
def replace(col, old, new):
13791395
raise _raise(col, kind="Series")

skrub/_dataframe/tests/test_common.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ def test_not_implemented():
3434
"copy_index",
3535
"index",
3636
"with_columns",
37+
"select_rows",
3738
}
3839
for func_name in sorted(set(ns.__all__) - has_default_impl):
3940
func = getattr(ns, func_name)
@@ -783,6 +784,23 @@ def test_slice(df_module, obj, s):
783784
assert_array_equal(out, expected)
784785

785786

787+
@pytest.mark.parametrize("obj", ["column", "dataframe"])
788+
@pytest.mark.parametrize("idx", [[], [1], [2, 1]])
789+
def test_select_rows(df_module, obj, idx):
790+
out = ns.select_rows(getattr(df_module, f"example_{obj}"), idx)
791+
if obj == "dataframe":
792+
out = ns.col(out, "float-col")
793+
out = ns.to_numpy(out)
794+
expected = ns.to_numpy(df_module.example_column)[idx]
795+
assert_array_equal(out, expected)
796+
797+
798+
def test_select_rows_array():
799+
a = np.arange(6).reshape((3, 2))
800+
assert_array_equal(ns.select_rows(a, (2, 1)), a[[2, 1], :])
801+
assert_array_equal(ns.select_rows(a[0], (1, 0)), a[0, [1, 0]])
802+
803+
786804
def test_is_null(df_module):
787805
s = ns.pandas_convert_dtypes(df_module.make_column("", [0, None, 2, None, 4]))
788806
expected = df_module.make_column("", [False, True, False, True, False])

0 commit comments

Comments
 (0)