From table to features¶
What happens to the counts between the input files and the first model.
Aggregation to a taxonomic level¶
Counts are summed into taxon counts at the level named in dataset.tax_level:
phylum, class, order, family, genus, species, or asv for no aggregation,
where the features are the sequence-hashed ASV ids. MetaPhlAn profiles add SGB, the
species-level genome bins the profiler reports, again without aggregation. Each level is
modelled independently; the wrapper runs the list and stacks the results.
Placeholder labels ("uncultured", "unassigned", a bare rank prefix) are treated as unassigned and their reads dropped at that level, with the lost fraction printed in the report. Species are always formed as Genus epithet.
Why not model ASVs across studies
ASVs are comparable between datasets only through their sequences, and only within one amplicon region. In a multi-study compilation most ASVs occur in a single study, so ASV-level features cannot support a cross-study claim. The price is paid at species level, which keeps only a small share of the reads.
Depth normalisation¶
Library size varies between samples and systematically between studies; a model trained
on raw counts learns sequencing depth instead of biology. preprocessing.normalization
picks the transform:
| value | meaning |
|---|---|
tss_log (default) |
log10(relative abundance + pseudocount) |
tss |
relative abundance: count / sample total |
tss_clr |
centred log-ratio on the relative abundances |
none |
raw counts |
Every transform is computed per sample; nothing is fitted on the training data, so the
identical function applies to the external test set and to any future sample. Relative
abundance always comes first: dividing by the sample total removes depth, and only then
are logs taken. There is no plain clr option: a log-ratio on sparse counts leaves most
of the depth effect in the zeros. The settings and the full pre-selection feature
space are saved next to the models (normalization.json, reference_features.csv), so
new data can be aligned later.
Class filter and subsampling¶
Classes with fewer than preprocessing.min_samples_per_class samples are dropped before
anything is fitted, and the report names them. samples_to_keep < 1 keeps a random
fraction of each class and exists for quick test runs only.
Filtering and feature selection¶
Two mechanisms, and the second replaces the first.
Filtering (preprocessing.filtering) is a cheap filter inside every learner's
preprocessing graph: constant-feature removal at minimum, variance, correlation or
information-gain filters when asked. It is ignored while selecting: true, when the
notebook forces "minimal".
Selection (preprocessing.selecting: true) runs on the training data alone, and
with nested: true again inside every outer fold:
- an information-gain filter reduces the features to a budget — the largest of 500, five times the sample size, and 1% of the taxa — capped at 2,500;
- recursive feature elimination, driven by a random forest's impurity importance, steps down a ladder of subset sizes, scoring each by cross-validation on balanced accuracy (five folds, or fewer where the number of groups — the rarest class when there is no grouping — cannot carry five);
- the one-standard-error rule stops at the smallest subset whose score is within one standard error of the best.
The kept taxa go to metrics/selected_features.csv. With ten training samples or fewer,
an ensemble mode (four learners, fifty repetitions) replaces the single forest; it is
far more expensive.
Class balancing¶
preprocessing.smote controls class balancing inside the learner graph, so it happens
after the split and never touches test data. One gate governs every setting: nothing is
resampled unless the majority/minority ratio exceeds smote_imbalance_threshold and
every class has at least min_samples_per_class samples. A named method chooses how to
balance, not whether to.
With evaluation.prior_correction: true (the default), "" resolves to none. An
explicit method that the imbalance gate would apply stops the run: the
decision rule already corrects for the class
frequencies, and the two together over-predict the small classes. The table applies
with prior_correction: false.
| value | meaning |
|---|---|
"" (default) |
none under the default decision rule; otherwise smote on two classes, balance on more |
none |
never balance |
smote |
interpolate new minority samples anywhere in the minority class |
blsmote |
the same, only near the boundary with the majority |
adasyn |
the same, concentrated where the minority class is sparsest |
balance |
classbalancing: copy rows until every class matches the largest |
The report prints the method actually used, as Final SMOTE decision.
SMOTE is binary only
The SMOTE family interpolates towards one minority class. On a multi-class target it
closes the biggest gap and leaves the small classes as they were. smote, blsmote
and adasyn are therefore replaced by balance on such a target, and "" picks
balance there to begin with.
balance duplicates rows, so closing a wide gap means copying the smallest class
many times over. Where the majority dominates — dairy is two thirds of the samples in
config_cfmd_category.yaml — none with the default decision rule, scored on
balanced accuracy, which weights every class equally, is the better answer.