Skip to content

Commit cb74f2b

Browse files
committed
add slide on comparing 2 sample means to stats_primer.qmd; tweak sampling-variation.html
1 parent 4dda70c commit cb74f2b

File tree

3 files changed

+394
-11
lines changed

3 files changed

+394
-11
lines changed

blog/stats_primer.qmd

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@ Where:
7373
<iframe src="../resources/sampling-variation.html" width="100%" height="550px" frameBorder="0"></iframe>
7474
:::
7575

76+
## Comparing 2 samples
77+
78+
::: {.column-page}
79+
<iframe src="../resources/compare-means.html" width="100%" height="560px" frameBorder="0"></iframe>
80+
:::
81+
7682
## Next slides
7783

7884
To do

resources/compare-means.html

Lines changed: 368 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,368 @@
1+
<!DOCTYPE html>
2+
<html>
3+
4+
<head>
5+
<meta charset="""utf-8">
6+
<title>Compare 2 samples</title>
7+
<script src="https://cdn.plot.ly/plotly-2.24.1.min.js"></script>
8+
<script src="""https://cdn.jsdelivr.net/npm/d3@7"></script>
9+
<style>
10+
body {
11+
font-family: system-ui, sans-serif;
12+
max-width: 1100px;
13+
/* Reduced from 1200px */
14+
margin: 0 auto;
15+
padding: 5px;
16+
/* Reduced from 10px */
17+
font-size: 14px;
18+
line-height: 1.2;
19+
}
20+
21+
.plot-container {
22+
display: flex;
23+
gap: 5px;
24+
/* Reduced from 10px */
25+
margin-bottom: 0.5rem;
26+
}
27+
28+
.plot-box {
29+
flex: 1;
30+
}
31+
32+
.plot-box p {
33+
margin: 0.2em 0;
34+
}
35+
36+
.controls {
37+
margin: 0.5rem 0;
38+
display: flex;
39+
gap: 0.5rem;
40+
align-items: center;
41+
font-size: 13px;
42+
}
43+
44+
.radio-group {
45+
display: flex;
46+
gap: 0.3rem;
47+
flex-wrap: wrap;
48+
}
49+
50+
button {
51+
padding: 4px 8px;
52+
font-size: 13px;
53+
}
54+
55+
#sampling-message {
56+
font-size: 13px;
57+
margin-top: 0.2em;
58+
}
59+
60+
select {
61+
padding: 2px 4px;
62+
font-size: 13px;
63+
margin: 0 4px;
64+
}
65+
</style>
66+
</head>
67+
68+
<body>
69+
<div id="app">
70+
<div class="controls">
71+
<div style="display: flex; gap: 1rem;">
72+
<div>
73+
<label for="sample-size-1">Pop 1 Sample: </label>
74+
<select id="sample-size-1">
75+
<option value="10">10</option>
76+
<option value="100">100</option>
77+
<option value="1000">1000</option>
78+
</select>
79+
</div>
80+
<div>
81+
<label for="sample-size-2">Pop 2 Sample: </label>
82+
<select id="sample-size-2">
83+
<option value="10">10</option>
84+
<option value="100">100</option>
85+
<option value="1000">1000</option>
86+
</select>
87+
</div>
88+
</div>
89+
<div>
90+
<label>Confidence Level: </label>
91+
<div class="radio-group" id="confidence-levels"></div>
92+
</div>
93+
<button id="take-samples">Take 1000 Samples</button>
94+
</div>
95+
<div class="plot-container">
96+
<div class="plot-box">
97+
<div id="population-plot"></div>
98+
<div id="population-stats"></div>
99+
</div>
100+
<div class="plot-box">
101+
<div id="sampling-plot"></div>
102+
<div id="sample-stats"></div>
103+
</div>
104+
</div>
105+
<div id="sampling-message"></div>
106+
</div>
107+
108+
<script>
109+
const CONFIDENCE_LEVELS = {
110+
60: 0.841,
111+
70: 1.036,
112+
80: 1.28,
113+
85: 1.44,
114+
90: 1.645,
115+
95: 1.96,
116+
99: 2.576,
117+
99.9: 3.291
118+
};
119+
120+
// Generate population data
121+
function generatePopulation(size = 10000, mean = 120, std = 10) {
122+
return Array.from({ length: size }, () => {
123+
let u1, u2;
124+
do {
125+
u1 = Math.random();
126+
u2 = Math.random();
127+
} while (u1 === 0);
128+
129+
const z = Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(2.0 * Math.PI * u2);
130+
return Math.round(mean + z * std);
131+
});
132+
}
133+
134+
// Initialize state with two populations and sample sizes
135+
const state = {
136+
population1: generatePopulation(10000, 120, 10),
137+
population2: generatePopulation(10000, 130, 10),
138+
sampleMeans: [],
139+
sampleSize1: 10,
140+
sampleSize2: 10,
141+
confidenceLevel: 95
142+
};
143+
144+
// Calculate population statistics
145+
function calculatePopulationStats(population) {
146+
const mean = population.reduce((a, b) => a + b, 0) / population.length;
147+
const squaredDiffs = population.map(x => Math.pow(x - mean, 2));
148+
const variance = squaredDiffs.reduce((a, b) => a + b, 0) / population.length;
149+
const std = Math.sqrt(variance);
150+
return { mean, std };
151+
}
152+
153+
// Setup UI for both sample size controls
154+
document.getElementById('sample-size-1').addEventListener('change', (e) => {
155+
state.sampleSize1 = parseInt(e.target.value);
156+
});
157+
158+
document.getElementById('sample-size-2').addEventListener('change', (e) => {
159+
state.sampleSize2 = parseInt(e.target.value);
160+
});
161+
162+
// Setup confidence level radio buttons
163+
const confidenceLevelsDiv = document.getElementById('confidence-levels');
164+
Object.keys(CONFIDENCE_LEVELS).sort((a, b) => a - b).forEach(level => {
165+
const radio = document.createElement('input');
166+
radio.type = 'radio';
167+
radio.id = `cl-${level}`;
168+
radio.name = 'confidence-level';
169+
radio.value = level;
170+
radio.checked = level == 95;
171+
172+
const label = document.createElement('label');
173+
label.htmlFor = `cl-${level}`;
174+
label.textContent = `${level}%`;
175+
176+
confidenceLevelsDiv.appendChild(radio);
177+
confidenceLevelsDiv.appendChild(label);
178+
});
179+
180+
document.getElementById('take-samples').addEventListener('click', () => {
181+
const newMeans = Array.from({ length: 1000 }, () => {
182+
const sample1 = Array.from({ length: state.sampleSize1 }, () =>
183+
state.population1[Math.floor(Math.random() * state.population1.length)]
184+
);
185+
const sample2 = Array.from({ length: state.sampleSize2 }, () =>
186+
state.population2[Math.floor(Math.random() * state.population2.length)]
187+
);
188+
const mean1 = sample1.reduce((a, b) => a + b, 0) / sample1.length;
189+
const mean2 = sample2.reduce((a, b) => a + b, 0) / sample2.length;
190+
return mean2 - mean1; // return difference in means
191+
});
192+
state.sampleMeans = newMeans;
193+
updatePlots();
194+
});
195+
196+
function updatePlots() {
197+
const popStats1 = calculatePopulationStats(state.population1);
198+
const popStats2 = calculatePopulationStats(state.population2);
199+
200+
// Population plots
201+
const populationTrace1 = {
202+
x: state.population1,
203+
type: 'histogram',
204+
name: 'Population 1 (120mmHg)',
205+
nbinsx: 40,
206+
marker: {
207+
color: 'rgba(136, 132, 216, 0.6)' // light purple
208+
}
209+
};
210+
211+
const populationTrace2 = {
212+
x: state.population2,
213+
type: 'histogram',
214+
name: 'Population 2 (130mmHg)',
215+
nbinsx: 40,
216+
marker: {
217+
color: 'rgba(130, 202, 157, 0.6)' // light green
218+
}
219+
};
220+
221+
const populationLayout = {
222+
title: {
223+
text: 'Two Population Distributions (N=10,000 each)',
224+
font: { size: 14 }
225+
},
226+
xaxis: {
227+
title: { text: 'Blood Pressure (mmHg)', font: { size: 12 } },
228+
range: [80, 170],
229+
tickfont: { size: 11 }
230+
},
231+
yaxis: {
232+
title: { text: 'Count', font: { size: 12 } },
233+
tickfont: { size: 11 }
234+
},
235+
width: 450, /* Reduced from 500 */
236+
height: 300,
237+
margin: { t: 30, r: 20, l: 40, b: 40 },
238+
showlegend: true,
239+
legend: {
240+
orientation: 'h',
241+
y: -0.2 /* Adjusted from -0.3 */
242+
}
243+
};
244+
245+
Plotly.newPlot('population-plot', [populationTrace1, populationTrace2], populationLayout);
246+
247+
// Display population statistics
248+
document.getElementById('population-stats').innerHTML = `
249+
<p>Population 1 Mean: ${popStats1.mean.toFixed(2)} mmHg</p>
250+
<p>Population 2 Mean: ${popStats2.mean.toFixed(2)} mmHg</p>
251+
<p>True Difference: ${(popStats2.mean - popStats1.mean).toFixed(2)} mmHg</p>
252+
`;
253+
254+
// Update sampling distribution plot if we have samples
255+
if (state.sampleMeans.length > 0) {
256+
const sampleStats = calculatePopulationStats(state.sampleMeans);
257+
const selectedConfidenceLevel = parseInt(document.querySelector('input[name="confidence-level"]:checked').value);
258+
const criticalValue = CONFIDENCE_LEVELS[selectedConfidenceLevel];
259+
260+
// Fixed standard error calculation for difference in means
261+
const se1 = Math.pow(popStats1.std, 2) / state.sampleSize1;
262+
const se2 = Math.pow(popStats2.std, 2) / state.sampleSize2;
263+
const standardError = Math.sqrt(se1 + se2);
264+
const marginOfError = criticalValue * standardError;
265+
266+
const trueDiff = popStats2.mean - popStats1.mean;
267+
const lowerCI = trueDiff - marginOfError;
268+
const upperCI = trueDiff + marginOfError;
269+
270+
// Count samples outside CI
271+
const samplesOutsideCI = state.sampleMeans.filter(diff =>
272+
diff < lowerCI || diff > upperCI
273+
).length;
274+
275+
const samplingTrace = {
276+
x: state.sampleMeans,
277+
type: 'histogram',
278+
name: 'Sample Mean Differences',
279+
nbinsx: 30,
280+
marker: {
281+
color: 'rgba(255, 127, 80, 0.7)' // coral
282+
}
283+
};
284+
285+
const samplingLayout = {
286+
title: {
287+
text: 'Sampling Distribution of Mean Differences',
288+
font: { size: 14 }
289+
},
290+
xaxis: {
291+
title: { text: 'Difference in Sample Means (mmHg)', font: { size: 12 } },
292+
range: [0, 20],
293+
tickfont: { size: 11 }
294+
},
295+
yaxis: {
296+
title: { text: 'Count', font: { size: 12 } },
297+
tickfont: { size: 11 }
298+
},
299+
width: 450, /* Reduced from 500 */
300+
height: 300,
301+
margin: { t: 30, r: 20, l: 40, b: 40 },
302+
shapes: [
303+
// True difference line
304+
{
305+
type: 'line',
306+
x0: trueDiff,
307+
x1: trueDiff,
308+
y0: 0,
309+
y1: 1,
310+
yref: 'paper',
311+
line: {
312+
color: 'red',
313+
width: 2
314+
}
315+
},
316+
// Lower CI line
317+
{
318+
type: 'line',
319+
x0: lowerCI,
320+
x1: lowerCI,
321+
y0: 0,
322+
y1: 1,
323+
yref: 'paper',
324+
line: {
325+
color: 'blue',
326+
width: 2,
327+
dash: 'dash'
328+
}
329+
},
330+
// Upper CI line
331+
{
332+
type: 'line',
333+
x0: upperCI,
334+
x1: upperCI,
335+
y0: 0,
336+
y1: 1,
337+
yref: 'paper',
338+
line: {
339+
color: 'blue',
340+
width: 2,
341+
dash: 'dash'
342+
}
343+
}
344+
]
345+
};
346+
347+
Plotly.newPlot('sampling-plot', [samplingTrace], samplingLayout);
348+
349+
// Update sample statistics
350+
document.getElementById('sample-stats').innerHTML = `
351+
<p>Mean Difference: ${sampleStats.mean.toFixed(2)} mmHg</p>
352+
<p>Standard Error: ${sampleStats.std.toFixed(2)} mmHg</p>
353+
<p>${selectedConfidenceLevel}% CI: ${lowerCI.toFixed(2)} to ${upperCI.toFixed(2)} mmHg</p>
354+
<p>Samples outside CI: ${samplesOutsideCI}</p>
355+
`;
356+
357+
document.getElementById('sampling-message').innerHTML = `
358+
<p>Took ${state.sampleMeans.length} samples (Population 1 size: ${state.sampleSize1}, Population 2 size: ${state.sampleSize2})</p>
359+
`;
360+
}
361+
}
362+
363+
// Initial plot
364+
updatePlots();
365+
</script>
366+
</body>
367+
368+
</html>

0 commit comments

Comments
 (0)