P-Values: What They Really Mean (and the Mistakes Everyone Makes)

A p-value answers one narrow question: if the null hypothesis were true, how often would you see data at least this extreme? It does not tell you whether your hypothesis is true, whether your effect matters, or whether your result will replicate. Misreading it has distorted published research for decades, and the same errors show up in A/B tests, model comparisons, and feature selection pipelines every day.

Quick Takeaways

  • A p-value is P(data this extreme or more | H₀ true). It is not P(H₀ true | data).
  • A small p-value signals incompatibility with the null model. It says nothing about effect size or practical importance.
  • Running many tests without correction inflates false positives. Twenty independent tests at α = 0.05 give a 64% chance of at least one false hit.
  • Report the effect size, the confidence interval, and the exact p-value together.
Concept What it measures Common misreading
p-value Data compatibility with H₀ “Probability H₀ is true”
α (alpha) Pre-set false positive rate “Probability my result is wrong”
Effect size Magnitude of difference Ignored when p < 0.05
Confidence interval Plausible parameter range “95% chance the true value is inside”
Power (1 − β) Chance of detecting a real effect Rarely computed before the study

What a P-Value Actually Is

Every hypothesis test follows the same logic.

  1. State a null hypothesis (H₀), usually “no effect” or “no difference.”
  2. Choose a test statistic (t, z, χ², F) that summarizes the data.
  3. Compute the sampling distribution of that statistic assuming H₀ is true.
  4. Calculate the probability of observing a statistic at least as extreme as yours.

That probability is the p-value. Formally, for a two-sided test:

p = P(|T| ≥ |t_obs| | H₀)

Consider a coin you suspect is biased. You flip it 100 times and get 62 heads. Under H₀ (a fair coin), the chance of 62 or more heads, or 38 or fewer, is about 0.021. The data look unusual if the coin is fair. That is all the p-value tells you.

The Role of Alpha

The significance level α is a threshold you set before collecting data. If p ≤ α, you reject H₀. The conventional α = 0.05 is a convention, not a law of nature. Fields like particle physics use 5σ (p ≈ 3×10⁻⁷). Clinical trials with safety implications often use stricter thresholds.

Why 0.05 Is Not a Cliff

A result with p = 0.049 is not meaningfully different from one with p = 0.051. Treating the threshold as a bright line produces the “significant vs. not significant” fallacy: two studies with nearly identical evidence get opposite conclusions.

Seeing a P-Value in Python

Run this simulation to build intuition. It draws two samples, runs a Welch’s t-test, and computes the effect size alongside the p-value.

import numpy as np
from scipy import stats

rng = np.random.default_rng(seed=42)  # reproducible randomness

# Two groups: true mean difference of 0.4 standard deviations
control = rng.normal(loc=100, scale=15, size=80)
treatment = rng.normal(loc=106, scale=15, size=80)

# Welch's t-test: does not assume equal variances
t_stat, p_value = stats.ttest_ind(treatment, control, equal_var=False)

# Cohen's d: standardized effect size
pooled_sd = np.sqrt((control.var(ddof=1) + treatment.var(ddof=1)) / 2)
cohens_d = (treatment.mean() - control.mean()) / pooled_sd

# 95% confidence interval for the mean difference
diff = treatment.mean() - control.mean()
se = np.sqrt(control.var(ddof=1)/len(control) + treatment.var(ddof=1)/len(treatment))
ci_low, ci_high = diff - 1.96 * se, diff + 1.96 * se

print(f"t = {t_stat:.3f}, p = {p_value:.4f}")
print(f"Cohen's d = {cohens_d:.3f}")
print(f"Mean difference = {diff:.2f}, 95% CI [{ci_low:.2f}, {ci_high:.2f}]")

Always report all three outputs. The p-value alone hides whether the effect is tiny or large.

Simulating the Null Distribution (Permutation Test)

A permutation test makes the definition concrete. You shuffle group labels thousands of times to build the distribution of differences under H₀, then see where your observed difference falls.

import numpy as np

rng = np.random.default_rng(seed=7)

observed_diff = treatment.mean() - control.mean()
pooled = np.concatenate([control, treatment])
n_treat = len(treatment)

n_permutations = 10_000
null_diffs = np.empty(n_permutations)

for i in range(n_permutations):
    shuffled = rng.permutation(pooled)  # break any real group structure
    null_diffs[i] = shuffled[:n_treat].mean() - shuffled[n_treat:].mean()

# Two-sided p-value: share of null differences at least as extreme as observed
p_perm = np.mean(np.abs(null_diffs) >= np.abs(observed_diff))
print(f"Permutation p-value: {p_perm:.4f}")

The permutation p-value is literally the fraction of shuffled datasets that look as extreme as yours. No formula, no distributional assumptions.

The 7 Most Common P-Value Mistakes

1. Treating p as the Probability That H₀ Is True

A p-value of 0.03 does not mean there is a 3% chance the null is true. It means that if the null is true, data this extreme occur 3% of the time. Getting P(H₀ | data) requires Bayes’ theorem and a prior.

2. Equating Significance with Importance

With enough data, trivial effects become significant. A study of 2 million users can return p < 0.001 for a 0.02% conversion lift that is worthless in business terms. Always pair the p-value with Cohen’s d, odds ratio, or the raw difference.

3. Reading Non-Significance as “No Effect”

p = 0.20 means the data are not incompatible with H₀. It does not prove H₀. Underpowered tests fail to detect real effects constantly. Check the confidence interval: a wide interval spanning both zero and large effects means the test was inconclusive.

4. P-Hacking and Multiple Comparisons

Test enough features, subgroups, or metrics and something crosses 0.05 by chance. The family-wise error rate grows quickly:

FWER = 1 − (1 − α)^m

where m is the number of tests. Use correction methods.

from statsmodels.stats.multitest import multipletests

p_values = [0.001, 0.012, 0.034, 0.048, 0.20, 0.51]

# Holm is uniformly more powerful than Bonferroni; fdr_bh controls false discovery rate
reject, p_adj, _, _ = multipletests(p_values, alpha=0.05, method="holm")

for raw, adj, r in zip(p_values, p_adj, reject):
    print(f"raw p = {raw:.3f} | adjusted p = {adj:.3f} | reject H0: {r}")

5. Optional Stopping in A/B Tests

Checking results daily and stopping when p < 0.05 inflates the false positive rate well above 5%. Fix the sample size in advance using a power calculation, or use sequential testing methods designed for continuous monitoring.

6. Ignoring Test Assumptions

A t-test assumes roughly normal residuals and independent observations. Violations, especially non-independence from repeated measures or clustered data, make p-values unreliable. Use Mann-Whitney U, bootstrap methods, or mixed-effects models when assumptions fail.

7. Misinterpreting the Confidence Interval Alongside It

A 95% CI does not mean “95% probability the true value lies inside.” It means that if you repeated the procedure many times, 95% of the intervals would contain the true parameter. Any single interval either contains it or does not.

Choosing the Right Approach: A Comparison

Method Answers Strength Weakness
Frequentist p-value Is the data compatible with H₀? Simple, standardized Misread constantly
Confidence interval What effect sizes are plausible? Shows magnitude and precision Counterintuitive interpretation
Bayes factor How much does data favor H₁ over H₀? Quantifies evidence for both sides Sensitive to prior choice
Bayesian posterior What is P(effect | data)? Direct probability statements Requires a prior; heavier compute
Permutation test Is the observed gap unusual under shuffling? Few assumptions Slower on large data
Correction method Controls Conservativeness Best for
Bonferroni FWER Very high Few tests, strict control
Holm FWER High (less than Bonferroni) General-purpose FWER control
Benjamini-Hochberg FDR Moderate Many tests, e.g., genomics

Real-World Use Cases

Case 1: A/B Testing a Checkout Redesign

An e-commerce team tests a new checkout flow. Control converts at 4.10%, treatment at 4.35%, with 120,000 users per arm. A two-proportion z-test gives a small p-value.

from statsmodels.stats.proportion import proportions_ztest, proportion_confint

conversions = np.array([5220, 4920])   # treatment, control
visitors = np.array([120000, 120000])

z_stat, p_val = proportions_ztest(conversions, visitors)
rate_t, rate_c = conversions / visitors

print(f"Treatment: {rate_t:.3%} | Control: {rate_c:.3%}")
print(f"z = {z_stat:.2f}, p = {p_val:.5f}")
print(f"Absolute lift: {rate_t - rate_c:.3%}")

The decision should rest on the absolute lift and its interval, weighed against implementation cost, not on the p-value alone.

Case 2: Feature Selection in Machine Learning

Univariate feature screening with SelectKBest and an F-test ranks features by p-value. With 5,000 candidate features, roughly 250 pass at α = 0.05 even if all are noise. Apply Benjamini-Hochberg correction, and validate on a held-out set.

Case 3: Detecting Drift in Sensor Streams

A Kolmogorov-Smirnov test comparing a recent window of sensor readings to a baseline flags distribution drift. In streaming settings you run the test repeatedly, so adjust thresholds for multiplicity or use control-chart methods instead of raw p-values.

How to Report Results Correctly

Use this checklist for every test you publish or present.

  1. State the hypotheses and the test used with its assumptions checked.
  2. Report the exact p-value (p = 0.032), not just “p < 0.05.”
  3. Include the effect size and confidence interval.
  4. Document how many tests you ran and which multiplicity correction applied.
  5. Report the sample size and power analysis done before collection.
  6. Share the code and data when possible.

A well-formed sentence looks like this: “Treatment increased conversion by 0.25 percentage points (95% CI [0.12, 0.38], Welch z-test p = 0.0004, Cohen’s h = 0.012).”

Frequently Asked Questions

What does a p-value of 0.05 mean?

It means that if the null hypothesis were true, you would see results at least this extreme in about 5% of repeated experiments. It is not a 5% probability that your result is wrong or that the null is true.

Is a lower p-value always better?

No. A lower p-value indicates stronger incompatibility with H₀, but it reflects both effect size and sample size. A tiny, irrelevant effect can produce a tiny p-value in a large dataset. Judge results by effect size and confidence intervals as well.

What is the difference between a p-value and a significance level?

The significance level (α) is a threshold chosen before the study. The p-value is computed from the data after the study. You reject H₀ when the p-value is at or below α.

Can a p-value be exactly zero?

No. Software that prints p = 0.000 is rounding a very small value. Report it as p < 0.001 instead.

Hot this week

Data Privacy 101: What Every Data Scientist Must Know

Master data privacy for data science: GDPR, anonymization, differential privacy, and Python code you can ship today. Read the full guide.

A/B Testing Fundamentals: Design, Run, and Analyze Your First Experiment

Learn A/B testing step by step: hypothesis, sample size, randomization, and Python analysis with code. Run your first valid experiment today.

Hypothesis Testing Explained: From Null Hypothesis to Decision

Master hypothesis testing in Python: null vs. alternative hypotheses, p-values, t-tests, and decision rules with copy-paste SciPy code. Start testing now.

Data Storytelling: Turn Analysis Into Decisions

Learn data storytelling with a repeatable framework, Python code, and chart rules that turn analysis into decisions. Get the full guide and templates.

How to Choose the Right Chart Type (With a Decision Guide)

Learn how to choose the right chart type for any dataset. Use our decision guide, Python code, and comparison tables to pick the best chart fast.

Topics

Data Privacy 101: What Every Data Scientist Must Know

Master data privacy for data science: GDPR, anonymization, differential privacy, and Python code you can ship today. Read the full guide.

A/B Testing Fundamentals: Design, Run, and Analyze Your First Experiment

Learn A/B testing step by step: hypothesis, sample size, randomization, and Python analysis with code. Run your first valid experiment today.

Hypothesis Testing Explained: From Null Hypothesis to Decision

Master hypothesis testing in Python: null vs. alternative hypotheses, p-values, t-tests, and decision rules with copy-paste SciPy code. Start testing now.

Data Storytelling: Turn Analysis Into Decisions

Learn data storytelling with a repeatable framework, Python code, and chart rules that turn analysis into decisions. Get the full guide and templates.

How to Choose the Right Chart Type (With a Decision Guide)

Learn how to choose the right chart type for any dataset. Use our decision guide, Python code, and comparison tables to pick the best chart fast.

Data Visualization Principles: How to Make Charts People Understand

Learn data visualization principles that make charts clear. Master chart selection, encoding, color, and Python code. Build better charts today.

The Math You Actually Need for Data Science

Learn the math for data science: linear algebra, calculus, probability, and statistics, with Python code and a clear study order. Start now.

Statistics for Data Science: What You Need and What You Can Skip

Learn which statistics matter for data science and which to skip. Get Python code, comparison tables, and a roadmap. Start learning smarter today.

Related Articles

Popular Categories