Skip to content
Merged
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
23 changes: 12 additions & 11 deletions config/defaults/pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -346,15 +346,17 @@ 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.
# Differential model: nb_glm (negative-binomial GLM), clr_welch (Welch test
# on zero-replaced CLR values), zinb or hurdle_nb (two-part count models),
# ancom_bc (bias-corrected log-linear model), aldex2 (Monte-Carlo CLR),
# songbird (CLR-centred multinomial regression), multinomial, or
# dirichlet_multinomial (joint compositional models). Two-part models test
# both group effects jointly and report the count-process fold change.
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).
# rle is refused when no taxon is present in every sample.
# Used by count models (nb_glm, zinb, hurdle_nb); clr_welch ignores it.
# How library size enters each count model, as a log offset: tss (total reads
# per sample) or rle (median-of-ratios over taxa present in every sample).
# rle is refused when no taxon is present in every sample. Used by nb_glm,
# zinb, and hurdle_nb; compositional methods do not use a size-factor offset.
offset: tss
# nb_glm only: where each taxon's negative-binomial theta comes from. ml
# (the default) takes the maximum-likelihood estimate MASS::glm.nb fits per
Expand All @@ -366,9 +368,8 @@ analysis:
dispersion: ml
# Fraction of samples, 0 to 1, in which a taxon must have reads to be
# tested. Taxa below it are listed as filtered, without a p-value. Applied
# before compositional subcomposition construction in clr_welch, ancom_bc,
# aldex2, and songbird. Bayesian replacement refuses a taxon seen in fewer
# than 2 samples; CZM requires at least one.
# before compositional subcomposition construction. Bayesian replacement
# refuses a taxon seen in fewer than 2 samples; CZM requires at least one.
min_prevalence: 0.0
# clr_welch only: how cmultRepl replaces zeros before the centred log-ratio.
# bayesian_multiplicative uses the geometric Bayesian-multiplicative (GBM)
Expand Down
49 changes: 24 additions & 25 deletions frontend/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,20 +403,21 @@ export interface DifferentialRow {
status: DifferentialStatus
note: string
/**
* nb_glm / ancom_bc: natural-log fold change, contrast over reference.
* nb_glm, two-part NB, ancom_bc and joint multinomial models: natural-log
* fold change, contrast over reference (or over the reference taxon).
* clr_welch: mean CLR difference. aldex2: standardized CLR effect size.
* songbird: CLR-centred multinomial differential.
*/
estimate: number | null
/** nb_glm and ancom_bc; null for CLR/differential methods. */
/** Count models, ancom_bc and joint multinomial models; null for CLR effects. */
log2_fold_change: number | null
standard_error: number | null
/** Wald z / W (nb_glm, ancom_bc, songbird) or Welch's t (clr_welch, aldex2). */
/** Wald z / W, Welch's t, or the two-part model's likelihood-ratio statistic. */
statistic: number | null
pvalue: number | null
/** Benjamini-Hochberg adjusted; null outside the tested family. */
padj: number | null
/** nb_glm only. */
/** nb_glm and two-part NB count-process dispersion. */
dispersion_theta: number | null
/** Group coefficient in the logistic zero/occurrence component (two-part NB only). */
zero_process_log_odds?: number | null
Expand All @@ -425,8 +426,8 @@ export interface DifferentialRow {

export type DifferentialMethod =
| 'nb_glm' | 'clr_welch' | 'zinb' | 'hurdle_nb'
| 'multinomial' | 'dirichlet_multinomial'
| 'ancom_bc' | 'aldex2' | 'songbird'
| 'multinomial' | 'dirichlet_multinomial'

/** Which row field holds the effect size, and what to call it. */
export interface DifferentialEffect {
Expand All @@ -442,7 +443,7 @@ export interface DifferentialResult {
groups: { reference: string; contrast: string }
n_samples: Record<string, number>
effect: DifferentialEffect
/** Empty for clr_welch; count models report their size factors. */
/** Count models report size factors; compositional models return an empty list. */
size_factors: { sample: string; group: string; factor: number }[]
config: {
method: DifferentialMethod
Expand All @@ -456,36 +457,25 @@ export interface DifferentialResult {
replacement_delta?: number
/** Null uses GBM's estimated concentration; positive values override it. */
bayesian_alpha?: number | null
/** multinomial and dirichlet_multinomial only. */
/** aldex2 only. */
mc_samples?: number
/** aldex2 only. */
seed?: number
/** multinomial and dirichlet_multinomial: the taxon every ratio is against. */
mn_reference_taxon?: string | null
/** multinomial and dirichlet_multinomial only. */
/** multinomial and dirichlet_multinomial: L2 penalty on the log-odds. */
mn_penalty?: number
/** dirichlet_multinomial only. */
/** dirichlet_multinomial only: how the concentration was estimated. */
dm_overdispersion_method?: 'mle' | 'mom'
/** Multinomial optimiser settings. */
/** multinomial and dirichlet_multinomial: the optimiser settings used. */
tolerance?: number
max_iter?: number
max_seconds?: number
/** aldex2 only. */
mc_samples?: number
/** aldex2 only. */
seed?: number
}
diagnostics: {
n_taxa: number; n_tested: number; n_failed: number; n_boundary: number; n_filtered: number
/** Safety note for two-part models. */
model_warning?: string
/** Diagnostics returned by the joint multinomial models. */
model?: {
reference_taxon: string
penalty: number
n_parameters: number
iterations: number
converged: boolean
log_likelihood: number
runtime_seconds: number
n_taxa_fitted: number
}
/** clr_welch only. */
zero_replacement?: {
method: string
Expand All @@ -498,6 +488,15 @@ export interface DifferentialResult {
concentration: { sample: string; value: number }[]
}
}
/** multinomial and dirichlet_multinomial only. */
model?: {
reference_taxon: string; penalty: number; n_parameters: number; iterations: number
converged: boolean; log_likelihood: number; runtime_seconds: number; n_taxa_fitted: number
}
/** dirichlet_multinomial only: c and theta = 1 / (1 + c) in (0, 1). */
overdispersion?: {
method: 'mle' | 'mom'; concentration: number; theta: number; at_bound: boolean
}
/** ancom_bc only. */
ancom_bc?: {
delta: number | null; delta_se: number | null
Expand Down
31 changes: 24 additions & 7 deletions frontend/src/components/DifferentialPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,10 @@ function csvBlob(result: DifferentialResult): Blob {
}

/**
* Differential abundance between exactly two runs or sub-groups: a model per
* taxon at the chosen rank (negative-binomial GLM, CLR Welch test after zero
* replacement, or one multinomial / Dirichlet-multinomial model over the whole
* composition, as configured), with BH-adjusted p-values, shown as a volcano
* plot and a table
* Differential abundance between exactly two runs or sub-groups: the configured
* per-taxon count model, zero-replaced CLR test, advanced compositional model,
* or joint multinomial / Dirichlet-multinomial model at the chosen rank, with
* BH-adjusted p-values, shown as a volcano plot and a table
* that can be downloaded or added to the report. The first selected condition
* is the reference. The Advanced expander reports the settings the last fit
* used, lets the reference taxon of the multinomial models be changed (only
Expand Down Expand Up @@ -168,6 +167,24 @@ export function DifferentialPanel({ study, runs, option, aggregate }: {
with a library-size offset. Tolerates overdispersion through its own dispersion
parameter and suits few samples per group, but each taxon is fitted on its own.
</li>
<li>
<strong>zinb / hurdle_nb</strong>: two-part negative-binomial models with a
logistic zero or occurrence process. Their p-value jointly tests both group
effects; the reported fold change belongs to the count process.
</li>
<li>
<strong>ancom_bc</strong>: bias-corrected log-linear model for sampling-fraction
bias, reporting a bias-corrected log2 fold change.
</li>
<li>
<strong>aldex2</strong>: Monte-Carlo CLR instances with a standardized CLR
effect size, integrating over Dirichlet posterior draws rather than one SE.
</li>
<li>
<strong>songbird</strong>: compositional multinomial regression reporting
CLR-centred differentials, which are relative log-ratios rather than absolute
fold changes.
</li>
<li>
<strong>multinomial</strong>: one multinomial logistic model over the whole
composition, so each coefficient is a log ratio against a reference taxon and no
Expand Down Expand Up @@ -313,7 +330,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 +343,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
20 changes: 14 additions & 6 deletions frontend/src/components/PipelineStages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@ 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 all nine supported methods', () => {
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,9 +28,13 @@ 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 exactly the methods accepted by the backend', () => {
expect(CONFIG_TYPES['analysis.differential.method']).toEqual({
kind: 'enum',
options: ['nb_glm', 'clr_welch', 'zinb', 'hurdle_nb'],
test('advanced compositional settings retain their editors and descriptions', () => {
expect(CONFIG_TYPES['analysis.differential.mc_samples']).toEqual({ kind: 'int' })
expect(CONFIG_DESCRIPTIONS['analysis.differential.mc_samples']).toContain('Dirichlet')
expect(CONFIG_TYPES['analysis.differential.dm_overdispersion_method']).toEqual({
kind: 'enum', options: ['mle', 'mom'],
})
expect(CONFIG_TYPES['analysis.differential.mn_penalty']).toMatchObject({ kind: 'float', min: 0 })
expect(CONFIG_TYPES['analysis.differential.max_iter']).toEqual({ kind: 'int' })
expect(CONFIG_DESCRIPTIONS['analysis.differential.mn_reference_taxon']).toContain('reference')
})
21 changes: 18 additions & 3 deletions frontend/src/components/PipelineStages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,13 +150,20 @@ export const CONFIG_DESCRIPTIONS: Record<string, string> = {
'analysis.alpha.rarefaction_iterations': 'rarefy draws averaged for each alpha diversity value.',
'analysis.beta.normalisation': 'Preparation before the Bray-Curtis dissimilarity used by NMDS and PERMANOVA: hellinger (square root of relative abundances), none, rarefy or srs.',
'analysis.beta.depth': 'Reads per sample for rarefy or srs; 0 uses the smallest non-empty sample.',
'analysis.differential.method': 'Model per taxon: nb_glm (negative-binomial GLM); clr_welch (Welch test on CLR values after configured zero replacement); zinb (zero-inflated negative binomial); hurdle_nb (logistic zero process plus zero-truncated negative binomial). Two-part NB p-values jointly test group effects in both processes; use only with evidence for excess zeros and interpret cautiously.',
'analysis.differential.method': 'Differential model: nb_glm (negative-binomial GLM); clr_welch (Welch test on zero-replaced CLR values); zinb or hurdle_nb (two-part count models with a joint zero/count-process test); ancom_bc (bias-corrected log-linear model); aldex2 (Monte-Carlo CLR); songbird (CLR-centred multinomial regression); multinomial or dirichlet_multinomial (joint compositional models with log ratios against a reference taxon). Compositional effects are relative log-ratios, not absolute abundance changes.',
'analysis.differential.offset': 'Library-size offset in the count component of nb_glm, zinb and hurdle_nb: tss (total reads per sample) or rle (median-of-ratios over taxa present in every sample; refused when there are none).',
'analysis.differential.dispersion': 'nb_glm only: ml estimates theta separately with MASS::glm.nb; glmGamPoi uses a cross-taxon dispersion trend in fixed-scale negative-binomial Wald fits.',
'analysis.differential.min_prevalence': 'Fraction of samples, 0 to 1, in which a taxon must have reads to be tested; taxa below it are listed as filtered.',
'analysis.differential.min_prevalence': 'Fraction of samples, 0 to 1, in which a taxon must have reads to be tested; taxa below it are listed as filtered. Compositional models filter before constructing their subcomposition.',
'analysis.differential.replacement_method': 'clr_welch only: bayesian_multiplicative uses cmultRepl GBM posterior estimates (Martín-Fernández et al. 2015); multiplicative_replacement uses CZM (Martín-Fernández et al. 2003). Both preserve observed-part ratios, but every replacement is biased. For sparse data consider NB on raw counts, prevalence filtering, occupancy or zero-inflated models instead.',
'analysis.differential.replacement_delta': "clr_welch only: cmultRepl frac, strictly between 0 and 1. CZM starts at delta × 0.5/sample total; either method caps high imputations at delta × the taxon's smallest observed proportion. Below 0.01 or at least 0.9 warns. Do not tune delta for significance; every replacement is biased.",
'analysis.differential.bayesian_alpha': "Bayesian replacement only: null uses GBM's estimated Dirichlet concentration; a positive number uses that concentration with the same leave-one-out prior mean. The posterior parameters are recorded; no posterior draws are generated. Larger alpha gives the prior more weight.",
'analysis.differential.mc_samples': 'aldex2 only: number of Dirichlet(count + 0.5) Monte-Carlo instances drawn per sample (whole number in [16, 4096]; seeded by the study top-level seed).',
'analysis.differential.mn_reference_taxon': 'multinomial and dirichlet_multinomial only: the reference taxon whose log-odds are the denominator of every reported log ratio; empty picks the most abundant fitted taxon. It must have reads in at least half the samples.',
'analysis.differential.mn_penalty': 'multinomial and dirichlet_multinomial only: L2 (ridge) penalty on the log-odds coefficients, 0 for maximum likelihood. Reported standard errors are the penalised ones; an L1 sparsity path is not implemented.',
'analysis.differential.dm_overdispersion_method': 'dirichlet_multinomial only: mle alternates profile-likelihood concentration and coefficient fits; mom takes one method-of-moments estimate. The result reports concentration c and intra-sample correlation theta = 1/(1+c).',
'analysis.differential.max_iter': 'multinomial and dirichlet_multinomial only: Newton iteration cap; a fit that stops at the cap is refused rather than reported.',
'analysis.differential.tolerance': 'multinomial and dirichlet_multinomial only: convergence tolerance for the largest coefficient update relative to coefficient size.',
'analysis.differential.max_seconds': 'multinomial and dirichlet_multinomial only: wall-clock budget in seconds; exceeding it returns an explicit error instead of holding the request open.',
'analysis.alpha.show_points': 'Show individual samples overlaid on alpha comparison boxplots.',
'analysis.alpha.annotate_significance': 'Annotate alpha comparison panels with Kruskal-Wallis significance.',
'analysis.alpha.pairwise_brackets': 'Run BH-adjusted pairwise Wilcoxon rank-sum tests between every group pair and draw brackets, including n.s. results.',
Expand Down Expand Up @@ -253,14 +260,22 @@ export const CONFIG_TYPES: Record<string, ConfigType> = {
'analysis.alpha.rarefaction_iterations': { kind: 'int' },
'analysis.beta.normalisation': { kind: 'enum', options: ['hellinger', 'none', 'rarefy', 'srs'] },
'analysis.beta.depth': { kind: 'int' },
'analysis.differential.method': { kind: 'enum', options: ['nb_glm', 'clr_welch', 'zinb', 'hurdle_nb'] },
'analysis.differential.method': { kind: 'enum', options: [
'nb_glm', 'clr_welch', 'zinb', 'hurdle_nb', 'ancom_bc', 'aldex2', 'songbird',
'multinomial', 'dirichlet_multinomial',
] },
'analysis.differential.offset': { kind: 'enum', options: ['tss', 'rle'] },
'analysis.differential.dispersion': { kind: 'enum', options: ['ml', 'glmGamPoi'] },
'analysis.differential.min_prevalence': { kind: 'float', min: 0, max: 1, step: 0.05 },
'analysis.differential.replacement_method': { kind: 'enum', options: ['bayesian_multiplicative', 'multiplicative_replacement'] },
'analysis.differential.replacement_delta': { kind: 'float', min: 0, max: 1, step: 0.05 },
'analysis.differential.bayesian_alpha': { kind: 'float', nullable: true, min: 0, step: 0.1 },
'analysis.differential.mc_samples': { kind: 'int' },
'analysis.differential.mn_penalty': { kind: 'float', min: 0, step: 0.05 },
'analysis.differential.dm_overdispersion_method': { kind: 'enum', options: ['mle', 'mom'] },
'analysis.differential.max_iter': { kind: 'int' },
'analysis.differential.tolerance': { kind: 'float', min: 0 },
'analysis.differential.max_seconds': { kind: 'float', min: 1 },
'analysis.alpha.show_points': { kind: 'boolean' },
'analysis.alpha.annotate_significance': { kind: 'boolean' },
'analysis.alpha.pairwise_brackets': { kind: 'boolean' },
Expand Down
Loading
Loading