TODO
Source:TODO.md
Owner-requested dose-response workflow
This checklist records the numbered CAMRA/owner-requested workflow items without presenting them as completed package documentation. Keep package behavior in README.md; use this file to track remaining alignment work and compatibility tradeoffs.
- Do data meet criteria: quantal response, more than one nonzero response, and minimum
N = 3? - Run the Cochrane-Armitage test of trend, evaluated by chi-squared.
- Fit exponential and typically appropriate beta-Poisson models via MLE, with absolute fit evaluated by chi-squared.
- Calculate bootstrapped 95th percentile confidence intervals from the MLE model.
- Compare relative fit: which is better, by difference in deviances compared with the chi-squared distribution?
- Pool if desired, typically by analysts manually running code for pooled versus unpooled models.
- Test sufficient fit by difference in deviances compared with the base case.
- Output statistics: deviances and chi-squared p-values.
- Output plots: exponential and beta-Poisson.
- Output the best-fitting model choice.
Verified implementation status (code audit, 2026-07-20)
Each item below was independently verified against the working source in R/*.R and tests/testthat/*.R — not against this file’s own self-description. Verdicts: IMPLEMENTED (code does what the item asks), PARTIAL (present but incomplete), MISMATCH (code does something methodologically different), MISSING (absent).
| # | Verdict | Summary |
|---|---|---|
| 1 | IMPLEMENTED | Quantal structure, ≥3 distinct dose groups, and >1 positive response total (reading B) enforced as hard errors with explanatory messages. |
| 2 | IMPLEMENTED | Directional one-sided pre-screen (Zca on log dose) that reproduces the Weir gate exactly; see clarification below. |
| 3 | IMPLEMENTED | Binomial MLE (optim/BFGS) + chi-squared deviance goodness-of-fit. |
| 4 | IMPLEMENTED* | Percentile bootstrap CIs, 95% default, MLE refit per replicate. Caveats below. |
| 5 | IMPLEMENTED | Δdeviance vs chi-squared, generalized to N models (exponential as the nested baseline; AIC tiebreak among non-nested equal-parameter models). |
| 6 | IMPLEMENTED |
poolability_test() (Haas likelihood-ratio test) + group_datasets() (subset grouping); datasets combined by stacking. |
| 7 | IMPLEMENTED | Residual deviance vs saturated base case, qchisq(df_residual). |
| 8 | IMPLEMENTED | Deviances + chi-squared p-values surfaced as tibble columns. |
| 9 | IMPLEMENTED | Plotting is model-generic (routes through model_probability / model_parameters); exact-model CI bands and parameter plots work. |
| 10 | IMPLEMENTED | Best-model choice across all requested models; exact model opt-in via models, fit / compared / assessed / plotted, with a separate exact bootstrap budget. |
Detail and evidence
Data criteria — PARTIAL. Quantal/binomial structure is enforced (
R/data.R:115-131: finite, non-negative whole-number counts,positive + negative > 0per group). Two gaps: (a) thenrow(data) < 3guard (R/data.R:119) runs before the group-by-dose aggregation (R/data.R:86-92), so it counts raw input rows, not distinct dose groups — 3 rows collapsing to 2 doses passes. (b) There is no check that more than one dose group haspositive > 0; a single-responding-dose or all-zero dataset validates clean. → Resolved: both gaps fixed invalidate_dose_response_groups()(see the DONE note below); the nonzero-response rule uses reading B (>1 positive response in total).Cochran-Armitage trend — IMPLEMENTED (requirement clarified). The item’s wording “evaluated by chi-squared” was a conflation. The stakeholder clarified (2026-07-20): item 2 is a pre-screening test for a monotonic increasing trend — response rising with dose — run before any model is fit, exactly as the Weir CAMRA code does it (
if (Zca > 1.644)indev/CAMRA_bootstrap_v10_5_kh.R:57-62).dose_trend_test()(R/fit.R) reproduces that reference line for line: log-dose scores, the sameZcaformula, and a one-sided gate (pnorm(Zca, lower.tail = FALSE) < alpha, i.e.Zca > 1.645). The test is directional by design — a decreasing trend must not pass — so a chi-squared (two-sidedZ^2) gate would be incorrect here. The chi-squared distribution is used elsewhere in the workflow (goodness-of-fit and best-model comparison, items 3/5/7/10), which is where the reproduction target’schisq.*columns come from — there is no trend statistic in that target at all. Reporting-only chi-squared columns briefly added to the trend test were reverted once the intent was clarified.Exp + beta-Poisson MLE, absolute fit by chi-squared — IMPLEMENTED. Negative binomial log-likelihood minimized by
stats::optim(method="BFGS")(R/fit.R:61-73); absolute fit via residual deviance vsstats::qchisq/stats::pchisqondf_residual(R/fit.R:231-238). Note “beta-Poisson” in the automatic two-model set (fit_dose_response_models,R/fit.R:136-139) means the approximate model; the exact beta-Poisson is fittable but opt-in only viafit_dose_response(data, "exact_beta_poisson").Bootstrapped 95% CIs — IMPLEMENTED, with caveats. Grouped binomial bootstrap refitting the MLE each replicate (
R/bootstrap.R:59,bootstrap_refitatR/bootstrap.R:105-109); percentile CIs with 95% default (bootstrap_confint,R/bootstrap.R:187-208). Works for all three models including the exact one (model-agnostic viamodel_parameters()+effective_dose()). Caveats: (a) defaultresample = "observed"draws from observed group proportions, not the fitted-MLE curve — strict parametric bootstrap requiresresample = "fitted"; (b) intervals cover parameters + ED10/ED50 but not a predicted-curve band; (c) no dedicated unit test exercisesbootstrap_confint(onlytest-bootstrap-plot.Rexists).Relative fit by Δdeviance vs chi-squared — IMPLEMENTED.
compare_dose_response_models()(R/fit.R) computes Δdeviance = simpler − fuller, compared toqchisq(1-alpha, df = Δparameters);preferredfollows this test (AIC/BIC reported separately, not drivingpreferred). It is hard-wired to exactly two fits (if (length(fits) != 2L) stop(...)), so the exact beta-Poisson third model cannot enter — see the three-model decision note below.-
Pooling — IMPLEMENTED.
R/pooling.Raddspoolability_test()andgroup_datasets(). Pooling in QMRA is a poolability decision (Haas, Rose & Gerba): given multiple trials for a pathogen, test whether they are statistically the same before combining.poolability_test(datasets, models, alpha)fits each dataset separately and the stacked combination, then comparesdeviance_pooled - sum(deviance_individual)to a chi-squared onk*(m-1)df, per model.group_datasets()extends this to find which trials are mutually poolable (greedy agglomerative by default; exhaustive set-partition search for smallm). Datasets are combined by stacking (each trial’s dose groups kept as separate binomial observations), enabled by splittingfit_core()out offit_dose_response()so the stacked frame is fit without theas_dose_response()sum-aggregation.This resolves the earlier aggregate-vs-stack divergence: the QMRA-wiki pooled experiment
253, 254now yields the reference stacked exponential deviance ~8.85 (verified intests/testthat/test-pooling.R), not the aggregated 3.05. The poolability LRT is textbook-grounded; the subset-grouping heuristic is a documented convenience (not a theorem). Single-datasetas_dose_response()still aggregates same-dose rows — that within-dataset behavior is unchanged. -
Sufficient fit vs base case — IMPLEMENTED. The base case is the saturated model:
saturated_log_lik(R/fit.R:78-83), `deviance = 2*(saturated_log_lik- log_lik)
(R/fit.R:99), tested againstqchisq(1-alpha, df_residual)ingoodness_of_fit()(R/fit.R:231-238`). Genuinely distinct from item 5 (which compares the two fitted models to each other on Δparameters df).
- log_lik)
Output deviances + chi-squared p-values — IMPLEMENTED. Surfaced as real columns, not internal scalars:
goodness_of_fit()→deviance,p_value(R/fit.R:236-237);compare_dose_response_models()→deviance,deviance_difference,chi_square_p_value;glance.qdr_fit()→deviance(R/tidiers.R:66).analyze_dose_response()carries both tibbles on theqdr_analysisobject.Output plots — PARTIAL.
plot_dose_response()/autoplot.qdr_analysisplot data points, fitted curve, and bootstrap CI ribbons for exponential and approximate beta-Poisson. The exact model breaks in two spots that hardcode the approximate-BPn50algebra:bootstrap_prediction_matrix()(R/plot.R:60-70) andautoplot.qdr_bootstrap()(R/plot.R:152-166), both usingif (model == "exponential") … else {n50 algebra}. The exact model’s bare estimate line works (it routes throughmodel_probability), but its CI bands and bootstrap-parameter plot do not.Best-fitting model choice — IMPLEMENTED, two-model scope. Delivered as a sorted per-model tibble with a
recommendationlabel (recommended/acceptable_alternative/preferred_but_inadequate/not_recommended) inbuild_model_assessment()(R/assessment.R:27-67), printed byprint.qdr_analysis(R/analysis.R:83-87).preferredis driven by the chi-squared deviance test;consensus_model_decision()adds an experimental three-vote (chi-squared/AIC/BIC) summary. Two-model only — the exact model is excluded viafit_dose_response_modelsand thelength(fits) != 2Lguard.
Corrections to the planning docs
-
consensus_model_decision()is present and exported (R/assessment.R:111-182,NAMESPACE). Earlier planning notes claimed it was “missing from the working tree” — that claim is stale; the function is in the working source. - The exact beta-Poisson model exists and is wired into fitting,
effective_dose, and bootstrap (as of the Phase 3 commit), but is opt-in and absent from the “Current implementation notes” below.
Remaining work, grouped
-
Independent of the three-model decision — DONE (2026-07-20):
- Item 1:
validate_dose_response_groups()enforces the fitting criteria on the aggregated dose groups (R/data.R) as hard errors with explanatory console messages: ≥3 distinct dose groups (the minimum leaving one residual df for a two-parameter fit) and >1 positive response in total. The nonzero-response rule uses reading B (“more than one positive response counted in total”,sum(positive) > 1) rather than reading A (“more than one responding dose group”). Reading B matches the CAMRA reference, which fits experiments with a single responding dose but multiple responders (e.g. Enterovirus 62, E. coli 39); reading A wrongly rejected them. Initial guard commit5776110; reading-B correction after validating against the QMRA-wiki target. - Item 2: confirmed
dose_trend_test()correctly implements the directional monotonic-increasing pre-screen (WeirZca > 1.645gate). Sharpened the docs and added a directional test (increasing passes, decreasing does not). The chi-squared reporting columns from the earlier misread of the requirement (commitd8e0fb9) were reverted after the stakeholder clarified the intent. - Item 4: dedicated
test-bootstrap-confint.Rcovering percentile bounds, level widening, converged-only filtering, and input validation. Commit8f60fb0.
- Item 1:
-
Pinch Point 7 decided (2026-07-20): full N-model generalization.
- Item 5 — DONE.
compare_dose_response_models()now accepts two or more fits. The exponential is the common nested sub-model: each higher-parameter model is tested against it by the deviance difference onparameters - 1df;preferredis the lowest-AIC model among those that significantly beat the exponential (else the exponential). Equal-parameter, non-nested models (approximate vs exact beta-Poisson) are separated by AIC/BIC only. With exactly two models the output reduces to the previous simpler-vs-fuller test (existing tests unchanged).consensus_model_decision()already handled N models and works on a three-model set as-is (verified: Ward selects the exact beta-Poisson unanimously). A three-model comparison test was added. - Item 10 — DONE.
fit_dose_response_models()andanalyze_dose_response()gained amodelsargument (default the two legacy models; add"exact_beta_poisson"to opt in). All requested models flow through fitting, the N-model comparison,build_model_assessment()(verified on three models), consensus, and plotting. The exact model’s bootstrap is budgeted separately viaexact_bootstrap_times(default 10000; zero to fit and compare it without bootstrapping) since it is ~6x slower to fit. Default (two-model) behavior and output shape are unchanged. - Item 9 — DONE.
bootstrap_prediction_matrix()now reconstructs each bootstrap curve throughmodel_probability(), andautoplot.qdr_bootstrap()drives its parameter plot offmodel_parameters(), so both are model-generic — the exact model’s CI bands and(alpha, beta)parameter scatter plot correctly. No per-model plotting branches remain. (Optimization note: the exact model’s band reconstruction evaluates the Kummer series per draw; revisit if 10k-replicate band plots prove slow.)
- Item 5 — DONE.
Historical v3 reference validation and the multi-start fitting pivot (2026-07-20)
The two-model outputs were historically validated against the QMRA-wiki v3 Excel target (QMRA wiki data_v3_Camila.xlsx, ~120 fittable single-experiment datasets). That superseded workbook is no longer tracked, but this record is retained as provenance. The fit / goodness-of-fit / comparison arithmetic reproduced the reference; the divergences traced to optimizer start values, not formulas.
Design pivot (documented so end users are not surprised): the single geometric-mean-dose default_start() was replaced by candidate_starts() plus a multi-start loop in fit_dose_response(). A single seed cannot avoid every start-dependent local optimum — an exponential fit to beta-Poisson-shaped data (Ward) falls into a spurious saturated optimum, while a low-infectivity pathogen (e.g. Coxiella, true k ~ 5.7e-11) collapses to k -> 0. Fitting from several seeds (capped ID50, geometric-mean dose, and the CAMRA fixed exp(-5)) and keeping the highest-likelihood result resolves both. Result on the reference set: preferred-model match 96% -> 97%, ID50 within 3% 92 -> 114 / 119, beta-Poisson deviance matches 81 -> 100, and all six tiny-rate fit crashes eliminated. Multi-start runs only on the initial fit (no start supplied); bootstrap warm-starts pass an explicit start, so the 1k-10k refits are unaffected. See README.md (Fitting robustness) for the user-facing note.
With the item-1 nonzero-response screen set to reading B, experiments 62 and 39 (single responding dose, multiple responders) now fit, so every fittable reference experiment runs with zero errors and the preferred-model match is 118/121 (98%). Pooled experiments are handled by the stacking-based pooling feature (see item 6), which reproduces their reference stacked deviances.
The current reproducible audit targets data/raw/QMRA wiki data_v4.xlsx through dev/validation/validate_qmra_v4.R. It records every workbook experiment or an explicit skip reason and writes the tracked experiment, prediction, and Markdown validation results under dev/validation/.
Current implementation notes
-
dose_trend_test()currently preserves the package’s existing log-dose, one-sided normal/Z trend gate. The owner-requested checklist item names chi-squared evaluation; if needed for reporting, that can be added compatibility-preservingly asZ^2with 1 degree of freedom without changing the currentp_valueor pass/fail rule. - The
comparisonresult keeps decision rules separate:preferredfollows the legacy chi-squared deviance test, whilepreferred_by_AICandpreferred_by_BICreport the corresponding information-criterion choices. - The combined
assessmentresult is intended for programmatic parsing. Itsappropriateflag comes only from absolute goodness-of-fit, whilerecommendationis one ofrecommended,acceptable_alternative,preferred_but_inadequate, ornot_recommended. A model is onlyrecommendedwhen it both fits the data adequately and is preferred by the chi-squared deviance comparison. Theconclusioncolumn provides the matching human-readable statement. -
consensus_model_decision()is experimental. It treats the chi-squared comparison, AIC, and BIC as three model-selection votes and reports each vote, the total per model, and an agreement level ofunanimous,majority, orno_consensus. This is a relative selection summary only; useanalysis$assessmentto determine whether the selected model also fits the data adequately. - Boosterpak remains optional repository-development tooling. It is not a package dependency and is not needed to install or use
singlehit.