Bind the source identifier; load inside the graph; mark X / y at
the split. When declaring a new pipeline, the root
skrub.var(name, value=...) binds a source identifier — a
file path, URL, table name, query — and the loader is the first
.skb.apply_func. Inside the graph, derive X and y, then apply
.skb.mark_as_X() / .skb.mark_as_y() at the split.
The preview value is an optional caller-supplied parameter,
not a literal baked into pipeline.py. value= controls what
learner.skb.preview() sees during interactive iteration —
nothing else. A literal like value="data/train.parquet"
resolves against the CWD at execution time, which silently
breaks every run that isn't started from the project root.
Expose the preview as an optional keyword on build_learner and
leave it None for production fit / cross-validate — the
env-dict supplies the binding regardless.
def build_learner(data_dir_preview: str | Path | None = None):
data_dir = (
skrub.var("data_dir", value=str(data_dir_preview))
if data_dir_preview is not None
else skrub.var("data_dir")
)
data = data_dir.skb.apply_func(load_parquet)
X = data.drop(["id", "target"], axis=1).skb.mark_as_X()
y = data["target"].skb.mark_as_y()
X = X.skb.apply_func(feature_engineering_step)
predictions = X.skb.apply(predictor, y=y)
return predictions.skb.make_learner()
The experiment script supplies an absolute path, anchored on the
package's PROJECT_ROOT (set up by organize-ml-workspace in
<pkg>/__init__.py):
from <pkg> import PROJECT_ROOT
DATA_DIR = PROJECT_ROOT / "data"
learner = build_learner(data_dir_preview=DATA_DIR)
report = skore.evaluate(
learner, data={"data_dir": str(DATA_DIR)}, splitter=splitter,
)
The env-dict at fit / cross-validate time is one binding per
source (data={"data_dir": str(DATA_DIR)}); swapping the source
is a one-line change at the call site, with no edit to
pipeline.py.
Note on the downstream evaluation contract. A SkrubLearner
does not implement sklearn's fit(X, y) signature — it takes a
single environment dict. Pair it with
skore.evaluate(learner, data={"path": ..., ...}, splitter=...)
(or data={"X": X, "y": y} for a materialized binding) — never
with skore.evaluate(learner, X, y, ...), which raises. The
data= argument is the env-dict-style equivalent of X / y on
skore.evaluate, CrossValidationReport, and the
train_data= / test_data= pair on EstimatorReport. See
evaluate-ml-pipeline and skore-api/reports.md § "skrub
interop".
Cross-validation metadata at the X marker. If the data has
group structure (subjects, sessions, customer IDs, repeated
measures) or temporal ordering, attach the relevant column at
the X marker via .skb.mark_as_X(split_kwargs={...}). The keys
in split_kwargs map directly to the keyword arguments of the
future cross-validator's split(X, y, **split_kwargs) (e.g.
groups). See skrub.DataOp.skb.mark_as_X.
X = data.drop([...]).skb.mark_as_X(
split_kwargs={"groups": data["customer_id"]},
)
Ask the user when you can't tell from the data alone whether
such structure exists — name the suspect columns (anything ending
in _id, columns called subject / session / region, or
any date / timestamp for temporal ordering) and ask whether
to wire them. Do not silently leave split_kwargs empty when
group structure is plausible — that produces optimistic
evaluations downstream. Choosing the splitter itself is owned by
evaluate-ml-pipeline; this skill only ensures the metadata is
wired in.
The skrub.X(...) / skrub.y(...) shortcuts are not acceptable
roots: per skrub-api → data_ops.md they are literally
var("X", value).skb.mark_as_X() and var("y", value).skb.mark_as_y(),
which bakes in the variable name and the marker at the root and
forces a pre-loaded, pre-split binding.
When editing an existing pipeline that already binds materialized
data (or uses the shortcuts), do not auto-rewrite. Surface the
source-bound alternative and ask whether to refactor.
Full catalogue (encouraged / discouraged / OK-but-offer-an-upgrade):
references/source-binding.md.