Skip to content

Settings Guide

Every training and evaluation setting explained: what it does, what it defaults to, a concrete example, and — most importantly — which other settings it interacts with.

This page is the deep companion to the Settings Reference. If you want a compact table of all parameters, use the reference. If you want to understand why a setting exists and how it fights with the others, stay here.

How to read this page

Each setting is documented the same way:

Block Meaning
Default The value used when you omit the key
Example A copy-pasteable YAML fragment
Values A table of practical values and their effect
Interacts with The settings that change this setting's behaviour

!!! note "Measured numbers" The tables marked (measured) were produced by training on a 4 000-row, 4-feature synthetic dataset with otherwise default settings. They show the direction and rough size of each effect, not a guarantee for your data.

Where each setting acts

Most settings act at one of four places in the pipeline. Knowing where a setting acts explains most of its interactions:

flowchart TB subgraph S1["1. Sample the node"] A1["max_eval_fit
rows per class per node"] A2["min_eval_fit
stop below this"] A3["frac_eval_cat
count / group split"] end subgraph S2["2. Bin the features"] B1["n_cat
bins per feature"] end subgraph S3["3. Score split candidates"] C1["n_rep
repetitions"] C2["adaptive_n_rep
scale repetitions down"] C3["n_dims
dimensions combined"] C4["calcs_per_dim
combination budget"] C5["score_method
scoring formula"] C6["smoothing
pseudo-counts"] C7["min_split_improvement
barrier for dim >= 2"] C8["pair_corr_filter
mask redundant features"] C9["pair_corr_min_main"] C10["pair_corr_max_features"] end subgraph S4["4. Cut the node"] D1["spore_diff
branches per cut"] D2["max_depth
hard depth cap"] end subgraph S5["5. Score the leaf"] E1["leaf_score
leaf formula"] E2["leaf_alpha
leaf prior"] end S1 --> S2 --> S3 --> S4 --> S5 style A2 fill:#ffcccc style D2 fill:#ffcccc

The two stop conditions are highlighted in red: min_eval_fit and max_depth. A node becomes a leaf as soon as either fires.


Tree size and shape

These settings decide how big a model you get and how it is shaped.

max_depth: the hard depth cap

Default: 100

max_depth is the maximum number of dot-separated segments in a spore's depth field. Because every segment corresponds to one split on the path from the root to that leaf, max_depth is exactly the maximum number of splits that separate a leaf from the root.

The check lives in _train_pilz_node() and runs before the node splits:

# src/pilz/service/train.py
if train_df.is_final_size() or len(depth.split(".")) >= self.settings.max_depth:
    return self.make_spore(...)   # stop, this node becomes a leaf

Because the stop happens before splitting, the depth of the leaves can never exceed max_depth — that invariant holds for every value.

How the depth string is built

The root starts with an empty depth "". Each branch appends its own segment, and a branch whose bin combination was never observed in the count sample gets the residual segment n:

Depth string Segments Path from root Spores in this tree
"" 0 (root) no split yet 1
"0" 1 one split 1
"0.0" 2 two splits 1
"0.3.n" 3 two splits, then the residual branch 1

The dot-separated encoding is what makes a leaf's path exactly reconstructible — see Splitting Nodes into Branches.

The one edge case: max_depth: 1

Python splits an empty string into a one-element list, so "".split(".") has length 1, not 0. The root and its direct children therefore count the same:

>>> "".split(".")     # -> ['']      -> counted as 1
>>> "0".split(".")    # -> ['0']     -> counted as 1
>>> "0.0".split(".")  # -> ['0', '0'] -> counted as 2

Consequence: max_depth: 1 produces a tree without any split — a single leaf that predicts one constant score for every row. It is a degenerate setting, useful only as a smoke test that the data pipeline works.

For every value from 2 upwards the behaviour is regular: the deepest leaf has exactly max_depth segments, i.e. max_depth splits from the root.

(measured, binary splits, n_dims: 1, n_cat: 2, min_eval_fit: 2)

max_depth Deepest leaf Segments Total spores
1 (root) 0 2 (one per target, no split)
2 0.0 2 8
3 0.0.0 3 16
4 0.0.0.0 4 32
5 0.0.0.0.0 5 64
6 0.0.0.0.0.0 6 125

max_depth is a safety net, not a knob

The default is 100 precisely so that it stays out of the way. In practice min_eval_fit stops the recursion long before the cap, because a node can no longer be split meaningfully once one class runs out of rows. Set max_depth only when you want a guaranteed bound regardless of data size — for example to keep the generated SQL small (see SQL Rules for Deployment).

!!! warning "Do not tune max_depth to fight overfitting" The Training settings description in the source code says it outright: "It is better not to set this parameter — min_eval_fit provides a more natural boundary for tree depth." Raising max_depth on data that is already depth-limited by min_eval_fit changes nothing at all.

Interacts with: spore_diff (decides how many nodes exist per level), min_eval_fit (the setting that usually binds first), n_dims and n_cat (decide the maximum branch count per level).


min_eval_fit: the real depth knob

Default: 10

Minimum number of rows one class must still have at a node for the recursion to continue. If either class drops below this, the node becomes a leaf:

# src/pilz/model/dataframes.py
def is_final_size(self) -> bool:
    return (
        self.target_df_size < self.min_size
        or self.non_target_df_size < self.min_size
    )

This is the setting that actually determines tree depth in almost every case. It works because each split roughly divides the rows, so raising min_eval_fit raises the number of levels the tree can sustain.

(measured, max_depth: 100 so the cap never binds, n_dims: 1, n_cat: 2)

min_eval_fit Deepest leaf (segments) Total spores Model size
2 11 576 large
10 (default) 8 134 medium
50 5 33 small
200 3 12 very small
400 2 6 near-constant

That is almost a tenfold change in model size from one setting.

Interacts with: max_depth (the two stop conditions compete — whichever fires first wins), max_eval_fit, frac_eval_cat, and n_cat (a node needs at least n_cat rows per class before its bins mean anything).


max_eval_fit: rows per node

Default: 1000

Upper bound on how many rows per class are read for one node. The cached frame is shuffled once at load time, so the first matching rows are already a random sample — no per-node ORDER BY RANDOM().

max_eval_fit: 1000   # at most 1000 target + 1000 non-target rows per node

Interacts with: min_eval_fit — max_eval_fit caps what a node can see, min_eval_fit decides when the remainder is too small to split. Raising max_eval_fit gives deeper nodes more to work with but costs time on every node. See Downsampling and Imbalanced Data.


spore_diff: the branch condenser

Default: 1.0

Controls how many branches one cut produces. The winning split's observed bin combinations are sorted by their regularized target/non-target difference, and the adjacent pair with the smallest gap is merged repeatedly while the gap stays below spore_diff — as long as at least two observed branches remain.

Value Branches per cut Use when
0.0 one per observed combination (fan-out), plus residual n sparse high-dimensional tables where the grouping is information loss
0 < x < 1 intermediate fan-out you want to tune the trade-off
1.0 (default) 2 (binary) almost everything — clean, compact, fast to score

Example:

spore_diff: 0.0   # keep every cell combination as its own branch

Interacts with: max_depth — together they set the model size, because total spores grow roughly as branches_per_level ** max_depth. This is the single most important interaction on this page:

(measured, n_dims: 2, n_cat: 3, min_eval_fit: 2)

spore_diff max_depth: 2 max_depth: 3 max_depth: 4
1.0 (binary) 8 spores 16 spores 34 spores
0.0 (fan-out) 24 spores 96 spores 500 spores

The theoretical worst case is (n_cat ** n_dims + 1) ** max_depth spores — for n_cat: 3, n_dims: 2, max_depth: 4 that is 10 ** 4 = 10 000 spores. Lowering max_depth is the emergency brake if you enable fan-out.

See Splitting Nodes into Branches for the exact merge algorithm.


n: how many trees per target

Default: 1

Number of independently built trees per target value. Every tree sees the same data and the same settings but different random repetitions, so the trees disagree — that disagreement is the ensemble.

Example:

n: 5   # 5 trees for every target value

Interacts with: everything downstream — total training time scales linearly, and inference cost scales with n. See same_target_pilz_comb_method for how the trees get merged, and Ensemble Combination for the mechanism.


n_dims: how many features combine per cut

Default: 3

How many dimensions (features) may be combined into one split candidate. 1 = single features only, 2 = pairs, 3 = triplets.

Example:

n_dims: 2   # only single features and pairs

Interacts with: calcs_per_dim (the budget is spent per dimension, and n_dims adds whole stages), pair_corr_filter (inactive at n_dims: 1, since there are no combinations), min_split_improvement (only applies from n_dims: 2 upwards), and spore_diff (more dimensions means more bin combinations per cut, so fan-out branches multiply faster). See Multi-Dimensional Splits.


n_cat: bins per feature

Default: 3

Number of categories each feature is binned into before any scoring happens. Finer bins mean more precise cuts but sparser cells.

Example:

n_cat: 5   # quintiles instead of terciles

Interacts with: min_eval_fit (a node needs at least n_cat rows per class for its bins to be meaningful), adaptive_n_rep (the repetition thresholds are multiples of n_cat), and spore_diff via the branch cap n_cat ** n_dims + 1. See Feature Categorization.


calcs_per_dim: the combination budget

Default: 5000

Maximum number of feature combinations evaluated per dimension stage. null means no limit. The limit is checked after a wave, so a stage evaluates up to calcs_per_dim + 1 combinations.

Example:

calcs_per_dim: 20000   # thorough search on wide datasets

Interacts with: n_dims (each dimension stage gets its own budget), n_rep (more repetitions mean more work per combination, but the count of combinations the budget permits does not change), and pair_corr_filter (masked combinations are skipped without consuming budget, so enabling the filter effectively stretches the budget).


Split quality

These settings decide which cut is chosen, not how deep the tree goes.

score_method: the scoring formula

Default: "diff_dir_1.5"

How the discriminating power of a split candidate is computed. All variants are built on the proportion difference diff_plain = p - q, where p is the target share of a bin and q the non-target share.

Value Formula Character
"diff_all_1.0" 0.5 * sum(abs(p - q)) dimension-neutral, linear
"diff_all_1.5" 0.5 * sum(abs(p - q) ** 1.5) dampens near-zero cells
"diff_all_2.0" 0.5 * sum((p - q) ** 2) quadratic, strong cells dominate
"diff_dir_1.0" directional sum of abs(p - q) dominant side wins
"diff_dir_1.5" (default) directional, abs(p - q) ** 1.5 entropy-style weighting
"diff_dir_2.0" directional, squared strongest cells dominate
"lift_dir_1.0" directional n * abs(ln(n / E)) expected log-likelihood lift, mass-weighted
"lift_dir_1.5" directional lift ** 1.5 like the default but lift-based
"lift_dir_2.0" directional lift ** 2.0 strongest cells dominate
"hellinger" 0.5 * sum((sqrt(p) - sqrt(q)) ** 2) dimension-neutral, noise-tolerant

The diff_all_* family is dimension-neutral: every bin contributes equally regardless of which side of the split favours which class. The diff_dir_* and lift_dir_* families are directional: contributions are summed separately for target-favouring and non-target-favouring bins and only the dominant side counts. This matters when the two sides are very unbalanced.

!!! warning "lift_dir_* prefers high-dimensional splits" lift_dir_* multiplies each cell's log-likelihood lift by its row count n, so cells with many rows dominate the score. The winning combination must therefore isolate those cells across several features at once, which pushes the winner towards high dimensionality. Measured on the airline satisfaction dataset (n_dims: 6, n_cat: 3, max_depth: 3):

| | `diff_dir_1.5` | `lift_dir_1.5` |
|---|---|---|
| Winner's dimensionality | 1–3 | **3–6** |
| Cells in the winner's table (mean) | 6 | **200** |
| Peak RSS | 0,59 GiB | **5,43 GiB** |

Because the winner's correlation table grows with
`(n_cat + 1) ** dimensionality`, a high `n_dims` combined with
`lift_dir_*` can exhaust memory. If you hit that, lower `n_dims` first —
`n_rep` and `adaptive_n_rep` do **not** help. See
[BUG.md](https://gitlab.com/gwhe/pilz/-/blob/dev/BUG.md) B2.

Example:

score_method: "hellinger"   # robust on sparsely populated bins

Interacts with: smoothing (smoothing is applied to the proportions all these formulas consume), n_cat (more bins means more terms in the sum, so the choice of exponent changes the noise floor), and n_dims — critically, the lift_dir_* family actively rewards high-dimensional winners, which is the opposite of the dimension-neutral diff_all_* family.


smoothing: Laplace smoothing

Default: 0.0

Pseudo-count added to both sides of every bin before the proportions used for scoring are computed. 0 disables it. Smoothing only affects candidate scoring — the branch classification always uses the raw proportions, so the model structure is untouched.

Example:

smoothing: 1.0   # standard add-one Laplace smoothing

Interacts with: score_method and n_cat — smoothing exists to damp sparse cells, so it only matters when cells are sparse. Note this is a different smoothing from leaf_alpha, which smooths leaf scores instead of candidate scores.


n_rep: repetitions per candidate

Default: 5

Each feature is categorized over n_rep independent random splits of the node into count and group sets. Every repetition is scored, and the median repetition is selected. More repetitions means more stable cuts and more time.

n_rep: 3   # lighter repetition

Interacts with: adaptive_n_rep (which can lower this automatically), n_dims (for n_dims >= 2 each combination is built with the matching repetition of each feature, preserving row alignment), and frac_eval_cat (which decides how many rows land in the count versus the group half). See Downsampling.


adaptive_n_rep: repetitions that scale down

Default: true

Repeating every candidate is wasteful on nodes that already have plenty of rows. With adaptive_n_rep on, the repetition count is reduced per node based on how many group rows the smaller class has — the group half (1 - frac_eval_cat) of it, because that is what the cuts are built from:

Condition Repetitions used
group >= 44 * n_cat 1
group >= 16 * n_cat min(n_rep, 3)
below that n_rep (no reduction)

With n_cat: 3 that means one repetition from 132 group rows, three from 48.

Example:

adaptive_n_rep: true    # cheap on large datasets

Interacts with: n_rep (the ceiling), n_cat (the thresholds are multiples of n_cat), and frac_eval_cat (it scales the group size, so raising frac_eval_cat makes the reduction kick in later).


min_split_improvement: the dimensional barrier

Default: 0.0

A multi-dimensional candidate (n_dims >= 2) is only selected when its score beats the current best split by at least this relative factor. 0 disables the requirement.

Example:

min_split_improvement: 0.05   # a pair must be 5% better than the best single feature

Interacts with: n_dims (inactive at n_dims: 1) and smoothing — smoothing raises the floor for sparse candidates, which is the same noise problem this barrier guards against.


pair_corr_filter: the redundancy mask

Default: true

In every dimension stage, each evaluated candidate checks its joint correlation table. If both classes put more than pair_corr_min_main of their mass into the dominant bins, one feature of the combination is redundant given the rest — so the 1D-weakest feature of the candidate is masked immediately and is no longer used for any further combination in this node.

Example:

pair_corr_filter: false   # keep every feature eligible

Interacts with: n_dims (inactive at 1), pair_corr_min_main, pair_corr_max_features and calcs_per_dim (masked combinations are skipped for free, so the budget stretches further). See Pair-Correlation Filter.


pair_corr_min_main: concentration threshold

Default: 0.9

The minimum share of both classes that must sit inside the dominant bin for a candidate to count as correlated. It mirrors the 1D is_diff_to_low threshold of 0.9.

Example:

pair_corr_min_main: 0.95   # only prune near-deterministic dependencies

Interacts with: pair_corr_filter (higher = less aggressive pruning).


pair_corr_max_features: limit the pair stage

Default: null (no limit)

Restricts the dimension-2 candidate check to the top-K features by 1D score. A useful guard on wide datasets such as pixel data, where the number of pairs grows quadratically.

Example:

pair_corr_max_features: 50   # only check the 50 strongest features for pairs

Interacts with: pair_corr_filter and the total feature count. See the MNIST Example for a real 784-feature use.


Leaf scores

These settings decide what value a leaf stores, which is what inference actually reads.

leaf_score: leaf formula

Default: "log_odds"

Value Formula Character
"log_odds" ln((target + leaf_alpha) / (non_target + leaf_alpha)) unbounded, antisymmetric under swapping the classes, comparable across targets
"weakened_diff" (target - non_target) / n * (1 - ln(n) / n) legacy, bounded to [-1, 1]

log_odds exists because it is designed to be combined with mean across trees and then passed through a softmax across targets, which yields approximate posteriors.

Example:

leaf_score: "weakened_diff"   # legacy bounded score

Interacts with: leaf_alpha (only used by log_odds), same_target_pilz_comb_method (the bounded score is less suited to mean), and min_eval_fit (small leaves are exactly where leaf_alpha and the weakening factor matter). See Imbalanced Data.


leaf_alpha: prior for the leaf score

Default: 0.5

Pseudo-count (Beta prior concentration) used by leaf_score: "log_odds". 0.5 is the Jeffreys prior, 1.0 the Laplace prior. Larger values pull small leaves harder towards the balanced score 0.

Example:

leaf_alpha: 1.0   # classic Laplace prior, stronger shrinkage on small leaves

Interacts with: leaf_score (ignored unless log_odds) and min_eval_fit (how small leaves actually get). Not to be confused with smoothing, which smooths candidate scoring.


Data sampling

frac_eval_cat

Default: 0.5

Fraction of each class's rows used as the count half; the remainder is the group half. The group half carries the weights and drives the binning, the count half supplies the bin codes that form the correlation tables.

frac_eval_cat: 0.5   # equal halves

Interacts with: adaptive_n_rep (it scales the group size the thresholds compare against), n_rep and min_eval_fit (both evaluated against rows derived from this split). See Downsampling.


train_files and out_folder

Required. The files to train on (.csv or .parquet) and the directory the trained Pilze are written to. See the DataCard Reference for the matching input description.

train_files:
  - /path/to/train.csv
out_folder: my_model

Training is resumable: existing trees are skipped, so re-running the same command continues where it stopped. Deleting out_folder forces a full retrain.


Evaluation settings

These live in EvalSettings and only affect pilz eval and pilz infer.

same_target_pilz_comb_method: combining trees

Default: "mean"

How the n trees of one target label are merged: mean or max. mean is more robust — with max a single outlier tree dominates the whole ensemble.

Example:

same_target_pilz_comb_method: "max"   # let the most confident tree win

Interacts with: n (only meaningful when n > 1) and different_target_pilz_comb_method — the two are not independent: softmax is documented as identical to max when same_target_pilz_comb_method is max, and can differ under mean. See Ensemble Combination.


different_target_pilz_comb_method: cross-target argmax

Default: "softmax"

How the per-target scores become one prediction.

Value Behaviour
"softmax" Applies a sigmoid nonlinearity below the tree aggregation: per tree the log-odds becomes a probability, then the trees are aggregated, then the cross-target argmax runs over the resulting probabilities
"max" Plain argmax over the per-target scores
"youden" Predicts the largest threshold margin, with a majority-class fallback

Example:

different_target_pilz_comb_method: "youden"   # threshold-margin prediction

Interacts with: same_target_pilz_comb_method (the two interact, see above) and leaf_score (softmax expects the unbounded log_odds scale that log_odds produces).


max_parallel_where: SQL batching

Default: 100

Maximum number of WHERE clauses per generated SQL statement. A tree whose spore count exceeds this is scored through several statements instead of one.

!!! warning "Evaluation only" max_parallel_where belongs to EvalSettings. It has no effect during training — some older documentation listed it as a training setting, which was wrong.

Example:

max_parallel_where: 500   # fewer, larger statements

Interacts with: total spore count, which is driven by max_depth and spore_diff. Lower it when your database chokes on very large WHERE lists. See SQL Rules for Deployment.


keep_cols and out_file

Defaults: [] and null

keep_cols passes extra input columns through to the output next to the scores; out_file writes the result to a .csv or .parquet file.

keep_cols:
  - customer_id
  - signup_date
out_file: predictions.parquet

The interaction map

Read this table as "if you want this, change these together".

Goal Settings to combine Why together
Bigger model raise min_eval_fit, raise max_depth min_eval_fit is the real lever; max_depth only guarantees the ceiling
Smaller / faster SQL lower min_eval_fit, lower max_depth, spore_diff: 1.0 depth caps levels, spore_diff caps nodes per level
Fan-out model spore_diff: 0.0, and lower max_depth spores grow as branches ** depth — see the measured table
Faster training n_dims: 1-2, lower calcs_per_dim, n_rep: 1, adaptive_n_rep: true combinations and repetitions are the cost drivers
Better on wide data pair_corr_filter: true, pair_corr_max_features, higher calcs_per_dim the mask stops redundant pairs from eating the budget
Cleaner cuts higher n_cat, score_method: "diff_dir_1.5", smoothing: 1.0, higher n_rep finer bins need more evidence per cut
Imbalanced data high max_eval_fit, min_eval_fit near max_eval_fit / 20, leaf_score: "log_odds" the minority class must survive to the leaves
Stronger ensemble n: 5-10, same_target_pilz_comb_method: "mean", different_target_pilz_comb_method: "softmax" the combination methods assume the log_odds scale
Stable small data n_rep: 10, smoothing: 1.0, min_eval_fit: 20 more evidence per cut instead of more cuts
Lower memory ceiling lower n_dims, or switch to diff_dir_1.5 lift_dir_* wins high-dimensional splits, and the winner's correlation table grows with (n_cat + 1) ** n_dims

Avoiding a memory blow-up

Three settings reinforce each other and can exhaust RAM together:

  1. score_method: "lift_dir_*" wins high-dimensional splits (measured: dimension 3–6 instead of 1–3)
  2. n_cat and n_dims set the cell space (n_cat + 1) ** n_dims — at n_cat: 5, n_dims: 8 that is 1 679 616 cells per candidate
  3. spore_diff below 0.1 fans every node out into all observed combinations, multiplying the number of nodes

!!! note "Current high-dimensional residual handling" The residual branch now uses a compact CellSet membership condition instead of an Or term for every missing cell; Boolean filters are balanced and the expression cache is bounded per node. A local recursive smoke run with the corrected calcs_per_dim: 5000, n_dims: 8, n_cat: 5, and spore_diff: 0.01 reached 1,250 completed nodes in 120 s with a 2.7 GiB RSS peak, but was stopped by the tool timeout before the tree finished. The remaining original settings were unavailable, so this is not a full reproduction or a low-memory guarantee. See the B2 diagnostics.

If training aborts with SIGBUS/SIGSEGV and no error message, lower n_dims first. Changing n_rep or adaptive_n_rep does not help — those were measured and ruled out as the cause.

The two stop conditions

flowchart TB A["Node reached"] --> B{"min_eval_fit:
either class below it?"} B -->|yes| L["Leaf"] B -->|no| C{"max_depth:
segments >= cap?"} C -->|yes| L C -->|no| S["Categorize, score, cut"] S --> A style L fill:#ffcccc

Both are checked in the same if inside _train_pilz_node() in src/pilz/service/train.py, left to right — min_eval_fit is evaluated first, but since both lead to the same outcome the order only matters for which one you observe in the logs.


Ready-made recipes

Small, clean tabular data

train_files: [train.csv]
out_folder: model
n: 5
n_dims: 2
n_cat: 3
n_rep: 5
min_eval_fit: 10

Leave max_depth at its default — min_eval_fit bounds the depth.

Wide data (pixels, many correlated features)

train_files: [train.parquet]
out_folder: model
n: 3
n_dims: 2
n_cat: 3
calcs_per_dim: 20000
pair_corr_filter: true
pair_corr_max_features: 50
min_eval_fit: 5
max_depth: 8          # explicit: pixel data would otherwise run deep

The correlation mask does the heavy lifting here; max_depth is the safety net. Compare with the MNIST Example.

Highly imbalanced data

train_files: [train.csv]
out_folder: model
n: 5
max_eval_fit: 5000
min_eval_fit: 200
n_dims: 2
leaf_score: "log_odds"
leaf_alpha: 1.0

A large min_eval_fit prevents the minority class from being split into single-row leaves. See Imbalanced Data and the Credit Card Fraud Example.

Fast iteration

train_files: [train.csv]
out_folder: model_smoke
n: 1
n_rep: 1
n_dims: 1
calcs_per_dim: 100
min_eval_fit: 20
adaptive_n_rep: false

See also