Skip to content

How to compare two proportions with a z-test in Statsmodels (with Code)

The Short Answer (TL;DR)

Pass two success counts and their corresponding trial counts to proportions_ztest with value=0 to test equal proportions in independent groups. Use an explicit two-sided alternative, inspect the normal-approximation conditions and report the observed difference alongside the p-value.

from statsmodels.stats.proportion import proportions_ztest
counts, trials = [42,30], [100,120]
print(proportions_ztest(counts,trials,value=0,alternative="two-sided",prop_var=False))

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 pandas==3.0.6 statsmodels==0.15.0
from pathlib import Path
import numpy as np
from statsmodels.stats.proportion import proportions_ztest

def pooled_proportion_test(counts, trials):
    counts, trials = np.asarray(counts), np.asarray(trials)
    if counts.shape != (2,) or trials.shape != (2,):
        raise ValueError("Supply success and trial counts for exactly two independent groups.")
    if not all(np.issubdtype(a.dtype,np.integer) for a in (counts,trials)):
        raise ValueError("Use integer counts, not percentages or weighted totals.")
    if np.any(trials <= 0) or np.any(counts < 0) or np.any(counts > trials):
        raise ValueError("Require positive trial counts and successes between zero and trials.")
    pooled = counts.sum()/trials.sum()
    expected = np.column_stack((trials*pooled,trials*(1-pooled)))
    # An illustrative screening rule, not a proof of approximation accuracy.
    if expected.min() < 5:
        raise ValueError("Sparse expected counts: use an inference method suited to the design.")
    return proportions_ztest(counts,trials,value=0,alternative="two-sided",prop_var=False)

# Synthetic independent binary outcomes, ordered Group A then Group B.
counts = np.array([42,30])
trials = np.array([100,120])
z_stat, p_value = pooled_proportion_test(counts,trials)
rates = counts/trials
difference = rates[0]-rates[1]
report = (
    f"rates A={rates[0]:.6f}, B={rates[1]:.6f}; A-minus-B={difference:.6f}\n"
    f"difference={100*difference:.3f} percentage points\n"
    f"z={z_stat:.9f}; two-sided approximate p={p_value:.9f}\n"
)
Path("two-proportion-z-test.txt").write_text(report,encoding="utf-8")
print(report)

The reported rates are 0.420000 and 0.250000, the difference is 17.000 percentage points, z=2.675800798 and the approximate two-sided p-value is 0.007455094.

Which direction and effect size are reported?

Group A has 42 successes among 100 trials and Group B has 30 among 120. Their observed rates are 0.42 and 0.25, so A minus B is 0.17, or 17 percentage points. This is an absolute rate difference, not a 17% relative increase. Reversing the group order changes the sign of the z statistic but preserves this two-sided p-value.

Which variance is used under the equal-proportions null?

For this two-group value=0 call, prop_var=False uses the pooled sample proportion: 72 successes among 220 trials. It does not mean the variance is zero. The full example independently reconstructs the pooled standard error, uses the standard-library complementary error function for the two-sided probability, and confirms the equivalence with an uncorrected two-by-two Pearson chi-square test.

When is the approximation suitable?

Each independent unit contributes one binary outcome to one group. Inspect expected success and failure counts under the pooled null. The example stops if any expected count is below five; this is an illustrative screening rule, not a sufficient guarantee of normal-approximation accuracy. Sparse counts, clustering, repeated observations or sampling weights need inference that respects the design. A fixed-margin Fisher exact test is one relevant option for suitable independent two-by-two tables.

How should the p-value be interpreted?

The approximate two-sided p-value is about 0.007455 under equality of the population proportions and the model assumptions. It is not the probability that the null is true, and it does not quantify the chance that a finding will replicate. Choose the alternative and significance level before examining the result. A statistically detectable difference can still be practically unimportant; report the effect and its uncertainty in the actual study. Several comparisons also require a prespecified multiplicity strategy.

Mathematical Context

For group i, p_hat_i=k_i/n_i. Under H0:p1=p2, pool the counts as p_pool=(k1+k2)/(n1+n2). The standard error is sqrt(p_pool(1-p_pool)(1/n1+1/n2)), and z=(p_hat_1-p_hat_2)/SE. The two-sided normal-reference probability is erfc(|z|/sqrt(2)). Here the observed difference is 0.17 and z=2.675800798, giving p=0.007455094. The rates and z statistic are dimensionless; multiplying the rate difference by 100 expresses it in percentage points. The formula and normal calibration are tied to the zero-difference null used here.

\[ z=\frac{\widehat p_1-\widehat p_2}{\sqrt{\widehat p(1-\widehat p)(1/n_1+1/n_2)}} \]

Reproducibility

Executed with Python 3.12.14; numpy 2.5.4, scipy 1.18.1, pandas 3.0.6, statsmodels 0.15.0. Automated checks confirm the stated output properties.

Primary documentation

Related guides