Skip to content
Statistics LabMAST20005 · 2020

Statistics Lab / Back matter

Methods and decision records

Where the data come from, how the 2020 work was ported and checked, how the 2026 additions were evaluated, what the site assumes and where it falls short. The choices behind it are written up as decision records, each stating the decision first and the numbers that followed, weak ones included.

docs/methods.md

Methods

This page describes where the data come from, how the 2020 analyses were ported, how the port and the methods themselves were evaluated, what the site assumes, and where it falls short. The decisions behind each choice are written up as decision records further down.

Data provenance

  • The 2020 coursework inputs. Plant heights (A1 Q1), a lognormal sample (A1 Q2), two walking-time samples (A2 Q2), coffee-shop sales and customers (A2 Q3) and a survey of two councils (A2 Q1) came with the assignments from the University of Melbourne. They are kept in coursework/ and in the test fixtures, and they are never served by the website. The site publishes only aggregates computed from them: the values in the parity ledger and the statistics in the assumption checks.
  • Simulated samples. Every interactive lab that used a university dataset starts from a simulated sample of the same size and shape, drawn with a fixed seed that is shown on screen. Visitors can paste their own data instead.
  • Public data. Daily new COVID-19 cases in Victoria from 7 to 27 September 2020, as published by the Victorian Department of Health and Human Services, are used directly in Chapter 4 and on /diagnostics.
  • Reference values. R 4.6.1 (scripts/r_reference.R, scripts/r_diagnostics.R, scripts/r_studies.R) and SciPy 1.18 (scripts/py_reference.py) produce the values the tests compare against. They are test fixtures only.

Method

The 2020 R code was ported to TypeScript function by function, including R’s own conventions for quantiles, ranks, printing and p-value formatting. The random number generator is a port of R’s default Mersenne-Twister with Inversion normals, Ahrens–Dieter exponentials and rejection sampling for sample(), so a seed on this site produces the same draws as set.seed() in R. Everything runs in the visitor’s browser, with large simulations in a Web Worker.

The 2026 upgrade adds three layers around the 2020 work without changing it.

  1. Uncertainty on every simulated quantity. Wilson intervals and Monte Carlo standard errors for rates, paired bootstrap percentile intervals for comparisons between estimators, and Monte Carlo standard errors for simulated mean squared errors.
  2. Simulation studies (/studies) that put five 2020 methods under repeated sampling to measure coverage, size and power.
  3. Assumption checks (/diagnostics) for every assumption the 2020 methods rely on, plus interactive versions that run on any data.

Evaluation design

  • Parity. Every number printed in the 2020 submission is recomputed by the port and compared as a string at the printed precision. All 34 printed R outputs match character for character, and the 6 hand calculations agree to their rounding.
  • Helper accuracy. Each new statistical helper is tested against R or SciPy on known inputs. Shapiro–Wilk matches shapiro.test() on 14 datasets from n = 3 to n = 200, and the Monte Carlo p-values replay R’s loops from the same seeds exactly.
  • Simulation studies. Five studies, 49 cells and 245,000 replications. The grid, the seeds and the number of replications are declared in code, and the design was committed together with its results, so this is a fixed design rather than a pre-registration. Each cell has 5 seed blocks of 1,000 replications. Where theory gives an exact rate it is shown beside the simulated one, and an independent R implementation reproduces every count.
  • Comparisons are paired. When two procedures are compared they run on the same simulated datasets, and the difference is reported with Newcombe’s paired interval and an exact McNemar test.
  • Assumption checks use one fixed reading rule for every check. A p-value below 0.01 is strong evidence against the assumption, below 0.05 is some evidence, and anything else is no evidence against, which is never read as confirmation.

Assumptions

  • The ported methods carry the 2020 assumptions with them: normal errors for t intervals and regression, independent observations everywhere, equal variances for the pooled interval and a random sample for the case-count tests. /diagnostics shows which of these the 2020 data support.
  • The simulation studies assume the data-generating models they name. A coverage of 95.0% under normal data says nothing about skewed data unless a skewed parent is in the grid.
  • The R-compatible generator matches R’s uniform stream bit for bit. Normal and exponential draws agree with R to within a few units in the last place, because R’s build fuses multiply-adds inside qnorm. No printed result or simulation count is affected.

Limitations

  • The 2020 datasets are small, from 8 to 30 values, so most assumption checks have little power, and “no evidence against” is a weak statement.
  • The two-proportion study looks at one pair of true proportions, and the power study looks at location shifts only. Neither covers the conditions where the weaker method fails worst.
  • The case counts are a trending time series. The 2020 sign and Wilcoxon results for them are shown as submitted, but the checks show that their random-sample assumption does not hold.
  • The optional AI explanations are tested only with mocked network calls. They are never used to produce a number.

What I’d change

  • Size each simulation study from a target interval width instead of using 5,000 replications everywhere.
  • Add the Welch t test and unequal-variance settings to the power study, since the 2020 walking-time samples had unequal variances.
  • Replace the two-sample comparison of case counts with a model that includes a trend, such as a Poisson regression on day.
  • Add an evaluation harness for the AI explanations if they ever do more than restate the numbers on the page.

docs/decisions/

Decision records

Each record follows the same format: context, the decision, the options considered, why, what happened and what I would change. A past record is never edited to change its decision. A new record supersedes it.

  1. DR-001 · 2026-10-09Keep the paired midrange-vs-mean design and report relative efficiency with a bootstrap interval
  2. DR-002 · 2026-10-09Estimate a uniform endpoint with the scaled maximum, never the method of moments
  3. DR-003 · 2026-10-09Keep the 2020 interval for the lognormal variance and show its unequal tails
  4. DR-004 · 2026-10-09Keep the Wald interval for the council survey and recommend Agresti–Caffo for small groups
  5. DR-005 · 2026-10-09Prefer the Wilcoxon test at about 20 per group for heavy-tailed or skewed data, report both at 7 per group, and state the attainable size
  6. DR-006 · 2026-10-09Fix every simulation design in code, report Monte Carlo error, and replay it in R
  7. DR-007 · 2026-10-09Check every assumption the 2020 methods rely on, and publish the checks beside the unchanged results
  8. DR-008 · 2026-10-09Optional “Explain this result” with the visitor’s own key, grounded on the numbers on screen and audit-logged locally

DR-001accepted2026-10-09

Keep the paired midrange-vs-mean design and report relative efficiency with a bootstrap interval

Decision: the 2020 comparison of the sample midrange and the sample mean (A1 Q4) stays exactly as submitted, including its paired design and its printed variances. Beside it the lab now reports the relative efficiency var(midrange) / var(mean) with a paired bootstrap percentile interval, and the sample mean remains the recommended estimator for normal data.

Scope: /estimators#midrange, web/src/lib/stats/bootstrap.ts

Context

Assignment 1 Q4 asked whether the midrange (the average of the smallest and largest values) or the sample mean is the better estimator of a normal mean for samples of 20. The notebook drew 100 samples with set.seed(2529) and printed a variance of 0.158578 for the midrange and 0.069188 for the mean. It drew a second matrix for the mean but then applied the mean to the first matrix, so both estimators saw the same 100 samples. The 2020 answer reported two variances and no uncertainty, and 100 replications is a small simulation.

Decision

Keep the 2020 numbers and the paired design. Report one effect size, the relative efficiency var(midrange) / var(mean), with a 95% percentile interval from a paired bootstrap that resamples the 100 (midrange, mean) pairs together (B = 2,000, seed 1). State the theoretical variance of the mean beside its simulated value.

Options considered

  1. Leave the two variances as printed. Faithful, but it says nothing about how far 100 replications can be trusted.
  2. Treat the two columns as independent and use an F test. This ignores the pairing, which is the strongest part of the design.
  3. Paired bootstrap of the variance ratio (chosen). Keeps the pairing and needs no normality of the estimates.
  4. Re-run with many more replications and replace the 2020 numbers. More precise, but it would silently change a submitted result.

Why

Because both estimators were computed on the same samples, the noise that is common to both cancels in their ratio, and resampling pairs keeps that benefit. A percentile interval is honest about the skewed sampling distribution of a variance ratio. The bootstrap uses the site’s port of R’s sample(), so the same loop in R gives the same interval to the last digit, and the test suite checks this.

What happened

  • The relative efficiency on the 2020 draws is 2.29, with a 95% bootstrap interval from 1.76 to 3.05. R reproduces both endpoints exactly.
  • A reference run of 100,000 samples in R (set.seed(1), n = 20) gives variances of 0.1425 and 0.0502, a relative efficiency of 2.84. That value sits inside the interval but well above the 2020 point estimate.
  • The 2020 variance of the mean, 0.0692, came out 38% above its theoretical value σ²/n = 0.05. With 100 replications the variance of a variance is large, so the 2020 run understated how much better the mean is. The conclusion that the mean is better stands. The size of the advantage was uncertain, and the interval now says so.

What I’d change

  • Choose the number of replications from a target Monte Carlo standard error before running, as the simulation studies on /studies now do.
  • Report the theoretical variance σ²/n next to every simulated one by default, so a run that drifts from theory is visible at once.

DR-002accepted2026-10-09

Estimate a uniform endpoint with the scaled maximum, never the method of moments

Decision: for X₁, …, Xₙ from Unif(0, θ) the lab recommends the MSE-optimal scaled maximum (n + 2)/(n + 1) · max(Xᵢ), or the unbiased (n + 1)/n · max(Xᵢ) when unbiasedness is required, and never the method-of-moments estimator 2X̄. Every simulated mean squared error is now shown with its Monte Carlo standard error.

Scope: /estimators#uniform, web/src/lib/stats/bootstrap.ts (mseWithMcse)

Context

Assignment 1 Q3 derived the method-of-moments estimator 2X̄ and the maximum likelihood estimator max(Xᵢ) for a uniform endpoint, their mean squared errors θ²/(3n) and 2θ²/{(n + 1)(n + 2)}, and the multiple of the MLE that minimises MSE, a = (n + 2)/(n + 1). The 2020 answer was a derivation on paper. The 2026 lab adds a simulation of all three estimators, which needs uncertainty attached before it can be compared with the theory.

Decision

Recommend the scaled maximum. Show the simulated MSE of each estimator with its Monte Carlo standard error, sd((θ̂ − θ)²)/√R, next to the theoretical MSE, so a reader can judge whether a gap between the two is noise.

Options considered

  1. Method of moments, 2X̄. Unbiased and simple, but it ignores that no observation can exceed θ.
  2. Maximum likelihood, max(Xᵢ). Always below θ, so biased downwards.
  3. Unbiased MLE, (n + 1)/n · max(Xᵢ). Removes the bias.
  4. MSE-optimal scaled MLE, (n + 2)/(n + 1) · max(Xᵢ) (chosen as the default). Accepts a little bias for a lower MSE.

Why

The maximum is a sufficient statistic, so any estimator built on X̄ throws information away. Its variance shrinks like 1/n² rather than 1/n. Between the two rescalings the difference is small and the choice depends on the loss. Mean squared error was the criterion in the question, so the MSE-optimal multiple is the default.

What happened

With the lab’s default settings (n = 10, θ = 3, 2,000 samples from set.seed(20005)) the simulated MSEs are 0.288 ± 0.009 for 2X̄, 0.127 ± 0.005 for the maximum and 0.069 ± 0.003 for the scaled maximum, against theoretical values of 0.300, 0.136 and 0.074. The MSE of 2X̄ is 4.19 times that of the scaled maximum, with a paired bootstrap interval from 3.80 to 4.61, and the theoretical ratio of 4.03 lies inside it.

All three simulated MSEs sit between 1.3 and 1.8 standard errors below their theoretical values. They come from the same 2,000 samples, so their errors move together, and a run of slightly small maxima pulls all three down at once. The ranking of the estimators does not depend on it.

What I’d change

  • Add the unbiased (n + 1)/n · max(Xᵢ) to the lab as a fourth estimator so the bias-variance trade-off between the two rescalings is visible.
  • Use a paired interval for every between-estimator comparison in the lab, not only in this record.

DR-003accepted2026-10-09

Keep the 2020 interval for the lognormal variance and show its unequal tails

Decision: the A1 Q2 interval for λ = σ² stays as submitted, (n − 1)λ̂ / χ², with its printed values (1.118327, 5.189367). It is labelled “as submitted” and shown beside the textbook equal-tailed interval SS / χ². The simulation study on /studies reports the exact and simulated coverage of both, tail by tail.

Scope: /fitting#lognormal, /studies#lambda-interval, web/src/lib/studies/definitions.ts

Context

For a lognormal sample, λ is the variance of log X. The 2020 code computed the MLE λ̂ = SS/n, where SS = Σ(log xᵢ − mean)², and then plugged it into the pivot (n − 1)S²/λ ~ χ²ₙ₋₁, which is built for the unbiased S² = SS/(n − 1). The result is the textbook interval multiplied by (n − 1)/n. Nothing in the 2020 answer was wrong arithmetically, but the interval is not the one the pivot gives.

Decision

Do not change the submitted interval. Add the textbook interval beside it in the lab, and measure both under repeated sampling with exact coverage from the χ² distribution and Monte Carlo coverage from 5,000 samples per sample size.

Options considered

  1. Replace the 2020 interval with the textbook one. Correct, but it rewrites a submitted result.
  2. Keep it and say nothing. Faithful, but hides a known flaw.
  3. Keep it, label it, and measure it (chosen).

Why

The shift by (n − 1)/n moves both cut-offs on SS/λ upwards by n/(n − 1). Whether that matters is an empirical question about coverage, and the χ² distribution answers it exactly, so the honest response is to compute it and show it rather than to assume.

What happened

  • Overall coverage of the 2020 interval is close to nominal: 95.49% at n = 5, 95.14% at n = 15 (the coursework’s sample size) and 95.04% at n = 50, from the χ² distribution.
  • The tails are not balanced. At n = 15 the 2020 interval lies entirely below λ 3.43% of the time and entirely above it 1.43% of the time, against 2.5% each for the textbook interval. It is shifted downwards, as a plug-in of the downward-biased MLE would suggest.
  • The simulation agrees with the exact values in every cell. At n = 15 the 2020 interval covered in 94.86% of 5,000 samples (Wilson 95% interval 94.21% to 95.44%). On the same samples the paired difference in coverage between the two intervals is 0.0 percentage points (−0.4 to 0.4).
  • On the 2020 data the textbook interval is (1.1982, 5.5600), against the submitted (1.1183, 5.1894).
  • A larger problem showed up in the assumption checks. Shapiro–Wilk on the logs of the 2020 sample gives p = 0.009, so the normal model behind any χ² interval for these data is doubtful (DR-007). That matters more than the divisor.

What I’d change

  • Report a one-sided miss rate for every interval in the lab, because an interval can hit 95% overall while missing on one side three times as often as on the other.
  • For the 2020 data, a bootstrap interval for the variance of the logs would not lean on normality.

DR-004accepted2026-10-09

Keep the Wald interval for the council survey and recommend Agresti–Caffo for small groups

Decision: the 2020 Wald interval for p₁ − p₂ (A2 Q1, 520 of 800 against 600 of 1,000) stays as submitted, because at those sample sizes it behaves the same as the Agresti–Caffo interval. For groups of a few dozen or fewer the lab recommends Agresti–Caffo, which adds one success and one failure to each group.

Scope: /intervals#proportions, /studies#two-proportion, web/src/lib/stats/proportion.ts

Context

The 2020 answer used prop.test(..., correct = FALSE), whose interval for the difference is the Wald interval p̂₁ − p̂₂ ± z · se. The Wald interval is known to undercover when samples are small or proportions are near 0 or 1. The question is whether that applies at the coursework’s sample sizes.

Decision

Measure both intervals at the coursework’s sizes and at two smaller sizes with the same 4 : 5 ratio, with true proportions p₁ = 0.65 and p₂ = 0.60 (the 2020 estimates). Use exact coverage, from summing binomial probabilities over every possible pair of counts, and Monte Carlo coverage from 5,000 simulated surveys per size, with both intervals computed on the same surveys.

Options considered

  1. Wald (as submitted).
  2. Agresti–Caffo (chosen for small groups). One line of code, close to nominal coverage.
  3. Newcombe’s hybrid score interval. Good coverage, more involved to explain.
  4. Exact (conditional) intervals. Conservative, and awkward for a difference of two independent proportions.

Why

Agresti–Caffo fixes the Wald interval’s worst behaviour with almost no extra complexity, and it agrees with the Wald interval when samples are large. A paired comparison on the same simulated surveys shows directly how often the choice changes the answer.

What happened

Groups (n₁, n₂)Wald, exactAgresti–Caffo, exactAgresti–Caffo − Wald on the same surveys
16, 2093.83%95.58%+1.6 points (1.3 to 2.0)
40, 5094.42%95.13%+0.7 points (0.4 to 0.9)
800, 100094.98%94.99%0.0 points (−0.1 to 0.1)

At the coursework’s sizes the two intervals disagreed about covering the true difference in 2 of 5,000 simulated surveys. The 2020 interval needs no change. At 16 and 20 the Wald interval misses 6.2% of the time instead of 5%.

This study looks at one pair of true proportions only. The Wald interval is worst when a proportion is near 0 or 1, and this design does not probe that region, so it understates the case for Agresti–Caffo in general.

What I’d change

  • Extend the grid to proportions near 0.05 and 0.95, where the difference between the intervals is largest.
  • Show the Agresti–Caffo interval as a second line in the two-proportion lab.

DR-005accepted2026-10-09

Prefer the Wilcoxon test at about 20 per group for heavy-tailed or skewed data, report both at 7 per group, and state the attainable size

Decision: with about 20 observations per group and data that may be heavy-tailed or skewed, the lab treats the exact Wilcoxon rank-sum test as the default (+11.6 and +21.7 points of power at a shift of 0.5 SD, against −1.7 under normality) and shows the pooled t test beside it. At 7 per group neither test dominates, so the lab reports both. Every exact Wilcoxon result states its attainable size, which is 3.79% rather than 5% for two groups of 7.

Scope: /tests#wilcoxon, /studies#two-sample-power

Context

The coursework used both tests. A2 Q2(c) built a pooled t interval for two walking-time samples, and A3 Q1(b) ran a Wilcoxon rank-sum test on two weeks of daily case counts, 7 days each. Neither answer said why one test was chosen over the other, or what each costs in power.

Decision

Choose between the tests by sample size and by what is known about the shape of the data, using a paired simulation:

  • About 20 per group, tails or skew likely: the exact Wilcoxon test is the default, with the pooled t test beside it.
  • 7 per group: report both tests and the Wilcoxon test’s attainable size of 3.79%. Neither dominates.

The evidence comes from running both tests on the same simulated datasets, for 7 and 20 observations per group, for normal, heavy-tailed (t with 3 degrees of freedom) and skewed (exponential) data, at shifts of 0, 0.5, 1 and 1.5 standard deviations. Report each test’s rejection rate with a Wilson interval and the paired difference with Newcombe’s interval and an exact McNemar p-value.

Options considered

  1. Pooled t test. Exact size under normality and equal variances.
  2. Welch t test. Drops the equal-variance assumption.
  3. Exact Wilcoxon rank-sum test (chosen as the default at about 20 per group when tails or skew are likely). Distribution-free size.
  4. Permutation test on the mean difference. Exact size, more computation, harder to explain on paper.

Why

The Wilcoxon test’s size does not depend on the shape of the data, and the study shows its power loss under normality is small at 20 per group while its gain under heavy tails and skew is large. At 7 per group the evidence does not support a default either way. The pooled t test kept Type I error control for all three shapes, and the Wilcoxon test gained power only for skewed data at small shifts. With 7 per group the exact test cannot reach 5%, which costs power under normality, so stating the attainable size is part of reporting it honestly.

What happened

Rejection rates in percentage points, 5,000 datasets per cell:

Data, n per groupShiftPooled tWilcoxonWilcoxon − t (95% interval)
Normal, 71 SD40.834.2−6.7 (−7.5 to −5.8)
Exponential, 70.5 SD17.019.9+2.9 (2.1 to 3.7)
t₃, 71 SD55.455.1−0.3 (−1.2 to 0.6)
Normal, 200.5 SD34.732.9−1.7 (−2.4 to −1.0)
t₃, 200.5 SD43.855.3+11.6 (10.5 to 12.6)
Exponential, 200.5 SD36.958.6+21.7 (20.5 to 22.9)
  • Under no shift the Wilcoxon test rejected 3.78% of normal datasets of 7 per group, against an exact attainable size of 3.79%. The t test rejected 5.08%.
  • At 7 per group the paired difference clearly favoured the t test in 4 of the 9 shifted cells (normal data at all three shifts, −3.0 to −7.2 points, and t₃ at 0.5 SD, −1.7) and clearly favoured the Wilcoxon test in 2 (exponential data at 0.5 and 1 SD, +2.9 and +1.5). In the other 3 the difference was under half a point and its interval included zero. “Clearly” means Newcombe’s interval excludes zero.
  • With 7 per group the t test became conservative on skewed data (3.68% rejections under no shift, Wilson interval 3.19% to 4.24%). It still controls Type I error there, but it loses its exact size, and with it some power.
  • At 7 per group the Wilcoxon’s lower attainable size accounts for part of its deficit under normality. A randomised or mid-p version would recover some of it, at the cost of a less familiar test.

What I’d change

  • Add the Welch t test. The 2020 walking-time samples had clearly unequal variances (DR-007), a situation this study does not cover because it shifts location only.
  • Add scale differences between the groups, where the pooled t test’s size breaks down.

DR-006accepted2026-10-09

Fix every simulation design in code, report Monte Carlo error, and replay it in R

Decision: every simulation study on /studies has its design declared in code: the grid of settings, 5 seed blocks of 1,000 replications per cell, and seeds of the form base + 100 × cell + block. Every rate is shown with a Wilson 95% interval and a Monte Carlo standard error, the spread across the five seeds is shown next to what binomial noise predicts, and an independent R script must reproduce every count exactly. The design and its results were committed together, so this is a fixed, checkable design, not a pre-registration.

Scope: web/src/lib/studies, web/src/content/study-results.json, scripts/r_studies.R

Context

The 2020 coursework ran two small simulations (100 replications each) and printed point estimates without any measure of their own noise. The revival adds five larger studies of coverage, size and power. A simulation result is itself an estimate, and it can mislead in the same ways as any other estimate if its error is not reported or if the design is chosen after looking at the output.

Decision

  • Each cell runs 5,000 replications in 5 independent blocks, so the Monte Carlo standard error of a rate near 95% is about 0.3 percentage points and a Wilson interval is about ±0.6 points wide.
  • The committed results file holds only raw per-seed counts. Rates, intervals and comparisons are derived from those counts by tested helpers, so nothing on the page is typed in by hand.
  • Paired designs are used wherever two procedures are compared, and the comparison uses Newcombe’s paired interval and an exact McNemar test rather than the overlap of two separate intervals.
  • Where theory gives an exact rate, it is shown beside the simulated one.
  • scripts/r_studies.R re-runs every cell in R with R’s own t.test(), wilcox.test(), qchisq() and qgamma(), and the test suite requires identical counts.

Options considered

  1. A single seed per cell. Simpler, but gives no direct look at seed-to-seed variation.
  2. Many seeds with few replications each. Shows the spread well but makes each block’s rate very noisy.
  3. Five blocks of 1,000 (chosen). Enough replications for a ±0.6-point interval, and enough blocks to compare the observed spread with the binomial prediction.

Why

A design declared in code and replayed in R can be checked: anyone can confirm that the published counts come from that grid, those seeds and that number of replications, and from nothing else. It does not prove the design was chosen before any output was seen, because the design and the results arrived in the same commit. Five blocks make the claim “the Monte Carlo error is what the binomial model says” checkable instead of assumed. An independent replay in R is the strongest check available that the simulation does what its description says. It catches a wrong formula, a wrong seed or a wrong draw order, and the R-compatible random number generator makes the replay exact rather than approximate.

What happened

  • 49 cells and 245,000 replications in total. R reproduces every per-seed count in every cell. R takes about 70 seconds and the TypeScript engine about 3 seconds.
  • 51 rates have an exact value from theory (the t interval under normality, both λ intervals, both two-proportion intervals, the Wilcoxon test’s attainable size and the likelihood-ratio test’s power curve). All 51 exact values lie inside their Wilson intervals. With 95% intervals about 2 or 3 misses would be expected if the rates were independent. Many of them share replications, which makes a clean sweep less surprising than it looks, so I read this as “no sign of miscalibration” rather than as better-than-expected accuracy.
  • Across the 115 rates whose spread can be compared, the standard deviation over the five seeds averages 1.04 times the binomial prediction (root mean square of the ratio) and never exceeds 2 times it. With only five blocks each of those ratios is itself noisy.
  • The seeds are visible on the page, and a reader can re-run any study in the browser with another seed base.

What I’d change

  • Pre-register future studies properly: commit the design (the definitions and the R script) on its own, before generating any results, and cite that commit’s hash in the decision record.
  • Compare the 2020 rate test in B.5 with the exact-size likelihood-ratio region. The 2020 answer used equal tails, (37.11, 64.78) for n = 50, while Λ(a) = Λ(b) with size 5% gives (37.37, 65.20). The study as designed checks the algebra of the 2020 test, not whether equal tails were the right choice.
  • Choose the number of replications per study from a target interval width rather than one number for every study. The power study would benefit from more replications near 50% power, where binomial error is largest.
  • Add a regression test that runs a reduced version of each study under the R replay in CI, so a change to R itself would be noticed. Today the R side runs only when the fixtures are regenerated.

DR-007accepted2026-10-09

Check every assumption the 2020 methods rely on, and publish the checks beside the unchanged results

Decision: each method in the 2020 coursework gets a fixed check of the assumption it depends on. The checks run on the original inputs inside the test suite, only their aggregates are published (on /diagnostics), and they are read with one fixed rule. The 2020 results are never changed because of them.

Scope: /diagnostics, web/src/lib/assumption-checks.ts, web/src/lib/stats/diagnostics.ts, web/src/lib/stats/series.ts

Context

The 2020 answers checked assumptions by eye, with a QQ plot for the plant heights and the four plot(lm) panels for the coffee regression, and ran one formal check, the F test for equal variances in A2 Q2(e). Assignment 3 Q1 asked students to treat 21 consecutive days of case counts as a random sample, and the question itself called that unrealistic. A reader of the revived site should be able to see which 2020 conclusions rest on firm ground and which do not.

Decision

Twelve checks, one per assumption:

CourseworkAssumptionCheck
A1 Q1Heights are exponentialKolmogorov–Smirnov with the rate estimated, p-value by parametric bootstrap (999 resamples)
A1 Q2Logs are normalShapiro–Wilk
A2 Q2Each walking-time sample is normalShapiro–Wilk
A2 Q2Equal variancesF test (the 2020 answer’s own)
A2 Q3Normal, constant-variance, independent errors and no dominant pointShapiro–Wilk, Breusch–Pagan, Durbin–Watson with a Monte Carlo p-value, Bonferroni outlier test
A3 Q1Days are a random sampleRuns test, Ljung–Box, Mann–Kendall with a Theil–Sen slope

One reading rule is applied to every check. A p-value below 0.01 is “strong evidence against”, below 0.05 is “some evidence against”, and anything else is “no evidence against”. The last never means the assumption holds.

Options considered

  1. Leave the visual checks as they were. Faithful, but the case-count problem stays invisible.
  2. Replace the 2020 analyses where a check fails. That would rewrite submitted work.
  3. Publish the checks beside the unchanged results (chosen).

Why

Checks beside the results let a reader weigh each 2020 conclusion without the site deciding for them. Running them in the test suite keeps the university datasets out of the deployed site, the same arrangement as the parity ledger. Every statistic is checked against R, or against SciPy for the Theil–Sen interval.

What happened

Five of the twelve checks found strong evidence against an assumption. Seven found none.

  • A1 Q2. Shapiro–Wilk on the logs gives W = 0.830, p = 0.009. The lognormal model behind the 2020 χ² interval is doubtful for this sample.
  • A2 Q2. The 2020 F test rejected equal variances (F = 15.28, p = 0.0016), yet part (c) used the pooled interval. The Welch interval, (−7.578, −2.952) on 10.4 degrees of freedom, is close to the pooled (−7.719, −2.811), and both exclude zero, so the 2020 conclusion that route Y is slower survives.
  • A3 Q1. The case counts fall steadily over the 21 days. There are 4 runs above and below the median where about 11 are expected (exact p = 0.002), Ljung–Box gives Q = 25.8 on 7 lags (p = 0.0005), and Kendall’s τ against time is −0.69 (p = 0.00002). The Theil–Sen slope is −2.21 cases per day (95% interval −2.90 to −1.64). The sign test’s p = 0.9608 for “the median is below 15” therefore answers a question the data cannot pose. There is no single median for a series that is trending downwards.
  • No evidence against the exponential model for the plant heights (D = 0.142, bootstrap p = 0.296 ± 0.014), normality of either walking-time sample, or any of the four regression assumptions (Shapiro–Wilk p = 0.99, Breusch–Pagan p = 0.59, Durbin–Watson p = 0.67 ± 0.01, largest studentised residual Bonferroni p = 0.51). These samples have between 8 and 30 values, so the checks have little power, and “no evidence against” is the most they can say.
  • With twelve checks at the 5% level, about 0.6 false alarms would be expected by chance. All five flagged checks have p below 0.01, and three of them describe the same trending series.

What I’d change

  • Report power for each check at the sample size used, so “no evidence against” can be read as weak or strong.
  • For the case counts, replace the two-sample comparison with a model that has a trend in it, such as a Poisson regression on day, if the question were being set today.

DR-008accepted2026-10-09

Optional “Explain this result” with the visitor’s own key, grounded on the numbers on screen and audit-logged locally

Decision: the only AI feature is an optional “Explain this result” panel. It uses the visitor’s own Anthropic or OpenAI key, calls the provider directly from the browser, sends only the numeric summary already shown on the page, labels every output as AI-generated, flags any number in the output that cannot be traced to that summary, and records every call (without the key) in an audit log in the visitor’s IndexedDB. No AI output ever feeds a computed result.

Scope: web/src/lib/ai, web/src/components/ai, /ai-log, the AI use statement on /methods

Context

The upgrade had to show GenAI fluency with governance on a static site that has no server and no budget for AI. The principles it is informed by (the Australian Digital Transformation Agency’s policy for the responsible use of AI in government, the transparency principles of the EU AI Act and the NIST AI Risk Management Framework) point the same way. People should be told when content is AI-generated, a human should stay in the loop, calls should be recorded, and it should be clear what data goes where. The site makes no claim of compliance with any of them.

Decision

Bring your own key only. Keys live in sessionStorage by default, or in localStorage if the visitor ticks “remember on this device”, and one button forgets them. Anthropic calls go through the official @anthropic-ai/sdk with dangerouslyAllowBrowser, which sends the anthropic-dangerous-direct-browser-access header. The SDK is loaded with a dynamic import on the first request, so pages that never use AI do not download it. OpenAI calls use fetch against the Chat Completions API. Replies are constrained with structured outputs, validated with zod, checked for untraceable numbers, and shown with Accept, Edit and Reject controls whose outcome is written to the audit log. The default model is Claude Haiku 4.5, the cheapest, with Claude Sonnet 5.5 as an option. The OpenAI model id is free text.

Options considered

  1. No AI at all.
  2. A server-side proxy with a site-owned key. Needs a budget, rate limiting and abuse handling.
  3. Bring your own key through a proxy. The visitor’s key would pass through infrastructure they do not control.
  4. Bring your own key, called directly from the browser (chosen).
  5. An on-device model. Gigabyte downloads and a capable GPU for a few sentences of text.

Why

Option 4 costs nothing to run, keeps the key away from any server of ours and keeps the audit trail with the person who made the calls. The feature is deliberately narrow. It explains results and never produces them, so a wrong explanation cannot change a number on the page. The facts sent are the same numbers the page shows, which makes the grounding check meaningful: any other number in the reply is either a rounding of a fact or something the model made up. The official SDK is used for Anthropic because its typed errors map cleanly to messages a visitor can act on, and the dynamic import keeps that choice from weighing down the static pages.

What happened

  • The adapters, key storage, audit store, CSV and JSON export, grounding check and error mapping are covered by 36 unit tests with fetch mocked, including the exact headers the SDK sends. They have not been run against the live APIs in CI, by design, because there is no key to use. The request shapes follow the providers’ documentation and are checked only through mocks, which is a real limitation.
  • The grounding check catches invented numbers but not wrong reasoning built on correct numbers, so it is shown as a warning, never as a guarantee. It is also loose for whole numbers: one of 10 or more passes if it is within 0.5 of a fact or of a proportion × 100. Of the integers from 0 to 100, 28 trace to the t-coverage facts by chance, and 68 to the power-study facts, which hold many rates. Numbers quoted to one decimal are stricter (58 and 182 of the 1,001 values from 0.0 to 100.0). Whole numbers below 10 must match a fact exactly, and the panel’s pass message says that larger ones are only loosely checked.
  • Direct browser access means any script running on the page could read the key. The site loads no third-party scripts, keeps the key in sessionStorage by default and sends a Content-Security-Policy that keeps fetches and resource loads on this site, with connect-src adding only the two provider APIs. That reduces the ways an injected script could send a key elsewhere, but it cannot stop a script from navigating the page away, so it is a mitigation rather than a guarantee.
  • Each call is one attempt. The SDK’s automatic retries are switched off because the visitor pays for every request and should see every failure.
  • OpenAI’s 401 message echoes a masked fragment of the key. Error text is redacted before it is shown or logged, and a record that contains the key is refused outright.

What I’d change

  • Add an evaluation harness if the feature grows: a fixed set of results with reference readings, scored for agreement with the facts across repeated runs and models, with intervals on the scores.
  • Tighten the Content-Security-Policy with nonce-based script-src instead of 'unsafe-inline'.
  • Let visitors import an exported audit log into another browser, and add a retention setting.

docs/model-card.md

Model and data card

This card covers the statistical models and procedures the lab uses, the simulation engine that evaluates them, and the data behind them. It is written for a reader who wants to know what the lab can and cannot be trusted for.

Intended use

  • Teaching and demonstration. The lab shows how second-year estimation, interval, testing and regression methods behave, using the 2020 coursework as worked examples.
  • Checking a small analysis. A visitor can paste their own data into a lab and get the same output R would give for the same call.
  • Not intended for decisions about real businesses, public health or anyone’s wellbeing. The 2020 case counts are used to teach testing and its assumptions, not to say anything about the epidemic.

Models and procedures

CourseworkModel or procedureEstimation or test
A1 Q1Exponential model for plant heightsMaximum likelihood, Type 6 QQ plot
A1 Q2Lognormal modelMaximum likelihood for μ and λ, χ² interval for λ as submitted
A1 Q3, Q4Estimators of a uniform endpoint and a normal meanMethod of moments, maximum likelihood, midrange, sample mean
A2 Q1Two independent binomial proportionsWald interval and χ² test (prop.test, no continuity correction)
A2 Q2Normal samplest interval, pooled t interval, F interval for a variance ratio, sample size for a z interval
A2 Q3Simple linear regression with normal errorsLeast squares, summary(lm), confidence and prediction intervals, plot(lm) diagnostics
A2 Q5, A3 Q5Exponential rateSingle-observation test, equal-tailed version of the likelihood-ratio test with gamma cut-offs
A3 Q1Location of a series of countsSign test, Wilcoxon rank-sum test
A3 Q3Gamma posterior for a rateConjugate update

Data provenance

  • The university datasets are kept in coursework/ and in test fixtures. The site never serves them. It shows only aggregates computed from them, in the parity ledger and the assumption checks.
  • The labs start from simulated samples of the same size and shape, with the seed on screen.
  • The Victorian case counts for 7 to 27 September 2020 are public figures from the Victorian Department of Health and Human Services.

Evaluation

The numbers below are generated from web/src/content/study-results.json and web/src/content/assumption-checks.json by the test suite (pnpm docs:sync in web/). They are not typed by hand.

Parity with 2020. 34 of 34 printed R outputs are reproduced character for character, and all 6 hand calculations agree to their rounding.

Coverage of the 95% t interval (A2 Q2(a)). Simulated coverage with Wilson 95% intervals, 5,000 samples per cell. Under normal data the exact coverage is 95%.

Datan = 5n = 10n = 20n = 50
Normal(15, 3²)95.2% (94.6% to 95.8%)95.0% (94.4% to 95.6%)95.0% (94.4% to 95.6%)94.6% (93.9% to 95.2%)
Exponential, mean 1589.1% (88.2% to 89.9%)90.2% (89.4% to 91.0%)91.8% (91.0% to 92.5%)93.2% (92.5% to 93.9%)
Lognormal(0, 1)81.9% (80.8% to 82.9%)84.1% (83.1% to 85.1%)87.4% (86.5% to 88.3%)89.8% (88.9% to 90.6%)

The A1 Q2 interval for λ under its own lognormal model. Exact values come from the χ² distribution; simulated coverage (5,000 samples per cell) is shown with its Wilson 95% interval.

nAs submitted: exact coverageAs submitted: simulated (Wilson 95%)lies below λlies above λTextbook SS/χ²: exact coverageTextbook: simulated (Wilson 95%)
595.49%95.5% (94.9% to 96.0%)3.76%0.75%95.00%95.2% (94.6% to 95.7%)
1595.14%94.9% (94.2% to 95.4%)3.43%1.43%95.00%94.9% (94.2% to 95.4%)
5095.04%94.6% (93.9% to 95.2%)3.05%1.91%95.00%94.4% (93.8% to 95.0%)

Intervals for p₁ − p₂ (A2 Q1, p₁ = 0.65, p₂ = 0.60). Exact coverage, and the paired difference in simulated coverage on the same 5,000 simulated surveys, with a Newcombe 95% interval.

GroupsWald (2020)Agresti–CaffoAgresti–Caffo − Wald, points
16, 2093.83%95.58%1.6 (1.3 to 2.0)
40, 5094.42%95.13%0.7 (0.4 to 0.9)
800, 100094.98%94.99%0.0 (−0.1 to 0.1)

Pooled t against Wilcoxon (A2 Q2(c), A3 Q1(b)). Rejection rates at α = 0.05 with Wilson 95% intervals, on the same 5,000 datasets per cell, for a shift of 0.5 standard deviations. The paired difference has a Newcombe 95% interval.

Datan per groupPooled tWilcoxonWilcoxon − t, points
Normal713.8% (12.8% to 14.7%)10.7% (9.9% to 11.6%)−3.0 (−3.6 to −2.4)
t, 3 df (heavy tails)719.8% (18.7% to 20.9%)18.1% (17.1% to 19.2%)−1.7 (−2.4 to −1.0)
Exponential (skewed)717.0% (16.0% to 18.0%)19.9% (18.8% to 21.0%)2.9 (2.1 to 3.7)
Normal2034.7% (33.4% to 36.0%)32.9% (31.7% to 34.3%)−1.7 (−2.4 to −1.0)
t, 3 df (heavy tails)2043.8% (42.4% to 45.1%)55.3% (54.0% to 56.7%)11.6 (10.5 to 12.6)
Exponential (skewed)2036.9% (35.5% to 38.2%)58.6% (57.2% to 59.9%)21.7 (20.5 to 22.9)

Power of the A3 Q5 rate test, the 2020 equal-tailed version of the likelihood-ratio test (n = 50, λ₀ = 1, α = 0.05). Exact power from the gamma distribution against simulation.

True λExact powerSimulated (Wilson 95%)
0.773.6%73.8% (72.6% to 75.0%)
0.838.2%38.3% (37.0% to 39.7%)
0.912.7%13.4% (12.5% to 14.4%)
15.0%5.1% (4.5% to 5.7%)
1.19.4%9.5% (8.7% to 10.3%)
1.222.5%22.6% (21.4% to 23.8%)
1.462.6%62.7% (61.3% to 64.0%)

Assumption checks on the 2020 inputs (reading rule: p < 0.01 strong evidence against, p < 0.05 some evidence against).

CourseworkAssumptionStatisticp-valueReading
A1 Q1Heights follow an exponential distributionD = 0.1420.296no evidence against
A1 Q2log X is normal, so the χ² interval for λ appliesW = 0.8300.009strong evidence against
A2 Q2(a)Times are normal (t interval)W = 0.9120.295no evidence against
A2 Q2(c)Times are normal (pooled t interval)W = 0.9190.425no evidence against
A2 Q2(c)–(e)Equal variances (pooled t interval)F = 15.2800.002strong evidence against
A2 Q3Errors are normalW = 0.9860.988no evidence against
A2 Q3Constant error varianceBP = 0.2890.591no evidence against
A2 Q3Errors are independent from day to dayDW = 2.1780.672no evidence against
A2 Q3No single day dominates the fitmax |t| = 2.4460.512no evidence against
A3 Q1Days are a random sample (sign and Wilcoxon tests)runs = 40.002strong evidence against
A3 Q1No serial correlationQ = 25.8345.4e−4strong evidence against
A3 Q1No trend over the 21 daysKendall τ = -0.6861.5e−5strong evidence against

Known failure modes

  • Skewed data and the t interval. At small n the t interval undercovers badly for skewed data, and almost all its misses fall on one side (see the coverage table above).
  • Small groups and the Wald interval. The Wald interval for p₁ − p₂ undercovers for groups of a few dozen. Agresti–Caffo is the recommended alternative (DR-004).
  • Unequal tails. The 2020 χ² interval for λ reaches about 95% overall, but at the coursework’s n = 15 it lies entirely below λ 3.43% of the time and entirely above it 1.43% of the time, against 2.5% each for an equal-tailed interval (DR-003).
  • Discrete tests. The exact Wilcoxon test cannot reach 5% with 7 per group. Its attainable size is 3.79%, which costs power. At 7 per group neither test dominates, so both are reported (DR-005).
  • Equal-tailed rate test. The A3 Q5 test rejects outside the equal-tailed points (37.11, 64.78) of Gamma(50, 1), not the exact-size likelihood-ratio region (37.37, 65.20). It is slightly biased: its power dips to 4.98% at λ = 1.01, below α (DR-006).
  • Trending series. The sign and Wilcoxon tests assume a random sample. Applied to a trending series such as the case counts, their p-values do not mean what they appear to (DR-007).
  • Floating-point agreement with R. Normal and exponential draws can differ from R’s in the last few bits. No printed result or simulation count is affected, and the tests check both.

Ethical considerations

  • The case counts describe real illness during a lockdown. They appear only as daily totals, as published, and the lab avoids epidemiological claims.
  • The coursework questions belong to the University of Melbourne. They are paraphrased. The website never serves the original datasets, which are kept only in the repository’s coursework/ folder and test fixtures (see Data provenance above).
  • The academic integrity notice on the home page asks current students not to reuse the 2020 answers.
  • The optional AI explanations are labelled as AI-generated, are grounded only on the numbers already shown, and are recorded in a local audit log with the reader’s decision. They never produce a number on the page (DR-008).

docs/ai-use.md

AI use statement

The lab works fully without AI. One optional feature, “Explain this result”, can use a large language model if you supply your own API key. This statement says what that feature does, what it never does, what data it sends and how you stay in control. It is informed by the Australian Digital Transformation Agency’s policy for the responsible use of AI in government, the transparency principles of the EU AI Act and the NIST AI Risk Management Framework. It does not claim compliance with any of them.

What the AI does

  • It writes a short plain-language reading of a result that is already on the page, such as a coverage table, a power comparison or an assumption check.
  • It works only from a JSON summary of the numbers shown beside the panel. The full prompt is visible under each explanation.

What the AI never does

  • It never computes, changes or replaces a number on the page. Every statistic comes from the tested TypeScript code.
  • It never runs without your key, and it never runs automatically. Nothing is sent until you press “Explain”.
  • It never receives your data beyond the summary in the prompt. Pasted datasets stay in your browser unless their summary statistics appear in the facts sent.

Where your key and data go

  • Your key is stored in this browser only, in sessionStorage by default (cleared when the tab closes) or in localStorage if you choose “remember on this device”. “Forget key” removes it.
  • Requests go directly from your browser to the provider you chose: api.anthropic.com or api.openai.com. This site is static and has no backend of its own, so your key and the prompt are never sent to its host, never logged by it and never committed anywhere.
  • The provider receives the prompt, your key and the usual request metadata your browser sends, and bills the call to your account under its own terms.

Transparency and human oversight

  • Every explanation is labelled “AI-generated”, with the model, latency and token usage.
  • A grounding check compares every number in the explanation with the numbers that were sent and warns about any that cannot be traced. Numbers are matched at the precision they are quoted. A number written with a minus sign must match a negative fact, so a reversed direction is flagged, but an unsigned number is matched on its size alone, so “fell by 2.21” traces to a fact of -2.21. Whole numbers below 10 must match a fact exactly, but larger whole numbers only need to round to one, so a made-up whole number can pass by chance. A clean check means no untraceable number was found, not that the explanation is verified. It cannot detect wrong reasoning built on correct numbers.
  • You can accept, edit or reject each explanation. Your decision, any edited text, and the full input and output are recorded in an audit log in this browser’s IndexedDB. Failed calls are recorded too. When the provider completed a call but its reply was not shown (refused, cut off at the length limit, or failing validation), the log keeps the token usage it reported and the text it wrote.
  • An explanation is shown only after it has been written to the audit log. If the browser refuses the write (storage full or blocked), the explanation is not shown and you are told why. If IndexedDB stops working part-way through a visit, the log switches to memory for the rest of that tab and /ai-log says so.
  • The audit log is at /ai-log, where it can be exported as JSON or CSV, or cleared.

Models

  • Anthropic: Claude Haiku 4.5 by default (the cheapest), or Claude Sonnet 5.5.
  • OpenAI: any chat model that supports JSON-schema output. The model id is free text, with gpt-5-mini as the default.