How to run a paired-sample t-test in SciPy (with Code)
The Short Answer (TL;DR)
Use scipy.stats.ttest_rel(after, before) when each after measurement is matched to the same subject in before. Report the mean change and its confidence interval, preserving the order of the pairs and the direction of subtraction.
from scipy import stats
before, after = [10, 12, 9, 14, 11, 15, 13, 8], [11, 14, 10, 13, 13, 17, 14, 10]
result = stats.ttest_rel(after, before, alternative="two-sided", nan_policy="raise"); print(result)
Detailed Implementation
This example uses synthetic data and does not need a network connection. Install the recorded package versions in a separate Python environment before running it.
python -m pip install numpy==2.5.4 scipy==1.18.1
from pathlib import Path
import numpy as np
from scipy import stats
# Each row is one complete synthetic subject pair, in matching order.
subject_ids = np.array(["S01", "S02", "S03", "S04", "S05", "S06", "S07", "S08"])
before = np.array([10, 12, 9, 14, 11, 15, 13, 8], dtype=float)
after = np.array([11, 14, 10, 13, 13, 17, 14, 10], dtype=float)
if before.shape != after.shape or before.size != subject_ids.size:
raise ValueError("Expected matching subject pairs.")
if np.unique(subject_ids).size != subject_ids.size:
raise ValueError("Subject identifiers must be unique.")
if not (np.isfinite(before).all() and np.isfinite(after).all()):
raise ValueError("Resolve missing or non-finite pairs explicitly.")
differences = after - before
if differences.size < 2 or differences.std(ddof=1) == 0:
raise ValueError("At least two pairs and nonzero change variance are needed.")
result = stats.ttest_rel(after, before, alternative="two-sided", nan_policy="raise")
interval = result.confidence_interval(confidence_level=0.95)
report = (
f"Complete pairs: {differences.size}\n"
f"Mean change (after - before): {differences.mean():.6f}\n"
f"Paired t: {result.statistic:.6f}\n"
f"Degrees of freedom: {result.df:.0f}\n"
f"Two-sided p-value: {result.pvalue:.8g}\n"
f"95% CI for mean change: [{interval.low:.6f}, {interval.high:.6f}]\n"
)
print(report)
Path("paired-t-test-output.txt").write_text(report, encoding="utf-8")
The report uses eight complete pairs and seven degrees of freedom. The mean change after minus before is 1.25 synthetic units, and its two-sided 95% confidence interval is entirely above zero. Reversing the sample order reverses the statistic and interval signs while preserving the two-sided p-value.
How are the pairs aligned?
The arrays must refer to the same subjects in the same order. Equal array lengths alone do not establish matching. If two tables arrive separately, join by a verified unique subject identifier before extracting the paired columns; reject duplicate identifiers and inspect missing matches. The example starts with eight already aligned complete pairs.
Which observations must be independent?
The before and after values within a subject are allowed to be related; that is the reason for pairing. The differences from separate subjects must be suitably independent. For exact small-sample t inference, the population of differences should be normal with a finite, nonzero variance. Inspect differences for outliers and skew; normality of each original column is not the relevant requirement.
What does the result establish?
The test targets a zero population mean change, not equality of every subject's measurements. The confidence interval describes uncertainty in the mean change. A small p-value is not the probability that the null hypothesis is true and does not establish causation in a before-and-after observational design. Choose the direction and any decision threshold before looking at the result.
Mathematical Context
Define each difference as after minus before, and let dbar and s_d be the sample mean and sample standard deviation of those differences. The paired test is a one-sample t-test of the differences against zero: its standard error is s_d divided by the square root of the number of complete pairs, and its degrees of freedom are n minus one. The two-sided p-value is twice the upper-tail Student t probability beyond the absolute statistic. A two-sided 95% interval for the population mean change is dbar plus or minus the 97.5th percentile of that t distribution times the standard error. Correlation within a pair is retained in the variance of its difference; treating both columns as unrelated groups discards that information.
\[ t=\frac{\bar d}{s_d/\sqrt{n}},\qquad \nu=n-1 \]
Reproducibility
Executed with Python 3.12.14; numpy 2.5.4, scipy 1.18.1. Automated checks confirm the stated output properties.