#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| viewerHeight: 1100
#| components: [viewer]
# Statistical Test Navigator: browser app with two tabs.
# Edit this file; statistics.qmd includes it at render time.
library(shiny)
library(bslib)
library(colorspace)
library(munsell)
library(ggplot2)
# nnet is only used in the displayed multinomial example.
# Pure decision logic: no Shiny dependency. Hidden UI values never influence a path.
exposure_choices <- function(outcome) {
if (identical(outcome, "continuous")) c("Reference value"="none", "Two groups / occasions"="two", "Three or more groups / occasions"="many", "Continuous predictor"="numeric")
else c("Two groups / occasions"="two", "Three or more groups / occasions"="many", "Continuous predictor"="numeric")
}
select_method <- function(outcome="continuous", exposure="two", design="independent",
normality="normal", adjust=FALSE, sparse="unsure", complex=FALSE) {
stopifnot(outcome %in% c("continuous","binary","nominal"),
exposure %in% unname(exposure_choices(outcome)),
design %in% c("independent","related"),
normality %in% c("normal","nonnormal","unsure"),
sparse %in% c("adequate","sparse","unsure"))
note <- character()
if (complex) return(list(id="complex", note="Clustering, incomplete longitudinal records, or time-to-event outcomes require a design-specific model."))
if (outcome == "continuous") {
if (adjust && design=="related" && exposure!="none") return(list(id="complex",note="Covariate adjustment with repeated observations requires a dependence-aware model."))
if (adjust && exposure!="none") return(list(id="linear",note="Include prespecified confounders and assess model form."))
if (exposure=="none") {
id <- "one"
if (normality!="normal") note <- "One-sample t-test is conditional on adequate inference for the mean. With strong skew or outliers, consider a justified bootstrap/robust mean analysis. Signed-rank changes the estimand and requires symmetry."
} else if (exposure=="numeric") {
id <- "linear"
if (design=="related") return(list(id="complex",note="A continuous predictor with repeated observations requires a mixed-effects model or GEE."))
} else if (exposure=="two") {
id <- if(design=="related") {if(normality=="nonnormal") "signed" else "paired"} else {if(normality=="nonnormal") "rank" else "welch"}
} else {
id <- if(design=="related") {if(normality=="nonnormal") "friedman" else "repeated"} else {if(normality=="nonnormal") "kruskal" else "anova"}
}
if (normality=="unsure") note <- c(note,"Provisional recommendation: inspect relevant Q-Q plots, outliers, sample size and design. A significant Shapiro-Wilk test alone does not choose the method.")
} else if (design=="related") {
if (outcome=="nominal" || exposure=="numeric" || adjust) {
id <- "complex"; note <- "Use a categorical mixed-effects model or GEE; paired nominal tables may need a marginal-homogeneity test."
} else id <- if(exposure=="two") "mcnemar" else "cochran"
} else if (outcome=="binary" && (exposure=="numeric" || adjust)) {
id <- "logistic"
} else if (outcome=="nominal" && (exposure=="numeric" || adjust)) {
id <- "multinomial"
} else {
id <- if(sparse=="sparse") "fisher" else "chi"
if(sparse=="unsure") note <- "Inspect expected cell counts before choosing Pearson chi-squared versus Fisher's exact test; larger sparse tables may need simulation."
}
list(id=id, note=note)
}
# Each entry contains scientific guidance and a standalone, seeded R example.
make_info <- function(title, target, h0, assumptions, example, limitations, alternatives, code,
structure="One row per participant; use explicit factors for groups.", interpretation=NULL) {
list(title=title, target=target, h0=h0, h1=paste("Departure from:",h0),
assumptions=assumptions, example=example, limitations=limitations,
alternatives=alternatives, code=paste("set.seed(2026)",code,sep="\n"), structure=structure,
interpretation=if(is.null(interpretation)) paste("Hypothetical result: p = 0.03 would provide evidence against the stated null under the model assumptions. Report the effect size and confidence interval where available; it does not establish clinical importance or causality.") else interpretation)
}
test_information <- list(
one=make_info("One-sample t-test", "Population mean minus a specified reference mean.",
"The population mean equals the prespecified reference (130 mmHg).",
"Independent observations; approximately normal sampling distribution of the mean. Small samples need approximately normal observations without influential outliers.",
"Outcome: systolic blood pressure. Exposure: none. Design: independent sample compared with a clinical reference of 130 mmHg.",
"A reference estimated from another sample has uncertainty and should not be treated as a fixed constant.",
"Robust or bootstrap inference for the mean; signed-rank only for a symmetric location estimand.",
"bp <- rnorm(60, 134, 12)\nfit <- t.test(bp, mu = 130)\nfit\nfit$estimate - 130\nfit$conf.int # CI for population mean, not mean minus 130\nfit$p.value"),
welch=make_info("Welch independent-samples t-test", "Difference in population means; does not assume equal variances.",
"Mean LDL is the same in statin and placebo populations.",
"Independent participants; approximately normal group means, considering sample sizes, skew and influential outliers.",
"Outcome: LDL cholesterol. Exposure: statin/placebo. Design: different participants in each group.",
"Non-normality alone is not a sufficient reason to change estimands; severe skew with small samples needs care.",
"Linear regression for adjustment; robust methods; rank-sum for a distributional question.",
"d <- data.frame(group = factor(rep(c('placebo','statin'), each=60)), ldl = c(rnorm(60,140,25),rnorm(60,120,20)))\nfit <- t.test(ldl ~ group, data=d)\nfit\nfit$estimate\nfit$conf.int # placebo minus statin\nfit$p.value"),
paired=make_info("Paired t-test", "Mean within-person difference.",
"The population mean after-minus-before blood pressure difference is zero.",
"Pairs independent of other pairs; within-pair differences approximately normal for small samples. Marginal before/after normality is not required.",
"Outcome: blood pressure. Exposure: before/after antihypertensive treatment. Design: same people measured twice.",
"A before-after change without a comparison group may reflect time trends, regression to the mean or confounding.",
"Signed-rank for symmetric differences; mixed models for more complex longitudinal designs.",
"before <- rnorm(50,145,15)\nafter <- before + rnorm(50,-6,8)\nfit <- t.test(after,before,paired=TRUE)\nfit\nfit$estimate\nfit$conf.int\nfit$p.value",
"Two aligned numeric vectors; each position must refer to the same participant."),
rank=make_info("Wilcoxon rank-sum / Mann-Whitney test", "Rank-based distributional comparison; a location-shift interpretation requires similarly shaped distributions.",
"The two outcome distributions are identical (under the exchangeability null).",
"Independent observations; at least ordinal outcomes. A simple location-shift interpretation requires common shape and spread.",
"Outcome: hospital length of stay. Exposure: two treatments. Design: independent patients, strongly skewed stays.",
"Not automatically a test of medians. Different spreads may drive the result. It does not estimate a difference of means.",
"A suitable GLM or robust mean analysis when the scientific question concerns means.",
"control <- rlnorm(60,1.8,0.6)\ntreatment <- rlnorm(60,1.5,0.6)\nfit <- wilcox.test(treatment,control,conf.int=TRUE,exact=FALSE)\nfit\nfit$estimate # Hodges-Lehmann shift; not difference of sample medians\nfit$conf.int\nfit$p.value"),
signed=make_info("Wilcoxon signed-rank test", "Symmetric location of within-pair differences (pseudomedian).",
"The difference distribution is symmetric about zero.",
"Independent pairs; differences continuous and approximately symmetric. Non-normal does not imply symmetric.",
"Outcome: blood pressure. Exposure: before/after treatment. Design: paired; differences have symmetric heavy tails.",
"Strongly asymmetric differences violate the standard location interpretation; zero differences and ties affect inference.",
"Sign test for a median difference without symmetry; robust analysis matched to the estimand.",
"before <- rnorm(60,145,15)\nafter <- before + 5*rt(60,df=4) - 4\nfit <- wilcox.test(after,before,paired=TRUE,conf.int=TRUE,exact=FALSE)\nfit\nfit$estimate\nfit$conf.int\nfit$p.value", "Aligned before/after vectors; differences formed within participant."),
anova=make_info("One-way ANOVA / Welch ANOVA", "Omnibus comparison of three or more population means.",
"All treatment-group population mean HbA1c values are equal.",
"Independent participants; approximately normal errors. Classical ANOVA assumes equal error variances; Welch ANOVA relaxes equal variances.",
"Outcome: HbA1c. Exposure: three treatments. Design: independent groups.",
"An omnibus result does not identify which groups differ. Prespecify contrasts or adjust post-hoc comparisons for multiplicity.",
"Welch ANOVA for unequal variances; linear regression for covariates; Kruskal-Wallis for a rank-based question.",
"d <- data.frame(group=factor(rep(c('A','B','C'),each=40)), hba1c=rnorm(120,rep(c(7.8,7.3,7.0),each=40),0.9))\nfit <- aov(hba1c ~ group,data=d)\nsummary(fit)\nmodel.tables(fit,type='means')\nTukeyHSD(fit) # adjusted pairwise differences and CIs\nwelch <- oneway.test(hba1c ~ group,data=d,var.equal=FALSE)\nwelch$p.value"),
kruskal=make_info("Kruskal-Wallis test", "Omnibus rank-based comparison of independent group distributions.",
"All group outcome distributions are identical.",
"Independent observations, ordinal or continuous outcome. Similar distribution shapes are needed for a location-only interpretation.",
"Outcome: length of stay. Exposure: three treatment groups. Design: independent patients.",
"Does not identify differing pairs or automatically compare medians; adjust post-hoc tests for multiplicity.",
"GLM, robust mean comparisons, or prespecified rank contrasts.",
"d <- data.frame(group=factor(rep(LETTERS[1:3],each=40)),stay=rlnorm(120,rep(c(1.5,1.7,2),each=40),0.7))\nfit <- kruskal.test(stay ~ group,data=d)\nfit\nfit$p.value\naggregate(stay ~ group,d,median) # descriptive, not the tested estimand\n# No single effect estimate or CI is supplied by this omnibus test."),
repeated=make_info("Repeated-measures ANOVA (conditional)", "Mean differences across three or more related occasions.",
"All population occasion means are equal.",
"Independent subjects; appropriate normal error structure and sphericity for the univariate F test with >2 occasions. Balanced complete data for this example.",
"Outcome: HbA1c. Exposure: three visits. Design: same participants at each visit.",
"Check sphericity and apply appropriate corrections if needed. Missing/unbalanced data and complex covariance often favour mixed models. This example does not implement sphericity corrections.",
"Friedman for complete rank-based blocks; mixed-effects models for flexible covariance and missingness assumptions.",
"id <- factor(rep(1:40,each=3))\nvisit <- factor(rep(c('baseline','month3','month6'),40),levels=c('baseline','month3','month6'))\nsubject <- rep(rnorm(40,0,0.7),each=3)\nd <- data.frame(id,visit,hba1c=rep(c(8,7.5,7.2),40)+subject+rnorm(120,0,0.5))\nfit <- aov(hba1c ~ visit + Error(id/visit),data=d)\nsummary(fit)\naggregate(hba1c ~ visit,d,mean)\n# Read the visit F-test p-value in the within-subject stratum.\n# No single contrast CI is supplied by this omnibus test.","Long format: id, visit factor, outcome; one complete measurement per subject/visit."),
friedman=make_info("Friedman test", "Within-subject ranks across three or more complete occasions.",
"Occasion effects are equal in the blocked rank model.",
"Independent subjects; complete blocks, one observation per subject per occasion; outcomes rankable.",
"Outcome: symptom score. Exposure: three occasions. Design: the same patients each time.",
"Does not accommodate incomplete blocks or covariate adjustment; a significant omnibus test needs adjusted follow-up contrasts.",
"Mixed-effects models; repeated-measures ANOVA if its assumptions suit the estimand.",
"d <- expand.grid(id=factor(1:40),visit=factor(c('A','B','C')))\nd$score <- rep(rnorm(40),3)+rep(c(0,0.5,1),each=40)+rt(120,4)\nfit <- friedman.test(score ~ visit | id,data=d)\nfit\nfit$p.value\n# No single effect estimate or CI is supplied by this omnibus test.","Long format with complete id-by-occasion blocks."),
linear=make_info("Linear regression", "Conditional mean outcome change per unit predictor, with optional covariate adjustment.",
"The specified predictor coefficient equals zero conditional on included covariates.",
"Correct conditional mean form; independent errors; constant variance for classical standard errors; approximately normal residuals for small-sample t inference. Predictors and raw outcomes need not be normal.",
"Outcome: systolic BP. Exposure: age. Design: independent adults. An adjusted model also includes BMI.",
"Assess residuals, nonlinearity, heteroscedasticity, leverage and collinearity. Adjustment does not guarantee removal of confounding.",
"Splines for nonlinearity; robust standard errors for heteroscedasticity; mixed models for dependence.",
"d <- data.frame(age=runif(150,25,80),bmi=rnorm(150,27,4))\nd$sbp <- 90+0.5*d$age+0.7*d$bmi+rnorm(150,0,10)\nfit <- lm(sbp ~ age + bmi,data=d)\nsummary(fit)\ncoef(fit)\nconfint(fit)\ncoef(summary(fit))[,4]\n# For diagnostics: plot(fit,which=c(1,2))"),
chi=make_info("Pearson chi-squared test (check counts)", "Association between independent categorical variables.",
"Outcome category and exposure group are independent.",
"Independent participants; mutually exclusive categories; adequate expected counts. A common diagnostic is no expected count <1 and at most 20% <5, not a universal guarantee.",
"Outcome: adverse reaction yes/no. Exposure: treatment A/B. Design: independent treatment groups. Also applies to larger nominal tables.",
"Use counts, not percentages. Examine expected counts and study sampling design. No adjustment for confounding.",
"Fisher's exact for sparse tables; logistic or multinomial regression for adjustment.",
"tab <- matrix(c(20,80,35,65),nrow=2,byrow=TRUE,dimnames=list(treatment=c('A','B'),reaction=c('yes','no')))\nfit <- chisq.test(tab,correct=FALSE)\nfit\nfit$expected\nfit$p.value\nprop.table(tab,1) # group risks for this cohort-style example\n# Test supplies no risk-difference CI; estimate effects separately.","Contingency table of independent participant counts; one category per variable per participant."),
fisher=make_info("Fisher's exact test", "Association in a contingency table conditional on margins; a 2x2 table supports a conditional odds ratio.",
"The population odds ratio is one for a 2x2 table (independence).",
"Independent participants; fixed margins or a sampling context supporting conditional inference. No normality requirement.",
"Outcome: rare adverse reaction. Exposure: two treatments. Design: independent patients, sparse table.",
"Odds ratios are not risk ratios. Larger exact tables may be computationally expensive; Monte Carlo inference is an option.",
"Pearson chi-squared with adequate expected counts; logistic regression for covariates.",
"tab <- matrix(c(2,28,8,22),nrow=2,byrow=TRUE)\nfit <- fisher.test(tab)\nfit\nfit$estimate # conditional odds ratio\nfit$conf.int\nfit$p.value\n# For a large sparse RxC table: fisher.test(tab,simulate.p.value=TRUE,B=10000)","Contingency table of independent participant counts."),
mcnemar=make_info("McNemar's test", "Marginal binary proportion change using discordant pairs.",
"The two discordant-pair probabilities are equal (equal marginal proportions).",
"Paired binary outcomes; independent pairs. Asymptotic chi-square inference needs enough discordant pairs.",
"Outcome: smoking yes/no. Exposure: before/after cessation intervention. Design: the same participants twice.",
"An ordinary independent-samples chi-square test discards pairing; small discordant counts need an exact binomial version.",
"Exact discordant-pair binomial test; conditional logistic or longitudinal models where appropriate.",
"tab <- matrix(c(30,18,6,46),nrow=2,byrow=TRUE,dimnames=list(before=c('smoking','not'),after=c('smoking','not')))\nfit <- mcnemar.test(tab)\nfit\nfit$p.value\nexact <- binom.test(tab[1,2],tab[1,2]+tab[2,1],p=0.5)\nexact$p.value\n# exact$conf.int is for discordance direction, not marginal risk change.\n(rowSums(tab)[1]-colSums(tab)[1])/sum(tab) # smoking reduction",
"2x2 transition counts: rows before, columns after, same people."),
cochran=make_info("Cochran's Q test", "Omnibus equality of related binary proportions across three or more occasions.",
"All occasion marginal success probabilities are equal.",
"Independent subjects, complete binary repeated records; sufficiently large sample for chi-square approximation.",
"Outcome: smoking abstinence yes/no. Exposure: three visits. Design: same participants across visits.",
"No covariate adjustment; degenerate all-constant data make Q undefined. Follow-up paired tests need multiplicity control.",
"Binary GEE or mixed-effects logistic regression for covariates, clustering and longitudinal questions.",
"y <- sapply(c(0.3,0.45,0.6),function(p) rbinom(100,1,p))\nk <- ncol(y); C <- colSums(y); R <- rowSums(y); T <- sum(y)\nden <- k*T-sum(R^2)\nstopifnot(den > 0)\nQ <- (k-1)*(k*sum(C^2)-T^2)/den\np_value <- pchisq(Q,df=k-1,lower.tail=FALSE)\nc(Q=Q,df=k-1,p_value=p_value)\ncolMeans(y) # observed proportions; Q supplies no single CI",
"Binary matrix: rows subjects, columns occasions, complete records."),
logistic=make_info("Binary logistic regression", "Conditional log odds, or odds ratios after exponentiation, for a binary outcome.",
"The predictor log-odds coefficient is zero (conditional odds ratio one).",
"Independent observations, correct logit mean form, adequate information/events, no complete separation or problematic collinearity.",
"Outcome: diabetes yes/no. Exposure: BMI. Design: independent adults; age included for adjustment.",
"An odds ratio is not a risk ratio, particularly when outcomes are common. Coefficients depend on coding and included covariates.",
"Modified Poisson with robust variance for risk ratios in suitable designs; mixed models/GEE for correlated binary data.",
"d <- data.frame(bmi=rnorm(300,27,4),age=runif(300,25,80))\nd$diabetes <- rbinom(300,1,plogis(-7+0.12*d$bmi+0.04*d$age))\nfit <- glm(diabetes ~ bmi+age,family=binomial(),data=d)\nsummary(fit)\nexp(coef(fit))\nexp(confint.default(fit)) # Wald odds-ratio CIs\ncoef(summary(fit))[,4]"),
multinomial=make_info("Multinomial logistic regression", "Category-specific odds relative to a reference nominal category.",
"All specified predictor coefficients across non-reference logits equal zero for an overall predictor test.",
"Independent observations; nominal outcome, appropriate category-specific linear predictors; adequate category counts, no separation; assess independence-of-irrelevant-alternatives suitability.",
"Outcome: no reaction, GI reaction or skin reaction. Exposure: age or treatment; design: independent patients.",
"Nominal models ignore order. For ordered outcomes consider an ordinal model and assess proportional odds. Wald tests per coefficient are not an overall predictor test.",
"Chi-squared for unadjusted categorical association; ordinal regression for ordered categories.",
"d <- data.frame(age=runif(240,20,80),outcome=factor(sample(c('none','GI','skin'),240,replace=TRUE)))\nd$outcome <- relevel(d$outcome,ref='none')\nfit <- nnet::multinom(outcome ~ age,data=d,trace=FALSE)\ns <- summary(fit)\nexp(coef(fit)) # category-vs-reference odds ratios\nexp(cbind(as.vector(s$coefficients)-1.96*as.vector(s$standard.errors),as.vector(s$coefficients)+1.96*as.vector(s$standard.errors)))\n2*pnorm(-abs(s$coefficients/s$standard.errors)) # coefficient-wise Wald p-values\nnull <- nnet::multinom(outcome ~ 1,data=d,trace=FALSE)\nanova(null,fit,test='Chisq') # overall age test"),
complex=make_info("Design-specific modelling required", "Specify the estimand and dependence structure before selecting a model.",
"A prespecified model parameter or contrast equals its null value; no universal null applies.",
"Assumptions depend on outcome distribution, covariance, missingness and the scientific question.",
"Repeated patient measurements within hospitals require explicit patient and hospital dependence, not an ordinary independent-groups test.",
"The four basic questions cannot identify a valid single model for every longitudinal, clustered or survival design. Seek statistical input.",
"Mixed-effects models, GEE, Cox regression, or a paired nominal marginal-homogeneity analysis as dictated by design.",
"# Reproducible illustration of correlated data, not a fitted recommendation.\nd <- expand.grid(id=1:40,visit=0:2)\nsubject <- rnorm(40,0,5)\nd$bp <- 140+subject[d$id]-3*d$visit+rnorm(nrow(d),0,3)\nhead(d)\n# Choose a model and covariance structure before fitting.\n# lme4::lmer(bp ~ visit + (1 | id),data=d) is one possible\n# random-intercept model, not a universal solution.","Usually long format with explicit subject and cluster identifiers.",
"No universal p-value or CI can be supplied until a scientifically justified model and contrast are specified.")
)
concepts_ui <- function() tagList(
h3("Begin with the estimand"),p("A mean difference, distributional shift, risk ratio, odds ratio and regression coefficient answer different questions. Decide what population quantity matters before selecting a method."),
h3("Normality is specific to a method"),p("Paired t-tests concern differences; regression inference concerns errors conditional on predictors. Categorical tests do not require normal outcomes. Use Q-Q plots, design knowledge, sample size and sensitivity analyses; do not use Shapiro-Wilk as an automatic gatekeeper."),
h3("Dependence comes first"),p("Repeated observations, matched pairs, patients within hospitals and household surveys are not independent records. Ordinary independent-group tests can give misleading standard errors."),
h3("P-values and confidence intervals"),p("A p-value measures how incompatible the observed test statistic (or a more extreme one) is with the null model. It is not the probability that H0 is true. A 95% confidence procedure covers its target in 95% of repeated samples under its assumptions; an observed interval is not a 95% probability statement about a fixed parameter."),
h3("Clinical importance and causality"),p("Report effect sizes, units, precision, absolute risks and clinical thresholds. A small p-value can accompany a trivial effect. Observational association does not establish causality; adjustment requires a defensible causal and sampling framework."),
h3("Ranks, odds and risks"),p("Wilcoxon tests do not automatically compare medians. A risk ratio compares probabilities; an odds ratio compares odds and can be far from the risk ratio for common outcomes. In case-control sampling, risks generally cannot be recovered directly from the sampled table.")
)
beyond_ui <- function() tagList(
h3("Poisson regression"),p("Models event counts with a log link. Use log(person-time) as an offset for rates. Standard Poisson inference assumes conditional mean equals conditional variance. Modified Poisson risk-ratio modelling of binary outcomes requires robust variance, not naive Poisson standard errors."),
h3("Negative binomial regression"),p("A count model allowing overdispersion relative to Poisson. Investigate zero inflation, exposure time, dependence and the data-generating process rather than choosing by variance alone."),
h3("Cox proportional hazards regression"),p("Models time-to-event hazards with right censoring. Requires an appropriate censoring assumption and assessment of proportional hazards. A hazard ratio is not a risk ratio or a ratio of survival times."),
h3("Mixed-effects models"),p("Represent subject or cluster variation with random effects. Useful for repeated measurements, clustering and some unbalanced longitudinal records. Specify covariance and missing-data assumptions; random effects do not automatically solve informative missingness."),
h3("Generalised estimating equations (GEE)"),p("Estimate population-averaged mean relationships for correlated outcomes. Choose a working correlation and use suitable variance estimation; sandwich inference needs enough independent clusters. Subject-specific mixed-model effects and GEE effects can differ."),
h3("When the basic selector is insufficient"),p("Survival times, counts, survey weights, matched sets with multiple controls, ordered outcomes, missingness and causal estimands need additional information. Use the complex-design checkbox to leave the basic-test pathways.")
)
reference_ui <- function() {
# Open reference links in a separate browser tab.
ref_link <- function(url, label = "View source") {
tags$a(
href = url,
target = "_blank",
rel = "noopener noreferrer",
label
)
}
tags$ul(
# Existing references -----------------------------------------------
tags$li(
ref_link(
"https://stat.ethz.ch/R-manual/R-release/library/stats/html/t.test.html",
"R stats: t.test"
),
" — Welch and paired inference."
),
tags$li(
ref_link(
"https://stat.ethz.ch/R-manual/R-devel/library/stats/html/wilcox.test.html",
"R stats: wilcox.test"
),
" — Symmetry, shift estimates and confidence intervals."
),
tags$li(
ref_link(
"https://stat.ethz.ch/R-manual/R-patched/library/stats/html/chisq.test.html",
"R stats: chisq.test"
),
"; ",
ref_link(
"https://stat.ethz.ch/R-manual/R-release/library/stats/html/fisher.test.html",
"fisher.test"
),
"; ",
ref_link(
"https://stat.ethz.ch/R-manual/R-devel/library/stats/html/mcnemar.test.html",
"mcnemar.test"
)
),
tags$li(
ref_link(
"https://stat.ethz.ch/R-manual/R-release/library/stats/html/oneway.test.html",
"R stats: oneway.test"
),
"; ",
ref_link(
"https://stat.ethz.ch/R-manual/R-release/library/stats/html/friedman.test.html",
"friedman.test"
)
),
tags$li(
ref_link(
"https://stat.ethz.ch/R-manual/R-release/library/stats/html/glm.html",
"R stats: glm"
),
"; ",
ref_link(
"https://stat.ethz.ch/R-manual/R-release/library/nnet/html/multinom.html",
"R nnet: multinom"
)
),
tags$li(
"Agresti, A. 2018. ",
tags$em("An Introduction to Categorical Data Analysis."),
" 3rd ed. Wiley."
),
tags$li(
"Friedman, M. 1937. ",
"“The Use of Ranks to Avoid the Assumption of Normality ",
"Implicit in the Analysis of Variance.” ",
tags$em("Journal of the American Statistical Association"),
" 32: 675–701."
),
tags$li(
"Cochran, W. G. 1950. ",
"“The Comparison of Percentages in Matched Samples.” ",
tags$em("Biometrika"),
" 37: 256–266."
),
tags$li(
"Wasserstein, R. L., and N. A. Lazar. 2016. ",
"“The ASA Statement on P-Values: Context, Process, and Purpose.” ",
tags$em("The American Statistician"),
" 70: 129–133."
),
# Additional references --------------------------------------------
tags$li(
"Armitage, Peter, Geoffrey Berry, and J. N. S. Matthews. 2013. ",
tags$em("Statistical Methods in Medical Research."),
" 4th ed. Wiley-Blackwell."
),
tags$li(
"Australian Bureau of Statistics. 2024. ",
tags$em("Causes of Death, Australia, 2023."),
" ",
ref_link(
paste0(
"https://www.abs.gov.au/statistics/health/",
"causes-death/causes-death-australia/latest-release"
)
)
),
tags$li(
"Australian Institute of Health and Welfare. 2024. ",
tags$em("Australia’s Mothers and Babies."),
" Australian Institute of Health and Welfare. ",
ref_link(
paste0(
"https://www.aihw.gov.au/reports/mothers-babies/",
"australias-mothers-babies/contents/about"
)
)
),
tags$li(
"Australian Institute of Health and Welfare. 2025. ",
tags$em("Australia’s Health."),
" ",
ref_link(
"https://www.aihw.gov.au/reports-data/australias-health"
)
),
tags$li(
"Bland, Martin. 2015. ",
tags$em("An Introduction to Medical Statistics."),
" 4th ed. Oxford University Press."
),
tags$li(
"Boers, Maarten. 2018. ",
"“Graphics and Statistics for Cardiology: Designing Effective ",
"Tables for Presentation and Publication.” ",
tags$em("Heart"),
" 104 (3): 192–200. ",
ref_link(
"https://doi.org/10.1136/heartjnl-2017-311581",
"DOI"
)
),
tags$li(
"Brown, Lawrence D., T. Tony Cai, and Anirban DasGupta. 2001. ",
"“Interval Estimation for a Binomial Proportion.” ",
tags$em("Statistical Science"),
" 16 (2): 101–117. ",
ref_link(
"https://www.jstor.org/stable/2676784",
"JSTOR"
)
),
tags$li(
"Deeks, Jon. 1998. ",
"“When Can Odds Ratios Mislead?” ",
tags$em("BMJ"),
" 317 (7166): 1155. ",
ref_link(
"https://doi.org/10.1136/bmj.317.7166.1155a",
"DOI"
)
),
tags$li(
"Delacre, Marie, Daniël Lakens, and Christophe Leys. 2017. ",
"“Why Psychologists Should by Default Use Welch’s t-Test ",
"Instead of Student’s t-Test.” ",
tags$em("International Review of Social Psychology"),
" 30 (1): 92. ",
ref_link(
"https://doi.org/10.5334/irsp.82",
"DOI"
)
),
tags$li(
"Freiman, Jennie A., Thomas C. Chalmers, Harry Smith, ",
"and Roy R. Kuebler. 1978. ",
"“The Importance of Beta, the Type II Error and Sample Size ",
"in the Design and Interpretation of the Randomized Control Trial.” ",
tags$em("New England Journal of Medicine"),
" 299 (13): 690–694. ",
ref_link(
"https://doi.org/10.1056/NEJM197809282991304",
"DOI"
)
),
tags$li(
"Kirkwood, Betty, and Jonathan Sterne. 2001. ",
tags$em("Essentials of Medical Statistics."),
" 2nd ed. Wiley-Blackwell."
),
tags$li(
"Lowitja Institute. 2021. ",
tags$em("Indigenous Data Governance and Sovereignty."),
" ",
ref_link(
paste0(
"https://www.lowitja.org.au/wp-content/uploads/",
"2023/10/328550_data-governance-and-sovereignty.pdf"
),
"PDF"
)
),
tags$li(
"National Indigenous Australians Agency. 2024. ",
tags$em("Framework for Governance of Indigenous Data."),
" ",
ref_link(
paste0(
"https://www.niaa.gov.au/resource-centre/",
"framework-governance-indigenous-data"
)
)
),
tags$li(
"Ruxton, Graeme D. 2006. ",
"“The Unequal Variance t-Test Is an Underused Alternative ",
"to Student’s t-Test and the Mann–Whitney U Test.” ",
tags$em("Behavioral Ecology"),
" 17 (4): 688–690."
)
)
}
clinical_examples <- data.frame(
Question=c("LDL: statin vs placebo","BP: before vs after treatment","HbA1c: three treatments","Age and systolic BP","Adverse reactions by treatment","Smoking before/after intervention","BMI and diabetes","Hospital length of stay"),
Outcome=c("Continuous LDL","Continuous BP","Continuous HbA1c","Continuous BP","Binary reaction","Binary smoking","Binary diabetes","Continuous stay (skewed)"),
Exposure_design=c("Two independent groups","Two paired occasions","Three independent groups","Continuous age; independent adults","Two independent groups","Two paired occasions","Continuous BMI; independent adults","Two independent groups"),
Method=c("Welch t-test","Paired t-test","ANOVA / Welch ANOVA","Linear regression","Chi-squared / Fisher","McNemar","Logistic regression","Rank-sum if a distributional contrast is wanted"),
Interpretation=c("Mean LDL difference with CI","Mean within-person change; no automatic treatment causality","Omnibus mean comparison; adjusted contrasts needed","BP change in mmHg per age-year","Association; also report risks and an appropriate effect CI","Change in marginal smoking proportions","Odds ratio per BMI unit, conditional on covariates","Distributional evidence; not automatically a difference in medians"),
check.names=FALSE
)
outcomes <- c("Continuous numerical"="continuous","Binary categorical"="binary","Categorical: >2 nominal categories"="nominal")
designs <- c("Independent participants"="independent","Related / paired observations"="related")
normals <- c("Approximately normal, where relevant"="normal","Substantially non-normal / questionable"="nonnormal","Unsure: help me assess"="unsure")
panel <- function(title,...) card(card_header(title),...,fill=FALSE)
ui <- page_navbar(
fillable=FALSE,
title=tags$span(class="brand",tags$span(class="brand-mark","N"),"Statistical Test Navigator"),
id="page", theme=bs_theme(version=5,bootswatch=NULL,bg="#f4f7fa",fg="#152e47",primary="#006b68",secondary="#425e76",base_font=font_collection("Segoe UI","Arial","sans-serif")),
header=tagList(tags$head(tags$style(htmltools::HTML(r"---(:root{--navy:#152e47;--teal:#006b68;--muted:#50677b;--line:#d4dfe7}
body{font-family:'Segoe UI',Arial,sans-serif;line-height:1.6} .navbar{background:#fff;border-bottom:1px solid var(--line);padding:1rem 2.5rem}.navbar-brand{font-size:1.05rem;font-weight:700}.brand{display:flex;align-items:center;gap:.7rem}.brand-mark{background:var(--teal);color:white;padding:.2rem .65rem;border-radius:10px}.navbar-nav{font-size:.86rem}.nav-link.active{color:var(--teal)!important;font-weight:700}.hero{background:var(--navy);color:white;padding:3rem 4%;margin-bottom:1.75rem}.hero h1{font-size:clamp(1.9rem,3.5vw,3rem);font-weight:650;letter-spacing:-.04em;margin:.7rem 0}.hero p{color:#d5e4ec;max-width:750px;font-size:1.07rem}.eyebrow{font-size:.7rem;letter-spacing:.16em;font-weight:750;color:var(--teal)}.hero .eyebrow{color:#91ded5}.tab-content{padding:0 3% 2rem}.card{border:1px solid var(--line);border-radius:15px;box-shadow:0 5px 20px #152e4706;margin-bottom:1.3rem;overflow:visible}.card-header{background:transparent;border-bottom:1px solid var(--line);font-weight:650;padding:1.1rem 1.35rem}.card-body{padding:1.4rem}.card h2{font-size:1.65rem;font-weight:700;letter-spacing:-.025em}.card h3{font-size:1rem;font-weight:700;margin-top:1.2rem}.step-label{font-size:.68rem;font-weight:750;letter-spacing:.12em;color:var(--teal);margin:1.1rem 0 .6rem}.control-label{font-weight:650;font-size:.94rem}.form-control,.form-select,.selectize-input{border-radius:8px!important;min-height:42px}.shiny-input-container{width:100%;margin-bottom:1.15rem}.radio label{font-size:.92rem;padding:5px 0}.hint{font-size:.83rem;color:var(--muted)}details{border-top:1px solid var(--line);padding:.85rem 0}summary{cursor:pointer;font-weight:650;font-size:.9rem}details p{margin:.7rem 0 .3rem}.lead{font-size:1.07rem;color:var(--muted)}.notice{border-left:4px solid #986415;background:#fff5df;color:#654710;padding:1rem;border-radius:6px;margin:1rem 0}.path-strip{display:flex;align-items:center;gap:.6rem;padding:.4rem 0 1.2rem;font-weight:650;font-size:.85rem}.path-strip span:last-child{margin-left:auto;font-weight:400;color:var(--muted)}.status-dot{background:var(--teal);width:8px;height:8px;border-radius:50%}.tree{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:1rem;padding:1rem 0}.tree-stage{border-top:3px solid var(--teal);padding-top:.3rem}.tree-stage h3{font-size:.8rem!important}.tree-node{display:block;width:100%;padding:1rem;margin-bottom:.7rem;text-align:left;border:1px solid var(--line);border-radius:10px;background:#f4f7fa;color:var(--muted);font-size:.9rem}.tree-node.selected{background:#e2f3f0;color:#004f4c;border:2px solid var(--teal);font-weight:700}.tree-node.selected:before{content:'✓ ';}.tree-result{background:var(--navy);color:white;padding:1.3rem;border-radius:10px;font-weight:650}.btn{border-radius:8px;font-weight:600;font-size:.9rem;margin:.25rem .25rem .25rem 0}pre{background:#10283b;color:#e2f3f0;border-radius:10px;padding:1.5rem;margin-top:1rem;white-space:pre-wrap;overflow-wrap:anywhere;font-size:.85rem}#copy-status{font-size:.85rem;color:var(--teal);min-height:1.5rem}.footer{border-top:1px solid var(--line);padding:1.5rem 4%;font-size:.72rem;letter-spacing:.07em;font-weight:650;display:flex;justify-content:space-between;color:var(--muted)}a{color:#006965}a:focus-visible,button:focus-visible,input:focus-visible,summary:focus-visible{outline:3px solid #bd761d!important;outline-offset:3px}.table{font-size:.88rem}.table td{vertical-align:top;padding:.8rem}.shiny-plot-output{max-width:100%}
@media(max-width:1000px){.tree{grid-template-columns:1fr 1fr}.navbar{padding:.8rem}.hero{padding:2rem 5%}.path-strip span:last-child{display:none}}@media(max-width:600px){.tree{grid-template-columns:1fr}.tab-content{padding:0 .7rem 1rem}.card-body{padding:1rem}.footer{display:block}.footer span{display:block;margin-top:.5rem}.navbar-brand{font-size:.9rem}.table{display:block;overflow:auto}}
@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;transition:none!important}}
/* Natural document flow: the website owns scrolling. */
html,body{height:auto!important;min-height:0!important;width:100%;max-width:100%;margin:0;overflow:visible!important}
*,*::before,*::after{box-sizing:border-box}
.bslib-page-navbar,.tab-content,.tab-pane,.card,.card-body{height:auto;max-height:none!important;min-width:0;overflow:visible!important}
.bslib-grid>*,.bslib-grid .shiny-html-output{min-width:0;max-width:100%}
.navbar-brand{white-space:normal;min-width:0;max-width:100%}.brand{min-width:0}.navbar-nav{flex-wrap:wrap}
.hero,.tab-content,.footer{max-width:100%;overflow-wrap:anywhere}.path-strip{flex-wrap:wrap}
.shiny-input-container,.selectize-control,.selectize-input{max-width:100%;min-width:0}
.table{width:100%;table-layout:fixed;display:table!important}.table th,.table td{overflow-wrap:anywhere;word-break:normal}
.footer{flex-wrap:wrap;gap:.75rem}.shiny-plot-output img{max-width:100%;height:auto}
@media(max-width:767px){.bslib-grid{grid-template-columns:minmax(0,1fr)!important}.navbar{padding:.75rem}.brand{font-size:.88rem}.hero{padding:1.5rem 1rem}.table{font-size:.72rem}.table th,.table td{padding:.3rem}.card h2{font-size:1.4rem}}
)---")),tags$script(htmltools::HTML(r"---(
(() => {
const type = "statistical-navigator:height";
let pending = false, previous = 0;
function measure() {
pending = false;
const body = document.body;
if (!body) return;
const height = Math.ceil(body.getBoundingClientRect().height + 12);
if (height > 0 && Math.abs(height - previous) > 1) {
previous = height;
window.parent.postMessage({type, height}, "*");
}
}
function schedule() { if (!pending) { pending = true; requestAnimationFrame(measure); } }
function start() {
new ResizeObserver(schedule).observe(document.body);
new MutationObserver(schedule).observe(document.body, {subtree:true, childList:true, attributes:true});
document.addEventListener("load", schedule, true);
document.addEventListener("shown.bs.tab", schedule);
document.addEventListener("toggle", schedule, true);
window.addEventListener("resize", schedule);
window.addEventListener("message", event => { if(event.source === window.parent && event.data?.type === "statistical-navigator:measure") schedule(); });
schedule();
}
if(document.readyState === "loading") document.addEventListener("DOMContentLoaded", start); else start();
})();
)---"))),
div(class="hero",div(class="eyebrow","RESEARCH METHODS / INTERACTIVE GUIDE"),h1("A clearer path to your analysis."),p("Start with your question. Understand the assumptions. Choose a method you can explain."))),
nav_panel("Test Selector",
layout_columns(fill=FALSE,col_widths=c(4,8),
div(class="selector",panel("Build your research question",
div(class="step-label","01 / OUTCOME"),
selectInput("outcome","What is your outcome variable?",outcomes),
tags$details(tags$summary("Examples of outcome types"),p("Continuous: LDL, BP, HbA1c. Binary: diabetes yes/no. Nominal: no reaction, GI reaction, skin reaction. Ordered categories need an ordinal model.")),
div(class="step-label","02 / EXPOSURE"),uiOutput("exposure_ui"),
conditionalPanel("input.exposure != 'none'",div(class="step-label","03 / DEPENDENCE"),radioButtons("design","Are observations independent or related?",designs),p(class="hint","Independent: different statin and placebo participants. Related: the same participants before and after treatment.")),
conditionalPanel("input.outcome == 'continuous'",div(class="step-label","04 / ASSUMPTIONS"),radioButtons("normality","Are relevant normality assumptions reasonable?",normals,selected="unsure")),
conditionalPanel("input.outcome != 'continuous' && input.exposure != 'numeric' && input.design == 'independent' && !input.adjust",selectInput("sparse","Expected cell counts",c("Unsure / not yet checked"="unsure","Adequate for Pearson approximation"="adequate","Sparse: consider exact inference"="sparse"))),
tags$details(tags$summary("More about your design"),checkboxInput("adjust","Need to adjust for confounders",FALSE),checkboxInput("complex","Clustered, complex longitudinal, or time-to-event data",FALSE)),
actionButton("reset","Reset selections",class="btn-outline-secondary")
)),
div(uiOutput("path_summary"),uiOutput("recommendation"),panel("See the assumptions",uiOutput("assumption_help"),plotOutput("diagnostic",height="240px"),p(class="hint","Illustrative simulated data only. This app does not upload or diagnose study data.")))
)),
nav_panel("Statistical Concepts",layout_columns(fill=FALSE,col_widths=c(6,6),panel("Reason before testing",concepts_ui()),panel("Beyond the Basic Flowchart",beyond_ui())),panel("Clinical examples: question to interpretation",tableOutput("examples")),panel("Sources and further reading",tags$details(tags$summary("Statistical references"),reference_ui()))),
footer=div(class="footer","EDUCATIONAL DECISION SUPPORT",tags$span("Research question → estimand → design → assumptions → method"))
)
server <- function(input,output,session) {
output$exposure_ui <- renderUI({
choices <- exposure_choices(input$outcome)
previous <- isolate(input$exposure)
selected <- if(!is.null(previous) && previous %in% choices) previous else "two"
selectInput("exposure","What is your explanatory variable?",choices,selected=selected)
})
selection <- reactive({
req(input$exposure)
# A reactive UI rebuild may briefly retain a stale value; normalise it here.
exposure <- if(input$exposure %in% exposure_choices(input$outcome)) input$exposure else "two"
list(outcome=input$outcome,exposure=exposure,design=if(exposure=="none") "independent" else input$design,
normality=input$normality,adjust=isTRUE(input$adjust),sparse=input$sparse,complex=isTRUE(input$complex))
})
recommendation <- reactive(do.call(select_method,selection()))
information <- reactive(test_information[[recommendation()$id]])
output$recommendation <- renderUI({
info <- information(); s <- selection()
panel(div(class="eyebrow","YOUR METHOD / CONDITIONAL ON DESIGN"),
div(role="status",`aria-live`="polite",h2(info$title)),p(class="lead",info$target),
if(length(recommendation()$note)) div(class="notice",paste(recommendation()$note,collapse=" ")),
h3("Why this fits"),p(paste("Your choices:",paste(path_labels(),collapse=" → "),".",info$target)),
layout_columns(fill=FALSE,col_widths=c(6,6),div(h3("Null hypothesis · H0"),p(info$h0)),div(h3("Alternative · H1"),p(info$h1))),
h3("Assumptions to assess"),p(info$assumptions),
tags$details(open=NA,tags$summary("Clinical example & interpretation"),p(info$example),p(info$interpretation)),
tags$details(tags$summary("Limitations & alternative approaches"),p(info$limitations),p(info$alternatives)),
p(class="hint","Simple tests generally do not adjust for confounding. Association does not establish causality."))
})
path_labels <- reactive({
s <- selection()
c(names(outcomes)[match(s$outcome,outcomes)],names(exposure_choices(s$outcome))[match(s$exposure,exposure_choices(s$outcome))],
if(s$exposure=="none") "Single independent sample" else names(designs)[match(s$design,designs)],
if(s$outcome=="continuous") names(normals)[match(s$normality,normals)] else "Count / model assumptions")
})
output$path_summary <- renderUI(div(class="path-strip",tags$span(class="status-dot"),"Live recommendation",tags$span("4 decision stages • explore below")))
output$assumption_help <- renderUI({
s <- selection()
p(if(s$outcome!="continuous") "Normality is not required for categorical outcomes. Check participant independence, expected counts, pairing, model form and sparse-data problems."
else if(s$exposure=="numeric" || s$adjust) "For linear regression, assess residuals conditional on predictors, not whether the predictor or marginal outcome is normal. Check linearity, variance, influential observations and dependence."
else if(s$design=="related" && s$exposure=="two") "For paired tests, examine within-pair differences. Signed-rank needs symmetry of those differences; a non-normal marginal outcome does not choose the test."
else "Examine within-group errors or observations, sample sizes, skew and influential outliers. Equal variances are separate from normality. Welch methods relax equal variances, not independence.")
})
output$diagnostic <- renderPlot({
set.seed(31); s <- selection()
if(s$outcome!="continuous") {
d <- data.frame(group=rep(c("A","B"),each=2),outcome=rep(c("Event","No event"),2),n=c(20,80,35,65))
ggplot2::ggplot(d,ggplot2::aes(group,n,fill=outcome))+ggplot2::geom_col(position="fill")+ggplot2::scale_fill_manual(values=c("#006b68","#b3c5d5"))+ggplot2::labs(x="Independent group",y="Observed proportion",fill=NULL)+ggplot2::theme_minimal(base_size=12)
} else {
values <- if(s$normality=="nonnormal") rexp(80)-1 else rnorm(80)
label <- if(s$exposure=="numeric" || s$adjust) "Illustrative regression errors" else if(s$design=="related") "Illustrative within-person differences" else "Illustrative within-group observations"
ggplot2::ggplot(data.frame(value=values),ggplot2::aes(sample=value))+ggplot2::stat_qq(colour="#006b68")+ggplot2::stat_qq_line(colour="#7d8ea0")+ggplot2::labs(title=label,x="Normal theoretical quantiles",y="Sample quantiles")+ggplot2::theme_minimal(base_size=12)
}
}, alt="Illustrative simulated quantile or categorical proportion plot; not study data.")
output$examples <- renderTable(clinical_examples,striped=TRUE,spacing="m")
observeEvent(input$reset, {
updateSelectInput(session,"outcome",selected="continuous")
updateSelectInput(session,"exposure",selected="two")
updateRadioButtons(session,"design",selected="independent")
updateRadioButtons(session,"normality",selected="unsure")
updateSelectInput(session,"sparse",selected="unsure")
updateCheckboxInput(session,"adjust",value=FALSE)
updateCheckboxInput(session,"complex",value=FALSE)
})
}
shinyApp(ui,server)