11-01: One-Sample t-Test¶
The z-test of 10-02 needs σ. You almost never have it. Replace σ with the sample standard deviation s, and the test statistic no longer follows the normal distribution — it follows Student's t.
The Test Statistic¶
Identical in shape to the z-statistic, with s in place of σ — and a different reference distribution as a consequence.
Conditions¶
The t-test is fairly robust to mild non-normality, especially for n ≥ 15. It is not robust to outliers, because both x̄ and s are pulled by them — check a boxplot first (04-02).
The t-Distribution¶
| Property | t | z |
|---|---|---|
| Shape | Bell, symmetric about 0 | Bell, symmetric about 0 |
| Tails | Heavier | Lighter |
| Parameters | df = n − 1 |
none |
Critical value at α = 0.05 (two-tail) |
2.776 (df=4), 2.145 (df=14), 2.042 (df=30) |
1.960 |
| Limit | → z as df → ∞ |
— |
The extra uncertainty from estimating σ is exactly what the fatter tails encode: you need a larger test statistic to reject with a small sample.
Worked Example¶
A coffee machine is supposed to dispense 250 ml per cup. A technician samples 16 cups and finds x̄ = 246.8 ml with s = 5.4 ml. Is the machine off target? Test at α = 0.05.
STEP 1 H₀: μ = 250 H₁: μ ≠ 250 (two-tailed) α = 0.05
STEP 2 Random sample ✓ σ unknown ✓ volume is approximately normal ✓
df = 16 − 1 = 15
STEP 3 SE = s/√n = 5.4/√16 = 5.4/4 = 1.35
246.8 − 250 −3.2
t = ───────────── = ────── = −2.370
1.35 1.35
STEP 4 Critical values: ±t(0.025, 15) = ±2.131
|−2.370| > 2.131 → REJECT H₀
p-value: 2 × P(T₁₅ < −2.370) = 2(0.01581) = 0.0316 ≤ 0.05
→ REJECT H₀
STEP 5 There is sufficient evidence at the 5% level to conclude that the
mean dispensed volume differs from 250 ml.
Confidence interval (equivalent view).
250 is not in the interval — the same rejection, with the added information that the machine is under-filling by roughly 0.3 to 6.1 ml.
Effect size.
One-Tailed Version¶
If the technician only cared about under-filling:
H₀: μ ≥ 250 H₁: μ < 250
Critical value: t(0.05, 15) = −1.753
−2.370 < −1.753 → REJECT H₀
p-value: P(T₁₅ < −2.370) = 0.0158 ≤ 0.05 → REJECT H₀
Half the two-tailed p-value, and a smaller critical value — a one-tailed test is more powerful when the direction is justified in advance.
t-Test vs. z-Test — Which One¶
┌─────────────────────────────┐
│ Is σ (population SD) known?│
└──────────┬──────────────────┘
YES │ NO
┌────────────────┘ └───────────────┐
▼ ▼
Use the Z-TEST Use the T-TEST
z = (x̄−μ₀)/(σ/√n) t = (x̄−μ₀)/(s/√n), df = n−1
In coursework σ is sometimes supplied to make the z-test possible. In practice it almost never is — when in doubt, use t. With n ≥ 30 the two give nearly identical answers anyway.
Reporting the Result¶
The standard format in scientific writing:
t(15) = −2.37, p = .032, d = −0.59, 95% CI [243.92, 249.68]
│ │ │ │ │
df statistic p-value effect interval estimate
Report all four. The p-value alone tells the reader almost nothing about how big the effect is.
Excel¶
' ══ FROM SUMMARY STATISTICS ═════════════════════════════════════════
' B1 = x̄ = 246.8, B2 = μ₀ = 250, B3 = s = 5.4, B4 = n = 16, B5 = α = 0.05
=B3/SQRT(B4) ' standard error -> 1.35
=B4-1 ' degrees of freedom -> 15
=(B1-B2)/(B3/SQRT(B4)) ' t statistic -> -2.37037
=T.INV.2T(B5, B4-1) ' two-tailed critical t -> 2.13145
=T.INV(B5, B4-1) ' left-tailed critical t -> -1.75305
=T.INV(1-B5, B4-1) ' right-tailed critical -> 1.75305
=T.DIST.2T(ABS(B8), B4-1) ' two-tailed p-value -> 0.03163
=T.DIST(B8, B4-1, TRUE) ' left-tailed p-value -> 0.01581
=T.DIST.RT(B8, B4-1) ' right-tailed p-value -> 0.98419
=IF(B12<=B5, "Reject H0", "Fail to reject H0")
' Confidence interval
=B1-T.INV.2T(B5,B4-1)*B3/SQRT(B4) ' lower -> 243.923
=B1+T.INV.2T(B5,B4-1)*B3/SQRT(B4) ' upper -> 249.677
=CONFIDENCE.T(B5, B3, B4) ' margin of error E -> 2.877
' Effect size
=(B1-B2)/B3 ' Cohen's d -> -0.5926
' ══ FROM RAW DATA ═══════════════════════════════════════════════════
=AVERAGE(A2:A17)
=STDEV.S(A2:A17)
=(AVERAGE(A2:A17)-250)/(STDEV.S(A2:A17)/SQRT(COUNT(A2:A17))) ' t
=T.DIST.2T(ABS(F5), COUNT(A2:A17)-1) ' p-value
' Excel has no one-sample t-test dialog. The standard workaround:
' put a column of 250s next to your data and run
' Data ▸ Data Analysis ▸ t-Test: Paired Two Sample for Means
' The result is exactly the one-sample t-test.
=T.TEST(A2:A17, C2:C17, 2, 1) ' tails=2, type=1 (paired) -> 0.03163
Tip
T.TEST(array1, array2, tails, type) — type is 1 = paired, 2 = two-sample equal variance, 3 = two-sample unequal variance (Welch). The paired trick above turns it into a one-sample test.
R¶
# ══ FROM SUMMARY STATISTICS ════════════════════════════════════════
t_test_summary <- function(xbar, mu0, s, n,
alternative = c("two.sided", "less", "greater"),
conf = 0.95) {
alternative <- match.arg(alternative)
se <- s / sqrt(n); df <- n - 1
t <- (xbar - mu0) / se
p <- switch(alternative,
less = pt(t, df),
greater = pt(t, df, lower.tail = FALSE),
two.sided = 2 * pt(abs(t), df, lower.tail = FALSE))
tc <- qt(1 - (1 - conf)/2, df)
list(se = se, df = df, t = t, p.value = p, d = (xbar - mu0) / s,
conf.int = c(xbar - tc * se, xbar + tc * se))
}
t_test_summary(246.8, 250, 5.4, 16)
# $se 1.35 $df 15 $t -2.370370 $p.value 0.031627 $d -0.5926
# $conf.int 243.9226 249.6774
qt(0.975, 15) # two-tailed critical t -> 2.131450
qt(0.05, 15) # left-tailed critical -> -1.753050
# ══ FROM RAW DATA — t.test() ═══════════════════════════════════════
cups <- c(244, 250, 243, 249, 252, 241, 247, 245,
255, 240, 248, 246, 251, 243, 249, 246)
t.test(cups, mu = 250) # two-sided (default)
t.test(cups, mu = 250, alternative = "less") # one-sided
t.test(cups, mu = 250, conf.level = 0.99)
out <- t.test(cups, mu = 250)
out$statistic; out$parameter; out$p.value; out$conf.int; out$estimate
# ══ CHECKING THE ASSUMPTIONS ═══════════════════════════════════════
par(mfrow = c(1, 3))
hist(cups, breaks = 8, col = "#8A5FBF", border = "white", main = "Histogram")
boxplot(cups, col = "#0FA3A3", main = "Boxplot") # outlier check
qqnorm(cups, pch = 19); qqline(cups, col = "#0FA3A3", lwd = 2)
par(mfrow = c(1, 1))
shapiro.test(cups) # H0: data is normal — large p is reassuring
# ══ EFFECT SIZE ════════════════════════════════════════════════════
(mean(cups) - 250) / sd(cups) # Cohen's d, by hand
# library(effectsize); cohens_d(cups, mu = 250)
# ══ NONPARAMETRIC ALTERNATIVE (skewed data / outliers) ═════════════
wilcox.test(cups, mu = 250) # Wilcoxon signed-rank test
# ══ SEEING WHY t HAS FATTER TAILS ══════════════════════════════════
curve(dnorm(x), from = -4, to = 4, col = "#5B2A86", lwd = 2, ylab = "density")
curve(dt(x, df = 5), add = TRUE, col = "#0FA3A3", lwd = 2)
curve(dt(x, df = 30), add = TRUE, col = "#0B7A7A", lwd = 2, lty = 2)
legend("topright", c("z", "t, df=5", "t, df=30"), lwd = 2,
col = c("#5B2A86", "#0FA3A3", "#0B7A7A"))
Python¶
import numpy as np
from scipy import stats
# ══ FROM SUMMARY STATISTICS ════════════════════════════════════════
def t_test_summary(xbar, mu0, s, n, alternative="two-sided", conf=0.95):
se, df = s / np.sqrt(n), n - 1
t = (xbar - mu0) / se
if alternative == "less":
p = stats.t.cdf(t, df)
elif alternative == "greater":
p = stats.t.sf(t, df)
else:
p = 2 * stats.t.sf(abs(t), df)
tc = stats.t.ppf(1 - (1 - conf) / 2, df)
return {"se": se, "df": df, "t": t, "p_value": p,
"d": (xbar - mu0) / s,
"conf_int": (xbar - tc * se, xbar + tc * se)}
t_test_summary(246.8, 250, 5.4, 16)
# {'se': 1.35, 'df': 15, 't': -2.37037, 'p_value': 0.031627,
# 'd': -0.59259, 'conf_int': (243.9226, 249.6774)}
stats.t.ppf(0.975, 15) # 2.131450
stats.t.ppf(0.05, 15) # -1.753050
# ══ FROM RAW DATA ══════════════════════════════════════════════════
cups = np.array([244, 250, 243, 249, 252, 241, 247, 245,
255, 240, 248, 246, 251, 243, 249, 246])
res = stats.ttest_1samp(cups, popmean=250)
res.statistic, res.pvalue, res.df
res.confidence_interval(confidence_level=0.95)
stats.ttest_1samp(cups, 250, alternative="less") # one-sided
# statsmodels gives a fuller report
# from statsmodels.stats.weightstats import DescrStatsW
# d = DescrStatsW(cups); d.ttest_mean(250); d.tconfint_mean()
# ══ CHECKING THE ASSUMPTIONS ═══════════════════════════════════════
import matplotlib.pyplot as plt
fig, axes = plt.subplots(1, 3, figsize=(12, 3))
axes[0].hist(cups, bins=8, color="#8A5FBF", edgecolor="white")
axes[1].boxplot(cups)
stats.probplot(cups, dist="norm", plot=axes[2])
plt.tight_layout()
stats.shapiro(cups)
# ══ EFFECT SIZE ════════════════════════════════════════════════════
(cups.mean() - 250) / cups.std(ddof=1)
# ══ NONPARAMETRIC ALTERNATIVE ══════════════════════════════════════
stats.wilcoxon(cups - 250)
Quick Reference¶
| Task | Excel | R | Python |
|---|---|---|---|
| t statistic | (x̄-μ₀)/(s/SQRT(n)) |
(xbar-mu0)/(s/sqrt(n)) |
same |
| Critical t (two-tail) | T.INV.2T(α, df) |
qt(1-α/2, df) |
t.ppf(1-α/2, df) |
| Critical t (left) | T.INV(α, df) |
qt(α, df) |
t.ppf(α, df) |
| p-value (two-tail) | T.DIST.2T(ABS(t), df) |
2*pt(abs(t),df,lower.tail=FALSE) |
2*t.sf(abs(t), df) |
| p-value (left) | T.DIST(t, df, TRUE) |
pt(t, df) |
t.cdf(t, df) |
| p-value (right) | T.DIST.RT(t, df) |
pt(t,df,lower.tail=FALSE) |
t.sf(t, df) |
| Full test, raw data | T.TEST(x, const, 2, 1) |
t.test(x, mu = μ₀) |
ttest_1samp(x, μ₀) |
| Confidence interval | CONFIDENCE.T(α, s, n) |
t.test(x)$conf.int |
res.confidence_interval() |
| Normality check | SKEW, Q-Q scatter |
shapiro.test(x), qqnorm |
stats.shapiro, probplot |
| Nonparametric backup | — | wilcox.test(x, mu=μ₀) |
stats.wilcoxon(x-μ₀) |
Common Mistakes¶
- Using
df = ninstead ofn − 1. - Using
T.INVwhenT.INV.2Tis needed. Atdf = 15,T.INV(0.05,15) = −1.753(one tail) butT.INV.2T(0.05,15) = 2.131(two tails). - Running a t-test on data with a clear outlier without investigating it — the test is not robust to outliers.
- Using a z-test with
sin place ofσand calling it a z-test. If you estimated the SD, the reference distribution is t. - Reporting only the p-value. Always add
df, the effect size, and the CI. - Forgetting that
T.TEST'stypeargument (1/2/3) has nothing to do with the number of tails — that is the separatetailsargument.
Exercises: 11-01: Exercises — One-Sample t-Test
⬅️ Previous: 10-02: One-Sample Z-Tests ➡️ Next: 11-02: Two-Sample t-Test