Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 72 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,33 @@ not exclude samples from this analysis.
multiplicative (CZM) replacement; `replacement_delta` sets the replacement
fraction and optional `bayesian_alpha` overrides GBM's concentration. The
offset is not used. See [zero-handling notes](docs/statistics/zero-handling.md).
- `ancom_bc`: ANCOM-BC sampling-fraction bias-corrected log-linear model
(Lin & Peddada 2020, *Nat. Commun.* 11:3514). Estimates within-group sample
offsets and between-group sampling-fraction bias via an E-M Gaussian mixture
on `log(counts + 1)`, propagating bias-estimation variance into two-sided Wald
tests. The effect is a bias-corrected log2 fold change; recommended when
asymmetric shifts or microbial load changes would bias standard CLR centering
(requires at least 3 tested taxa; assumes fewer than half are differential).
- `aldex2`: Dirichlet(`count + 0.5`) Monte-Carlo CLR model (Fernandes et al.
2013, 2014). Draws `mc_samples` (default 128, seeded by the study's `seed`)
posterior instances per sample and reports standardized CLR effect sizes with
consistent-sign posterior predictive Welch's t-test p-values; recommended for
sparse or variable-depth cohorts where deterministic zero imputation is
undesirable.
- `songbird`: compositional baseline-category multinomial regression with
reference-frame invariant CLR-centred differentials summing to zero across
fitted taxa (Morton et al. 2019, *Nat. Commun.* 10:2719; Zhang et al. 2017).
Conditions on read totals without extra-multinomial biological overdispersion;
completely separated single-group taxa are marked `failed` rather than
diverging.
- `multinomial` and `dirichlet_multinomial`: one model for the whole
composition rather than a fit per taxon, reported as log ratios against a
reference taxon — `mn_reference_taxon`, or the most abundant fitted taxon when
empty, which must have reads in at least half the samples. `mn_penalty` is a
ridge penalty (0 for maximum likelihood); `dm_overdispersion_method` (`mle` or
`mom`) applies to the Dirichlet-multinomial only. A taxon with no reads in one
condition has an unbounded ratio: it is reported as failed with a
`complete separation` note and left out of the fit.

ZINB and hurdle models can overfit sparse data; do not select ZINB solely
because counts are sparse. Compare against NB diagnostics. The methods report
Expand All @@ -326,6 +353,41 @@ Implementation and reference tests: `src/analysis/differential.jl`,
`src/server/routes/analysis.jl`, `bench/compositional_differential/benchmark.jl`,
and `test/unit/test_differential.jl`.

### Evidence Mode

The **Evidence** tab turns a run's outputs into a citable epistemic record. A
claim is a sentence a reader could disagree with, a **scope** naming the part of
the data it is about (its *fibre*: the rows whose value at a rank, or whose
category in a set, is one of the named labels), and a **warrant** saying why it
follows. Claims are stored in `projects/{study}/[<group>/]<run>/evidence/claims.yml`
and can be downloaded as that exact YAML.

Three views share one record:

- **Claims** — the epistemic editor (avec fibre). A claim cannot be saved
without a scope and at least one value, and cannot be marked `supported`
without a measurement, citation or inference warrant: a definition alone
supports nothing.
- **Fibre & echo** — the fibre's share of every sample, its largest members, and
the same fibre seen through a second projection: the other classifier at the
same rank, and the parent rank. Agreement is reported with a verdict;
disagreement is a limit on how strongly the claim may be stated, and an echo
that cannot be computed says so rather than vanishing.
- **Residuals** — where the fibre departs from a null you declare, either
`proportional` (the fibre distributed across samples as the library is) or a
stated `claim_share`, with Pearson residuals, a log2 ratio and a χ² statistic
printed without a p-value. The worst departure is shown first.

The tab's Provenance card states whether `attestation.yml` is absent, degraded
or internally inconsistent before any claim is read. Longer notes on the
definitions, the schema and the limits are kept in `docs/evidence-mode.md`
(documentation under `docs/` is kept local by policy — see `.gitignore`).

Implementation: `src/core/evidence.jl` (schema, residuals, verdicts),
`src/server/routes/evidence.jl` (the SQL), `frontend/src/evidence/`
(editor, fibre visualiser, residual explorer) and `test/unit/test_evidence.jl`
with `test/unit/test_evidence_routes.jl`.

### Figures and report

The Figures tab lays analysis charts out on a page (A4, letter or a journal
Expand Down Expand Up @@ -662,13 +724,22 @@ analysis:
normalisation: hellinger # hellinger | none | rarefy | srs
depth: 0 # rarefy/srs reads per sample; 0 = smallest non-empty sample
differential:
method: nb_glm # nb_glm | clr_welch | ancom_bc | aldex2 | songbird
method: nb_glm # nb_glm | clr_welch | zinb | hurdle_nb | ancom_bc
# | aldex2 | songbird | multinomial | dirichlet_multinomial
offset: tss # count models: tss | rle
dispersion: ml # nb_glm: ml | glmGamPoi
min_prevalence: 0.0 # fraction of samples a taxon must appear in
replacement_method: bayesian_multiplicative
# clr_welch: bayesian_multiplicative | multiplicative_replacement
replacement_delta: 0.65 # clr_welch: strictly between 0 and 1
bayesian_alpha: ~ # clr_welch: null (GBM estimate) or a number > 0
mc_samples: 128 # aldex2: Dirichlet Monte-Carlo instances in [16, 4096]
mn_reference_taxon: ~ # multinomial/dirichlet: null = most abundant fitted taxon
mn_penalty: 0.0 # multinomial/dirichlet: L2 penalty, 0 = maximum likelihood
dm_overdispersion_method: mle # dirichlet_multinomial: mle | mom
max_iter: 500 # multinomial models: Newton iteration cap
tolerance: 1.0e-8 # multinomial models: convergence threshold
max_seconds: 300.0 # multinomial models: wall-clock budget for one fit
nmds:
max_stress: 0.2 # warn if NMDS stress exceeds this value
```
Expand Down
24 changes: 20 additions & 4 deletions config/defaults/pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -346,10 +346,26 @@ analysis:
# Differential abundance: compares two conditions on the raw read counts, with
# mandatory Benjamini-Hochberg adjusted p-values across the fitted family.
differential:
# Per-taxon model: nb_glm (negative-binomial GLM), clr_welch (Welch test on
# zero-replaced CLR values), zinb (zero-inflated NB), or hurdle_nb (logistic
# occurrence plus zero-truncated NB). Two-part models test both group effects
# jointly; their count-process fold change is reported separately.
# The model fitted across the two conditions.
# - nb_glm: negative-binomial GLM on read counts with a library-size offset;
# the effect is a log2 fold change.
# - zinb / hurdle_nb: a zero/occurrence process plus a count process, tested
# jointly; the count-process fold change is reported and the zero-process
# effect stays in diagnostics.
# - clr_welch: Welch's t-test on centred log-ratios over the taxa passing
# min_prevalence after zCompositions::cmultRepl zero replacement; the
# effect is a CLR difference, not a fold change.
# - ancom_bc: bias-corrected log-linear model (Lin & Peddada 2020) that
# estimates sample-specific and between-group sampling-fraction shifts via
# an E-M Gaussian mixture; the effect is a bias-corrected log2 fold change.
# - aldex2: Dirichlet(count + 0.5) Monte-Carlo CLR instances with consistent-sign
# Welch's t-test and Fernandes et al. (2014) standardized CLR effect size.
# - songbird: compositional baseline-category multinomial regression with
# reference-frame invariant CLR-centred differentials (Morton et al. 2019;
# Zhang et al. 2017); the effect is a CLR log-ratio differential, not an
# absolute fold change.
# - multinomial / dirichlet_multinomial: one joint fit of the whole
# composition, reported as log ratios against a reference taxon.
method: nb_glm
# How library size enters each model, as a log offset: tss (total reads per
# sample) or rle (median-of-ratios over the taxa present in every sample).
Expand Down
1 change: 1 addition & 0 deletions frontend/src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type {
ReferenceTreeDoc, ReferenceTreeSummary, ReferenceTreeDetail, AlignmentQC, TrimSettings,
CladeCumulusSourcesResponse, CladeCumulusRequest, CladeCumulusResponse,
CladeMoveValidationRequest, CladeMoveValidationResponse,
CladisticsSummary, CladisticsDetail, CladisticsCreateRequest,
} from './types'

import type { FigureDoc } from '../figure/types'
Expand Down
33 changes: 32 additions & 1 deletion frontend/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,16 @@ export interface DifferentialRow {
prevalence: number
}

export type DifferentialMethod = 'nb_glm' | 'clr_welch' | 'zinb' | 'hurdle_nb'
/**
* Methods accepted by `POST .../analysis/differential`. The first four model each
* taxon separately; `multinomial` and `dirichlet_multinomial` fit the whole
* composition at once, so they take a reference taxon and a penalty where the
* per-taxon models take a size-factor offset.
*/
export type DifferentialMethod =
| 'nb_glm' | 'clr_welch' | 'zinb' | 'hurdle_nb'
| 'ancom_bc' | 'aldex2' | 'songbird'
| 'multinomial' | 'dirichlet_multinomial'

/** Which row field holds the effect size, and what to call it. */
export interface DifferentialEffect {
Expand Down Expand Up @@ -457,6 +466,16 @@ export interface DifferentialResult {
mc_samples?: number
/** aldex2 only. */
seed?: number
/** multinomial and dirichlet_multinomial: taxon every log ratio is taken against. */
mn_reference_taxon?: string | null
/** multinomial and dirichlet_multinomial: L2 penalty on the coefficients. */
mn_penalty?: number
/** dirichlet_multinomial only: how the concentration is estimated. */
dm_overdispersion_method?: 'mle' | 'mom'
/** Compositional solvers: convergence threshold and cost ceiling. */
tolerance?: number
max_iter?: number
max_seconds?: number
}
diagnostics: {
n_taxa: number; n_tested: number; n_failed: number; n_boundary: number; n_filtered: number
Expand Down Expand Up @@ -487,6 +506,18 @@ export interface DifferentialResult {
mc_samples: number; seed: number; dirichlet_prior: number; test: string
n_taxa_in_composition: number; n_taxa_unobserved: number
}
/** Compositional models only: what the fit that produced these rows cost. */
model?: {
n_taxa_fitted: number
runtime_seconds: number
/** The taxon every log ratio was taken against. */
reference_taxon?: string | null
penalty?: number
n_parameters?: number
iterations?: number
converged?: boolean
loglik?: number
}
/** songbird only. */
songbird?: {
reference_taxon: string | null; reference_prevalence?: number
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/DifferentialPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ export function DifferentialPanel({ study, runs, option, aggregate }: {
<tr style={{ borderBottom: '2px solid var(--color-border)' }}>
<th style={{ ...head, textAlign: 'left' }}>{result.rank}</th>
<th style={head}>{result.effect.label}</th>
{['nb_glm', 'zinb', 'hurdle_nb'].includes(result.config.method) && <th style={head}>Estimate (ln)</th>}
{result.effect.key === 'log2_fold_change' && <th style={head}>Estimate (ln)</th>}
{['zinb', 'hurdle_nb'].includes(result.config.method) && <th style={head}>Zero-process log-odds</th>}
<th style={head}>SE</th>
<th style={head}>p</th>
Expand All @@ -326,7 +326,7 @@ export function DifferentialPanel({ study, runs, option, aggregate }: {
<tr key={r.taxon} style={{ borderBottom: '1px solid var(--color-border)' }}>
<td style={{ ...cell, textAlign: 'left' }}>{r.taxon}</td>
<td style={cell}>{formatStat(effectValue(result, r))}</td>
{['nb_glm', 'zinb', 'hurdle_nb'].includes(result.config.method) && <td style={cell}>{formatStat(r.estimate)}</td>}
{result.effect.key === 'log2_fold_change' && <td style={cell}>{formatStat(r.estimate)}</td>}
{['zinb', 'hurdle_nb'].includes(result.config.method) && <td style={cell}>{formatStat(r.zero_process_log_odds ?? null)}</td>}
<td style={cell}>{formatStat(r.standard_error)}</td>
<td style={cell}>{formatStat(r.pvalue)}</td>
Expand Down
23 changes: 17 additions & 6 deletions frontend/src/components/PipelineStages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,16 @@ test('dispersion uses the enum editor with both supported models', () => {
})
})

test('differential model editor exposes supported two-part NB methods', () => {
test('differential model editor exposes every method the backend accepts', () => {
// Every method the backend accepts, in one list: offering a method the server
// refuses, or hiding one it accepts, are both failures here.
expect(CONFIG_TYPES['analysis.differential.method']).toEqual({
kind: 'enum', options: ['nb_glm', 'clr_welch', 'zinb', 'hurdle_nb'],
kind: 'enum',
options: [
'nb_glm', 'clr_welch', 'zinb', 'hurdle_nb',
'ancom_bc', 'aldex2', 'songbird',
'multinomial', 'dirichlet_multinomial',
],
})
})

Expand All @@ -24,11 +31,15 @@ test('advanced zero handling exposes both methods, bounded delta and nullable al
expect(CONFIG_DESCRIPTIONS['analysis.differential.bayesian_alpha']).toContain('no posterior draws')
})

test('differential method enum lists all five compositional and count models', () => {
expect(CONFIG_TYPES['analysis.differential.method']).toEqual({
kind: 'enum',
options: ['nb_glm', 'clr_welch', 'ancom_bc', 'aldex2', 'songbird'],
test('compositional models expose their own settings and say what they mean', () => {
expect(CONFIG_TYPES['analysis.differential.mn_reference_taxon']).toEqual({ kind: 'string', nullable: true })
expect(CONFIG_TYPES['analysis.differential.mn_penalty']).toMatchObject({ kind: 'float', min: 0 })
expect(CONFIG_TYPES['analysis.differential.dm_overdispersion_method']).toEqual({
kind: 'enum', options: ['mle', 'mom'],
})
expect(CONFIG_TYPES['analysis.differential.mc_samples']).toEqual({ kind: 'int' })
expect(CONFIG_DESCRIPTIONS['analysis.differential.mc_samples']).toContain('Dirichlet')
// The penalty is the one setting a user is tempted to tune for significance,
// so the description has to warn against it.
expect(CONFIG_DESCRIPTIONS['analysis.differential.mn_penalty']).toContain('do not tune it for significance')
})
Loading
Loading