Pipeline quickstart
Train a classifier, read its held-out accuracy, predict new raw rows and verify that saving/reloading preserves the predictions. Everything runs in one Python process; no API server, Canvas, Spark session or MLflow service is needed.
Install and prepare the input
Use Python 3.12 or newer in an activated virtual environment:
uv venv --python 3.12
# Activate .venv using your shell, then install into that environment.
uv pip install skyulf-core
For a repository checkout, replace the last command with
uv pip install -e ./skyulf-core. The example uses pandas, NumPy and
scikit-learn supplied by the core installation. Preserve their versions when
moving a saved artifact between environments.
| Column | Training type | Meaning | New prediction rows |
|---|---|---|---|
age |
Floating point, nullable | Numeric feature | Include, even if missing |
city |
String | Categorical feature | Include; unseen values are allowed by this recipe |
target |
Integer 0 or 1 | Class label | Omit |
Split before learning the imputation mean or category vocabulary. Otherwise holdout observations can influence training features and make evaluation optimistic. The explicit first split below learns subsequent steps from six training rows, then applies the saved state to two held-out rows.
What data shape should I pass?
SkyulfPipeline.fit(...) supports two common inputs:
-
A single
pd.DataFramethat includes the target column.- You pass
target_column="..."and Skyulf splits features/target internally. - This is what the quickstart uses, because it's the simplest onboarding path.
- You pass
-
A
SplitDataset(recommended when you already have train/test splits).- Each split can be either a DataFrame (with the target column) or a tuple
(X, y).
- Each split can be either a DataFrame (with the target column) or a tuple
You do not need to manually build X and y for the pipeline unless you want to.
If you prefer a scikit-learn-style workflow (X/y + train_test_split), see:
Complete runnable example
This single snippet is intentionally end-to-end (no duplicated setup across steps).
from pathlib import Path
from tempfile import TemporaryDirectory
import numpy as np
import pandas as pd
from skyulf import SkyulfPipeline
config = {
"preprocessing": [
{"name": "split", "transformer": "TrainTestSplitter", "params": {
"test_size": 0.25, "validation_size": 0.0,
"random_state": 42, "shuffle": True, "stratify": True,
"target_column": "target",
}},
{"name": "impute", "transformer": "SimpleImputer",
"params": {"strategy": "mean", "columns": ["age"]}},
{"name": "encode", "transformer": "OneHotEncoder", "params": {
"columns": ["city"], "drop_original": True,
"handle_unknown": "ignore", "max_categories": None,
}},
],
"modeling": {
"type": "random_forest_classifier",
"params": {"n_estimators": 50, "random_state": 42},
},
}
training = pd.DataFrame({
"age": [10.0, 20.0, None, 40.0, 50.0, 60.0, None, 80.0],
"city": ["A", "B", "A", "C", "B", "A", "C", "B"],
"target": [0, 1, 0, 1, 1, 0, 1, 0],
})
pipeline = SkyulfPipeline(config)
metrics = pipeline.fit(training, target_column="target")
# This is the held-out report, rather than training accuracy or metric names.
test_report = metrics["modeling"]["splits"]["test"]
accuracy = test_report.metrics["accuracy"]
assert 0.0 <= accuracy <= 1.0
fill_values = metrics["preprocessing"]["steps"]["1:impute"]["details"]["fill_values"]
assert fill_values == {"age": 37.5}
print("Held-out accuracy:", accuracy)
print("Training fill values:", fill_values)
# Pass raw features: predict applies the fitted imputer and encoder once.
incoming = pd.DataFrame({"age": [25.0, None], "city": ["A", "NEW"]})
predictions = pipeline.predict(incoming)
assert len(predictions) == len(incoming)
assert set(predictions).issubset({0, 1})
print("Predictions:", predictions.tolist())
with TemporaryDirectory() as temporary:
model_path = Path(temporary) / "model.pkl"
pipeline.save(model_path)
restored = SkyulfPipeline.load(model_path)
np.testing.assert_array_equal(predictions, restored.predict(incoming))
print("Reload predictions match")
The example produces a held-out accuracy of 0.5, fill values
{'age': 37.5}, predictions [0, 0] and Reload predictions match with the
shown recipe. A two-row holdout demonstrates the API; it does not establish a
useful model. Evaluate representative data before using predictions for decisions.
The missing age uses the saved training mean, 37.5. The unseen city NEW receives
zeros in all learned city indicators because handle_unknown="ignore" is set;
it does not create a new feature or refit the vocabulary. Increasing the training
sample or changing package versions can change predictions and metrics.
Change the recipe deliberately
| Setting | Effect |
|---|---|
Split test_size, validation_size, random_state |
Choose evaluation populations and reproducibility; stratification needs enough rows per class |
Imputer columns, strategy |
Choose which missing values are filled and what is learned on training data |
Encoder handle_unknown, max_categories |
Control new-category behavior and grouping; None disables category grouping here |
Model type, params |
Select the registered estimator and its parameters |
If you provide an existing SplitDataset, omit the splitter from this recipe.
Keep original feature names, order and compatible types for prediction. Do not
pass already encoded/scaled features back into pipeline.predict: they would
be transformed again.
A modeling_error entry means evaluation failed even if fitting completed;
inspect it before treating the run as evaluated. Missing input columns, a
non-numeric unencoded feature or insufficient rows for stratification need input
or configuration correction. A split with no usable held-out rows has no usable
held-out accuracy. Only load pickle artifacts from trusted producers; they can
execute Python when loaded. The temporary file above is removed after the check.
Inspecting a pipeline
Evaluation population after filtering
Explicit row filters, such as dropping missing rows, select the eligible population for both ordinary evaluation and tuning/CV. Fitted cleaning bounds come from training data. Serving preserves the requested row alignment.
Held-out reports record input_rows, scored_rows, and excluded_rows.
The denominator begins when that test or validation split exists. For example,
filtering 12 rows out of a 120-row test split reports 120 input, 108 scored,
and 12 excluded. Filtering before splitting changes the source population;
those earlier exclusions are not counted again as test exclusions. Training
coverage describes the final training frame, not all rows loaded upstream.
Consequently, the displayed metrics describe eligible observations and should
not be interpreted as accuracy on the original unfiltered dataset.
A split with no eligible rows has no evaluation metrics and an explicit unavailable reason. Missing original counts remain unknown rather than zero. In Canvas, merging nonempty filtered evaluation branches without shared row lineage is rejected: equal counts do not establish that both branches contain the same rows. Merge branches before applying row filters. All-empty branches retain their coverage and remain visible in the report. This also rejects branches that inherit the same earlier filter: the recorded counts do not establish shared row identity, even when both branches happen to contain identical rows.
Pipeline description and diagram
A fitted (or unfitted) pipeline is fully introspectable — useful for logs, docs, PR descriptions, and model registries:
print(pipeline.describe())
# Human-readable summary: each preprocessing step with its params, then the model.
print(pipeline.to_mermaid())
# Mermaid flowchart of the topology (data -> steps -> model).
# Paste into GitHub docs/PRs, or render with any mermaid viewer.
card = pipeline.export_model_card()
# JSON-friendly dict: schema_version, fitted, fingerprint, preprocessing
# lineage, model + params, fit metrics, and `diagram` (the same mermaid
# string as to_mermaid()). Intended for audit logs and registries.
The same surfaces appear across the platform:
- Experiments page — every completed run persists its mermaid topology
(
metrics.pipeline_diagram); select runs and open the Pipeline Diagram tab. - Notebook export — exported notebooks embed the topology as a rendered "Pipeline topology" markdown cell (compact and full modes).
Diagram labels are human-readable — internal node ids are never shown. Each
node gets the step's display name, and a second line with the most relevant
detail: a digest of the step params for config diagrams (strategy: mean),
or the run's one-line summary for completed jobs (acc 0.87 · f1 0.84).
Model nodes show the algorithm in readable form (Random Forest Classifier).
Mermaid labels are always emitted double-quoted, so node names containing parentheses, brackets, or quotes stay parseable.
Continue from here
Use step-by-step without config to inspect each learned state, preprocessing recipes to select other registered transformations, and inference artifacts for explicit saved schemas and portable execution. Preprocessing placement explains leakage and filter placement.
This example is the standalone library. Canvas runs through its backend job and artifact services; its completed-run reports and inference page use those services rather than this temporary local pickle.