IAT 461 / 882 · Data Science for Human-Centered Systems · Summer 2026 · Alireza Karduni
To answer these questions we need data. But we can never measure everyone.
A population is the complete group you care about.
Populations are usually too large to measure directly.
We work with a sample instead.
The gap between \(\bar{x}\) and \(\mu\) is sampling error — it never fully disappears.
200 gamers · each circle = one person · orange = sampled
function mb32(seed) {
return () => {
seed |= 0; seed = seed + 0x6D2B79F5 | 0;
let t = Math.imul(seed ^ seed >>> 15, 1 | seed);
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
return ((t ^ t >>> 14) >>> 0) / 4294967296;
};
}
popData = {
const rng = mb32(42);
return Array.from({ length: N_POP }, (_, id) => {
const u1 = rng(), u2 = rng();
const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
const hrs = Math.max(0.1, Math.min(9.9, Math.exp(0.7 + 0.85 * z)));
return { id, hrs: +hrs.toFixed(2) };
});
}
popMean = +(popData.reduce((s, d) => s + d.hrs, 0) / N_POP).toFixed(2)currentSample = {
clickCount;
if (clickCount === 0) return new Set();
const rng = mb32((Date.now() ^ clickCount * 1000003) & 0xffffffff);
const idx = new Set();
while (idx.size < nSize) idx.add(Math.floor(rng() * N_POP));
return idx;
}
sampleMean = currentSample.size === 0 ? null
: +([...currentSample].map(i => popData[i].hrs)
.reduce((a, b) => a + b, 0) / currentSample.size).toFixed(2)// ── Controls: plain HTML, small font ─────────────────────────────────
controls = {
const div = document.createElement("div");
div.style.cssText = `
display: flex; align-items: center; gap: 12px;
margin-bottom: 6px; font-size: 12px; color: #555;
`;
// Button
const btn = document.createElement("button");
btn.textContent = "▶ Draw sample";
btn.style.cssText = `
background: ${ACCENT}; color: white; border: none;
border-radius: 5px; padding: 4px 12px;
font-size: 12px; font-weight: 600; cursor: pointer;
`;
btn.onclick = () => { mutable clickCount = clickCount + 1; };
// Label
const lbl = document.createElement("span");
lbl.textContent = "n =";
// Slider
const slider = document.createElement("input");
slider.type = "range";
slider.min = 5; slider.max = N_POP; slider.step = 1; slider.value = nSize;
slider.style.cssText = `
width: 130px; height: 4px; accent-color: ${ACCENT};
vertical-align: middle; cursor: pointer;
`;
// Value display
const val = document.createElement("span");
val.style.cssText = `color: ${ACCENT}; font-weight: 600; min-width: 28px;`;
val.textContent = nSize;
slider.oninput = () => {
mutable nSize = +slider.value;
val.textContent = slider.value;
};
div.append(btn, lbl, slider, val);
return div;
}{
const W = 660, H = 340;
// ── layout ────────────────────────────────────────────────────────
const BODY_Y = 16;
const BODY_H = H - BODY_Y - 32;
// grid
const GX = 0, GW = 280;
const COLS = 20, CELL = 13, R_P = 5;
// dot plot
const DX = GW + 28;
const pL = 30, pR = 8;
const cW = W - DX - pL - pR;
const cH = BODY_H;
const xMin = 0, xMax = 10;
const xS = v => DX + pL + ((v - xMin) / (xMax - xMin)) * cW;
const svg = d3.create("svg")
.attr("width", W).attr("height", H)
.style("font-family", "inherit")
.style("background", "transparent");
// ── LEFT: grid label ──────────────────────────────────────────────
svg.append("text")
.attr("x", GX).attr("y", BODY_Y - 2)
.attr("font-size", 10).attr("fill", "#999")
.text(`Population N = ${N_POP} μ = ${popMean} hrs`);
// ── LEFT: grid circles ────────────────────────────────────────────
popData.forEach(({ id }) => {
const col = id % COLS;
const row = Math.floor(id / COLS);
const cx = GX + col * CELL + CELL / 2;
const cy = BODY_Y + row * CELL + CELL / 2;
svg.append("circle")
.attr("cx", cx).attr("cy", cy).attr("r", R_P)
.attr("fill", currentSample.has(id) ? ACCENT : GREY)
.attr("opacity", currentSample.has(id) ? 1 : 0.7);
});
// ── RIGHT: dot plot label ─────────────────────────────────────────
svg.append("text")
.attr("x", DX + pL + cW / 2).attr("y", BODY_Y - 2)
.attr("text-anchor", "middle")
.attr("font-size", 10).attr("fill", "#999")
.text("Individual gaming hours (all 200 people)");
// ── RIGHT: dot plot ───────────────────────────────────────────────
const BIN = 0.2, R = 4, VGAP = 1;
const stacks = new Map();
popData.forEach(d => {
const k = +(Math.round(d.hrs / BIN) * BIN).toFixed(1);
if (!stacks.has(k)) stacks.set(k, []);
stacks.get(k).push({ ...d, sampled: currentSample.has(d.id) });
});
stacks.forEach(stack => {
stack.sort((a, b) => (b.sampled ? 1 : 0) - (a.sampled ? 1 : 0));
stack.forEach((d, j) => {
const cx = xS(d.hrs);
const cy = BODY_Y + cH - R - j * (R * 2 + VGAP);
if (cy < BODY_Y) return;
svg.append("circle")
.attr("cx", cx).attr("cy", cy).attr("r", R)
.attr("fill", d.sampled ? ACCENT : GREY)
.attr("opacity", d.sampled ? 1 : 0.5);
});
});
// population mean line
svg.append("line")
.attr("x1", xS(popMean)).attr("x2", xS(popMean))
.attr("y1", BODY_Y).attr("y2", BODY_Y + cH)
.attr("stroke", BLUE).attr("stroke-width", 2)
.attr("stroke-dasharray", "6,3");
svg.append("text")
.attr("x", xS(popMean) + 4).attr("y", BODY_Y + 13)
.attr("font-size", 10).attr("fill", BLUE)
.text("μ = " + popMean);
// sample mean line
if (sampleMean !== null) {
svg.append("line")
.attr("x1", xS(sampleMean)).attr("x2", xS(sampleMean))
.attr("y1", BODY_Y).attr("y2", BODY_Y + cH)
.attr("stroke", ACCENT).attr("stroke-width", 2)
.attr("stroke-dasharray", "4,2");
svg.append("text")
.attr("x", xS(sampleMean) + 4).attr("y", BODY_Y + 27)
.attr("font-size", 10).attr("fill", ACCENT)
.text("x̄ = " + sampleMean);
}
// x axis
svg.append("line")
.attr("x1", DX + pL).attr("x2", DX + pL + cW)
.attr("y1", BODY_Y + cH).attr("y2", BODY_Y + cH)
.attr("stroke", "#ccc").attr("stroke-width", 1);
[0, 2, 4, 6, 8, 10].forEach(v => {
svg.append("text")
.attr("x", xS(v)).attr("y", BODY_Y + cH + 14)
.attr("text-anchor", "middle")
.attr("font-size", 9).attr("fill", "#aaa")
.text(v);
});
svg.append("text")
.attr("x", DX + pL + cW / 2).attr("y", H - 4)
.attr("text-anchor", "middle")
.attr("font-size", 9).attr("fill", "#aaa")
.text("daily gaming hours");
return svg.node();
}This variability is not a mistake — it is the nature of sampling.
How much variability should we expect by chance?
Maybe Group B genuinely plays more.
Or maybe we just got a lucky draw.
We need a principled way to decide. That is hypothesis testing.
But before we claim that — we need to ask a harder question.
{
const W = 660, H = 200;
const svg = d3.create("svg")
.attr("width", W).attr("height", H)
.style("font-family", "inherit")
.style("background", "transparent");
// Two distributions
const groups = [
{ label: "Group A (casual)", mean: 120, color: BLUE ?? "#5b8dd9" },
{ label: "Group B (competitive)", mean: 210, color: ACCENT ?? "#e8925a" },
];
const W2 = 220, H2 = 120, sigma = 28;
const offsets = [60, 360];
groups.forEach(({ label, mean, color }, gi) => {
const ox = offsets[gi];
const oy = 30;
// bell curve
const pts = d3.range(mean - 90, mean + 90, 2).map(x => {
const y = Math.exp(-0.5 * ((x - mean) / sigma) ** 2);
return [ox + (x - (mean - 90)) * (W2 / 180), oy + H2 - y * H2 * 0.92];
});
const line = d3.line()(pts);
svg.append("path").attr("d", line)
.attr("fill", color).attr("opacity", 0.15)
.attr("stroke", "none");
svg.append("path").attr("d", line)
.attr("fill", "none")
.attr("stroke", color).attr("stroke-width", 2);
// mean tick
svg.append("line")
.attr("x1", ox + W2 / 2).attr("x2", ox + W2 / 2)
.attr("y1", oy + H2 - sigma * 0.9).attr("y2", oy + H2 + 8)
.attr("stroke", color).attr("stroke-width", 2)
.attr("stroke-dasharray", "4,3");
svg.append("text")
.attr("x", ox + W2 / 2).attr("y", oy - 8)
.attr("text-anchor", "middle")
.attr("font-size", 12).attr("fill", color)
.text(label);
});
// difference arrow
const ax1 = offsets[0] + W2 / 2 + 14;
const ax2 = offsets[1] + W2 / 2 - 14;
const ay = 100;
svg.append("line")
.attr("x1", ax1).attr("x2", ax2)
.attr("y1", ay).attr("y2", ay)
.attr("stroke", "#999").attr("stroke-width", 1.5)
.attr("marker-end", "url(#arr)");
svg.append("line")
.attr("x1", ax2).attr("x2", ax1)
.attr("y1", ay).attr("y2", ay)
.attr("stroke", "#999").attr("stroke-width", 1.5)
.attr("marker-end", "url(#arr2)");
// arrowhead markers
const defs = svg.append("defs");
["arr","arr2"].forEach(id => {
defs.append("marker").attr("id", id)
.attr("markerWidth", 6).attr("markerHeight", 6)
.attr("refX", 5).attr("refY", 3).attr("orient", "auto")
.append("path").attr("d", "M0,0 L6,3 L0,6 Z")
.attr("fill", "#999");
});
svg.append("text")
.attr("x", (ax1 + ax2) / 2).attr("y", ay - 8)
.attr("text-anchor", "middle")
.attr("font-size", 11).attr("fill", "#666")
.text("observed difference");
// question
svg.append("text")
.attr("x", W / 2).attr("y", H - 12)
.attr("text-anchor", "middle")
.attr("font-size", 12).attr("fill", "#888")
.attr("font-style", "italic")
.text("Is this difference real — or would we see it just by chance?");
return svg.node();
}H₀ is not a claim we believe. We ask: how likely is our data if H₀ were true?
{
const W = 660, H = 340;
const R = 70;
const CX = W / 2, CY = H * 0.32;
const aColor = BLUE ?? "#5b8dd9";
const bColor = ACCENT ?? "#e8925a";
const popColor = "#aaa";
const aX = W * 0.20, aY = H * 0.82;
const bX = W * 0.80, bY = H * 0.82;
const rS = 34; // sample circle radius
const svg = d3.create("svg")
.attr("width", W).attr("height", H)
.style("font-family", "inherit")
.style("background", "transparent");
// title
svg.append("text")
.attr("x", CX).attr("y", 22)
.attr("text-anchor", "middle")
.attr("font-size", 13).attr("font-weight", "600").attr("fill", "#555")
.text("H₀: The difference we observe is due to chance.");
// population circle
svg.append("circle")
.attr("cx", CX).attr("cy", CY).attr("r", R)
.attr("fill", popColor).attr("opacity", 0.08)
.attr("stroke", popColor).attr("stroke-width", 2);
svg.append("text")
.attr("x", CX).attr("y", CY + 5)
.attr("text-anchor", "middle")
.attr("font-size", 13).attr("fill", popColor)
.text("one population");
// line to A
svg.append("line")
.attr("x1", CX - R * 0.5).attr("x2", aX + rS * 0.7)
.attr("y1", CY + R).attr("y2", aY - rS)
.attr("stroke", aColor).attr("stroke-width", 1.8)
.attr("stroke-dasharray", "6,4");
// sample A
svg.append("circle")
.attr("cx", aX).attr("cy", aY).attr("r", rS)
.attr("fill", aColor).attr("opacity", 0.10)
.attr("stroke", aColor).attr("stroke-width", 1.8);
svg.append("text")
.attr("x", aX).attr("y", aY + 5)
.attr("text-anchor", "middle")
.attr("font-size", 12).attr("fill", aColor)
.text("sample A");
// line to B
svg.append("line")
.attr("x1", CX + R * 0.5).attr("x2", bX - rS * 0.7)
.attr("y1", CY + R).attr("y2", bY - rS)
.attr("stroke", bColor).attr("stroke-width", 1.8)
.attr("stroke-dasharray", "6,4");
// sample B
svg.append("circle")
.attr("cx", bX).attr("cy", bY).attr("r", rS)
.attr("fill", bColor).attr("opacity", 0.10)
.attr("stroke", bColor).attr("stroke-width", 1.8);
svg.append("text")
.attr("x", bX).attr("y", bY + 5)
.attr("text-anchor", "middle")
.attr("font-size", 12).attr("fill", bColor)
.text("sample B");
// difference annotation between A and B
svg.append("line")
.attr("x1", aX + rS).attr("x2", bX - rS)
.attr("y1", aY).attr("y2", bY)
.attr("stroke", "#ddd").attr("stroke-width", 1.5)
.attr("stroke-dasharray", "3,3");
svg.append("text")
.attr("x", (aX + bX) / 2).attr("y", aY + 20)
.attr("text-anchor", "middle")
.attr("font-size", 11).attr("fill", "#bbb")
.text("any difference = sampling noise");
return svg.node();
}Testing H₀ only rules out one explanation: pure chance.
It tells us nothing about which alternative is correct.
%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#f5f5f5", "primaryTextColor": "#555", "primaryBorderColor": "#aaa", "lineColor": "#aaa", "fontSize": "13px"}}}%%
flowchart LR
OBS["Group B plays more hours"]
OBS --> C["✓ causes more play"]
%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#f5f5f5", "primaryTextColor": "#555", "primaryBorderColor": "#aaa", "lineColor": "#aaa", "fontSize": "13px"}}}%%
flowchart LR
OBS["Group B plays more hours"]
OBS --> C["✓ causes more play"]
OBS --> S["Selection bias — heavy players join"]
%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#f5f5f5", "primaryTextColor": "#555", "primaryBorderColor": "#aaa", "lineColor": "#aaa", "fontSize": "13px"}}}%%
flowchart LR
OBS["Group B plays more hours"]
OBS --> C["✓ causes more play"]
OBS --> S["Selection bias — heavy players join"]
OBS --> CF["Confound — age / free time"]
OBS --> M["Measurement error — self-reported hours"]
Rejecting H₀ only means: We find it unreasonable to bellieve that the difference we observe is due to chance.
The p-value is the probability of obtaining results at least as extreme as the observed result, under the assumption that the null hypothesis is correct.
{
const W = 660, H = 300;
const pL = 50, pR = 30, pT = 40, pB = 50;
const cW = W - pL - pR;
const cH = H - pT - pB;
const ac = ACCENT ?? "#e8925a";
const bc = BLUE ?? "#5b8dd9";
// null distribution: normal curve centred at 0
const mu = 0, sigma = 1;
const xMin = -4, xMax = 4;
const observed = 2.1;
const xS = v => pL + ((v - xMin) / (xMax - xMin)) * cW;
const yS = v => pT + cH - v * cH;
const normal = x => Math.exp(-0.5 * ((x - mu) / sigma) ** 2) / (sigma * Math.sqrt(2 * Math.PI));
const yScale = v => pT + cH - (v / normal(0)) * cH * 0.82;
const pts = d3.range(xMin, xMax + 0.05, 0.05);
const svg = d3.create("svg")
.attr("width", W).attr("height", H)
.style("font-family", "inherit")
.style("background", "transparent");
// shaded tail (p-value region)
const tailPts = pts.filter(x => x >= observed);
const tailPath = [
`M ${xS(observed)} ${yScale(0)}`,
...tailPts.map(x => `L ${xS(x)} ${yScale(normal(x))}`),
`L ${xS(xMax)} ${yScale(0)}`,
"Z"
].join(" ");
svg.append("path").attr("d", tailPath)
.attr("fill", ac).attr("opacity", 0.25);
// full curve fill
const curvePts = pts;
const curvePath = [
`M ${xS(xMin)} ${yScale(0)}`,
...curvePts.map(x => `L ${xS(x)} ${yScale(normal(x))}`),
`L ${xS(xMax)} ${yScale(0)}`,
"Z"
].join(" ");
svg.append("path").attr("d", curvePath)
.attr("fill", bc).attr("opacity", 0.08);
// curve stroke
const strokePath = [
`M ${xS(xMin)} ${yScale(0)}`,
...curvePts.map(x => `L ${xS(x)} ${yScale(normal(x))}`)
].join(" ");
svg.append("path").attr("d", strokePath)
.attr("fill", "none")
.attr("stroke", bc).attr("stroke-width", 2);
// x axis
svg.append("line")
.attr("x1", pL).attr("x2", pL + cW)
.attr("y1", pT + cH).attr("y2", pT + cH)
.attr("stroke", "#ccc").attr("stroke-width", 1);
// x axis ticks
[-3, -2, -1, 0, 1, 2, 3].forEach(v => {
svg.append("text")
.attr("x", xS(v)).attr("y", pT + cH + 18)
.attr("text-anchor", "middle")
.attr("font-size", 10).attr("fill", "#aaa")
.text(v);
});
svg.append("text")
.attr("x", pL + cW / 2).attr("y", H - 6)
.attr("text-anchor", "middle")
.attr("font-size", 10).attr("fill", "#aaa")
.text("test statistic (difference, in standard units)");
// observed statistic line
svg.append("line")
.attr("x1", xS(observed)).attr("x2", xS(observed))
.attr("y1", pT).attr("y2", pT + cH)
.attr("stroke", ac).attr("stroke-width", 2)
.attr("stroke-dasharray", "5,3");
svg.append("text")
.attr("x", xS(observed) + 6).attr("y", pT + 16)
.attr("font-size", 11).attr("fill", ac)
.text("observed");
svg.append("text")
.attr("x", xS(observed) + 6).attr("y", pT + 30)
.attr("font-size", 11).attr("fill", ac)
.text("difference");
// p-value label in tail
svg.append("text")
.attr("x", xS(3.0)).attr("y", yScale(normal(2.6)) - 10)
.attr("text-anchor", "middle")
.attr("font-size", 11).attr("font-weight", "600").attr("fill", ac)
.text("p-value");
// centre label
svg.append("text")
.attr("x", xS(0)).attr("y", pT + 16)
.attr("text-anchor", "middle")
.attr("font-size", 10).attr("fill", bc)
.text("null distribution");
svg.append("text")
.attr("x", xS(0)).attr("y", pT + 30)
.attr("text-anchor", "middle")
.attr("font-size", 10).attr("fill", bc)
.text("(what we'd expect if H₀ were true)");
return svg.node();
}The p-value only answers one narrow question:
how surprising is this data, assuming nothing is going on?
Some fields use p < 0.01 or p < 0.001.
A result with p = 0.049 is not meaningfully different from p = 0.051.
Significance is binary. The world is not.
There are two flavours of resampling — each answers a different question.
%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#f0f0f0", "primaryTextColor": "#444", "primaryBorderColor": "#bbb", "lineColor": "#bbb", "fontSize": "13px"}}}%%
flowchart LR
A["One dataset"] --> B["Permutation test<br/>Shuffle the labels"]
A --> C["Bootstrap<br/>Resample with replacement"]
B --> D["Null distribution<br/>→ p-value"]
C --> E["Sampling distribution<br/>→ confidence interval"]
If H₀ is true — that there is no real difference — then the group labels are meaningless.
We can shuffle them and recompute the difference over and over.
The p-value = fraction of shuffles where chance matched or beat what we actually saw.
%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#f0f0f0", "primaryTextColor": "#444", "primaryBorderColor": "#bbb", "lineColor": "#bbb", "fontSize": "13px"}}}%%
flowchart TD
A["Group A (original)"] --> C["Resample A<br/>with replacement"]
B["Group B (original)"] --> D["Resample B<br/>with replacement"]
C --> E["Compute difference<br/>of means"]
D --> E
E --> F["Record it"]
F --> G["Repeat 1000×"]
G --> H["95% CI:<br/>middle 95% of gaps"]
The CI tells you how uncertain we are about the difference.
%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#f0f0f0", "primaryTextColor": "#444", "primaryBorderColor": "#bbb", "lineColor": "#bbb", "fontSize": "13px"}}}%%
flowchart LR
A["Same data<br/>n = 10"] --> B["p = 0.31<br/>not significant"]
C["Same data<br/>n = 100"] --> D["p = 0.0002<br/>significant ✓"]
E["Same data<br/>n = 1000"] --> F["p = 0.000001<br/>very significant ✓"]
The effect size stayed the same (~0.40) across all three.
The p-value just responded to the sample size.
\[d = \frac{\bar{x}_B - \bar{x}_A}{SD_{pooled}}\]
{
const W = 660, H = 260;
const pL = 40, pR = 20, pT = 30, pB = 50;
const cW = W - pL - pR;
const cH = H - pT - pB;
const ac = ACCENT ?? "#e8925a";
const bc = BLUE ?? "#5b8dd9";
const cases = [
{ d: 0.2, label: "small d = 0.2", x: cW * 0.22 },
{ d: 0.5, label: "medium d = 0.5", x: cW * 0.50 },
{ d: 0.8, label: "large d = 0.8", x: cW * 0.78 },
];
const sigma = 28;
const rowH = cH / 3;
const svg = d3.create("svg")
.attr("width", W).attr("height", H)
.style("font-family", "inherit")
.style("background", "transparent");
const normal = (x, mu) =>
Math.exp(-0.5 * ((x - mu) / sigma) ** 2);
cases.forEach(({ d, label, x }, i) => {
const cy = pT + i * rowH + rowH / 2;
const muA = 0;
const muB = d * sigma;
const xMin = -90, xMax = 90 + d * sigma;
const xS = v => pL + x - 60 + ((v - xMin) / (xMax - xMin)) * 120;
const yS = v => cy + 14 - v * (rowH * 0.42);
const pts = d3.range(xMin, xMax, 1);
// curve A
const pathA = "M " + pts.map(v =>
`${xS(v)} ${yS(normal(v, muA))}`).join(" L ");
svg.append("path").attr("d", pathA)
.attr("fill", bc).attr("opacity", 0.15)
.attr("stroke", bc).attr("stroke-width", 1.5);
// curve B
const pathB = "M " + pts.map(v =>
`${xS(v)} ${yS(normal(v, muB))}`).join(" L ");
svg.append("path").attr("d", pathB)
.attr("fill", ac).attr("opacity", 0.15)
.attr("stroke", ac).attr("stroke-width", 1.5);
// baseline
svg.append("line")
.attr("x1", pL + x - 60).attr("x2", pL + x + 60)
.attr("y1", cy + 14).attr("y2", cy + 14)
.attr("stroke", "#ddd").attr("stroke-width", 1);
// label
svg.append("text")
.attr("x", pL + 4).attr("y", cy + 4)
.attr("font-size", 10).attr("fill", "#888")
.text(label);
});
// legend
svg.append("circle").attr("cx", W - 120).attr("cy", pT + 8)
.attr("r", 5).attr("fill", bc);
svg.append("text").attr("x", W - 112).attr("y", pT + 12)
.attr("font-size", 10).attr("fill", bc).text("Group A");
svg.append("circle").attr("cx", W - 60).attr("cy", pT + 8)
.attr("r", 5).attr("fill", ac);
svg.append("text").attr("x", W - 52).attr("y", pT + 12)
.attr("font-size", 10).attr("fill", ac).text("Group B");
return svg.node();
}More overlap = smaller effect. Less overlap = larger effect.
%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#f0f0f0", "primaryTextColor": "#444", "primaryBorderColor": "#bbb", "lineColor": "#bbb", "fontSize": "13px"}}}%%
flowchart LR
A["Observed<br/>difference"] --> B["p-value<br/>Is it real?"]
A --> C["Cohen's d<br/>How big is it?"]
B --> D["Rules out<br/>chance"]
C --> E["Measures practical<br/>significance"]
Always report both. A result can be statistically significant and practically tiny.
Significance is a property of your test.
Importance is a property of your finding.
They are not the same thing.
%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#f0f0f0", "primaryTextColor": "#444", "primaryBorderColor": "#bbb", "lineColor": "#bbb", "fontSize": "13px"}}}%%
flowchart LR
A["Reality"] --> B["H₀ true"]
A --> C["H₁ true"]
B --> D["Reject H₀ <br/>→ Type I error ❌"]
B --> E["Fail to reject<br/>→ Correct ✓"]
C --> F["Reject H₀<br/>→ Correct ✓"]
C --> G["Fail to reject<br/>→ Type II error ❌"]
The conventional target is power = 0.80 — accepting a 20% chance of missing a real effect.
Power analysis lets you ask: how many participants do I need to detect an effect of this size?
Run it before your study, not after.
A p-value alone is never the whole story.


















IAT 461 · Data Science for Human-Centered Systems · Summer 2026