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:
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().
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:
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:
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:
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:
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:
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:
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:
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.
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:
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:
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:
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:
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:
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:
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:
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.
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.
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:
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:
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:
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.
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:
score_method: "lift_dir_*"wins high-dimensional splits (measured: dimension 3–6 instead of 1–3)n_catandn_dimsset the cell space(n_cat + 1) ** n_dims— atn_cat: 5, n_dims: 8that is 1 679 616 cells per candidatespore_diffbelow0.1fans 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¶
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¶
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¶
- Settings Reference — the compact parameter tables
- Training Internals — the algorithm the settings drive
- Downsampling —
max_eval_fit,min_eval_fit,frac_eval_cat - Feature Categorization —
n_cat - Multi-Dimensional Splits —
n_dims,calcs_per_dim - Pair-Correlation Filter — the
pair_corr_*family - Splitting Nodes into Branches —
spore_diff - Imbalanced Data — sampling and
leaf_score - SQL Rules for Deployment —
max_parallel_where - Best Practices — tuning order that respects the interactions