Databricks Python SDK: training and bounded scoring
Use the SDK to train, save, evaluate and score fitted pandas/Polars pipelines from Python, including inside a Databricks driver or serverless job. A generated Bundle is optional. Training and whole-frame scoring run in one Python process; Spark can read the pinned Delta source and publish the resulting predictions.
Three independent choices determine the workflow:
| Setting | What it selects |
|---|---|
runtime="standalone" or "databricks" |
General Python integration or Databricks integration. Both can run on a workstation or cloud compute; UC Delta publication requires databricks. This does not select the DataFrame engine or distributed execution. |
engine="pandas" or "polars" |
The DataFrame engine for training and whole-frame scoring. It must match the saved pipeline. |
Bundle inference_mode="local" or "spark" |
Bounded whole-frame scoring or distributed worker scoring. Spark scoring has separate fitted-recipe admission checks. |
Start with fit and score a small batch for a self-contained example. Reading UC tables additionally needs an authenticated Spark session; MLflow registration needs tracking/registry access. Set explicit row and byte limits before reading a source into Python memory.
For REST serving, see online feature lookup to package native entity-key lookups with saved completeness and freshness checks. See gradual serving rollout to increase a challenger's traffic by 10 percentage points per passing 24-hour stage and automatically promote it after its final 100% stage passes. The optional Bundle scheduled jobs start paused and disabled; neither workflow requires a batch prediction sink.
For generated online setup, select include_online_serving: "true" independently
of include_online_publication and include_daily_rollout. Configure
feature_lookup and online_features in config/training.yml, train and register,
then configure online_serving in config/serving.yml with the concrete version
and existing publication bindings. Deploy and run online_serving_setup explicitly.
Its returned request example follows the inspected native signature, including
required source fields beyond entity keys. The
complete init, config, deploy and request example
also covers externally managed publication. Python callers can invoke
skyulf.integrations.databricks.jobs.serving_job.run_online_serving_setup
with the same section, Spark session and authenticated workspace client; the
helper composes the existing native model and pinned endpoint APIs.
Upgrading integration code
Use the purpose-based module, class and function names shown in this guide.
The previous fitted-pipeline and integration names containing local have been
removed, including their forwarding modules. Update those imports and call sites.
Replace runtime="local" with runtime="standalone"; the old runtime value is
rejected during configuration validation. Continue to choose engine="pandas"
or "polars" separately. Use runtime="databricks" for UC Delta publication.
Saved pickle or MLflow packages that reference removed module or class names may fail to load with this version. Rebuild and log those model packages using the updated imports before upgrading their consumers.
Persisted values such as kind="local_pipeline", mode="local_pipeline" and
execution_scope="whole_frame_local" remain unchanged. Keep these artifact-format
values; Python naming does not change execution admission, payload
checks or resource budgets. Portable bundles continue to use predict_local.
Train a candidate when labels are ready
train_candidate reads a specific Delta table version, filters a
bounded event window in Spark when temporal splitting is selected. Random
splitting uses Core DataSplitter on stable record-key order and needs no dates.
Optional availability filtering excludes unknown results and those after the
independent result_cutoff from both fit and holdout.
For temporal splitting, the feature/model pipeline fits only on events before
holdout_start; later eligible events are reserved for evaluation. The fit engine can be pandas or
Polars. The run logs the Skyulf config, source and code identities, fitted model,
held-out metrics and candidate_comparison.json. It registers a new concrete
model version but never assigns @challenger or @champion.
The model-producing run also exposes native MLflow Inputs for the actual
training and holdout frames. These preserve the pinned source version and
bounded split profiles; dataset_inputs.json links the source, sampling, split
and optional feature lookup evidence. See
training datasets in Inputs
for the fields and the distinction between lineage metadata and saved data rows.
from datetime import UTC, datetime
from skyulf.integrations.databricks import TrainingSpec, train_candidate
spec = TrainingSpec(
table="catalog.schema.labeled_events", version=12,
split_strategy="temporal", filter_unavailable_results=True,
result_cutoff=datetime(2026, 3, 15, tzinfo=UTC),
start=datetime(2026, 1, 1, tzinfo=UTC),
holdout_start=datetime(2026, 2, 1, tzinfo=UTC),
cutoff=datetime(2026, 3, 1, tzinfo=UTC),
event_column="event_time", result_available_at_column="label_available_at",
record_key_columns=("event_id",), input_columns=("feature_a", "feature_b"),
target_column="target", max_rows=10_000, max_bytes=32_000_000,
)
result = train_candidate(
spark, spec, pipeline_config,
model_name="catalog.schema.customer_model",
tracking_uri="databricks", registry_uri="databricks-uc",
experiment_name="/Shared/customer-model", run_name="candidate-2026-03",
artifact_path="artifacts/customer-model", metric="heldout_rmse",
min_improvement=0.05, engine="polars",
champion_version="7", # Pin this concrete version before the job starts.
)
print(result.model_version, result.comparison.eligible)
The example uses native Spark timestamp instants; their stored instants are
retained independently of the Spark session timezone. A
missing or later label is not a training label, even if its target value is
already present in the pinned table. The source must have unique nonnull row
keys and distinct feature, target and timestamp columns. A nonempty training
set and holdout of at least two rows each are required. The row and byte limits
bound decoded rows and the local frame, not Spark's wire transfer size. The
caller supplies a concrete champion_version; omit it for the first candidate.
Review the comparison report, then use the separate guarded staging/promotion
operations if a human approves a change. If fit, packaging, registration or
comparison fails, no alias moves; a registered but unapproved candidate may
remain for inspection. A scheduling policy is separate from this service.
For string, local-clock or date-only source columns, supply independent rules:
from dataclasses import replace
from skyulf.integrations.databricks import TrainingDateSpec
spec = replace(
spec,
event_time_parsing=TrainingDateSpec(
format="%d/%m/%Y %H:%M:%S", timezone="Europe/Copenhagen"
),
result_time_parsing=TrainingDateSpec(format="%Y-%m-%dT%H:%M:%S%z"),
)
Leave the format unset for native timestamp columns. Naive local timestamps
need an explicit source timezone; dates additionally require
date_only="midnight". The default rejects date-only inputs. Ambiguous and
nonexistent local times are rejected, and strings are never guessed.
See the source-date contract
for examples, boundaries, supported directives and distributed validation cost.
candidate_training_spec.json retains both parsing rules for approval replay.
For a table without dates, create the same specification with no time fields:
spec = TrainingSpec(
table="catalog.schema.labeled_customers", version=12,
record_key_columns=("customer_id",), input_columns=("income", "age"),
target_column="target", max_rows=10_000, max_bytes=32_000_000,
split_strategy="random", test_size=0.2, random_state=42, stratify=False,
)
Pass this spec to the same train_candidate call above with either fit
engine. All targets must be known. Enable filter_unavailable_results and set
result_available_at_column plus result_cutoff if outcomes arrive later;
random splitting still needs no observation timestamp. The returned candidate
and saved spec record holdout membership evidence for approval replay.
See the split/availability matrix
for the four supported combinations.
Fit and score a small batch
Candidate training also accepts cv=CVSpec(enabled=True, folds=3) from
skyulf.integrations.databricks.training.tuning.cv. It reuses Core CV on training rows,
refits preprocessing per fold and logs a separate report to the candidate's
MLflow run. Use method="stratified_k_fold" for classification; time-series CV
requires an explicit event window and shuffle=False. The saved final pipeline
and its protected holdout are independent of the fold fits.
TrainingSpec(training_sample_rows=10_000, training_sample_seed=42, ...)
opts into seeded Spark-side selection before local transfer. The limit includes
training and final holdout rows and cannot exceed max_rows. Sample membership
is pinned for approval replay. Leaving it null preserves overflow-fail behavior.
See Bundle CV, sampling and selection
for full contracts and the Bundle's train calendar settings. The Bundle uses
the same action for manual and scheduled runs: null source version resolves
latest once, explicit version pins the snapshot, and rolling windows derive at
invocation. The direct train_candidate API still takes a concrete
TrainingSpec with its source version and any active date boundaries.
Install the same skyulf-core, pandas, Polars and scikit-learn versions in the
training and scoring environments. The fitted artifact records those versions,
the fit engine and the raw/feature schemas. It contains trusted Python pickle;
only load packages from your own controlled producer.
import pandas as pd
from skyulf.data.dataset import SplitDataset
from skyulf.inference.fitted_pipeline import save_pipeline
from skyulf.integrations.databricks import (
InputSource, WorkflowConfig, ModelSelection, OutputSink,
prepare_workflow,
)
from skyulf.pipeline import SkyulfPipeline
train = pd.DataFrame({
"x": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
"target": [2.0, 4.0, 6.0, 8.0, 10.0, 12.0],
})
pipeline = SkyulfPipeline({
"preprocessing": [],
"modeling": {"type": "linear_regression"},
})
pipeline.fit(
SplitDataset(train=train.iloc[:5], test=train.iloc[5:]),
target_column="target",
)
save_pipeline(pipeline, "artifacts/customer-model")
config = WorkflowConfig(
runtime="standalone", # general Python integration, on any compute
engine="pandas", # must match the recorded fit engine
source=InputSource(kind="caller_frame", max_rows=10_000, max_bytes=32_000_000),
model=ModelSelection(kind="local_pipeline", path="artifacts/customer-model"),
sink=OutputSink(kind="return_frame"),
)
prepared = prepare_workflow(config)
query = pd.DataFrame({"x": [7.0, 8.0]})
predictions = prepared.predict(query)
assert prepared.preflight.feature_order == ("x",)
print(predictions)
The example uses a path within one process. A Databricks job's temporary path
is not a cross-job model store. For separate training and scoring jobs, log the
artifact with log_pipeline_model, register that run model explicitly, then select
the registered version in scoring. An alias is read once and converted to a
concrete models:/name/version reference; later alias changes cannot alter the
already prepared predictor.
from skyulf.integrations.databricks import ModelSelection
selection = ModelSelection(
kind="local_pipeline",
name="catalog.schema.customer_model",
alias="champion", # or version="7"; never both
tracking_uri=tracking_uri,
registry_uri=registry_uri,
)
config = config.model_copy(update={"model": selection})
prepared = prepare_workflow(config) # read-only registry resolution and load
print(prepared.preflight.model_version, prepared.preflight.model_digest)
Obtain tracking_uri and registry_uri from your job's non-secret settings;
authenticate through the runtime's credential provider. Do not put tokens or
passwords into config or store URIs. Registering or changing an alias is a
separate, explicit operation. Preparing or scoring a workflow never promotes a model; lifecycle actions are explicit.
Preflight and custom pipelines
WorkflowConfig is frozen, serializable and has no implicit source
snapshot, period or promotion decision. preflight(config, artifact=...)
performs offline checks without a registry call, package load or job submission.
It returns issues with a code, category and suggested fix. For a registry
selection, offline preflight reports model_unresolved; call
prepare_workflow when read-only registry access is allowed. That step
loads the pinned package and performs full preflight before returning a
predictor. local_issues, remote_issues and remote_checked keep the two
stages visible. It may deserialize trusted pickle, but creates no cloud resource.
If representative rows are available before submitting a job, pass
probe_frame=sample to preflight or prepare_workflow. The probe
runs the actual fitted FE and model and reports a prediction_probe_failed
issue for an incompatible sample. A passing sample is evidence for that sample,
not a guarantee about every later batch.
from skyulf.inference.fitted_pipeline import load_pipeline
from skyulf.integrations.databricks import preflight
artifact = load_pipeline("artifacts/customer-model")
report = preflight(config.model_copy(update={
"model": ModelSelection(kind="local_pipeline", path="artifacts/customer-model")
}), artifact=artifact)
for issue in report.issues:
print(issue.category, issue.code, issue.fix)
For custom feature engineering, edit the normal SkyulfPipeline preprocessing
configuration, fit it on training data, save it, and compare predictions before
and after loading. The SDK has no separate node or model allowlist: it
restores the fitted SkyulfPipeline and calls its existing prediction path.
Integration examples cover imputation, scaling, encoding, binning, linear and
logistic regression, and a MinMaxScaler/random-forest combination on both DataFrame
engines. Other library combinations remain governed by their own fitted
inference behavior and should receive replay tests before production use.
Saving a pipeline does not itself admit it to independent worker batches or
single-row requests. Inspected pandas artifacts can use the separately certified
Spark pyfunc route when
their exact fitted recipe and model are supported. A portable InferenceBundle
uses a different state format and its own predict_local/predict_spark paths;
selecting the wrong package kind fails preflight. See
preprocessing context for groups and history.
For example, an advanced pipeline may fit explicit binning followed by encoding before it is packaged; its saved edges and category positions are then replayed by the fitted artifact:
pipeline = SkyulfPipeline({
"preprocessing": [
{"name": "bands", "transformer": "CustomBinning", "params": {
"columns": ["x"], "bins": [0.0, 2.0, 4.0, 6.0],
"output_suffix": "_band",
}},
{"name": "encode_bands", "transformer": "OneHotEncoder", "params": {
"columns": ["x_band"], "drop_original": True,
"handle_unknown": "ignore",
}},
],
"modeling": {"type": "linear_regression"},
})
# Fit with SplitDataset, then save_pipeline as in the first example.
InputSource defaults to 100,000 rows and 128 MiB. Set tighter limits for each
job. PreparedWorkflow.predict checks both before running FE and the model.
It preserves the caller's input column order and rejects schema mismatches; it
does not silently collect a Spark DataFrame. On Databricks, supply either a
bounded caller-owned frame or the explicit pinned UC source adapter below.
Evaluate the held-out split in the model run
Keep labeled test rows outside the fit split. After fitting and loading the
artifact, evaluate_holdout calls the saved pipeline's normal prediction
path on those raw test features. It returns heldout_mae, heldout_rmse and
heldout_r2 for regression; classification returns heldout_accuracy and
heldout_f1_weighted, plus heldout_f1 for binary models (the model's second
class is positive). A negative R2 is a valid poor-generalization result, not a
logging failure. The caller chooses the split and logs metrics to the same
MLflow run ID that contains the model artifact:
import mlflow
from skyulf.data.dataset import SplitDataset
from skyulf.integrations.databricks import evaluate_holdout, fit_workflow
from skyulf.integrations.mlflow.models.pipeline_model import log_pipeline_model
# `frame` is a bounded, labeled pandas or Polars frame with 1,000 rows.
training, heldout = frame[:800], frame[800:]
artifact_path = "artifacts/customer-model"
artifact = fit_workflow(
{"preprocessing": preprocessing, "modeling": {"type": model_type}},
SplitDataset(train=training, test=heldout),
target_column="target",
artifact_path=artifact_path,
max_rows=1000,
max_bytes=4_000_000,
)
metrics = evaluate_holdout(artifact, heldout, target_column="target")
with mlflow.start_run(run_name="customer-model") as run:
mlflow.log_param("heldout_rows", len(heldout))
mlflow.log_metrics(metrics)
model_uri = log_pipeline_model(
artifact_path, run_id=run.info.run_id,
artifact_path="model", tracking_uri="databricks",
)
The held-out labels are used only for evaluation. Monthly scoring reads raw features without labels and applies the already fitted FE and model.
Preprocessing diagnostics
Set TrainingSpec(..., preprocessing_probe=True) to inspect the saved and
reloaded preprocessing on at most the first 256 holdout rows. The option defaults
to False. The 8 MiB input/output budget keeps the sample bounded; the report
is saved as preprocessing_probe.json on the candidate's MLflow training run.
This checks saved state, schema, repeated application, chunk/order consistency
and mutation where the step's context allows those comparisons. It does not
refit or measure model quality and does not change promotion policy. A step
requiring groups or history reports requires_context, with later steps
not_run; that is a request for the right execution context, not proof of a
broken pipeline. See the runnable example and report guide
for all statuses and direct probe_fitted_preprocessing(artifact, sample) use.
The preflight prediction sample described above is a separate check that also
calls the model.
Fit and score a pinned UC source
fit_workflow requires an explicit SplitDataset; it bounds the total
rows and in-memory bytes before fitting and saves the same trusted artifact as
save_pipeline. Tracking and registration remain opt-in calls through
track_run, log_pipeline_model and register_model. They do not move an alias.
from datetime import UTC, datetime
from skyulf.integrations.databricks import (
InputSource, SourceSpec, WorkflowConfig, ModelSelection,
OutputSink, prepare_workflow, score_source,
)
table = "catalog.schema.new_entities"
config = WorkflowConfig(
runtime="databricks",
engine="polars", # must match the registered artifact's fit engine
source=InputSource(kind="uc_table", table=table, version=12,
max_rows=10_000, max_bytes=32_000_000),
model=ModelSelection(kind="local_pipeline",
name="catalog.schema.customer_model", version="7",
tracking_uri="databricks", registry_uri="databricks-uc"),
sink=OutputSink(kind="return_frame"),
)
prepared = prepare_workflow(config) # resolves a concrete model version
period = SourceSpec(
table=table, version=12, record_key_columns=("entity_id",),
input_columns=("amount", "city"), # exact saved raw input order
period_start=datetime(2026, 1, 1, tzinfo=UTC),
period_end=datetime(2026, 2, 1, tzinfo=UTC),
max_rows=10_000, max_bytes=32_000_000,
)
result = score_source(spark, period, prepared)
print(result.predictions, result.diagnostics)
The reader applies the pinned Delta version, half-open UTC period filter,
column projection, deterministic row-key order and max_rows + 1 limit before
iterating on the driver. It stops when decoded serialized rows or the resulting
local frame exceed max_bytes, and rejects duplicate row keys. This byte guard
measures decoded payload and local memory, not Spark's private wire framing;
very wide individual rows should therefore be excluded or handled by a
separately proven paged transport. The selected source and its limits must match
the prepared workflow. The result keeps row keys outside the model input and
records the source version, period, model digest and concrete model version.
No prediction table is created here. Whole-frame FE retains local pandas or
Polars behavior even though Spark performs the bounded UC read.
Publish one scored month to a UC Delta table
run_frame_batch keeps feature engineering and model prediction in the saved
pandas or Polars engine. Spark reads the pinned UC source and converts only the
bounded final prediction rows to a DataFrame with the target's explicit schema.
The guarded Delta writer then replaces exactly the requested half-open period.
A second month therefore leaves the first month's predictions and run metadata
unchanged; a backfill must explicitly name the earlier period.
Provision the source, prediction target and admission control table before the
job. The source and target must be different Delta tables. The target must have
the row-key columns with the same Spark types as the source (long or
string), a timestamp period column, the saved model's output columns at
their declared types, and three string columns:
run_id, model_name and model_version.
Here run_id identifies the prediction publication, not the MLflow training run;
the incremental writer derives it from the source/model snapshot. Existing test
targets with __skyulf_* columns retain their historical schema; create a
new target with these names for the updated writer.
The control table has exactly one row, target_id STRING equal to the target
Delta table ID and nullable owner STRING initially null. Every publisher
of this target must use that same control table. The job identity needs read
access to the source/model and write access to the target/control tables.
from datetime import UTC, datetime, timedelta
from importlib.metadata import version
from skyulf.integrations.databricks import (
BatchSpec, InputSource, SourceSpec, WorkflowConfig,
ModelSelection, OutputSink, prepare_workflow, run_frame_batch,
)
from skyulf.integrations.databricks.data.delta_io.delta_admission import DeltaTableAdmission
source_table = "catalog.schema.scoring_source"
target_table = "catalog.schema.predictions"
control_table = "catalog.schema.prediction_admission"
source_version = 12
source = SourceSpec(
table=source_table,
version=source_version,
period_start=datetime(2026, 1, 1, tzinfo=UTC),
period_end=datetime(2026, 2, 1, tzinfo=UTC),
record_key_columns=("entity_id",),
input_columns=("amount", "city"),
max_rows=10_000,
max_bytes=32_000_000,
)
config = WorkflowConfig(
runtime="databricks",
engine="polars",
source=InputSource(
kind="uc_table", table=source_table, version=source_version,
max_rows=source.max_rows, max_bytes=source.max_bytes,
),
model=ModelSelection(
kind="local_pipeline", name="catalog.schema.customer_model", version="7",
tracking_uri="databricks", registry_uri="databricks-uc",
),
sink=OutputSink(kind="uc_delta", table=target_table),
)
prepared = prepare_workflow(config)
spec = BatchSpec(
period_start=source.period_start,
period_end=source.period_end,
as_of=datetime.now(UTC) + timedelta(minutes=2),
record_key_columns=source.record_key_columns,
output_table=target_table,
model_name=config.model.name,
model_version=prepared.preflight.model_version,
model_digest=prepared.preflight.model_digest,
source_version=source_version,
code_version=version("skyulf-core"),
run_id="january-2026-v1",
expected_target_version=0, # read and pin before submission
mode="local_pipeline",
)
result = run_frame_batch(
spark, source, prepared, spec,
admission=DeltaTableAdmission(spark, control_table),
)
print(result.commit_version, result.replayed, result.manifest)
Use a new logical run ID and the current target version for the next month's
job. Retry an uncertain job with the identical spec, including its original
run ID and expected target version; a successful replay returns the existing
receipt. A new run with a stale expected version fails instead of overwriting
newer results. An empty period fails unless allow_empty=True is explicit.
The source snapshot must have been committed by as_of. The reader and
result both enforce the configured local row and memory budgets; this does not
measure Spark's private wire bytes. The control-row admission protocol protects
participating writers only; do not let other jobs bypass it or manually clear
an owner while a writer may still be active.
Score newly inserted rows automatically
run_incremental_batch selects records by Delta source commits, not
by calendar date. On the first run it scores the existing bounded snapshot. On
later runs it reads only inserts after the source version recorded in the last
prediction-table commit. It appends predictions without replacing an older
period. A source event_time column is optional; if you want to carry it into
the target, set period_column="event_time" in the stable job configuration.
It never filters rows by that column, including late arrivals.
Provision a distinct, initially empty Delta prediction table with the
globally unique key, saved model output columns and
run_id, model_name, model_version string
columns. If carrying an event timestamp, add a matching Spark timestamp
column to both tables. Enable Delta Change Data Feed on the source before
any changes that the job must consume. Provision the same shared admission
control table used by the explicit-period writer; all target writers must use
that admission authority. The job identity needs source, model, target and
control-table permissions.
from skyulf.integrations.databricks import (
InputSource, WorkflowConfig, ModelSelection, OutputSink,
prepare_workflow, run_incremental_batch,
)
from skyulf.integrations.databricks.data.delta_io.delta_admission import DeltaTableAdmission
config = WorkflowConfig(
runtime="databricks",
engine="polars",
source=InputSource(
kind="uc_table", table="catalog.schema.new_records",
read_mode="incremental", max_rows=10_000, max_bytes=32_000_000,
),
model=ModelSelection(
kind="local_pipeline", name="catalog.schema.customer_model", version="7",
tracking_uri="databricks", registry_uri="databricks-uc",
),
sink=OutputSink(kind="uc_delta", table="catalog.schema.predictions"),
)
prepared = prepare_workflow(config)
result = run_incremental_batch(
spark, prepared, record_key_columns=("event_id",),
admission=DeltaTableAdmission(spark, "catalog.schema.prediction_admission"),
)
print(result.input_count, result.commit_version, result.noop)
Keep this configuration for scheduled runs; do not supply a period or source version on each invocation. A repeat run with no new source inserts makes no target write. If an append succeeds but its acknowledgement is lost, the next run reads its committed receipt and continues after that source version. Source updates and deletes, duplicate keys, a missing or expired change feed, and an unrecognized target commit fail without advancing the watermark. Key uniqueness must hold across all runs, not only within one batch. Oversized increments fail under the same local row/decoded-memory limits; increase the budget deliberately or handle such a source with a separate distributed path. The target receipt is in the same Delta commit as the predictions. This protects participating writers only; changes outside the admission protocol must be handled explicitly.
Lag and rolling history across batches
LagFeatures and RollingAggregate default to history_mode="batch": only
the supplied frame participates. Set history_mode="carry" in a preprocessing
step to save a bounded training tail per entity. Put this step after splitting.
For example:
{"name": "recent_value", "transformer": "RollingAggregate",
"params": {"columns": ["value"], "window": 5,
"sort_by": "observation_time", "group_by": ["entity"],
"history_mode": "carry",
"history_max_rows": 1000, "history_max_bytes": 48000}}
Declare the observed value, clock and entity in input_columns. For the
Databricks source contract, this clock must be distinct from the job's
event_column and record keys. Drop or encode non-model columns after the
temporal step. Inputs must be available at prediction time; target-history
forecasting is not supported.
The clock must be numeric or a typed datetime; parse JSON/string timestamps in
an earlier preprocessing step before using them as temporal ordering keys.
- The artifact seed stays fixed. Lag 3 retains three prior rows per entity; rolling window 5 retains four. Window 1 retains one ordering marker.
- Training never reads its own saved tail. Temporal holdout and each CV fold use only their own training history. Carry mode requires Time Series CV (or nested Time Series); skipped gap rows do not enter history.
- Missing or tied entity/time keys and observations at or before the saved
entity time are rejected. New entities start with empty history. Returned
rows keep request order.
drop_nais disallowed; use an imputer instead. - Each chained temporal step stores its own input context. Context rows do not enter subsequent imputer/scaler/model fitting or returned predictions.
- Limits apply across all entities per step: by default 10,000 rows and 1 MiB. Exceeding a limit fails explicitly; inactive entities are not silently evicted.
Incremental Databricks scoring: the initial complete source snapshot
reconstructs history from that snapshot, without prepending the training seed.
Later increments read temporal_history from the last prediction receipt.
The new context, source watermark and predictions share one Delta commit.
A failed write leaves the prior committed context; a retry reads the actual
receipt, and a no-op makes no write. Receipts have a 64 KiB history budget.
A changed model or a prior receipt without history requires a fresh target;
contexts are never silently mixed across models. This is bounded local batch
execution, not Spark worker or streaming state.
Period scoring: run_frame_batch(..., history_state=...) accepts an explicit
earlier context. A successful result's manifest contains the next context.
Period replacement does not automatically choose another period's history.
Retry identity includes the supplied context, so conflicting retries fail.
Core continuation: use score_pipeline_with_history to receive a
PipelinePrediction with frame and detached history. Pass that history to
the next call. The complete history example
shows model reload, JSON transport and equivalence with one complete request.
Persist predictions and history atomically, and serialize writers or compare
and swap the prior state. Ordinary prediction reuses the immutable training
seed; it does not remember earlier calls.
The backend /deployment/predict accepts continue_history=true for the first
request and history_state thereafter; its response returns the next state.
The caller owns durable storage and request serialization. An HTTP error returns
no next state. Custom group/window callbacks still need complete caller-supplied
context; built-in carry mode does not create history for arbitrary callbacks.
Saved scoring policies and project assets
Generated projects separate src/features/pre_split.py, preprocessing.py
and scoring.py. Preprocessing fits transformations inside each training fold.
Scoring has a three-way mode and an independent target-filter switch:
SCORING_MODE = "pre_split" # "pre_split", "custom", or "combined"
SKIP_TARGET_PRE_SPLIT_STEPS = False # fail if reuse requires the actual target
In pre_split or combined, set the second switch to True to skip target-reading filters while keeping
other pre-split steps. This never changes the predicted target. For example,
"price is present" is a training-label check; "floor_area is present" can also
be useful when predicting an unknown price. A mixed target/feature filter is
skipped whole, because deleting one field would change its meaning. Fixed edits
are projected onto feature columns. Preview records skipped names and reused steps.
Reused rules use existing Core implementations and survivor guards on a copy.
Accepted original rows go to the model, so its saved normalization runs once.
Filter dependencies must exist in scoring input_columns; include a field there
and remove it from model features in preprocessing if necessary. Deduplication
checks the current batch only. Empty recipes in pre_split mode preserve ordinary output.
These choices are saved per model version; changing a local switch does not
change the behavior of an already registered model.
Use SCORING_MODE="custom" for custom rules alone, or "combined" to run
pre-split first and custom eligibility only on its survivors. First pre-split
exclusion reasons are retained. Custom callbacks receive original accepted inputs;
fixed model transformations still run once. All-excluded batches skip custom
callbacks and the model; combined mode with no pre-split steps still uses custom
rules. Both modes expose the same separate custom sections:
eligibility: checks before prediction. A null reason accepts a row; a text reason excludes it. The template shows both required-field and finite inclusive numeric-range checks, usingfeature_valueas an editable example.outputs: rules after prediction. Add fields such as a prediction band. They do not change model predictions or select training data.
build_eligibility_rules() and build_output_rules() configure the callable
paths and parameters; custom/scoring_custom.py implements the functions under
BEFORE/AFTER headings. Both lists start empty; the sample dictionaries in
scoring.py are commented out. Uncomment and adapt a desired example explicitly.
Selecting custom or combined mode alone does not activate sample rules.
The complete dictionary form
below is also supported (returning None explicitly disables all scoring rules):
# src/features/scoring.py
def build_scoring():
return {
"eligibility": [{
"name": "observed_fields", "version": "1",
"function": "custom.scoring_custom.require_observed_values",
"params": {"columns": ["feature_value"]},
}],
"outputs": [{
"name": "risk_band", "version": "1",
"function": "custom.scoring_custom.prediction_band",
"params": {"column": "prediction", "thresholds": [10.0, 50.0],
"labels": ["low", "medium", "high"], "output": "band"},
"columns": [{"name": "band", "dtype": "string"}],
}],
}
For classification, the band rule can use probability_0, probability_1, etc.;
class order comes from the saved artifact manifest. Callbacks are ordinary trusted
project functions saved with the model. Eligibility receives (frame, params)
and returns a Series of nullable reason strings: null means eligible. The first
non-null reason wins in configured order. Output callbacks receive
(frame, predictions, params) and return exactly their declared columns, types,
rows and index. Supported output types: float64, int64, string, bool.
Callbacks receive defensive pandas frames with a RangeIndex for both engines;
Core preprocessing and model prediction retain the recorded engine. Rules cannot
overwrite raw inputs, estimates, keys or publication metadata. Keep callbacks
deterministic; use saved assets instead of mutable files, clocks or network data.
Every source key has an output row. With a scoring policy enabled, that row has
scoring_status (predicted/excluded) and nullable exclusion_reason.
Excluded rows have null predictions and business outputs. Receipts include
predicted_count and excluded_count; output_count counts all outcome rows.
An all-excluded increment is a successful atomic publication that advances the
source watermark. Failed callbacks/writes advance nothing. Replays cannot publish
duplicate keys. Existing targets require the exact output schema, so introduce a
new scoring schema using a fresh target. Training, CV and holdout comparison still
use the raw predictor and their independently defined population.
Eligibility precedes lag/rolling processing. Excluded observations never enter continuation history. All-excluded batches retain the previous history unchanged. The saved input schema still applies to excluded rows; normalize input types before scoring rather than relying on eligibility to reinterpret an incompatible schema.
Deliver lookup files and external Python dependencies
src/features/assets.json includes detailed _help instructions and a files
list, initially empty. Create your data file and add its feature-root-relative
path, for example "files": ["assets/bands.json"]. The original plain-list format
["assets/bands.json"] also works. Help text is never embedded as model data.
Assets can support pre-split, preprocessing or scoring; they do not automatically
add a pipeline step. Inside saved code, read a declared file as bytes:
from skyulf.inference.project_package import read_project_asset
payload = read_project_asset(__package__, "assets/bands.json")
Code, encoded file contents and pins share the bounded 64 KiB source snapshot. Paths must remain inside the feature package and asset symlinks are rejected. Undeclared assets, large external model downloads and files outside the package are not embedded. Replacing a local lookup file cannot change an existing model.
Declare exact distribution==version pins in src/features/requirements.txt.
Training/load verifies installed versions before executing package code or
unpickling the model. The artifact manifest records the pins; MLflow includes them
in its saved environment. Provision the same dependencies in the training and
scoring job environments; prediction never installs packages. Ranges, URLs,
recursive requirement files, options, extras and environment markers are rejected.
Train independent target models in one job
Choose training_layout=multi_target when generating a project to use the existing
train job for several named training branches. Configure branches in
the named models mapping in config/training.yml. The default single_model
layout retains its ordinary training graph. Shared source, keys and budgets
belong under defaults; each model can override its target, inputs, estimator,
search/CV, split and quality settings.
A branch has its own target, input columns, preprocessing package, estimator, tuning/CV settings, quality metric and registered model name. For example, a customer dataset can train a purchase classifier, a spending regressor and a visit-count ensemble. Each model remains useful independently. Different targets have different metrics; their scores are not ranked against each other.
The coordinator resolves one Delta table version before training starts. Branches read that same immutable snapshot sequentially within their individual row/byte budgets. Missing labels are excluded separately per target before sampling and splitting. A missing spending label cannot remove an otherwise labeled purchase example. All target columns are forbidden as model inputs to prevent cross-target leakage. The existing training service fits learned preprocessing inside training folds and keeps the final holdout separate.
MLflow records a parent run and a linked run for each trained branch. The parent saves the exact branch plan and progress; child runs retain their fitted pipeline, CV/tuning evidence, holdout metrics, dependency pins and immutable model version. With a model set enabled, every candidate is compared with its corresponding component in the one champion set pinned before training. Independent component aliases cannot change that baseline. Train-only branches retain their own pinned champions.
All branches are required. If one fails, execution stops and the parent is failed; earlier immutable candidate versions remain available for inspection. A complete result is written only after all branches succeed. Replaying a saved plan keeps the source version and split/sample settings; it is a new attempt and can create new model versions. It is not an exactly-once registration retry.
Individual branches require promotion_policy=manual_approval and
score_handoff=disabled; activation is controlled by the complete set's separate
promotion_policy. Newly generated projects enable
the model_set mapping in config/inference.yml. It declares the set's
registered name, prediction table, publication settings and shared rule path.
Older Python-configured projects remain readable through their original loader.
The project still has two jobs. Training registers the complete set candidate;
training nominates the complete set as challenger. Automatic set activation can
move the set champion, while component aliases stay unchanged. A later candidate
moves the displaced contender to previous_challenger. The multi-target score job uses
score_models.py to select one complete saved set.
Activate and score a coherent model set
The set copies each component's exact fitted artifact and records its concrete
model name, version and digest. It also captures src/features/, the combined rule
configuration and typed record keys. Scoring and approval load these saved assets;
editing project files cannot change an existing set. Packaging changed rules
creates a new set candidate and preserves earlier registered packages.
Each newly registered Bundle set version exposes model_set_model_count and
model_set_<branch>_name, model_set_<branch>_version, model_set_<branch>_type
tags for quick inspection. The type identifies the selected estimator or ensemble,
including when tuning was used. Long qualified names continue in _name_2,
_name_3, etc. These tags are display metadata; the saved manifest remains the
execution authority. Existing registered versions are not automatically backfilled.
In features/scoring.py, build_model_rules() configures each model's eligibility
and output rules, while build_combined_rules() configures calculations across
model results. Both stages execute during multi-model scoring. The latter's
default empty list adds no combined business rules. Each
component retains its namespaced predictions and exclusion outcomes. Optional
rules declare a named function, rule version, parameters, output column types and
required_components. Functions receive (inputs, predictions, params) and
return a pandas DataFrame containing exactly their declared output columns. A
profit rule can require revenue and cost without depending on an unrelated churn
component. Excluded required predictions make that rule ineligible; missing
predictions are never substituted with zero.
inputs contains only the set's declared keys and component input columns.
Declare rule Python dependencies as exact pins in
src/features/requirements.txt; the saved set includes them in its MLflow
requirements and rejects pins that conflict with a component.
Legacy projects using composition_config and src/composition/ remain readable;
existing saved artifacts keep their original source without migration.
Choose output storage and consumer views
Multi-target initialization asks model_set_name, model_set_output_mode and a custom physical
table name. Edit model_set.publication in config/inference.yml. Modes are:
| Mode | Stored values | Consumer access |
|---|---|---|
all |
Every model output and combined rule result | One prediction table |
combined_only |
Combined results, keys, rule outcomes and set provenance | One prediction table |
separate_views |
All results, written once | Selected model views and a combined-result view |
Leaving names blank uses <project_name>_set for the registered model set,
<project_name>_set_scores for the physical result table,
<project_name>_predictions_<branch> for model views, and
<project_name>_business_results for the combined view. Custom names replace
these defaults. Registry names use the metadata schema; tables/views use the
output schema. The deployment suffix is added to both.
combined_only still computes all models and their scoring policies. It requires
saved combined rules; an empty rule list fails explicitly. Changing this storage
schema requires a new compatible table. Model output values are not stored in
this mode, even though needed for the combined calculations.
Only separate_views asks for model_view_prefix and combined_view_name.
Blank names use project defaults. Explicit names gain the active target catalog,
output schema and resource suffix. Fine-tune names and selection under model_set.publication; this dictionary shows the resolved setting:
"publication": {
"mode": "separate_views",
"model_views": {
"revenue": "{catalog}.{output_schema}.revenue_predictions{resource_suffix}",
"cost": "{catalog}.{output_schema}.cost_predictions{resource_suffix}",
},
"combined_view": "{catalog}.{output_schema}.profit_results{resource_suffix}",
}
model_views=None selects every saved branch using model_view_template;
an explicit mapping selects only listed branches; {} selects none.
Combined views exist only if the saved set has combined rules. Every view keeps
record keys, relevant output/status fields and set provenance. Branch output
names retain their prefix. These are views of one Delta table, not independently
written prediction copies. Setup verifies or creates all views before the data
write. A setup failure can leave views of empty/previous data, but no new subset
of predictions is published. Unrelated objects are never overwritten. Existing
view definitions must match their recorded projection; use new names to change
the source or columns. Removing configuration does not delete catalog objects.
Per-component quality gates and automatic set activation
Bundle initialization asks model_set_promotion_policy for multi-target projects.
The default is manual_approval; automatic validates and activates a passing
complete set after training. Set model_set.promotion_policy in
config/inference.yml:
model_set:
promotion_policy: automatic
In each named models entry in config/training.yml, define its task-appropriate
metric and limits. This resolved mapping illustrates a revenue regressor's policy:
"metric": "heldout_rmse",
"quality_threshold": 10.0, # RMSE must be <= 10.
"quality_gates": {"heldout_r2": 0.8}, # R2 must also be >= 0.8.
"min_improvement": 0.0, # Replacements must strictly improve RMSE.
A classifier can instead use metric="heldout_f1" and quality_threshold=0.8.
Ensembles use the same task-specific policy. These are illustrative limits, not
recommended thresholds for every dataset. Every branch must have a primary
quality_threshold for activation; automatic mode checks its presence before training.
The first set must pass all absolute gates. A replacement must additionally
improve every component over its counterpart in the pinned champion set by
min_improvement; ties fail even when that value is zero. All models are
evaluated on their saved heldout rows, separately for each target. Policies,
comparison digests and the expected set champion are frozen into the package.
Changing project files cannot relax an existing candidate's gates. Changed
policies require training a new candidate. Set replacements require the same
branch names and registered component names; use a new set for a changed layout.
An automatic quality failure leaves the candidate available and the champion
unchanged. The notebook result and model_set_quality/.../decision.json identify
every failed component and its metrics. Execution, missing evidence and stale
champion errors fail the job. Successful approval also requires each saved model
and business rule to execute on representative scoring input. Scoring remains a
separate job after activation.
Manual approval enforces the same gates. After reviewing evidence, run the training job with
lifecycle_action=approve, the set's candidate_version, and
expected_champion_version (none for first activation). Approval runs every
saved component and rule against one bounded scoring-source snapshot and requires
each to produce at least one result, then rechecks saved holdout quality under the
same alias admission. Save the returned promotion receipt. To restore its complete prior set,
use lifecycle_action=rollback, promotion_receipt_json and the expected current
champion version. Approval clears the promoted set's challenger alias and saves
the former champion as previous_champion. previous_challenger records displaced
candidates, not the former champion.
To explicitly reject the current candidate, run the same training job with
lifecycle_action=reject, candidate_version, expected_champion_version, and a
nonempty rejection_reason (at most 256 UTF-8 bytes). Rejection uses the frozen set
identity and controlled nomination receipt; it does not run training, scoring, or
require passing quality gates. The rejected version remains challenger for
inspection until a newer candidate replaces it. Its approval_status=rejected
and approval_reason tags block later approval. An identical repeat returns the
original rejection receipt; changed candidates, champions or reasons fail.
Rollback restores the whole previous set while preserving an unrelated current challenger and its history. A stale or manually changed alias blocks the operation.
Previously saved sets remain scoreable. Sets without saved quality evidence must be retrained and packaged before approval through this updated Bundle flow. The low-level SDK retains explicit functional-only approval for legacy packages; new quality-bound packages require controlled nomination and the quality validator. Rollback verifies the recorded successful evidence and restores the complete prior set.
Run the score job after approval. It resolves the set champion once, or uses an
explicit score_model_version, and joins component outcomes by unique non-null
integer, string or boolean keys. One final Delta publication contains the complete
selected result and set provenance. A component or rule exception fails the job
before a new data commit; previous successful output remains intact.
Repeated committed source versions are no-ops; model changes follow the configured
append or full rebuild policy. Both jobs use max_concurrent_runs=1 and require
exclusive ownership of set alias changes and prediction-table writes.
Append mode preserves older rows and their original set identities. Full rebuild atomically replaces the compatible output table when the selected set changes, even without new input. A transition involving temporal carry history requires full rebuild so lag/rolling state is reconstructed from the complete snapshot. Schema changes require a separate compatible output table. Source updates and deletes cannot be consumed as incremental inserts.
For example, inserting a new id=102 after scoring id=101 appends a new
prediction. Updating a feature or deleting the already-scored id=101 instead
fails the next incremental batch: there is no update/delete reconciliation of
previous predictions under the default source_change_policy="reject".
For model-set scoring, select rebuild_on_change in Bundle setup or the
model_set mapping in config/inference.yml to reuse the complete snapshot rebuild when
updates/deletes are observed, even without a model-set change. The selected set
rescores ALL current rows, recomputes model/combined rules and resets temporal
history. Deleted records disappear. This can replace earlier predictions from
older sets; new inserts alone still append. No model training is involved.
model_change_mode independently controls what happens when the model selection
changes. Source-triggered rebuilding respects the full snapshot row/byte limits
and commits results/history once after successful computation. Empty snapshots
clear the table and history. A failed calculation, exceeded budget, missing CDF
history or permission error preserves prior output. Only recognized readable
source changes trigger recovery. Receipts record source_change_policy,
source_rebuilt (source-triggered rebuild) and write_mode.
This setting applies to the model-set adapter; the standalone single-model
incremental scorer still rejects source updates/deletes.
Select preprocessing and pre-split recipes independently
Declare named lists under recipes in config/preprocessing.yml and
config/pre_split.yml. Keep custom function factories in the corresponding
src/features/preprocessing.py and src/features/pre_split.py modules.
Select the recipe names independently for each model in config/training.yml:
models:
revenue:
preprocessing_recipe: revenue_features
pre_split_recipe: complete_inputs
Different models can share one recipe declaration while learning separate fitted state in each training fold. Missing names and duplicate configuration owners fail before reading data. The saved model captures the selected declarations and custom source, so editing today's files cannot alter an existing candidate. Older projects with Python recipe builders remain readable through their original loader; do not define the same recipe in both Python and YAML.