Skip to content

Commit a6e30fa

Browse files
hyperpolymathArena Agentarena-agent
authored
fix: load the package, and restore the docs and test registrations main lost (#209)
Rebased onto `main` (c978994, which now carries #208/#211/#213), so this PR is only the part `main` still gets wrong — and `main` still cannot load the package. ## 1. `using MetaManifold` fails | File | Defect | Effect | |---|---|---| | `src/core/doi.jl` | `module DOI` declared twice (lines 4 and 21) with one terminator | `ParseError` at EOF, before a single test runs | | `src/MetaManifold.jl` | `analysis/clade_cumulus.jl` included twice; `analysis/compositional.jl` never included | module redefined; `UndefVarError: CompositionalModels` when `differential.jl` loads | | `src/server/server.jl` | `routes/cladistics.jl` never included | the Cladistics tab calls endpoints that return 404 while the route file sits unused | Each is a one-line fix in place. `cladistics.jl` is included after `clade_cumulus.jl`; it shares no route path or helper name with the other route files. ## 2. Test registration `test/runtests.jl` listed `test_clade_cumulus.jl` twice and had lost `test_evidence.jl` and `test_evidence_routes.jl`, which #201 added. Now 49 files, each once, and every file in `test/unit` is registered. ## 3. Two statistics tests that contradicted their own sources Both came from a branch whose tests were written against different source behaviour: - `test_exact_summaries.jl` expected `0/1` proportions for a zero-read sample. `_summarise` fills them with `nothing` and stores null, because a proportion is undefined — not zero — when a sample has no reads. The test is realigned to the source; the source is not weakened to the test. - `test_high_precision.jl` expected `UnsupportedRepresentationError` for a policy of the wrong numeric mode. `assert_mode` raises `ArgumentError` for that (a caller error, not an unrepresentable value). Realigned, with the reason stated inline. ## 4. Documentation main lost in the #196 merge - `docs/statistics/zero-handling.md` is back to the full treatment — replacement policies and their exact `cmultRepl` calls, `delta` semantics, what the Bayesian posterior parameterisation does and does **not** claim, warnings, and references — instead of the 60-line summary. - `README.md` regains the Evidence Mode section and the differential-abundance method guidance (why a count model rather than a t-test, choosing the reference taxon, overdispersion, which model to use), documents all nine methods and their settings, and the config sample lists every differential key including the multinomial optimiser budget. ## Validation Julia is not available in my environment, so the Julia side is static: tree-sitter parses every touched file, no file is included twice, every `test/unit` file is registered exactly once, and the YAML parses with the expected differential keys. Frontend: `npx tsc --noEmit` clean, `bun test src` 85/85, `npm run build` succeeds. CI is the first real execution of the Julia suite, and I will follow it and fix what it reports. Co-authored-by: Arena Agent <agent@arena.ai> Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
1 parent c978994 commit a6e30fa

8 files changed

Lines changed: 295 additions & 61 deletions

File tree

‎README.md‎

Lines changed: 159 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,33 @@ not exclude samples from this analysis.
305305
multiplicative (CZM) replacement; `replacement_delta` sets the replacement
306306
fraction and optional `bayesian_alpha` overrides GBM's concentration. The
307307
offset is not used. See [zero-handling notes](docs/statistics/zero-handling.md).
308+
- `ancom_bc`: ANCOM-BC sampling-fraction bias-corrected log-linear model
309+
(Lin & Peddada 2020, *Nat. Commun.* 11:3514). Estimates within-group sample
310+
offsets and between-group sampling-fraction bias via an E-M Gaussian mixture
311+
on `log(counts + 1)`, propagating bias-estimation variance into two-sided Wald
312+
tests. The effect is a bias-corrected log2 fold change; useful when asymmetric
313+
shifts or load changes would bias standard CLR centering (needs at least 3
314+
tested taxa; assumes fewer than half are differential).
315+
- `aldex2`: Dirichlet(`count + 0.5`) Monte-Carlo CLR model (Fernandes et al.
316+
2013, 2014). Draws `mc_samples` (default 128, seeded by the study's `seed`)
317+
instances per sample and reports standardized CLR effect sizes with
318+
consistent-sign posterior predictive Welch p-values; suited to sparse or
319+
variable-depth cohorts where deterministic zero imputation is undesirable.
320+
- `songbird`: compositional baseline-category multinomial regression with
321+
reference-frame invariant CLR-centred differentials summing to zero across
322+
fitted taxa (Morton et al. 2019, *Nat. Commun.* 10:2719; Zhang et al. 2017).
323+
Conditions on read totals without extra-multinomial overdispersion; taxa
324+
separated in one group are marked `failed` rather than diverging.
325+
- `multinomial`: one multinomial logistic model for the whole composition
326+
(Songbird's model in pure Julia), which needs no size factor and no zero
327+
replacement. The effect is a log2 fold change in relative abundance against
328+
`mn_reference_taxon`; `mn_penalty` adds an L2 ridge penalty, and a taxon with
329+
no reads in one group is refused rather than reported.
330+
- `dirichlet_multinomial`: the same joint model plus a Dirichlet prior on the
331+
composition (La Rosa et al. 2012), which absorbs between-sample
332+
overdispersion; `dm_overdispersion_method` picks `mle` (profiled) or `mom`.
333+
Both multinomial models bound their Newton fit with `max_iter`, `tolerance`
334+
and `max_seconds`, and a fit that does not converge is refused.
308335

309336
ZINB and hurdle models can overfit sparse data; do not select ZINB solely
310337
because counts are sparse. Compare against NB diagnostics. The methods report
@@ -316,6 +343,14 @@ histories and is not selectable in the standard analysis configuration or UI.
316343
It currently has no detection covariates. Do not use it as a substitute for
317344
the full workflow tracked in [issue #205](https://github.com/hyperpolymath/MetaManifold-WebUI/issues/205).
318345
See [occupancy model notes](docs/occupancy-models.md).
346+
347+
The multinomial models are the count-based, compositionally coherent
348+
alternative to `nb_glm` (which fits each taxon independently and so ignores the
349+
constraint the composition imposes) and to `clr_welch` (which is Gaussian on
350+
log-ratios and ignores the count likelihood). They are fitted in Julia without R
351+
and without a pseudocount; the dense observed information limits a fit to 1000
352+
taxa, above which raise `min_prevalence` or analyse a coarser rank.
353+
319354
Across the differential methods, `min_prevalence` (default 0) filters taxa by the fraction of
320355
samples with nonzero counts. Benjamini–Hochberg adjustment includes only taxa
321356
that produced a p-value. Failed or filtered taxa have no substitute p-value;
@@ -326,6 +361,119 @@ Implementation and reference tests: `src/analysis/differential.jl`,
326361
`src/server/routes/analysis.jl`, `bench/compositional_differential/benchmark.jl`,
327362
and `test/unit/test_differential.jl`.
328363

364+
#### Why a count model rather than a t-test
365+
366+
Sequencing measures a sample of the library, not an absolute quantity: a
367+
sample of twice the depth has twice every count, and one taxon can only increase
368+
at another's expense. Comparisons of raw or rarefied abundances therefore carry
369+
the compositionality problem (Gloor et al. 2017). The multinomial models fit the
370+
whole composition at once and condition on each sample's total, so each
371+
coefficient is the log ratio of two taxa. That ratio is unchanged when every
372+
count in a sample is multiplied by a constant, which is why no size factor and no
373+
pseudocount is needed. The intercept is the log-odds of the taxon against the
374+
reference at the reference group; the group coefficient is the change in that
375+
log-odds, i.e. a log fold change in relative abundance. Because a log ratio is
376+
the sum of log ratios through any third taxon, every reported effect is coherent
377+
under compositionality and can be re-expressed against any other taxon.
378+
379+
#### Choosing the reference taxon
380+
381+
Every reported effect is anchored on the reference taxon, so the choice matters
382+
for interpretability and for stability, but not for the fit: the likelihood and
383+
the log ratio between two non-reference taxa are the same whichever taxon is the
384+
reference. An empty `mn_reference_taxon` takes the most abundant taxon that
385+
passes the prevalence filter. An explicit one must be a taxon at the analysed
386+
rank with reads in at least half the samples, and reads in both groups; a taxon
387+
seen in fewer samples makes every ratio it anchors unstable, and a taxon absent
388+
from one group has no finite log fold change, so both are refused with the
389+
reason. Taxa with no reads in one group are removed from the composition and
390+
listed as `failed` with their note, rather than fitted: in a joint model a single
391+
diverging coefficient would otherwise pull the whole fit with it.
392+
393+
#### Overdispersion
394+
395+
The multinomial treats every read as independent, so its variance is the sampling
396+
variance and nothing else. Real samples vary more than that: the same community,
397+
sequenced twice, differs by more than resampling reads from one library. The
398+
Dirichlet-multinomial adds that between-sample variation as a concentration `c`,
399+
with `theta = 1/(1+c)` the model's intra-sample correlation; the count variance
400+
becomes `N p (1 - p) (1 + (N - 1) theta)`. When the data are overdispersed the
401+
multinomial's standard errors are too small, and the Dirichlet-multinomial
402+
corrects them; when they are not, `c` runs to its upper bound, the fit collapses
403+
to the multinomial exactly, and the affected rows are marked `boundary` with a
404+
note saying so. The two models are otherwise the same model on the same scale,
405+
so their coefficients are directly comparable.
406+
407+
#### Which model to use
408+
409+
| Data and question | Method |
410+
| --- | --- |
411+
| Counts, two groups, few samples, expected overdispersion | `nb_glm` (per-taxon negative binomial; the default) |
412+
| Counts, many taxa and samples, a joint composition and a single reference | `multinomial` (Songbird's model) |
413+
| As above, with visible between-sample overdispersion | `dirichlet_multinomial` |
414+
| Composition already transformed, or a quick robust rank-based check | `clr_welch` |
415+
| Excess zeros beyond negative-binomial overdispersion, with evidence for them | `zinb` or `hurdle_nb` (two-part; see the caution above) |
416+
| Asymmetric shifts or a community-wide load change | `ancom_bc` (bias-corrected) |
417+
| Sparse or variable-depth cohorts, Monte-Carlo CLR | `aldex2` |
418+
| Many taxa, a joint multinomial fit, but through R and with CLR-centred effects | `songbird` |
419+
420+
`nb_glm` fits a negative-binomial GLM per taxon with a library-size offset: it
421+
tolerates overdispersion through its own dispersion parameter and is the safer
422+
choice with very few samples per group, but each taxon is fitted independently
423+
and the offsets are estimated, not modelled. The multinomial models borrow
424+
strength across taxa by fitting one model, and their effect has a direct
425+
compositional reading, at the cost of a joint fit that a single separated taxon
426+
can compromise (hence the separation check and the optional ridge penalty), and
427+
of a dense information matrix that caps a fit at 1000 taxa. Songbird's L1
428+
sparsity path is not implemented: a lasso estimate has no valid Wald standard
429+
error, so this implements the L2-penalised model and Wald inference instead.
430+
431+
The multinomial models are fitted in Julia and need no R package.
432+
`max_iter`, `tolerance` and `max_seconds` bound one fit; a fit that runs out of
433+
time fails with an explicit error instead of holding the request open. Runtime
434+
and memory at 100, 1000 and 10000 taxa are measured by
435+
`bench/compositional_scaling/scaling.jl`, which writes `results/scaling.csv`.
436+
437+
Implementation and reference tests: `src/analysis/compositional.jl` (the two
438+
models), `src/analysis/differential.jl`, `src/server/routes/analysis.jl`, and
439+
`test/unit/test_differential.jl`. The reference values in the tests come from an
440+
independent implementation and are quoted to 12 digits; the special functions
441+
are cross-checked against mpmath at 40 digits.
442+
443+
### Evidence Mode
444+
445+
The **Evidence** tab turns a run's outputs into a citable epistemic record. A
446+
claim is a sentence a reader could disagree with, a **scope** naming the part of
447+
the data it is about (its *fibre*: the rows whose value at a rank, or whose
448+
category in a set, is one of the named labels), and a **warrant** saying why it
449+
follows. Claims are stored in `projects/{study}/[<group>/]<run>/evidence/claims.yml`
450+
and can be downloaded as that exact YAML.
451+
452+
Three views share one record:
453+
454+
- **Claims** — the epistemic editor (avec fibre). A claim cannot be saved
455+
without a scope and at least one value, and cannot be marked `supported`
456+
without a measurement, citation or inference warrant: a definition alone
457+
supports nothing.
458+
- **Fibre & echo** — the fibre's share of every sample, its largest members, and
459+
the same fibre seen through a second projection: the other classifier at the
460+
same rank, and the parent rank. Agreement is reported with a verdict;
461+
disagreement is a limit on how strongly the claim may be stated, and an echo
462+
that cannot be computed says so rather than vanishing.
463+
- **Residuals** — where the fibre departs from a null you declare, either
464+
`proportional` (the fibre distributed across samples as the library is) or a
465+
stated `claim_share`, with Pearson residuals, a log2 ratio and a χ² statistic
466+
printed without a p-value. The worst departure is shown first.
467+
468+
The tab's Provenance card states whether `attestation.yml` is absent, degraded
469+
or internally inconsistent before any claim is read. Longer notes on the
470+
definitions, the schema and the limits are kept in `docs/evidence-mode.md`
471+
(documentation under `docs/` is kept local by policy — see `.gitignore`).
472+
473+
Implementation: `src/core/evidence.jl` (schema, residuals, verdicts),
474+
`src/server/routes/evidence.jl` (the SQL), `frontend/src/evidence/`
475+
(editor, fibre visualiser, residual explorer) and `test/unit/test_evidence.jl`.
476+
329477
### Figures and report
330478

331479
The Figures tab lays analysis charts out on a page (A4, letter or a journal
@@ -662,13 +810,23 @@ analysis:
662810
normalisation: hellinger # hellinger | none | rarefy | srs
663811
depth: 0 # rarefy/srs reads per sample; 0 = smallest non-empty sample
664812
differential:
665-
method: nb_glm # nb_glm | clr_welch | ancom_bc | aldex2 | songbird
813+
method: nb_glm # nb_glm | clr_welch | zinb | hurdle_nb | ancom_bc
814+
# | aldex2 | songbird | multinomial | dirichlet_multinomial
815+
offset: tss # count models: tss | rle
666816
dispersion: ml # nb_glm: ml | glmGamPoi
817+
min_prevalence: 0.0 # fraction of samples a taxon must be seen in
667818
replacement_method: bayesian_multiplicative
668819
# clr_welch: bayesian_multiplicative | multiplicative_replacement
669820
replacement_delta: 0.65 # clr_welch: strictly between 0 and 1
670821
bayesian_alpha: ~ # clr_welch: null (GBM estimate) or a number > 0
671822
mc_samples: 128 # aldex2: Dirichlet Monte-Carlo instances in [16, 4096]
823+
mn_reference_taxon: ~ # multinomial models: null picks the most abundant taxon
824+
mn_penalty: 0.0 # multinomial models: L2 ridge penalty, >= 0
825+
dm_overdispersion_method: mle
826+
# dirichlet_multinomial: mle | mom
827+
max_iter: 500 # multinomial models: Newton iteration cap
828+
tolerance: 1.0e-8 # multinomial models: convergence tolerance
829+
max_seconds: 300.0 # multinomial models: wall-clock budget
672830
nmds:
673831
max_stress: 0.2 # warn if NMDS stress exceeds this value
674832
```

0 commit comments

Comments
 (0)