From 21a897cbc7d79541e81f0795f442404790938671 Mon Sep 17 00:00:00 2001 From: Peter Slijkhuis Date: Wed, 30 Sep 2026 10:05:19 +0000 Subject: [PATCH] Sample size: each design names the model the course fits --- src/pages/SampleSize.test.tsx | 3 +++ src/pages/SampleSize.tsx | 8 ++++++-- src/pages/sampleSizeDesigns.ts | 25 +++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/pages/SampleSize.test.tsx b/src/pages/SampleSize.test.tsx index e0ef74a..cd3a1f8 100644 --- a/src/pages/SampleSize.test.tsx +++ b/src/pages/SampleSize.test.tsx @@ -38,6 +38,8 @@ describe('sample size page', () => { test('walks a first-timer from the design to a plan: d = 0.30 from a raw difference', async () => { const { pick, next, fill } = setup(); expect(question()).toBe('What will you test?'); + // Each design names the model the course fits, not only the classic test. + expect(screen.getByRole('button', { name: /^Compare two separate groups/ }).textContent).toContain('In R: lm(outcome ~ group)'); await pick(/^Compare two separate groups/); await pick(/How many people do I need/); await pick(/I can guess the scores/); @@ -56,6 +58,7 @@ describe('sample size page', () => { // 176 / 0.9 rounds up to 196 per group. expect(report()).toContain('we will recruit 392 participants'); expect(screen.getByText(/pwr.t.test\(d = 0.3, sig.level = 0.05, power = 0.8/)).toBeTruthy(); + expect(screen.getByText(/is the independent-samples t-test \(Lesson 12-1\)/)).toBeTruthy(); expect(trail()).toEqual(['Compare two separate groups', 'Plan a new study', 'My own guess of the scores', 'd = 0.30', '80% power', 'α = .05', 'Two-sided', '10% dropout']); }); diff --git a/src/pages/SampleSize.tsx b/src/pages/SampleSize.tsx index 3e7c005..85a270e 100644 --- a/src/pages/SampleSize.tsx +++ b/src/pages/SampleSize.tsx @@ -420,10 +420,10 @@ export default function SampleSize() { ); } else if (step === 'design') { question = 'What will you test?'; - help = <>Pick the comparison your research question makes. Not sure? Which model should I use? helps you choose. An A/B test is two separate groups: choose the yes/no comparison for clicks or sign-ups, and the first option for a score. Repeated measures, with or without several stimuli per condition, are under "Measure the same people in several conditions"; a before-and-after study with a control group is a 2 × 2 design. Logistic regression, mediation and models with more random factors are not here: plan those by simulation, as Lesson 8-3 does.; + help = <>Pick the comparison your research question makes; each option shows the model from the course. Not sure? Which model should I use? helps you choose. An A/B test is two separate groups: a score, or a yes/no outcome such as clicks. A before-and-after study with a control group is a 2 × 2 design. Logistic regression with several predictors, mediation and models with more random factors are not here: plan those by simulation, as Lesson 8-3 does.; body = ( ({ value: d.id, label: d.title, note: `e.g. ${d.example} Analysis: ${d.test}.` }))} + options={DESIGNS.map((d) => ({ value: d.id, label: d.title, note: <>e.g. {d.example}
In R: {d.model} }))} chosen={answers.design} onPick={(value) => answer(value === answers.design ? {} : { design: value, route: undefined, effect: undefined, text: { ...answers.text, ...Object.fromEntries(['diff', 'sd', 'r', 'es', 'spread', 'eta2', 'r2', 'dr2', 'v', 'p2', 'eff1', 'eff2'].map((k) => [k, ''])), rpre: '.5' } })} /> @@ -807,6 +807,10 @@ export default function SampleSize() { )} +

The model you will fit

+

{design.model}

+

{design.modelNote}

+

What this assumes

{design.assumes}

diff --git a/src/pages/sampleSizeDesigns.ts b/src/pages/sampleSizeDesigns.ts index cc1a4b6..efe7ea1 100644 --- a/src/pages/sampleSizeDesigns.ts +++ b/src/pages/sampleSizeDesigns.ts @@ -7,6 +7,9 @@ export type Design = { example: string; /** The test the plan is for, as a student would name it. */ test: string; + /** The model the course fits for this question, and how its test relates to the planned one. */ + model: string; + modelNote: string; lessonId: string; /** The effect size's symbol and name, and what it measures. */ symbol: string; @@ -25,6 +28,8 @@ export const DESIGNS: Design[] = [ title: 'Compare two separate groups', example: 'Do remote and office workers differ in wellbeing? Or an A/B test on a score, such as time on a page.', test: 'independent-samples t-test', + model: 'lm(outcome ~ group)', + modelNote: 'The test of the group coefficient is the independent-samples t-test (Lesson 12-1).', lessonId: '12-1', symbol: 'd', effectName: "Cohen's d", @@ -39,6 +44,8 @@ export const DESIGNS: Design[] = [ title: 'Measure the same people twice', example: 'Does engagement change between the first and second measurement?', test: 'paired t-test', + model: 'lmer(outcome ~ time + (1 | person))', + modelNote: 'With two time points and nobody missing, the test of time is the paired t-test (Lesson 14-3).', lessonId: '14-3', symbol: 'dz', effectName: 'Cohen\'s dz', @@ -53,6 +60,8 @@ export const DESIGNS: Design[] = [ title: 'Measure the same people in several conditions', example: 'Does reaction time differ between four conditions that every participant does?', test: 'repeated-measures ANOVA (the overall F test)', + model: 'lmer(outcome ~ condition + (1 | person))', + modelNote: 'The F test for condition is the repeated-measures ANOVA (Lesson 14-4).', lessonId: '14-4', symbol: 'f', effectName: "Cohen's f", @@ -67,6 +76,8 @@ export const DESIGNS: Design[] = [ title: 'Two factors at once, like a 2 × 2 design', example: 'Does a training raise wellbeing more than a waiting list, measured before and after? Or: do photo and text ads work differently for young and old viewers?', test: '2 × 2 ANOVA', + model: 'lm(outcome ~ a * b)', + modelNote: 'Each effect is one term of this model: a * b gives both main effects and their interaction (Lesson 13-2). When a factor is within people, add a random intercept per person: lmer(outcome ~ a * b + (1 | person)) (Lesson 14-4).', lessonId: '13-2', symbol: 'f', effectName: "Cohen's f", @@ -81,6 +92,8 @@ export const DESIGNS: Design[] = [ title: 'Compare one group with a fixed value', example: 'Is average wellbeing different from a benchmark score of 50?', test: 'one-sample t-test', + model: 'lm(outcome - value ~ 1)', + modelNote: 'The test of the intercept is the one-sample t-test against the fixed value (Lesson 8-2 runs it as t.test()).', lessonId: '08-2', symbol: 'd', effectName: "Cohen's d", @@ -94,6 +107,8 @@ export const DESIGNS: Design[] = [ title: 'Compare three or more groups', example: 'Does wellbeing differ between the four departments?', test: 'one-way ANOVA (the overall F test)', + model: 'lm(outcome ~ group)', + modelNote: 'With group a factor of three or more categories, the overall F test of the model is the one-way ANOVA (Lesson 12-2).', lessonId: '12-2', symbol: 'f', effectName: "Cohen's f", @@ -108,6 +123,8 @@ export const DESIGNS: Design[] = [ title: 'Relate two numeric variables', example: 'Is workload related to wellbeing?', test: 'Pearson correlation test', + model: 'lm(outcome ~ predictor)', + modelNote: 'The test of the slope gives the same p-value as the test of the correlation (Lesson 10-2).', lessonId: '10-1', symbol: 'r', effectName: 'the correlation r', @@ -122,6 +139,8 @@ export const DESIGNS: Design[] = [ title: 'Predict an outcome from several predictors', example: 'Do workload, autonomy and tenure together predict wellbeing?', test: 'F test of R² in a multiple regression', + model: 'lm(outcome ~ x1 + x2 + x3)', + modelNote: 'The F test at the bottom of summary() tests the whole model (Lesson 11-3).', lessonId: '11-3', symbol: 'f²', effectName: "Cohen's f²", @@ -135,6 +154,8 @@ export const DESIGNS: Design[] = [ title: 'Test predictors on top of others', example: 'Does autonomy predict wellbeing over and above workload and tenure?', test: 'F test of the R² change (with one added predictor, the same as its t-test)', + model: 'anova(small_model, big_model)', + modelNote: 'The F test comparing lm(outcome ~ controls) with lm(outcome ~ controls + x) is the test of the R² change (Lesson 11-3).', lessonId: '11-3', symbol: 'f²', effectName: "Cohen's f²", @@ -149,6 +170,8 @@ export const DESIGNS: Design[] = [ title: 'Compare a yes/no outcome between two groups', example: 'Do fewer mentored employees leave the company? Or an A/B test: do more visitors click with version B than with version A?', test: 'test of two proportions (a chi-square test on a 2 × 2 table)', + model: 'glm(yes_no ~ group, family = binomial)', + modelNote: 'The page plans with the test of two proportions. The group coefficient in this logistic regression has almost the same power, at most a few percentage points less, so a few extra people cover it (Module 15).', lessonId: '09-3', symbol: 'p₂', effectName: 'the two proportions', @@ -162,6 +185,8 @@ export const DESIGNS: Design[] = [ title: 'Relate two categorical variables', example: 'Is department related to working remotely?', test: 'chi-square test of independence (or goodness of fit)', + model: 'chisq.test(table(a, b))', + modelNote: 'As a model: glm(count ~ a + b, family = poisson) on the table of counts gives the same chi-square test (Lesson 15-4).', lessonId: '09-3', symbol: 'w', effectName: "Cohen's w",