Two hospitals are testing the same treatment. Drag the sliders to change how many patients each hospital treats and what their success rates are. The blue bars show each hospital separately; the gray bar shows the combined total.
<p class="hint">Two hospitals test the same treatment on different mixes of patients. Each hospital shows treatment winning — yet the <b>combined</b> bar can flip. Adjust the sliders and watch Simpson’s Paradox appear.</p>
<div class="hospitals">
<div class="hospital">
<div class="hosp-label">Hospital A <span class="tag severe">Severe cases</span></div>
<label>Treatment patients: <b id="nTA-val">300</b>
<input type="range" id="nTA" min="10" max="500" value="300" step="10">
</label>
<label>Treatment success: <b id="rTA-val">30%</b>
<input type="range" id="rTA" min="5" max="95" value="30" step="1">
</label>
<label>Control patients: <b id="nCA-val">100</b>
<input type="range" id="nCA" min="10" max="500" value="100" step="10">
</label>
<label>Control success: <b id="rCA-val">20%</b>
<input type="range" id="rCA" min="5" max="95" value="20" step="1">
</label>
</div>
<div class="hospital">
<div class="hosp-label">Hospital B <span class="tag mild">Mild cases</span></div>
<label>Treatment patients: <b id="nTB-val">30</b>
<input type="range" id="nTB" min="10" max="500" value="30" step="10">
</label>
<label>Treatment success: <b id="rTB-val">90%</b>
<input type="range" id="rTB" min="5" max="95" value="90" step="1">
</label>
<label>Control patients: <b id="nCB-val">90</b>
<input type="range" id="nCB" min="10" max="500" value="90" step="10">
</label>
<label>Control success: <b id="rCB-val">70%</b>
<input type="range" id="rCB" min="5" max="95" value="70" step="1">
</label>
</div>
</div>
<div class="chart-area">
<div class="chart-col">
<div class="bar-pair" id="bars-A"></div>
<div class="col-label">Hospital A</div>
</div>
<div class="chart-col">
<div class="bar-pair" id="bars-B"></div>
<div class="col-label">Hospital B</div>
</div>
<div class="chart-col combined">
<div class="bar-pair" id="bars-C"></div>
<div class="col-label">Combined</div>
</div>
</div>
<div class="legend"><span class="dot treat"></span>Treatment <span class="dot ctrl"></span>Control</div>
<div id="verdict" class="verdict"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.45; }
.hospitals { display: flex; gap: .7rem; margin-bottom: .9rem; flex-wrap: wrap; }
.hospital { flex: 1 1 210px; background: #f3f6f9; border: 1px solid #dde3ea; border-radius: 10px; padding: .65rem .8rem; }
.hosp-label { font-weight: 700; margin-bottom: .45rem; display: flex; align-items: center; gap: .4rem; }
.tag { font-size: .72rem; font-weight: 600; border-radius: 4px; padding: 1px 6px; }
.tag.severe { background: #fde8e8; color: #b91c1c; }
.tag.mild { background: #e8f5e9; color: #166534; }
.hospital label { display: flex; align-items: center; gap: .4rem; font-size: .82rem; margin-bottom: .3rem; flex-wrap: wrap; }
input[type=range] { flex: 1 1 80px; accent-color: #1d3557; }
.chart-area { display: flex; gap: .6rem; align-items: flex-end; margin: .5rem 0 .25rem; height: 150px; }
.chart-col { display: flex; flex-direction: column; align-items: center; flex: 1; }
.chart-col.combined .bar-pair { border-left: 2px dashed #aaa; padding-left: 4px; }
.bar-pair { display: flex; gap: 4px; align-items: flex-end; height: 130px; width: 100%; justify-content: center; }
.bar { width: 38%; border-radius: 5px 5px 0 0; position: relative; min-height: 4px; transition: height .2s; }
.bar.treat { background: #1d7abd; }
.bar.ctrl { background: #9fb2c8; }
.bar .pct { position: absolute; top: -18px; left: 50%; transform: translateX(-50%); font-size: .7rem; font-weight: 700; white-space: nowrap; color: #222; }
.col-label { font-size: .78rem; font-weight: 600; margin-top: .3rem; color: #333; text-align: center; }
.legend { font-size: .8rem; display: flex; align-items: center; gap: .25rem; margin: .1rem 0 .4rem; }
.dot { display: inline-block; width: 10px; height: 10px; border-radius: 2px; }
.dot.treat { background: #1d7abd; }
.dot.ctrl { background: #9fb2c8; }
.verdict { font-size: .88rem; font-weight: 600; border-radius: 8px; padding: .4rem .7rem; min-height: 2em; }
.verdict.paradox { background: #fef3c7; color: #92400e; border: 1px solid #fbbf24; }
.verdict.normal { background: #e8f5e9; color: #166534; border: 1px solid #86efac; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Simpson’s Paradox demo.
// Each hospital has separate treatment-arm and control-arm patient counts.
// This lets the confounding pattern arise naturally: Hospital A (severe)
// sends most patients to treatment; Hospital B (mild) sends most to control.
// Within each hospital treatment beats control, but the combined rate reverses.
function val(id) { return parseInt(document.getElementById(id).value, 10); }
function setLbl(id, v, pct) {
document.getElementById(id + '-val').textContent = pct ? v + '%' : v;
}
function update() {
const nTA = val('nTA'), rTA = val('rTA');
const nCA = val('nCA'), rCA = val('rCA');
const nTB = val('nTB'), rTB = val('rTB');
const nCB = val('nCB'), rCB = val('rCB');
setLbl('nTA', nTA, false); setLbl('rTA', rTA, true);
setLbl('nCA', nCA, false); setLbl('rCA', rCA, true);
setLbl('nTB', nTB, false); setLbl('rTB', rTB, true);
setLbl('nCB', nCB, false); setLbl('rCB', rCB, true);
// Success counts
const sTA = nTA * rTA / 100;
const sCA = nCA * rCA / 100;
const sTB = nTB * rTB / 100;
const sCB = nCB * rCB / 100;
// Combined rates (treatment patients pooled; control patients pooled)
const rTC = Math.round((sTA + sTB) / (nTA + nTB) * 100);
const rCC = Math.round((sCA + sCB) / (nCA + nCB) * 100);
drawBars('bars-A', rTA, rCA);
drawBars('bars-B', rTB, rCB);
drawBars('bars-C', rTC, rCC);
// Paradox: treatment beats control in both hospitals but loses combined (or vice versa)
const winA = rTA > rCA, winB = rTB > rCB, winC = rTC > rCC;
const verdict = document.getElementById('verdict');
if ((winA && winB && !winC) || (!winA && !winB && winC)) {
verdict.className = 'verdict paradox';
verdict.textContent =
fmt('Simpson’s Paradox! Treatment %v% in both hospitals individually but %v2% in the combined data (%rTC%% vs %rCC%%).', { v: winA ? 'wins' : 'loses', v2: winC ? 'wins' : 'loses', rTC: rTC, rCC: rCC });
console.log(fmt('Paradox! A:%rTA%% vs %rCA%% B:%rTB%% vs %rCB%% Combined:%rTC%% vs %rCC%%', { rTA: rTA, rCA: rCA, rTB: rTB, rCB: rCB, rTC: rTC, rCC: rCC }));
} else {
verdict.className = 'verdict normal';
verdict.textContent =
fmt('No paradox — combined result agrees with both hospitals (%rTC%% vs %rCC%%). Try making Hospital A have many more treatment patients than control patients.', { rTC: rTC, rCC: rCC });
}
}
function drawBars(containerId, treat, ctrl) {
const el = document.getElementById(containerId);
el.innerHTML = '';
[['treat', treat], ['ctrl', ctrl]].forEach(([cls, pct]) => {
const d = document.createElement('div');
d.className = 'bar ' + cls;
d.style.height = pct + '%';
const lbl = document.createElement('span');
lbl.className = 'pct';
lbl.textContent = pct + '%';
d.appendChild(lbl);
el.appendChild(d);
});
}
['nTA','rTA','nCA','rCA','nTB','rTB','nCB','rCB'].forEach(id => {
document.getElementById(id).addEventListener('input', update);
});
update();
<p class="hint">Two hospitals test the same treatment on different mixes of patients. Each hospital shows treatment winning — yet the <b>combined</b> bar can flip. Adjust the sliders and watch Simpson’s Paradox appear.</p>
<div class="hospitals">
<div class="hospital">
<div class="hosp-label">Hospital A <span class="tag severe">Severe cases</span></div>
<label>Treatment patients: <b id="nTA-val">300</b>
<input type="range" id="nTA" min="10" max="500" value="300" step="10">
</label>
<label>Treatment success: <b id="rTA-val">30%</b>
<input type="range" id="rTA" min="5" max="95" value="30" step="1">
</label>
<label>Control patients: <b id="nCA-val">100</b>
<input type="range" id="nCA" min="10" max="500" value="100" step="10">
</label>
<label>Control success: <b id="rCA-val">20%</b>
<input type="range" id="rCA" min="5" max="95" value="20" step="1">
</label>
</div>
<div class="hospital">
<div class="hosp-label">Hospital B <span class="tag mild">Mild cases</span></div>
<label>Treatment patients: <b id="nTB-val">30</b>
<input type="range" id="nTB" min="10" max="500" value="30" step="10">
</label>
<label>Treatment success: <b id="rTB-val">90%</b>
<input type="range" id="rTB" min="5" max="95" value="90" step="1">
</label>
<label>Control patients: <b id="nCB-val">90</b>
<input type="range" id="nCB" min="10" max="500" value="90" step="10">
</label>
<label>Control success: <b id="rCB-val">70%</b>
<input type="range" id="rCB" min="5" max="95" value="70" step="1">
</label>
</div>
</div>
<div class="chart-area">
<div class="chart-col">
<div class="bar-pair" id="bars-A"></div>
<div class="col-label">Hospital A</div>
</div>
<div class="chart-col">
<div class="bar-pair" id="bars-B"></div>
<div class="col-label">Hospital B</div>
</div>
<div class="chart-col combined">
<div class="bar-pair" id="bars-C"></div>
<div class="col-label">Combined</div>
</div>
</div>
<div class="legend"><span class="dot treat"></span>Treatment <span class="dot ctrl"></span>Control</div>
<div id="verdict" class="verdict"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.45; }
.hospitals { display: flex; gap: .7rem; margin-bottom: .9rem; flex-wrap: wrap; }
.hospital { flex: 1 1 210px; background: #f3f6f9; border: 1px solid #dde3ea; border-radius: 10px; padding: .65rem .8rem; }
.hosp-label { font-weight: 700; margin-bottom: .45rem; display: flex; align-items: center; gap: .4rem; }
.tag { font-size: .72rem; font-weight: 600; border-radius: 4px; padding: 1px 6px; }
.tag.severe { background: #fde8e8; color: #b91c1c; }
.tag.mild { background: #e8f5e9; color: #166534; }
.hospital label { display: flex; align-items: center; gap: .4rem; font-size: .82rem; margin-bottom: .3rem; flex-wrap: wrap; }
input[type=range] { flex: 1 1 80px; accent-color: #1d3557; }
.chart-area { display: flex; gap: .6rem; align-items: flex-end; margin: .5rem 0 .25rem; height: 150px; }
.chart-col { display: flex; flex-direction: column; align-items: center; flex: 1; }
.chart-col.combined .bar-pair { border-left: 2px dashed #aaa; padding-left: 4px; }
.bar-pair { display: flex; gap: 4px; align-items: flex-end; height: 130px; width: 100%; justify-content: center; }
.bar { width: 38%; border-radius: 5px 5px 0 0; position: relative; min-height: 4px; transition: height .2s; }
.bar.treat { background: #1d7abd; }
.bar.ctrl { background: #9fb2c8; }
.bar .pct { position: absolute; top: -18px; left: 50%; transform: translateX(-50%); font-size: .7rem; font-weight: 700; white-space: nowrap; color: #222; }
.col-label { font-size: .78rem; font-weight: 600; margin-top: .3rem; color: #333; text-align: center; }
.legend { font-size: .8rem; display: flex; align-items: center; gap: .25rem; margin: .1rem 0 .4rem; }
.dot { display: inline-block; width: 10px; height: 10px; border-radius: 2px; }
.dot.treat { background: #1d7abd; }
.dot.ctrl { background: #9fb2c8; }
.verdict { font-size: .88rem; font-weight: 600; border-radius: 8px; padding: .4rem .7rem; min-height: 2em; }
.verdict.paradox { background: #fef3c7; color: #92400e; border: 1px solid #fbbf24; }
.verdict.normal { background: #e8f5e9; color: #166534; border: 1px solid #86efac; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Simpson’s Paradox demo.
// Each hospital has separate treatment-arm and control-arm patient counts.
// This lets the confounding pattern arise naturally: Hospital A (severe)
// sends most patients to treatment; Hospital B (mild) sends most to control.
// Within each hospital treatment beats control, but the combined rate reverses.
function val(id) { return parseInt(document.getElementById(id).value, 10); }
function setLbl(id, v, pct) {
document.getElementById(id + '-val').textContent = pct ? v + '%' : v;
}
function update() {
const nTA = val('nTA'), rTA = val('rTA');
const nCA = val('nCA'), rCA = val('rCA');
const nTB = val('nTB'), rTB = val('rTB');
const nCB = val('nCB'), rCB = val('rCB');
setLbl('nTA', nTA, false); setLbl('rTA', rTA, true);
setLbl('nCA', nCA, false); setLbl('rCA', rCA, true);
setLbl('nTB', nTB, false); setLbl('rTB', rTB, true);
setLbl('nCB', nCB, false); setLbl('rCB', rCB, true);
// Success counts
const sTA = nTA * rTA / 100;
const sCA = nCA * rCA / 100;
const sTB = nTB * rTB / 100;
const sCB = nCB * rCB / 100;
// Combined rates (treatment patients pooled; control patients pooled)
const rTC = Math.round((sTA + sTB) / (nTA + nTB) * 100);
const rCC = Math.round((sCA + sCB) / (nCA + nCB) * 100);
drawBars('bars-A', rTA, rCA);
drawBars('bars-B', rTB, rCB);
drawBars('bars-C', rTC, rCC);
// Paradox: treatment beats control in both hospitals but loses combined (or vice versa)
const winA = rTA > rCA, winB = rTB > rCB, winC = rTC > rCC;
const verdict = document.getElementById('verdict');
if ((winA && winB && !winC) || (!winA && !winB && winC)) {
verdict.className = 'verdict paradox';
verdict.textContent =
fmt('Simpson’s Paradox! Treatment %v% in both hospitals individually but %v2% in the combined data (%rTC%% vs %rCC%%).', { v: winA ? 'wins' : 'loses', v2: winC ? 'wins' : 'loses', rTC: rTC, rCC: rCC });
console.log(fmt('Paradox! A:%rTA%% vs %rCA%% B:%rTB%% vs %rCB%% Combined:%rTC%% vs %rCC%%', { rTA: rTA, rCA: rCA, rTB: rTB, rCB: rCB, rTC: rTC, rCC: rCC }));
} else {
verdict.className = 'verdict normal';
verdict.textContent =
fmt('No paradox — combined result agrees with both hospitals (%rTC%% vs %rCC%%). Try making Hospital A have many more treatment patients than control patients.', { rTC: rTC, rCC: rCC });
}
}
function drawBars(containerId, treat, ctrl) {
const el = document.getElementById(containerId);
el.innerHTML = '';
[['treat', treat], ['ctrl', ctrl]].forEach(([cls, pct]) => {
const d = document.createElement('div');
d.className = 'bar ' + cls;
d.style.height = pct + '%';
const lbl = document.createElement('span');
lbl.className = 'pct';
lbl.textContent = pct + '%';
d.appendChild(lbl);
el.appendChild(d);
});
}
['nTA','rTA','nCA','rCA','nTB','rTB','nCB','rCB'].forEach(id => {
document.getElementById(id).addEventListener('input', update);
});
update();
Comments
Loading comments...