Databricks Bundle tuning and ensembles
Choose an estimator in the model catalog, then
choose how to search its parameters here. This guide describes the generated
Bundle's config/training.yml contract. The
training reference owns the complete workflow
field list; Core tuning describes the Python API.
The Bundle accepts a bounded subset of the underlying sklearn and Optuna APIs.
| Strategy | Use when | Candidate selection |
|---|---|---|
| grid | A small, deliberate set of combinations covers the question. | Every Cartesian-product combination. |
| random | Several axes make a full grid too expensive. | Up to n_trials sampled combinations. |
| halving_grid | A small grid has a useful cheap training budget. | Full initial grid, then fewer candidates at larger resources. |
| halving_random | A large space has a useful cheap training budget. | Sampled initial candidates, then successive elimination. |
| optuna | Adaptive trial selection or supported pruning is useful. | TPE, random or CMA-ES sampling. |
flowchart LR
A["Choose estimator and fixed params"] --> B["Choose search space"]
B --> C["grid / random / halving / optuna"]
C --> D["Training-side CV and budget"]
D --> E["Select parameters and refit"]
E --> F["Separate heldout evaluation"]
Search selection uses training-side validation. The final holdout measures the selected fitted model; it does not choose the winning trial.
Where settings belong
Use models.<name>.model.params for fixed constructor values and
models.<name>.tuning.search_space for searched values. For example, keep
fit_intercept: true fixed and search alpha: [0.1, 1.0, 10.0]. Do not also fix
alpha: 1.0: a conflicting explicit axis is rejected. A matching singleton
axis is allowed, but adds no search. Defaults inherited from defaults are
shallow overrides: a model's tuning mapping replaces the shared mapping.
Omit tuning entirely to fit once. {} requests tuning; it does not disable it.
An empty search_space asks Core to construct the selected model's registered
space, with explicit fixed parameters pinned. The model catalog shows the
registered grid/random families and halving resources. Supplying a nonempty
space replaces the automatic axes. Model defaults still supply omitted
constructor settings.
Each axis must be a nonempty list of finite JSON scalars: numbers, strings,
booleans or null. At most 128 axes, 100 values per axis and 65,536 encoded JSON
bytes are admitted. Use native YAML null, true and false. Distribution
objects, Python callables, conditional list-of-dictionaries grids and arbitrary
Optuna constructor options are not YAML search-space values. Unknown estimator
parameter names fail admission. A list such as weights: [[1, 2], [2, 1]] is
therefore not an accepted search axis; set ensemble weights structurally in
model.params instead.
All strategies use these fields inside tuning; their cards explain which
controls affect that strategy.
| Field | Type | Default | Accepted values and effect |
|---|---|---|---|
strategy |
String | random |
grid, random, halving_grid, halving_random, optuna. |
metric |
String | Required for a single model or branch; competition can inherit its objective | Native Core task-compatible objective, for example rmse, mae, mse, r2, accuracy, balanced_accuracy, f1, f1_weighted, precision, recall, roc_auc. Do not use heldout_ names here. The wizard supplies rmse or accuracy outside competition. |
n_trials |
Integer | 10 |
1 through 1000. Random/Optuna trial count or halving-random initial candidate count. Grid variants still validate this field but enumerate their grid independently of it. |
random_state |
Integer | 42 |
0 through 4,294,967,295. Seeds search sampling, independently of workflow split and CV seeds. Does not automatically seed every estimator. |
max_candidates |
Integer | 1000 |
1 through 10,000. Rejects an exhaustive grid whose Cartesian product exceeds this limit. It is not the random/Optuna trial budget. |
search_space |
Mapping | {} |
Empty means the registered space; otherwise parameter names map to finite scalar candidate lists subject to the bounds above. |
timeout |
Number or null | null |
Positive finite seconds, supported only by Optuna. Soft search stop, not an interrupt or a whole-job deadline. |
strategy_params |
Mapping | {} |
Empty for grid/random; only the exact halving or Optuna controls in the cards below are admitted. |
tune_threshold |
Boolean | false |
Legacy binary-classification threshold tuning, requiring enabled nested CV and probability output. Incompatible with an active decision_threshold policy. Prefer the explicit policy documented in the training reference. |
The Bundle fixes search n_jobs to 1 and requires all explicitly configured
estimator and nested-estimator workers to be 1. n_jobs, parallel_backend,
custom scorers, refit, error_score, and CV keys are not accepted inside the
YAML tuning mapping. Set shared CV fields at workflow/defaults level. This
keeps candidate fits bounded; a valid configuration is not a promise that its
grid is cheap.
Loss objectives such as RMSE are ranked with sklearn's negative-loss scorer.
Saved tuning best_score can therefore be negative: higher is better in the
search evidence. Final lifecycle metrics such as heldout_rmse keep their own
meaning and direction. The final holdout is never the search validation set.
The complete admitted metric names are:
| Task | Native tuning metrics |
|---|---|
| Regression | mse, mae, rmse, r2, explained_variance |
| Classification | accuracy, balanced_accuracy, f1, f1_weighted, precision, recall, roc_auc, roc_auc_ovr, roc_auc_ovo, roc_auc_ovr_weighted, roc_auc_ovo_weighted, pr_auc, pr_auc_weighted, log_loss, matthews_corrcoef, g_score |
These are admission choices, not a promise that every model/label distribution
can compute every metric. Probability metrics need appropriate model outputs;
multiclass AUC needs a compatible variant and class coverage. g_score requires
imbalanced-learn. The wizard exposes a smaller common metric menu; edit YAML
for the other admitted names.
Common setup for the worked examples
In a generated single_model project, replace config/training.yml with this
header followed by exactly one strategy card's complete models block. Keep the
generated config/inference.yml, Python package and job resources. The source
table needs unique nonnull BIGINT customer_id, numeric DOUBLE income and
numeric DOUBLE revenue. Replace the illustrative table/model names and quality
bound for your data.
version: 1
defaults:
config_version: 1
training_layout: single_model
engine: pandas
task: regression
training_table: "{catalog}.{input_schema}.customer_training"
training_table_version: null
record_key_columns: [customer_id]
input_columns: [income]
target_column: revenue
model_name: "{catalog}.{metadata_schema}.revenue_model{resource_suffix}"
split_strategy: random
training_window_mode: full_snapshot
test_size: 0.2
random_state: 42
stratify: false
filter_unavailable_results: false
max_rows: 10000
max_input_mb: 64
cv_enabled: true
cv_type: k_fold
cv_folds: 2
cv_shuffle: true
cv_random_state: 42
metric: heldout_rmse
min_improvement: 0.0
quality_threshold: 5.0
promotion_policy: manual_approval
preprocessing_recipe: default
pre_split_recipe: default
Use this companion content in both config/pre_split.yml and
config/preprocessing.yml. Empty recipes keep the examples focused on search;
choose fitted preprocessing appropriate to your real data before training.
version: 1
recipes:
default: []
After saving the selected example, run from the generated project:
uv pip install -r deployment/train-requirements.txt
python src/tools/smoke.py
python src/tools/preview.py --action train --catalog workspace --input-schema demo --output-schema demo --metadata-schema demo
python src/tools/refresh_training_graph.py
Use bindings for your intended target. Preview resolves model/search/recipe settings but does not read Delta or fit the model. Deployment and an actual run follow the Bundle walkthrough. The examples are locally exercised with 40 numeric rows, a separate 8-row final holdout and saved/reloaded predictors. Those checks do not create a table, deploy a job or establish Spark-worker/serving admission for a model or recipe.
Grid search
Choose grid when every combination has a reason to exist. Four alpha
values times two intercept values mean eight candidates, each evaluated on the
configured training folds, then a best-model refit. Cost grows multiplicatively.
| Control | Grid behavior |
|---|---|
strategy |
Set grid; omission selects random. |
search_space |
Finite Cartesian product; empty resolves the model's grid defaults. |
max_candidates |
Default 1000; admission bound on that full product, not truncation. |
n_trials |
Default 10, still validated, but does not limit grid enumeration. |
random_state |
Default 42; does not randomize grid enumeration. CV and estimator seeds remain separate. |
strategy_params / timeout |
Only {} and omitted/null, respectively. |
All other common fields retain the contract above. This four-candidate example
deliberately has n_trials: 1 to make its non-capping behavior explicit:
models:
revenue:
model:
type: ridge_regression
params: {fit_intercept: true}
tuning:
strategy: grid
metric: rmse
n_trials: 1
max_candidates: 4
random_state: 42
search_space: {alpha: [0.1, 1.0, 10.0, 100.0]}
Common mistake: reducing n_trials to shrink an expensive grid. Reduce its axes
or use random search; raising max_candidates permits more work rather than
making it faster.
Random search
Choose random to explore several axes within a fixed candidate budget.
With Bundle finite-list axes, sampled combinations are drawn without replacement;
if the Cartesian product is smaller than n_trials, only that many distinct
combinations are available. This is parameter sampling, not row sampling.
| Control | Random behavior |
|---|---|
strategy |
random, also the default. |
search_space |
Empty resolves the model's random defaults; nonempty lists replace them. |
n_trials |
Default 10; 1..1000 sampled candidates, limited by available combinations. |
random_state |
Default 42; seeds candidate sampling. |
max_candidates |
Validated but does not cap random candidates; use n_trials. |
strategy_params / timeout |
Only {} and omitted/null, respectively. |
All other common fields apply. Four trials here exhaust the small example space; for a larger space they would select only four combinations.
models:
revenue:
model:
type: ridge_regression
params: {fit_intercept: true}
tuning:
strategy: random
metric: rmse
n_trials: 4
random_state: 42
search_space: {alpha: [0.1, 1.0, 10.0, 100.0]}
Common mistake: expecting a search seed to control the estimator's randomness.
For a random forest also choose its fixed model.params.random_state.
Successive halving over a grid
Choose halving_grid when ranking candidates with a smaller budget is a
useful approximation to ranking them with the full budget. It begins with the
grid, increases the resource by factor, and retains roughly one in factor
candidates. An early weak candidate is eliminated even if it would have won at
full resources. The chosen model is subsequently refitted on the training data.
The two halving strategies accept exactly these strategy_params:
| Field | Type | Default | Meaning and bounds |
|---|---|---|---|
factor |
Integer | 3 |
2..10; resource multiplier and approximate elimination ratio. |
resource |
String | n_samples |
Training-row resource, or a real compatible estimator parameter such as n_estimators. The wizard offers those two; custom YAML names must exist on the prepared estimator. |
min_resources |
Integer or string | exhaust |
With n_samples: exhaust, smallest, or 2..1,000,000. exhaust selects a starting budget intended to use the last round efficiently; smallest starts at the estimator/task minimum. With an estimator resource, an explicit integer is required. |
max_resources |
Integer or string | auto |
With n_samples: auto uses available training rows; integers 2..1,000,000 are capped to the current search population, including nested folds. Estimator resources require an explicit integer in that range. |
ASCII decimal strings such as "16" normalize to integer resource counts.
min_resources must not exceed max_resources or the available sample budget.
Small numeric sample budgets must still leave viable training/validation folds
and enough rows per class. There is no aggressive_elimination, n_candidates
or other sklearn scheduler setting in Bundle strategy_params.
For an estimator resource, remove that parameter from both fixed params and
manual search axes. For example, resource: n_estimators requires explicit
min_resources: 10 and max_resources: 40 on a compatible forest, without a
fixed/searched n_estimators. Automatic spaces remove the resource axis, but an
explicit fixed value still conflicts. Non-count parameters may exist on an
estimator yet be unsuitable resources; use a positive training-budget parameter.
| Control | Halving-grid behavior |
|---|---|
strategy |
Set halving_grid. |
search_space / max_candidates |
Same initial grid/product bound as grid search. |
n_trials |
Default 10, validated but does not cap the initial grid or later rounds. |
random_state |
Default 42; controls halving sampling where used. |
timeout |
Omitted/null; no halving timeout control. |
All remaining common fields apply. With enough rows, this example evaluates four candidates at 8 rows, two survivors at 16 and one at 32: seven candidate evaluations, each with two CV folds. The resource describes the population budget before fold fitting, not eight rows per individual fold.
models:
revenue:
model:
type: ridge_regression
params: {fit_intercept: true}
tuning:
strategy: halving_grid
metric: rmse
max_candidates: 4
random_state: 42
search_space: {alpha: [0.1, 1.0, 10.0, 100.0]}
strategy_params:
factor: 2
resource: n_samples
min_resources: 8
max_resources: 32
Common mistake: using the default max_resources: auto with n_estimators, or
fixing the same resource in model.params. Both fail admission.
Successive halving over random candidates
Choose halving_random when a full grid is too large and a small resource
budget provides a useful initial ranking. It first samples up to n_trials
candidates, then eliminates candidates and increases resources just as
halving-grid does. Its registered automatic search space uses the random family.
| Control | Halving-random behavior |
|---|---|
strategy |
Set halving_random. |
n_trials |
Default 10; 1..1000 initial candidates, not total round evaluations. |
random_state |
Default 42; seeds initial candidates and resource sampling. |
max_candidates |
Validated but does not cap sampled candidates or later rounds. |
strategy_params |
Exactly factor, resource, min_resources, max_resources, with the same types/defaults/bounds as halving-grid. |
timeout |
Omitted/null. |
All remaining common fields apply. The example has the same possible four, two, one round counts as halving-grid; a larger candidate space makes its first round a sample instead of an exhaustive grid.
models:
revenue:
model:
type: ridge_regression
params: {fit_intercept: true}
tuning:
strategy: halving_random
metric: rmse
n_trials: 4
random_state: 42
search_space: {alpha: [0.1, 1.0, 10.0, 100.0]}
strategy_params:
factor: 2
resource: n_samples
min_resources: 8
max_resources: 32
Common mistake: treating n_trials: 4 as four total fits. Surviving candidates
are evaluated again, and every evaluation includes the selected CV splits.
Optuna search
Choose optuna for adaptive TPE/CMA-ES proposals or supported early pruning.
The training environment needs optuna and the sklearn integration
optuna-integration; CMA-ES additionally needs cmaes. Select the wizard's
matching optional dependencies and check the generated training requirements.
Installing Optuna alone is insufficient for Bundle admission.
| Control | Type | Default | Meaning |
|---|---|---|---|
n_trials |
Integer | 10 |
1..1000 attempted trials; repeated configurations are possible. |
random_state |
Integer | 42 |
Seeds the sampler; the common integer bounds apply. |
timeout |
Number or null | null |
Positive finite seconds. A running fit is not interrupted; nested CV can start separate studies, so this is not a global deadline. |
strategy_params.sampler |
String | tpe |
tpe, random, cmaes; no additional sampler-constructor kwargs. |
strategy_params.pruner |
String | median |
median, hyperband, none; no additional pruner-constructor kwargs. |
strategy_params.pruning |
Boolean | Omitted permits supported pruning; wizard emits true |
false disables pruning; pruner: none also disables it. |
All remaining common fields apply; max_candidates does not cap Optuna.
search_space: {} uses the registered random-family space. With TPE or the
random sampler, each list is categorical, including numeric lists. CMA-ES
instead converts a numeric list into an inclusive min/max integer range or a
continuous float range. Thus [0.1, 10.0] can produce intermediate values under
CMA-ES. Strings, booleans and null-containing lists remain categorical and use
independent random fallback where CMA-ES cannot handle them.
Pruning depends on actual estimator capabilities and validation. Supported native boosters can report iteration checkpoints; incremental estimators can report training updates; ordinary estimators can prune between completed CV folds. A single complete fold has no remaining fold work to prune. The logs state which mode is available. Pruned and failed trials cannot become the selected model; if none completes, there is no winner to refit. Choosing a pruner does not guarantee that any trial will be pruned, especially in a small study with its pruner's default warm-up behavior.
This example disables pruning to make the four-trial local exercise predictable:
models:
revenue:
model:
type: ridge_regression
params: {fit_intercept: true}
tuning:
strategy: optuna
metric: rmse
n_trials: 4
random_state: 42
search_space: {alpha: [0.1, 1.0, 10.0, 100.0]}
strategy_params: {sampler: tpe, pruner: none, pruning: false}
Common mistake: assuming numeric lists always mean discrete choices, or passing an Optuna distribution object in YAML. Check the sampler's semantics above.
CV, nested CV and total work
All five strategies use the workflow's shared CV contract. Configure it beside
task and target_column, usually under defaults, rather than inside tuning.
| Workflow control | Type/default | Search behavior |
|---|---|---|
cv_enabled |
Boolean, false |
False still searches on one training-only 80/20 validation split. It does not disable tuning or use the final holdout. |
cv_type |
String, k_fold |
k_fold, stratified_k_fold, shuffle_split, time_series_split, group_k_fold, stratified_group_k_fold, nested_cv. |
cv_folds |
Integer, 5; 2..20 |
Fold/repeated-split count; outer-fold count for nested CV. |
cv_shuffle |
Boolean, true |
Must be false for temporal CV and true for shuffle-split. |
cv_random_state |
Integer, 42; 0..4,294,967,295 |
CV seed, distinct from search sampling and final holdout seeds. |
cv_inner_folds |
Integer or null, null; explicit 2..20 |
Nested search folds. Null chooses min(3, cv_folds - 1) when outer folds exceed two, otherwise two. |
cv_nested_type |
String, auto |
Nested only: auto selects stratified k-fold for classification, k-fold for regression. Explicit k-fold, stratified, time-series, group or stratified-group policies are supported. |
cv_group_column |
String or null, null |
Required for group policies; not a model feature. Group search requires enabled CV. |
cv_gap |
Integer, 0 |
Nonnegative row gap for temporal CV. |
cv_test_size |
Integer or null, null |
Positive validation-window row count for temporal CV. |
cv_max_train_size |
Integer or null, null |
Positive maximum training-window row count for temporal CV. |
Temporal search additionally needs the workflow's explicit event_column and
enabled CV; it preserves chronological boundaries. Stratified policies require
classification. Group/time metadata, class counts and actual fold sizes are
validated before fitting. Every admitted CV fold needs at least two training
and two validation rows; model-specific requirements can be stricter. Learned
preprocessing is fitted anew inside each training fold. Preprocessing splitter
nodes are rejected because the Bundle owns splitting.
For nested CV, set cv_type: nested_cv, for example cv_folds: 3 and
cv_inner_folds: 2. Each outer training partition runs its own search; its
winner is evaluated on that outer validation partition. After outer evaluation,
a fresh full-training search selects the saved model. The outer scores estimate
the search procedure; the final search's best score is not the outer estimate.
The untouched final holdout remains separate. Stacking/calibration internal
folds add further fits; they do not replace these validation levels.
Temporal and grouped evaluation
Time-series CV needs a selected observation window, an event_column and
cv_shuffle: false. Normalized timestamps order the folds and are removed before
preprocessing/model fitting. Equal timestamps across a fold boundary fail;
adjust folds or aggregate observations instead of allowing time leakage.
Nested temporal CV requires a temporal final holdout. cv_gap, cv_test_size
and cv_max_train_size are row counts at both nested levels and the separate
final search. A null maximum training size expands the window; an integer rolls it.
Grouped CV requires cv_group_column, excluded from model inputs. A random
final holdout selects whole groups using the saved seed; test_size is the
proportion of groups and stratify must be false. A temporal final holdout must
also contain disjoint groups. Missing groups/classes or insufficient groups fail
before fitting; row filters retain aligned group/time metadata.
During initialization, ordinary or nested temporal CV fixes the final split to
temporal, hides random/shuffle questions and opens the clock/window questions.
The default becomes a rolling-calendar holdout; explicit fixed-window dates
remain pinned. Group and other non-temporal CV retain the selected final split.
These wizard rules do not rewrite an existing config/training.yml.
See nested-temporal-init.example.json and
nested-group-threshold-init.example.json in the template's examples directory.
Nested binary tune_threshold generates inner out-of-fold probabilities for
each selected recipe; outer and final holdout labels never choose its threshold.
Multiclass or models without probabilities fail this legacy mode. Use the
explicit decision-threshold policy for its
supported alternatives. tuning.json records fold thresholds and provenance.
cross_validation.json separates outer selection scores, selected parameters
and mean/std from the final search. Fixed-model CV retains stability diagnostics;
historical artifacts without nested-search evidence remain diagnostic. Negative
loss scorers stay negative and larger is better. The independent heldout_*
metrics determine promotion. Search settings/results appear in tuning.json
and the train_and_tune summary. pandas and Polars use this same evaluation path.
Competition budget
In model_competition, candidates share CV and the selection objective.
competition_max_candidates defaults to 8 (2..32); the wizard offers up to
eight candidate slots. competition_max_trials defaults to 100 (1..10,000).
It rejects excessive planned work before source access; it does not truncate
candidates or measure CPU time. The trial bound is calculated as follows:
- A fixed model contributes one candidate evaluation. Grid contributes its
Cartesian product; random and Optuna contribute requested
n_trials. - Halving adds survivor rounds to that starting count. With starting count
Nand factorf, it sums1 + floor(log_f(N))rounds, repeatedly retainingceil(previous_count / f). This is conservative when resources stop early. - Nested CV multiplies this count by
cv_folds + 1, covering outer searches plus the final full-training search. Ordinary CV folds still multiply actual estimator fits; they are not another multiplier in this trial-bound field. - Sum across all candidates. An active explicit decision-threshold policy can add independent outer policy fits/searches; allow for that extra work too.
For the four-candidate, factor-two halving example, the ordinary bound is
4 + 2 + 1 = 7; with two outer folds it is 7 * (2 + 1) = 21. A budget of 6
or 20 respectively is rejected even if one expects an early stop. A second
candidate adds its own bound. Competition parallelism remains one candidate at
a time. Review preview's effective settings before increasing the budget.
Voting and stacking ensembles
An ensemble is one model candidate containing several learners. In contrast,
competition evaluates separate candidates and selects one winner. The
ensemble guide covers the voting_* and
stacking_* model.params fields, nested member search axes, worked voting,
stacking and calibrated examples, and the wizard questions for each slot.
Continue with the model catalog, training field reference, and preprocessing recipes to choose a complete training recipe. A successful local search does not grant its fitted preprocessing chain Spark-worker or online-serving support; use the selected inference mode's own admission and verification path.