T-Tests for Comparing Means
Run one-sample, independent two-sample, and paired t-tests with scipy.stats and interpret confidence intervals.
T-Tests for Comparing Means is a free Pandas & NumPy Academy lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Pandas & NumPy Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
What Is a T-Test?
A t-test is a hypothesis test that determines whether a difference in means between groups is statistically significant or likely due to random chance. It compares the observed difference to the variability in the data, producing a t-statistic and a p-value. If p ≤ 0.05 (the conventional threshold), we reject the null hypothesis that the means are equal. There are three common types: one-sample, independent two-sample, and paired t-test, each for different experimental designs.
One-Sample T-Test
The one-sample t-test tests whether the mean of a sample differs significantly from a known or hypothesised population mean. For example: 'Is our average delivery time significantly different from the industry standard of 3 days?' Use scipy.stats.ttest_1samp(data, popmean). The null hypothesis is H₀: mean = popmean. A small p-value leads you to reject H₀ and conclude the sample mean differs from the target.
import numpy as np
from scipy import stats
np.random.seed(42)
# Delivery times in days (true mean is ~3.5, not 3)
delivery_times = np.random.normal(loc=3.5, scale=0.8, size=50)
# Test: is our mean significantly different from 3 days?
stat, p = stats.ttest_1samp(delivery_times, popmean=3.0)
print(f'Sample mean: {delivery_times.mean():.3f}')
print(f't-statistic: {stat:.3f}')
print(f'p-value: {p:.4f}')
print('Differs from 3?', 'Yes' if p < 0.05 else 'No')Independent Two-Sample T-Test
The independent two-sample t-test compares the means of two independent groups. For example: 'Does the A/B test variant produce higher average revenue than the control?' Use scipy.stats.ttest_ind(group_a, group_b). The equal_var parameter matters: set equal_var=False (Welch's t-test) when the two groups may have different variances, which is the safe default for most real data.
import numpy as np
from scipy import stats
np.random.seed(0)
# A/B test revenue per session
control = np.random.normal(loc=25.0, scale=8.0, size=80)
variant = np.random.normal(loc=28.0, scale=9.0, size=80)
# Welch's t-test (does not assume equal variances)
stat, p = stats.ttest_ind(control, variant, equal_var=False)
print(f'Control mean: {control.mean():.2f}')
print(f'Variant mean: {variant.mean():.2f}')
print(f't-statistic: {stat:.3f}')
print(f'p-value: {p:.4f}')
print('Significant difference?', 'Yes' if p < 0.05 else 'No')One-Tailed vs Two-Tailed Tests
By default, t-tests are two-tailed: they test whether means differ in either direction (greater OR less than). If you have a directional hypothesis ('the variant increases revenue'), use a one-tailed test with the alternative parameter: 'greater' or 'less'. A one-tailed test has more power for the specific direction but provides no evidence about the other direction. Decide on the direction before looking at the data to avoid HARKing (Hypothesising After Results are Known).
import numpy as np
from scipy import stats
np.random.seed(1)
control = np.random.normal(25, 8, 100)
variant = np.random.normal(27, 8, 100)
# Two-tailed (default): does mean differ at all?
stat, p_two = stats.ttest_ind(control, variant, equal_var=False,
alternative='two-sided')
# One-tailed: is variant > control?
stat, p_one = stats.ttest_ind(control, variant, equal_var=False,
alternative='less')
print(f'Two-tailed p: {p_two:.4f}')
print(f'One-tailed p (variant greater): {p_one:.4f}')Paired T-Test
The paired t-test is used when each observation in group A has a natural partner in group B — for example, measurements before and after a treatment on the same subjects, or the same stores in two different months. Pairing controls for between-subject variability, making the test more powerful than an independent t-test for the same data. Use scipy.stats.ttest_rel(before, after). The order of elements must be matched: before[i] and after[i] must come from the same subject.
import numpy as np
from scipy import stats
np.random.seed(5)
# Blood pressure before and after medication (paired)
before = np.random.normal(130, 10, 30)
after = before - np.random.normal(5, 3, 30) # Treatment reduces BP by ~5
stat, p = stats.ttest_rel(before, after)
print(f'Mean before: {before.mean():.2f}')
print(f'Mean after: {after.mean():.2f}')
print(f't-statistic: {stat:.3f}')
print(f'p-value: {p:.4f}')
print('Treatment effective?', 'Yes' if p < 0.05 else 'No')Interpreting the T-Statistic
The t-statistic measures how many standard errors the observed difference is from zero. A t-statistic of 2 means the observed difference is 2 standard errors above what would be expected under the null. For a two-tailed test at α=0.05 with a large sample, the critical value is approximately ±1.96 — so |t| > 1.96 gives p < 0.05. The p-value converts the t-statistic to a probability, making it easier to interpret without consulting t-distribution tables.
import numpy as np
from scipy import stats
# Demonstrate relationship between t-statistic and p-value
for t_val in [1.0, 1.96, 2.5, 3.0, 4.0]:
# degrees of freedom = large sample -> t ~ normal
p = 2 * stats.t.sf(abs(t_val), df=100) # two-tailed
print(f't = {t_val:4.2f} -> p = {p:.4f} '
f'({'significant' if p < 0.05 else 'not significant'})')Confidence Intervals for the Mean Difference
A p-value alone does not tell you the magnitude of the effect. Always compute a confidence interval for the mean difference. A 95% CI that includes zero is consistent with p > 0.05 (not significant); one that excludes zero means the difference is significant. The CI also tells you whether the effect size is practically meaningful — a statistically significant difference of $0.01 in revenue may be irrelevant, while a $50 difference with p=0.06 may still matter for the business.
import numpy as np
from scipy import stats
np.random.seed(0)
control = np.random.normal(25, 8, 80)
variant = np.random.normal(28, 8, 80)
diff = variant.mean() - control.mean()
se = np.sqrt(control.var()/len(control) + variant.var()/len(variant))
df = len(control) + len(variant) - 2
t_crit = stats.t.ppf(0.975, df=df)
ci_low = diff - t_crit * se
ci_high = diff + t_crit * se
print(f'Mean difference: {diff:.2f}')
print(f'95% CI: [{ci_low:.2f}, {ci_high:.2f}]')
print('CI excludes zero:', ci_low > 0 or ci_high < 0)Effect Size: Cohen's d
Statistical significance depends on sample size — with large enough samples, even trivial differences become significant. Cohen's d measures practical significance (effect size): the mean difference divided by the pooled standard deviation. Guidelines: d < 0.2 is negligible, 0.2–0.5 is small, 0.5–0.8 is medium, > 0.8 is large. Always report effect size alongside p-values — a significant p-value with d = 0.05 means the difference is real but probably not meaningful in practice.
import numpy as np
from scipy import stats
np.random.seed(0)
g1 = np.random.normal(25, 8, 200)
g2 = np.random.normal(28, 8, 200)
stat, p = stats.ttest_ind(g1, g2)
# Cohen's d
pooled_std = np.sqrt((g1.var() + g2.var()) / 2)
cohens_d = (g2.mean() - g1.mean()) / pooled_std
print(f'p-value: {p:.4f}')
print(f'Cohen\'s d: {cohens_d:.3f}')
if cohens_d < 0.2: print('Effect: negligible')
elif cohens_d < 0.5: print('Effect: small')
elif cohens_d < 0.8: print('Effect: medium')
else: print('Effect: large')Assumptions of the T-Test
The independent t-test assumes: (1) Independence — observations within each group are independent of each other; (2) Normality — data in each group is approximately normal (robust for n > 30 via CLT); (3) for Student's t-test (equal_var=True), equal variances. Welch's t-test (equal_var=False) relaxes assumption 3 and is preferred. When normality is seriously violated with small samples (< 30), use the Mann-Whitney U test instead.
import numpy as np
from scipy import stats
np.random.seed(0)
g1 = np.random.normal(10, 2, 30)
g2 = np.random.normal(11, 5, 30) # Very different variance!
# Levene test for equal variances
stat_l, p_l = stats.levene(g1, g2)
print(f'Levene test p: {p_l:.4f}')
if p_l < 0.05:
print('Unequal variances -> use Welch\'s (equal_var=False)')
stat, p = stats.ttest_ind(g1, g2, equal_var=False)
else:
print('Equal variances OK -> Student\'s t-test')
stat, p = stats.ttest_ind(g1, g2, equal_var=True)
print(f'Result: t={stat:.3f}, p={p:.4f}')T-Tests on Pandas Series
In practice, the data you want to test lives in a Pandas DataFrame column. You can pass a Pandas Series directly to scipy.stats functions — they work seamlessly with Series as well as NumPy arrays. A common workflow: filter the DataFrame to get each group's Series, run the t-test, and store results in a summary DataFrame. This pattern scales to testing many pairs of columns or groups in a loop.
import pandas as pd
import numpy as np
from scipy import stats
np.random.seed(0)
df = pd.DataFrame({
'group': ['A']*60 + ['B']*60,
'revenue': np.concatenate([
np.random.normal(100, 20, 60),
np.random.normal(110, 22, 60)
])
})
grp_a = df.loc[df['group'] == 'A', 'revenue']
grp_b = df.loc[df['group'] == 'B', 'revenue']
stat, p = stats.ttest_ind(grp_a, grp_b, equal_var=False)
print(f'A mean: {grp_a.mean():.2f}, B mean: {grp_b.mean():.2f}')
print(f'p-value: {p:.4f}')Multiple Comparisons Problem
Running many t-tests simultaneously inflates the chance of a false positive. If you run 20 t-tests at α=0.05, you expect 1 false positive by chance. This is the multiple comparisons problem. Apply the Bonferroni correction: divide α by the number of tests (p_threshold = 0.05 / n_tests). For more power with less conservatism, use the Benjamini-Hochberg False Discovery Rate (FDR) correction available in statsmodels. Always correct for multiple comparisons in A/B testing with many metrics.
import numpy as np
from scipy import stats
np.random.seed(0)
n_tests = 10
results = []
for i in range(n_tests):
g1 = np.random.normal(0, 1, 50)
g2 = np.random.normal(0.1, 1, 50) # Tiny true effect
_, p = stats.ttest_ind(g1, g2)
results.append(p)
print('p-values:', [round(p, 3) for p in results])
bonferroni_thresh = 0.05 / n_tests
print(f'Bonferroni threshold: {bonferroni_thresh}')
print('Significant after Bonferroni:', sum(p < bonferroni_thresh for p in results))Quick Check
Test your understanding of Data Analysis concepts from this lesson.
Lesson Recap
In this lesson you learned: ttest_1samp tests a sample mean against a reference value, ttest_ind (Welch's with equal_var=False) compares two independent groups, and ttest_rel handles paired measurements. Always report Cohen's d alongside the p-value to distinguish statistical from practical significance. Next up we test relationships between categorical variables with the chi-squared test.
Frequently asked questions
Is the “T-Tests for Comparing Means” lesson free?
Yes — the full text of “T-Tests for Comparing Means” is free to read here on the web, and the Pandas & NumPy Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Pandas & NumPy Academy course, upgrade to CoddyKit PRO.
What will I learn in “T-Tests for Comparing Means”?
Run one-sample, independent two-sample, and paired t-tests with scipy.stats and interpret confidence intervals. You practise Pandas & NumPy Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start Pandas & NumPy Academy?
No prior experience is required. Pandas & NumPy Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “T-Tests for Comparing Means” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this Pandas & NumPy Academy lesson?
Yes. Every Pandas & NumPy Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Descriptive Stats and Normality Testing
- T-Tests for Comparing Means
- Chi-Squared Test for Independence
- ANOVA and Post-Hoc Tests