Skill Tracer
Skills › Scientific Agent Skills › statsmodels › stats_diagnostics.md

stats_diagnostics.md

904 lines · skills/statsmodels/references/stats_diagnostics.md at 5f2bc03

Open on GitHub
1# Statistical Tests and Diagnostics Reference
2
3This document provides comprehensive guidance on statistical tests, diagnostics, and tools available in statsmodels.
4

Statistical Tests and Diagnostics Reference

This document provides comprehensive guidance on statistical tests, diagnostics, and tools available in statsmodels.

Raw lines 1 to 4
5## Overview
6
7Statsmodels provides extensive statistical testing capabilities:
8- Residual diagnostics and specification tests
9- Hypothesis testing (parametric and non-parametric)
10- Goodness-of-fit tests
11- Multiple comparisons and post-hoc tests
12- Power and sample size calculations
13- Robust covariance matrices
14- Influence and outlier detection
16Fragments require the appropriate result type and aligned inputs; OLS diagnostics do not apply automatically to GLMs or dependent observations. A non-significant assumption test does not validate its null. Prespecify the inferential target and covariance from the study design, rather than selecting inference after a sequence of pretests. VIF/influence cutoffs are screening heuristics, and condition numbers depend on units.

Overview

Statsmodels provides extensive statistical testing capabilities:

  • Residual diagnostics and specification tests
  • Hypothesis testing (parametric and non-parametric)
  • Goodness-of-fit tests
  • Multiple comparisons and post-hoc tests
  • Power and sample size calculations
  • Robust covariance matrices
  • Influence and outlier detection

Fragments require the appropriate result type and aligned inputs; OLS diagnostics do not apply automatically to GLMs or dependent observations. A non-significant assumption test does not validate its null. Prespecify the inferential target and covariance from the study design, rather than selecting inference after a sequence of pretests. VIF/influence cutoffs are screening heuristics, and condition numbers depend on units.

Raw lines 5 to 17
18## Residual Diagnostics

Residual Diagnostics

Raw lines 18 to 19
20### Autocorrelation Tests
22**Ljung-Box Test**: Tests for autocorrelation in residuals
24```python
25from statsmodels.stats.diagnostic import acorr_ljungbox
27# Test residuals for autocorrelation
28lb_test = acorr_ljungbox(residuals, lags=10, return_df=True)
29print(lb_test)
31# H0: No autocorrelation up to lag k
32# If p-value < 0.05, reject H0 (autocorrelation present)
33```
35**Durbin-Watson Test**: Tests for first-order autocorrelation
37```python
38from statsmodels.stats.stattools import durbin_watson
40dw_stat = durbin_watson(residuals)
41print(f"Durbin-Watson: {dw_stat:.4f}")
43# DW ≈ 2: no autocorrelation
44# DW < 2: positive autocorrelation
45# DW > 2: negative autocorrelation
46# Exact critical values depend on n and k
47```
49**Breusch-Godfrey Test**: More general test for autocorrelation
51```python
52from statsmodels.stats.diagnostic import acorr_breusch_godfrey
54bg_test = acorr_breusch_godfrey(results, nlags=5, result_object=True)
55lm_stat, lm_pval = bg_test.lm, bg_test.lmpval
57print(f"LM statistic: {lm_stat:.4f}, p-value: {lm_pval:.4f}")
58# H0: No autocorrelation up to lag k
59```

Autocorrelation Tests

Ljung-Box Test: Tests for autocorrelation in residuals

from statsmodels.stats.diagnostic import acorr_ljungbox

# Test residuals for autocorrelation
lb_test = acorr_ljungbox(residuals, lags=10, return_df=True)
print(lb_test)

# H0: No autocorrelation up to lag k
# If p-value < 0.05, reject H0 (autocorrelation present)

Durbin-Watson Test: Tests for first-order autocorrelation

from statsmodels.stats.stattools import durbin_watson

dw_stat = durbin_watson(residuals)
print(f"Durbin-Watson: {dw_stat:.4f}")

# DW ≈ 2: no autocorrelation
# DW < 2: positive autocorrelation
# DW > 2: negative autocorrelation
# Exact critical values depend on n and k

Breusch-Godfrey Test: More general test for autocorrelation

from statsmodels.stats.diagnostic import acorr_breusch_godfrey

bg_test = acorr_breusch_godfrey(results, nlags=5, result_object=True)
lm_stat, lm_pval = bg_test.lm, bg_test.lmpval

print(f"LM statistic: {lm_stat:.4f}, p-value: {lm_pval:.4f}")
# H0: No autocorrelation up to lag k
Raw lines 20 to 60
61### Heteroskedasticity Tests
63**Breusch-Pagan Test**: Tests for heteroskedasticity
65```python
66from statsmodels.stats.diagnostic import het_breuschpagan
68bp_test = het_breuschpagan(residuals, exog)
69lm_stat, lm_pval, f_stat, f_pval = bp_test
71print(f"Breusch-Pagan test p-value: {lm_pval:.4f}")
72# H0: Homoskedasticity (constant variance)
73# If p-value < 0.05, reject H0 (heteroskedasticity present)
74```
76**White Test**: More general test for heteroskedasticity
78```python
79from statsmodels.stats.diagnostic import het_white
81white_test = het_white(residuals, exog)
82lm_stat, lm_pval, f_stat, f_pval = white_test
84print(f"White test p-value: {lm_pval:.4f}")
85# H0: Homoskedasticity
86```
88**ARCH Test**: Tests for autoregressive conditional heteroskedasticity
90```python
91from statsmodels.stats.diagnostic import het_arch
93arch_test = het_arch(residuals, nlags=5, result_object=True)
94lm_stat, lm_pval = arch_test.lm, arch_test.lmpval
96print(f"ARCH test p-value: {lm_pval:.4f}")
97# H0: No ARCH effects
98# If significant, consider GARCH model
99```

Heteroskedasticity Tests

Breusch-Pagan Test: Tests for heteroskedasticity

from statsmodels.stats.diagnostic import het_breuschpagan

bp_test = het_breuschpagan(residuals, exog)
lm_stat, lm_pval, f_stat, f_pval = bp_test

print(f"Breusch-Pagan test p-value: {lm_pval:.4f}")
# H0: Homoskedasticity (constant variance)
# If p-value < 0.05, reject H0 (heteroskedasticity present)

White Test: More general test for heteroskedasticity

from statsmodels.stats.diagnostic import het_white

white_test = het_white(residuals, exog)
lm_stat, lm_pval, f_stat, f_pval = white_test

print(f"White test p-value: {lm_pval:.4f}")
# H0: Homoskedasticity

ARCH Test: Tests for autoregressive conditional heteroskedasticity

from statsmodels.stats.diagnostic import het_arch

arch_test = het_arch(residuals, nlags=5, result_object=True)
lm_stat, lm_pval = arch_test.lm, arch_test.lmpval

print(f"ARCH test p-value: {lm_pval:.4f}")
# H0: No ARCH effects
# If significant, consider GARCH model
Raw lines 61 to 100
101### Normality Tests
103**Jarque-Bera Test**: Tests for normality using skewness and kurtosis
105```python
106from statsmodels.stats.stattools import jarque_bera
108jb_stat, jb_pval, skew, kurtosis = jarque_bera(residuals)
110print(f"Jarque-Bera statistic: {jb_stat:.4f}")
111print(f"p-value: {jb_pval:.4f}")
112print(f"Skewness: {skew:.4f}")
113print(f"Kurtosis: {kurtosis:.4f}")
115# H0: Residuals are normally distributed
116# Normal: skewness ≈ 0, kurtosis ≈ 3
117```
119**Omnibus Test**: Another normality test (also based on skewness/kurtosis)
121```python
122from statsmodels.stats.stattools import omni_normtest
124omni_stat, omni_pval = omni_normtest(residuals)
125print(f"Omnibus test p-value: {omni_pval:.4f}")
126# H0: Normality
127```
129**Anderson-Darling Test**: Distribution fit test
131```python
132from statsmodels.stats.diagnostic import normal_ad
134ad_stat, ad_pval = normal_ad(residuals)
135print(f"Anderson-Darling test p-value: {ad_pval:.4f}")
136```
138**Lilliefors Test**: Modified Kolmogorov-Smirnov test
140```python
141from statsmodels.stats.diagnostic import lilliefors
143lf_stat, lf_pval = lilliefors(residuals, dist='norm')
144print(f"Lilliefors test p-value: {lf_pval:.4f}")
145```

Normality Tests

Jarque-Bera Test: Tests for normality using skewness and kurtosis

from statsmodels.stats.stattools import jarque_bera

jb_stat, jb_pval, skew, kurtosis = jarque_bera(residuals)

print(f"Jarque-Bera statistic: {jb_stat:.4f}")
print(f"p-value: {jb_pval:.4f}")
print(f"Skewness: {skew:.4f}")
print(f"Kurtosis: {kurtosis:.4f}")

# H0: Residuals are normally distributed
# Normal: skewness ≈ 0, kurtosis ≈ 3

Omnibus Test: Another normality test (also based on skewness/kurtosis)

from statsmodels.stats.stattools import omni_normtest

omni_stat, omni_pval = omni_normtest(residuals)
print(f"Omnibus test p-value: {omni_pval:.4f}")
# H0: Normality

Anderson-Darling Test: Distribution fit test

from statsmodels.stats.diagnostic import normal_ad

ad_stat, ad_pval = normal_ad(residuals)
print(f"Anderson-Darling test p-value: {ad_pval:.4f}")

Lilliefors Test: Modified Kolmogorov-Smirnov test

from statsmodels.stats.diagnostic import lilliefors

lf_stat, lf_pval = lilliefors(residuals, dist='norm')
print(f"Lilliefors test p-value: {lf_pval:.4f}")
Raw lines 101 to 146
147### Linearity and Specification Tests
149**Ramsey RESET Test**: Tests for functional form misspecification
151```python
152from statsmodels.stats.diagnostic import linear_reset
154reset_test = linear_reset(results, power=2, use_f=True)
155f_stat, f_pval = reset_test.fvalue, reset_test.pvalue
157print(f"RESET test p-value: {f_pval:.4f}")
158# H0: the added nonlinear terms have zero coefficients; not a general validity test
159# If rejected, may need polynomial terms or transformations
160```
162**Harvey-Collier Test**: Tests for linearity
164```python
165from statsmodels.stats.diagnostic import linear_harvey_collier
167hc_stat, hc_pval = linear_harvey_collier(results)
168print(f"Harvey-Collier test p-value: {hc_pval:.4f}")
169# H0: Linear specification is correct
170```

Linearity and Specification Tests

Ramsey RESET Test: Tests for functional form misspecification

from statsmodels.stats.diagnostic import linear_reset

reset_test = linear_reset(results, power=2, use_f=True)
f_stat, f_pval = reset_test.fvalue, reset_test.pvalue

print(f"RESET test p-value: {f_pval:.4f}")
# H0: the added nonlinear terms have zero coefficients; not a general validity test
# If rejected, may need polynomial terms or transformations

Harvey-Collier Test: Tests for linearity

from statsmodels.stats.diagnostic import linear_harvey_collier

hc_stat, hc_pval = linear_harvey_collier(results)
print(f"Harvey-Collier test p-value: {hc_pval:.4f}")
# H0: Linear specification is correct
Raw lines 147 to 171
172## Multicollinearity Detection
174**Variance Inflation Factor (VIF)**:
176```python
177from statsmodels.stats.outliers_influence import variance_inflation_factor
178import pandas as pd
180# Calculate VIF for each variable
181vif_data = pd.DataFrame()
182vif_data["Variable"] = X.columns
183vif_data["VIF"] = [variance_inflation_factor(X.values, i)
184 for i in range(X.shape[1])]
186print(vif_data.sort_values('VIF', ascending=False))
188# Interpretation:
189# VIF = 1: No correlation with other predictors
190# VIF > 5: Moderate multicollinearity
191# VIF > 10: Serious multicollinearity problem
192# VIF > 20: Severe multicollinearity (consider removing variable)
193```
195**Condition Number**: From regression results
197```python
198print(f"Condition number: {results.condition_number:.2f}")
200# Interpretation:
201# < 10: No multicollinearity concern
202# 10-30: Moderate multicollinearity
203# > 30: Strong multicollinearity
204# > 100: Severe multicollinearity
205```

Multicollinearity Detection

Variance Inflation Factor (VIF):

from statsmodels.stats.outliers_influence import variance_inflation_factor
import pandas as pd

# Calculate VIF for each variable
vif_data = pd.DataFrame()
vif_data["Variable"] = X.columns
vif_data["VIF"] = [variance_inflation_factor(X.values, i)
                   for i in range(X.shape[1])]

print(vif_data.sort_values('VIF', ascending=False))

# Interpretation:
# VIF = 1: No correlation with other predictors
# VIF > 5: Moderate multicollinearity
# VIF > 10: Serious multicollinearity problem
# VIF > 20: Severe multicollinearity (consider removing variable)

Condition Number: From regression results

print(f"Condition number: {results.condition_number:.2f}")

# Interpretation:
# < 10: No multicollinearity concern
# 10-30: Moderate multicollinearity
# > 30: Strong multicollinearity
# > 100: Severe multicollinearity
Raw lines 172 to 206
207## Influence and Outlier Detection

Influence and Outlier Detection

Raw lines 207 to 208
209### Leverage
211High leverage points have extreme predictor values.
213```python
214from statsmodels.stats.outliers_influence import OLSInfluence
216influence = results.get_influence()
218# Hat values (leverage)
219leverage = influence.hat_matrix_diag
221# Rule of thumb: leverage > 2*p/n or 3*p/n is high
222# p = number of parameters, n = sample size
223threshold = 2 * len(results.params) / len(y)
224high_leverage = np.where(leverage > threshold)[0]
226print(f"High leverage observations: {high_leverage}")
227```

Leverage

High leverage points have extreme predictor values.

from statsmodels.stats.outliers_influence import OLSInfluence

influence = results.get_influence()

# Hat values (leverage)
leverage = influence.hat_matrix_diag

# Rule of thumb: leverage > 2*p/n or 3*p/n is high
# p = number of parameters, n = sample size
threshold = 2 * len(results.params) / len(y)
high_leverage = np.where(leverage > threshold)[0]

print(f"High leverage observations: {high_leverage}")
Raw lines 209 to 228
229### Cook's Distance
231Measures overall influence of each observation.
233```python
234# Cook's distance
235cooks_d = influence.cooks_distance[0]
237# Rule of thumb: Cook's D > 4/n is influential
238threshold = 4 / len(y)
239influential = np.where(cooks_d > threshold)[0]
241print(f"Influential observations (Cook's D): {influential}")
243# Plot
244import matplotlib.pyplot as plt
245plt.stem(range(len(cooks_d)), cooks_d)
246plt.axhline(y=threshold, color='r', linestyle='--', label=f'Threshold (4/n)')
247plt.xlabel('Observation')
248plt.ylabel("Cook's Distance")
249plt.legend()
250plt.show()
251```

Cook's Distance

Measures overall influence of each observation.

# Cook's distance
cooks_d = influence.cooks_distance[0]

# Rule of thumb: Cook's D > 4/n is influential
threshold = 4 / len(y)
influential = np.where(cooks_d > threshold)[0]

print(f"Influential observations (Cook's D): {influential}")

# Plot
import matplotlib.pyplot as plt
plt.stem(range(len(cooks_d)), cooks_d)
plt.axhline(y=threshold, color='r', linestyle='--', label=f'Threshold (4/n)')
plt.xlabel('Observation')
plt.ylabel("Cook's Distance")
plt.legend()
plt.show()
Raw lines 229 to 252
253### DFFITS
255Measures influence on fitted value.
257```python
258# DFFITS
259dffits = influence.dffits[0]
261# Rule of thumb: |DFFITS| > 2*sqrt(p/n) is influential
262p = len(results.params)
263n = len(y)
264threshold = 2 * np.sqrt(p / n)
266influential_dffits = np.where(np.abs(dffits) > threshold)[0]
267print(f"Influential observations (DFFITS): {influential_dffits}")
268```

DFFITS

Measures influence on fitted value.

# DFFITS
dffits = influence.dffits[0]

# Rule of thumb: |DFFITS| > 2*sqrt(p/n) is influential
p = len(results.params)
n = len(y)
threshold = 2 * np.sqrt(p / n)

influential_dffits = np.where(np.abs(dffits) > threshold)[0]
print(f"Influential observations (DFFITS): {influential_dffits}")
Raw lines 253 to 269
270### DFBETAs
272Measures influence on each coefficient.
274```python
275# DFBETAs (one for each parameter)
276dfbetas = influence.dfbetas
278# Rule of thumb: |DFBETA| > 2/sqrt(n)
279threshold = 2 / np.sqrt(n)
281for i, param_name in enumerate(results.params.index):
282 influential = np.where(np.abs(dfbetas[:, i]) > threshold)[0]
283 if len(influential) > 0:
284 print(f"Influential for {param_name}: {influential}")
285```

DFBETAs

Measures influence on each coefficient.

# DFBETAs (one for each parameter)
dfbetas = influence.dfbetas

# Rule of thumb: |DFBETA| > 2/sqrt(n)
threshold = 2 / np.sqrt(n)

for i, param_name in enumerate(results.params.index):
    influential = np.where(np.abs(dfbetas[:, i]) > threshold)[0]
    if len(influential) > 0:
        print(f"Influential for {param_name}: {influential}")
Raw lines 270 to 286
287### Influence Plot
289```python
290from statsmodels.graphics.regressionplots import influence_plot
292fig, ax = plt.subplots(figsize=(12, 8))
293influence_plot(results, ax=ax, criterion='cooks')
294plt.show()
296# Combines leverage, residuals, and Cook's distance
297# Large bubbles = high Cook's distance
298# Far from x=0 = high leverage
299# Far from y=0 = large residual
300```

Influence Plot

from statsmodels.graphics.regressionplots import influence_plot

fig, ax = plt.subplots(figsize=(12, 8))
influence_plot(results, ax=ax, criterion='cooks')
plt.show()

# Combines leverage, residuals, and Cook's distance
# Large bubbles = high Cook's distance
# Far from x=0 = high leverage
# Far from y=0 = large residual
Raw lines 287 to 301
302### Studentized Residuals
304```python
305# Studentized residuals (outliers)
306student_resid = influence.resid_studentized_internal
308# Externally studentized residuals use the leave-one-out variance estimate
309student_resid_external = influence.resid_studentized_external
311# Outliers: |studentized residual| > 3 (or > 2.5)
312outliers = np.where(np.abs(student_resid_external) > 3)[0]
313print(f"Outliers: {outliers}")
314```

Studentized Residuals

# Studentized residuals (outliers)
student_resid = influence.resid_studentized_internal

# Externally studentized residuals use the leave-one-out variance estimate
student_resid_external = influence.resid_studentized_external

# Outliers: |studentized residual| > 3 (or > 2.5)
outliers = np.where(np.abs(student_resid_external) > 3)[0]
print(f"Outliers: {outliers}")
Raw lines 302 to 315
316## Hypothesis Testing

Hypothesis Testing

Raw lines 316 to 317
318### t-tests
320**One-sample t-test**: Test if mean equals specific value
322```python
323from scipy import stats
325# H0: population mean = mu_0
326t_stat, p_value = stats.ttest_1samp(data, popmean=mu_0)
328print(f"t-statistic: {t_stat:.4f}")
329print(f"p-value: {p_value:.4f}")
330```
332**Two-sample t-test**: Compare means of two groups
334```python
335# H0: mean1 = mean2 (equal variances)
336t_stat, p_value = stats.ttest_ind(group1, group2)
338# Welch's t-test (unequal variances)
339t_stat, p_value = stats.ttest_ind(group1, group2, equal_var=False)
341print(f"t-statistic: {t_stat:.4f}")
342print(f"p-value: {p_value:.4f}")
343```
345**Paired t-test**: Compare paired observations
347```python
348# H0: mean difference = 0
349t_stat, p_value = stats.ttest_rel(before, after)
351print(f"t-statistic: {t_stat:.4f}")
352print(f"p-value: {p_value:.4f}")
353```

t-tests

One-sample t-test: Test if mean equals specific value

from scipy import stats

# H0: population mean = mu_0
t_stat, p_value = stats.ttest_1samp(data, popmean=mu_0)

print(f"t-statistic: {t_stat:.4f}")
print(f"p-value: {p_value:.4f}")

Two-sample t-test: Compare means of two groups

# H0: mean1 = mean2 (equal variances)
t_stat, p_value = stats.ttest_ind(group1, group2)

# Welch's t-test (unequal variances)
t_stat, p_value = stats.ttest_ind(group1, group2, equal_var=False)

print(f"t-statistic: {t_stat:.4f}")
print(f"p-value: {p_value:.4f}")

Paired t-test: Compare paired observations

# H0: mean difference = 0
t_stat, p_value = stats.ttest_rel(before, after)

print(f"t-statistic: {t_stat:.4f}")
print(f"p-value: {p_value:.4f}")
Raw lines 318 to 354
355### Proportion Tests
357**One-proportion test**:
359```python
360from statsmodels.stats.proportion import proportions_ztest
362# H0: proportion = p0
363count = 45 # successes
364nobs = 100 # total observations
365p0 = 0.5 # hypothesized proportion
367z_stat, p_value = proportions_ztest(count, nobs, value=p0, prop_var=p0)
369print(f"z-statistic: {z_stat:.4f}")
370print(f"p-value: {p_value:.4f}")
371```
373**Two-proportion test**:
375```python
376# H0: proportion1 = proportion2
377counts = [45, 60]
378nobs = [100, 120]
380z_stat, p_value = proportions_ztest(counts, nobs)
381print(f"z-statistic: {z_stat:.4f}")
382print(f"p-value: {p_value:.4f}")
383```

Proportion Tests

One-proportion test:

from statsmodels.stats.proportion import proportions_ztest

# H0: proportion = p0
count = 45  # successes
nobs = 100  # total observations
p0 = 0.5    # hypothesized proportion

z_stat, p_value = proportions_ztest(count, nobs, value=p0, prop_var=p0)

print(f"z-statistic: {z_stat:.4f}")
print(f"p-value: {p_value:.4f}")

Two-proportion test:

# H0: proportion1 = proportion2
counts = [45, 60]
nobs = [100, 120]

z_stat, p_value = proportions_ztest(counts, nobs)
print(f"z-statistic: {z_stat:.4f}")
print(f"p-value: {p_value:.4f}")
Raw lines 355 to 384
385### Chi-square Tests
387**Chi-square test of independence**:
389```python
390from scipy.stats import chi2_contingency
392# Contingency table
393contingency_table = pd.crosstab(variable1, variable2)
395chi2, p_value, dof, expected = chi2_contingency(contingency_table)
397print(f"Chi-square statistic: {chi2:.4f}")
398print(f"p-value: {p_value:.4f}")
399print(f"Degrees of freedom: {dof}")
401# H0: Variables are independent
402```
404**Chi-square goodness-of-fit**:
406```python
407from scipy.stats import chisquare
409# Observed frequencies
410observed = [20, 30, 25, 25]
412# Expected frequencies (equal by default)
413expected = [25, 25, 25, 25]
415chi2, p_value = chisquare(observed, expected)
417print(f"Chi-square statistic: {chi2:.4f}")
418print(f"p-value: {p_value:.4f}")
420# H0: Data follow the expected distribution
421```

Chi-square Tests

Chi-square test of independence:

from scipy.stats import chi2_contingency

# Contingency table
contingency_table = pd.crosstab(variable1, variable2)

chi2, p_value, dof, expected = chi2_contingency(contingency_table)

print(f"Chi-square statistic: {chi2:.4f}")
print(f"p-value: {p_value:.4f}")
print(f"Degrees of freedom: {dof}")

# H0: Variables are independent

Chi-square goodness-of-fit:

from scipy.stats import chisquare

# Observed frequencies
observed = [20, 30, 25, 25]

# Expected frequencies (equal by default)
expected = [25, 25, 25, 25]

chi2, p_value = chisquare(observed, expected)

print(f"Chi-square statistic: {chi2:.4f}")
print(f"p-value: {p_value:.4f}")

# H0: Data follow the expected distribution
Raw lines 385 to 422
423### Non-parametric Tests
425**Mann-Whitney U test** (independent samples):
427```python
428from scipy.stats import mannwhitneyu
430# H0: Distributions are equal
431u_stat, p_value = mannwhitneyu(group1, group2, alternative='two-sided')
433print(f"U statistic: {u_stat:.4f}")
434print(f"p-value: {p_value:.4f}")
435```
437**Wilcoxon signed-rank test** (paired samples):
439```python
440from scipy.stats import wilcoxon
442# H0: the paired-difference distribution is symmetric about zero
443w_stat, p_value = wilcoxon(before, after)
445print(f"W statistic: {w_stat:.4f}")
446print(f"p-value: {p_value:.4f}")
447```
449**Kruskal-Wallis H test** (>2 groups):
451```python
452from scipy.stats import kruskal
454# H0: All groups have same distribution
455h_stat, p_value = kruskal(group1, group2, group3)
457print(f"H statistic: {h_stat:.4f}")
458print(f"p-value: {p_value:.4f}")
459```
461**Sign test**:
463```python
464from statsmodels.stats.descriptivestats import sign_test
466# H0: Median = m0
467result = sign_test(data, mu0=0)
468print(result)
469```

Non-parametric Tests

Mann-Whitney U test (independent samples):

from scipy.stats import mannwhitneyu

# H0: Distributions are equal
u_stat, p_value = mannwhitneyu(group1, group2, alternative='two-sided')

print(f"U statistic: {u_stat:.4f}")
print(f"p-value: {p_value:.4f}")

Wilcoxon signed-rank test (paired samples):

from scipy.stats import wilcoxon

# H0: the paired-difference distribution is symmetric about zero
w_stat, p_value = wilcoxon(before, after)

print(f"W statistic: {w_stat:.4f}")
print(f"p-value: {p_value:.4f}")

Kruskal-Wallis H test (>2 groups):

from scipy.stats import kruskal

# H0: All groups have same distribution
h_stat, p_value = kruskal(group1, group2, group3)

print(f"H statistic: {h_stat:.4f}")
print(f"p-value: {p_value:.4f}")

Sign test:

from statsmodels.stats.descriptivestats import sign_test

# H0: Median = m0
result = sign_test(data, mu0=0)
print(result)
Raw lines 423 to 470
471### ANOVA
473**One-way ANOVA**:
475```python
476from scipy.stats import f_oneway
478# H0: All group means are equal
479f_stat, p_value = f_oneway(group1, group2, group3)
481print(f"F-statistic: {f_stat:.4f}")
482print(f"p-value: {p_value:.4f}")
483```
485**Two-way ANOVA** (with statsmodels):
487```python
488from statsmodels.formula.api import ols
489from statsmodels.stats.anova import anova_lm
491# Fit model
492model = ols('response ~ C(factor1) + C(factor2) + C(factor1):C(factor2)',
493 data=df).fit()
495# ANOVA table
496anova_table = anova_lm(model, typ=2)
497print(anova_table)
498```
500**Repeated measures ANOVA**:
502```python
503from statsmodels.stats.anova import AnovaRM
505# Fully balanced within-subject data; no between-subject effects or sphericity correction
506aovrm = AnovaRM(df, depvar='score', subject='subject_id', within=['time'])
507results = aovrm.fit()
509print(results.summary())
510```

ANOVA

One-way ANOVA:

from scipy.stats import f_oneway

# H0: All group means are equal
f_stat, p_value = f_oneway(group1, group2, group3)

print(f"F-statistic: {f_stat:.4f}")
print(f"p-value: {p_value:.4f}")

Two-way ANOVA (with statsmodels):

from statsmodels.formula.api import ols
from statsmodels.stats.anova import anova_lm

# Fit model
model = ols('response ~ C(factor1) + C(factor2) + C(factor1):C(factor2)',
            data=df).fit()

# ANOVA table
anova_table = anova_lm(model, typ=2)
print(anova_table)

Repeated measures ANOVA:

from statsmodels.stats.anova import AnovaRM

# Fully balanced within-subject data; no between-subject effects or sphericity correction
aovrm = AnovaRM(df, depvar='score', subject='subject_id', within=['time'])
results = aovrm.fit()

print(results.summary())
Raw lines 471 to 511
512## Multiple Comparisons

Multiple Comparisons

Raw lines 512 to 513
514### Post-hoc Tests
516**Tukey's HSD** (Honest Significant Difference):
518```python
519from statsmodels.stats.multicomp import pairwise_tukeyhsd
521# Perform Tukey HSD test
522tukey = pairwise_tukeyhsd(data, groups, alpha=0.05, use_var="equal")
523# In statsmodels 0.15+, unequal variances use Games-Howell:
524games_howell = pairwise_tukeyhsd(data, groups, alpha=0.05, use_var="unequal")
526print(tukey.summary())
528# Plot confidence intervals
529tukey.plot_simultaneous()
530plt.show()
531```
533**Bonferroni correction**:
535```python
536from statsmodels.stats.multitest import multipletests
538# P-values from multiple tests
539p_values = [0.01, 0.03, 0.04, 0.15, 0.001]
541# Apply correction
542reject, pvals_corrected, alphac_sidak, alphac_bonf = multipletests(
543 p_values,
544 alpha=0.05,
545 method='bonferroni'
548print("Rejected:", reject)
549print("Corrected p-values:", pvals_corrected)
550```
552**False Discovery Rate (FDR)**:
554```python
555# FDR correction (less conservative than Bonferroni)
556reject, pvals_corrected, alphac_sidak, alphac_bonf = multipletests(
557 p_values,
558 alpha=0.05,
559 method='fdr_bh' # Benjamini-Hochberg
562print("Rejected:", reject)
563print("Corrected p-values:", pvals_corrected)
564```

Post-hoc Tests

Tukey's HSD (Honest Significant Difference):

from statsmodels.stats.multicomp import pairwise_tukeyhsd

# Perform Tukey HSD test
tukey = pairwise_tukeyhsd(data, groups, alpha=0.05, use_var="equal")
# In statsmodels 0.15+, unequal variances use Games-Howell:
games_howell = pairwise_tukeyhsd(data, groups, alpha=0.05, use_var="unequal")

print(tukey.summary())

# Plot confidence intervals
tukey.plot_simultaneous()
plt.show()

Bonferroni correction:

from statsmodels.stats.multitest import multipletests

# P-values from multiple tests
p_values = [0.01, 0.03, 0.04, 0.15, 0.001]

# Apply correction
reject, pvals_corrected, alphac_sidak, alphac_bonf = multipletests(
    p_values,
    alpha=0.05,
    method='bonferroni'
)

print("Rejected:", reject)
print("Corrected p-values:", pvals_corrected)

False Discovery Rate (FDR):

# FDR correction (less conservative than Bonferroni)
reject, pvals_corrected, alphac_sidak, alphac_bonf = multipletests(
    p_values,
    alpha=0.05,
    method='fdr_bh'  # Benjamini-Hochberg
)

print("Rejected:", reject)
print("Corrected p-values:", pvals_corrected)
Raw lines 514 to 565
566## Robust Covariance Matrices

Robust Covariance Matrices

Raw lines 566 to 567
568### Heteroskedasticity-Consistent (HC) Standard Errors
570```python
571# After fitting OLS
572results = sm.OLS(y, X).fit()
574# HC0 (White's heteroskedasticity-consistent SEs)
575results_hc0 = results.get_robustcov_results(cov_type='HC0')
577# HC1 (degrees of freedom adjustment)
578results_hc1 = results.get_robustcov_results(cov_type='HC1')
580# HC2 (leverage adjustment)
581results_hc2 = results.get_robustcov_results(cov_type='HC2')
583# HC3 uses squared leverage adjustment; it is not a universal small-sample guarantee
584results_hc3 = results.get_robustcov_results(cov_type='HC3')
586print("Standard OLS SEs:", results.bse)
587print("Robust HC3 SEs:", results_hc3.bse)
588```

Heteroskedasticity-Consistent (HC) Standard Errors

# After fitting OLS
results = sm.OLS(y, X).fit()

# HC0 (White's heteroskedasticity-consistent SEs)
results_hc0 = results.get_robustcov_results(cov_type='HC0')

# HC1 (degrees of freedom adjustment)
results_hc1 = results.get_robustcov_results(cov_type='HC1')

# HC2 (leverage adjustment)
results_hc2 = results.get_robustcov_results(cov_type='HC2')

# HC3 uses squared leverage adjustment; it is not a universal small-sample guarantee
results_hc3 = results.get_robustcov_results(cov_type='HC3')

print("Standard OLS SEs:", results.bse)
print("Robust HC3 SEs:", results_hc3.bse)
Raw lines 568 to 589
590### HAC (Heteroskedasticity and Autocorrelation Consistent)
592**Newey-West standard errors**:
594```python
595# For time series with autocorrelation and heteroskedasticity
596results_hac = results.get_robustcov_results(cov_type='HAC', maxlags=4)
598print("HAC (Newey-West) SEs:", results_hac.bse)
599print(results_hac.summary())
600```

HAC (Heteroskedasticity and Autocorrelation Consistent)

Newey-West standard errors:

# For time series with autocorrelation and heteroskedasticity
results_hac = results.get_robustcov_results(cov_type='HAC', maxlags=4)

print("HAC (Newey-West) SEs:", results_hac.bse)
print(results_hac.summary())
Raw lines 590 to 601
602### Cluster-Robust Standard Errors
604```python
605# For clustered/grouped data
606results_cluster = results.get_robustcov_results(
607 cov_type='cluster',
608 groups=cluster_ids
611print("Cluster-robust SEs:", results_cluster.bse)
612```

Cluster-Robust Standard Errors

# For clustered/grouped data
results_cluster = results.get_robustcov_results(
    cov_type='cluster',
    groups=cluster_ids
)

print("Cluster-robust SEs:", results_cluster.bse)
Raw lines 602 to 613
614## Descriptive Statistics
616**Basic descriptive statistics**:
618```python
619from statsmodels.stats.api import DescrStatsW
621# Comprehensive descriptive stats
622desc = DescrStatsW(data)
624print("Mean:", desc.mean)
625print("Std Dev:", desc.std)
626print("Variance:", desc.var)
627print("Confidence interval:", desc.tconfint_mean())
629# Quantiles
630print("Median:", desc.quantile(0.5))
631quartiles = desc.quantile([0.25, 0.75], return_pandas=False)
632print("IQR:", quartiles[1] - quartiles[0])
633```
635**Weighted statistics**:
637```python
638# With weights
639desc_weighted = DescrStatsW(data, weights=weights)
641print("Weighted mean:", desc_weighted.mean)
642print("Weighted std:", desc_weighted.std)
643```
645**Compare two groups**:
647```python
648from statsmodels.stats.weightstats import CompareMeans
650# Create comparison object
651cm = CompareMeans(DescrStatsW(group1), DescrStatsW(group2))
653# t-test
654print("t-test:", cm.ttest_ind())
656# Confidence interval for difference
657print("CI for difference:", cm.tconfint_diff())
659# For an independent-samples variance check, use SciPy (not a CompareMeans method):
660from scipy.stats import levene
661print("Brown-Forsythe:", levene(group1, group2, center="median"))
662```

Descriptive Statistics

Basic descriptive statistics:

from statsmodels.stats.api import DescrStatsW

# Comprehensive descriptive stats
desc = DescrStatsW(data)

print("Mean:", desc.mean)
print("Std Dev:", desc.std)
print("Variance:", desc.var)
print("Confidence interval:", desc.tconfint_mean())

# Quantiles
print("Median:", desc.quantile(0.5))
quartiles = desc.quantile([0.25, 0.75], return_pandas=False)
print("IQR:", quartiles[1] - quartiles[0])

Weighted statistics:

# With weights
desc_weighted = DescrStatsW(data, weights=weights)

print("Weighted mean:", desc_weighted.mean)
print("Weighted std:", desc_weighted.std)

Compare two groups:

from statsmodels.stats.weightstats import CompareMeans

# Create comparison object
cm = CompareMeans(DescrStatsW(group1), DescrStatsW(group2))

# t-test
print("t-test:", cm.ttest_ind())

# Confidence interval for difference
print("CI for difference:", cm.tconfint_diff())

# For an independent-samples variance check, use SciPy (not a CompareMeans method):
from scipy.stats import levene
print("Brown-Forsythe:", levene(group1, group2, center="median"))
Raw lines 614 to 663
664## Power Analysis and Sample Size
666**Power for t-test**:
668```python
669from statsmodels.stats.power import tt_ind_solve_power
671# Solve for sample size
672effect_size = 0.5 # Cohen's d
673alpha = 0.05
674power = 0.8
676n = tt_ind_solve_power(effect_size=effect_size,
677 alpha=alpha,
678 power=power,
679 alternative='two-sided')
681print(f"Required sample size per group: {int(np.ceil(n))}")
683# Solve for power given n
684power = tt_ind_solve_power(effect_size=0.5,
685 nobs1=50,
686 alpha=0.05,
687 alternative='two-sided')
689print(f"Power: {power:.4f}")
690```
692**Power for proportion test**:
694```python
695from statsmodels.stats.power import zt_ind_solve_power
697# For proportion tests (z-test)
698from statsmodels.stats.proportion import proportion_effectsize
699effect_size = proportion_effectsize(0.5, 0.35) # Cohen's h, not raw difference
700alpha = 0.05
701power = 0.8
703n = zt_ind_solve_power(effect_size=effect_size,
704 alpha=alpha,
705 power=power,
706 alternative='two-sided')
708print(f"Required sample size per group: {int(np.ceil(n))}")
709```
711**Power curves**:
713```python
714from statsmodels.stats.power import TTestIndPower
715import matplotlib.pyplot as plt
717# Create power analysis object
718analysis = TTestIndPower()
720# Plot power curves for different sample sizes
721sample_sizes = range(10, 200, 10)
722effect_sizes = [0.2, 0.5, 0.8] # Small, medium, large
724fig, ax = plt.subplots(figsize=(10, 6))
726for es in effect_sizes:
727 power = [analysis.solve_power(effect_size=es, nobs1=n, alpha=0.05)
728 for n in sample_sizes]
729 ax.plot(sample_sizes, power, label=f'Effect size = {es}')
731ax.axhline(y=0.8, color='r', linestyle='--', label='Power = 0.8')
732ax.set_xlabel('Sample size per group')
733ax.set_ylabel('Power')
734ax.set_title('Power Curves for Two-Sample t-test')
735ax.legend()
736ax.grid(True, alpha=0.3)
737plt.show()
738```

Power Analysis and Sample Size

Power for t-test:

from statsmodels.stats.power import tt_ind_solve_power

# Solve for sample size
effect_size = 0.5  # Cohen's d
alpha = 0.05
power = 0.8

n = tt_ind_solve_power(effect_size=effect_size,
                        alpha=alpha,
                        power=power,
                        alternative='two-sided')

print(f"Required sample size per group: {int(np.ceil(n))}")

# Solve for power given n
power = tt_ind_solve_power(effect_size=0.5,
                           nobs1=50,
                           alpha=0.05,
                           alternative='two-sided')

print(f"Power: {power:.4f}")

Power for proportion test:

from statsmodels.stats.power import zt_ind_solve_power

# For proportion tests (z-test)
from statsmodels.stats.proportion import proportion_effectsize
effect_size = proportion_effectsize(0.5, 0.35)  # Cohen's h, not raw difference
alpha = 0.05
power = 0.8

n = zt_ind_solve_power(effect_size=effect_size,
                        alpha=alpha,
                        power=power,
                        alternative='two-sided')

print(f"Required sample size per group: {int(np.ceil(n))}")

Power curves:

from statsmodels.stats.power import TTestIndPower
import matplotlib.pyplot as plt

# Create power analysis object
analysis = TTestIndPower()

# Plot power curves for different sample sizes
sample_sizes = range(10, 200, 10)
effect_sizes = [0.2, 0.5, 0.8]  # Small, medium, large

fig, ax = plt.subplots(figsize=(10, 6))

for es in effect_sizes:
    power = [analysis.solve_power(effect_size=es, nobs1=n, alpha=0.05)
             for n in sample_sizes]
    ax.plot(sample_sizes, power, label=f'Effect size = {es}')

ax.axhline(y=0.8, color='r', linestyle='--', label='Power = 0.8')
ax.set_xlabel('Sample size per group')
ax.set_ylabel('Power')
ax.set_title('Power Curves for Two-Sample t-test')
ax.legend()
ax.grid(True, alpha=0.3)
plt.show()
Raw lines 664 to 739
740## Effect Sizes
742**Cohen's d** (standardized mean difference):
744```python
745def cohens_d(group1, group2):
746 """Calculate Cohen's d for independent samples"""
747 n1, n2 = len(group1), len(group2)
748 var1, var2 = np.var(group1, ddof=1), np.var(group2, ddof=1)
750 # Pooled standard deviation
751 pooled_std = np.sqrt(((n1-1)*var1 + (n2-1)*var2) / (n1+n2-2))
753 # Cohen's d
754 d = (np.mean(group1) - np.mean(group2)) / pooled_std
756 return d
758d = cohens_d(group1, group2)
759print(f"Cohen's d: {d:.4f}")
761# Interpretation:
762# |d| < 0.2: negligible
763# |d| ~ 0.2: small
764# |d| ~ 0.5: medium
765# |d| ~ 0.8: large
766```
768**Eta-squared** (for ANOVA):
770```python
771# From ANOVA table
772# For a ONE-WAY ANOVA only: η² = SS_between / SS_total.
773# This sum is not a general multifactor type-II/III eta-squared denominator.
775def eta_squared(anova_table):
776 return anova_table['sum_sq'].iloc[0] / anova_table['sum_sq'].sum()
778# After running ANOVA
779eta_sq = eta_squared(anova_table)
780print(f"Eta-squared: {eta_sq:.4f}")
782# Interpretation:
783# 0.01: small effect
784# 0.06: medium effect
785# 0.14: large effect
786```

Effect Sizes

Cohen's d (standardized mean difference):

def cohens_d(group1, group2):
    """Calculate Cohen's d for independent samples"""
    n1, n2 = len(group1), len(group2)
    var1, var2 = np.var(group1, ddof=1), np.var(group2, ddof=1)

    # Pooled standard deviation
    pooled_std = np.sqrt(((n1-1)*var1 + (n2-1)*var2) / (n1+n2-2))

    # Cohen's d
    d = (np.mean(group1) - np.mean(group2)) / pooled_std

    return d

d = cohens_d(group1, group2)
print(f"Cohen's d: {d:.4f}")

# Interpretation:
# |d| < 0.2: negligible
# |d| ~ 0.2: small
# |d| ~ 0.5: medium
# |d| ~ 0.8: large

Eta-squared (for ANOVA):

# From ANOVA table
# For a ONE-WAY ANOVA only: η² = SS_between / SS_total.
# This sum is not a general multifactor type-II/III eta-squared denominator.

def eta_squared(anova_table):
    return anova_table['sum_sq'].iloc[0] / anova_table['sum_sq'].sum()

# After running ANOVA
eta_sq = eta_squared(anova_table)
print(f"Eta-squared: {eta_sq:.4f}")

# Interpretation:
# 0.01: small effect
# 0.06: medium effect
# 0.14: large effect
Raw lines 740 to 787
788## Contingency Tables and Association
790**McNemar's test** (paired binary data):
792```python
793from statsmodels.stats.contingency_tables import mcnemar
795# 2x2 contingency table
796table = [[a, b],
797 [c, d]]
799result = mcnemar(table, exact=True) # or exact=False for large samples
800print(f"p-value: {result.pvalue:.4f}")
802# H0: Marginal probabilities are equal
803```
805**Cochran-Mantel-Haenszel test**:
807```python
808from statsmodels.stats.contingency_tables import StratifiedTable
810# For stratified 2x2 tables
811strat_table = StratifiedTable(tables_list)
812result = strat_table.test_null_odds()
814print(f"p-value: {result.pvalue:.4f}")
815```

Contingency Tables and Association

McNemar's test (paired binary data):

from statsmodels.stats.contingency_tables import mcnemar

# 2x2 contingency table
table = [[a, b],
         [c, d]]

result = mcnemar(table, exact=True)  # or exact=False for large samples
print(f"p-value: {result.pvalue:.4f}")

# H0: Marginal probabilities are equal

Cochran-Mantel-Haenszel test:

from statsmodels.stats.contingency_tables import StratifiedTable

# For stratified 2x2 tables
strat_table = StratifiedTable(tables_list)
result = strat_table.test_null_odds()

print(f"p-value: {result.pvalue:.4f}")
Raw lines 788 to 816
817## Treatment Effects and Causal Inference
819**Treatment effect estimation** (`TreatmentEffect`):
821```python
822import statsmodels.api as sm
823from statsmodels.treatment.treatment_effects import TreatmentEffect
825# exog: observed confounders (with constant); treatment: 0/1 indicator
826exog = sm.add_constant(confounders)
827select_model = sm.Probit(treatment, exog) # treatment (selection) model
828outcome_model = sm.OLS(outcome, exog) # outcome model
830te = TreatmentEffect(outcome_model, treatment,
831 results_select=select_model.fit(disp=0))
833res = te.ipw() # inverse probability weighting
834print(res.summary_frame()) # ATE and potential-outcome means POM0/POM1 with GMM SEs
835print(te.ra().summary_frame()) # regression adjustment
836print(te.aipw().summary_frame()) # augmented IPW (doubly robust)
837# Also available: te.aipw_wls(), te.ipw_ra() -- both need a 6-parameter
838# selection model unless return_results=False; see the caveats below
839```
841**Propensity scores** (for diagnostics or custom weighting):
843```python
844ps_model = sm.Logit(treatment, exog).fit(disp=0)
845propensity_scores = ps_model.predict(exog)
846# statsmodels does not implement matching; estimating scores
847# alone is not propensity score matching
848```
850Causal caveats:
852- Causal interpretation needs consistency, no interference, no unmeasured confounding and positivity, plus the relevant estimator/model assumptions
853- Check overlap of propensity score distributions between groups; extreme
854 scores near 0 or 1 make IPW unstable
855- Check covariate balance after weighting (e.g. standardized mean differences)
856- Default target is the sample ATE (`effect_group="all"`). ATT/ATU:
857 `te.ipw(effect_group=1)` / `te.ipw(effect_group=0)` (also `ra`, `ipw_ra`).
858 Not available on `aipw` / `aipw_wls`
859- Outcome model is OLS-only (still true in 0.15); Logit/Poisson outcomes are
860 not supported yet
861- Verified in released 0.15.0: `ipw_ra` and `aipw_wls` raise a shape `ValueError` unless the
862 selection model has exactly 6 parameters; `return_results=False` avoids the
863 GMM step and returns `(ate, pom0, pom1)` without standard errors. Do not add
864 dummy covariates to force six parameters; this is an implementation limitation.
866**Difference-in-differences**:
868```python
869# Did formula: outcome ~ treatment * post
870model = ols('outcome ~ treatment * post', data=df).fit(
871 cov_type='cluster', cov_kwds={'groups': df['unit_id']})
873# DiD estimate is the interaction coefficient
874did_estimate = model.params['treatment:post']
875print(f"DiD estimate: {did_estimate:.4f}")
876```
878The displayed DiD is a simple two-group/two-period design. Causal interpretation additionally requires parallel untreated trends, no anticipation/interference, appropriate sampling, and a defensible clustering level; this formula is not a general staggered-adoption estimator.

Treatment Effects and Causal Inference

Treatment effect estimation (TreatmentEffect):

import statsmodels.api as sm
from statsmodels.treatment.treatment_effects import TreatmentEffect

# exog: observed confounders (with constant); treatment: 0/1 indicator
exog = sm.add_constant(confounders)
select_model = sm.Probit(treatment, exog)  # treatment (selection) model
outcome_model = sm.OLS(outcome, exog)      # outcome model

te = TreatmentEffect(outcome_model, treatment,
                     results_select=select_model.fit(disp=0))

res = te.ipw()             # inverse probability weighting
print(res.summary_frame())  # ATE and potential-outcome means POM0/POM1 with GMM SEs
print(te.ra().summary_frame())    # regression adjustment
print(te.aipw().summary_frame())  # augmented IPW (doubly robust)
# Also available: te.aipw_wls(), te.ipw_ra() -- both need a 6-parameter
# selection model unless return_results=False; see the caveats below

Propensity scores (for diagnostics or custom weighting):

ps_model = sm.Logit(treatment, exog).fit(disp=0)
propensity_scores = ps_model.predict(exog)
# statsmodels does not implement matching; estimating scores
# alone is not propensity score matching

Causal caveats:

  • Causal interpretation needs consistency, no interference, no unmeasured confounding and positivity, plus the relevant estimator/model assumptions
  • Check overlap of propensity score distributions between groups; extreme scores near 0 or 1 make IPW unstable
  • Check covariate balance after weighting (e.g. standardized mean differences)
  • Default target is the sample ATE (effect_group="all"). ATT/ATU: te.ipw(effect_group=1) / te.ipw(effect_group=0) (also ra, ipw_ra). Not available on aipw / aipw_wls
  • Outcome model is OLS-only (still true in 0.15); Logit/Poisson outcomes are not supported yet
  • Verified in released 0.15.0: ipw_ra and aipw_wls raise a shape ValueError unless the selection model has exactly 6 parameters; return_results=False avoids the GMM step and returns (ate, pom0, pom1) without standard errors. Do not add dummy covariates to force six parameters; this is an implementation limitation.

Difference-in-differences:

# Did formula: outcome ~ treatment * post
model = ols('outcome ~ treatment * post', data=df).fit(
    cov_type='cluster', cov_kwds={'groups': df['unit_id']})

# DiD estimate is the interaction coefficient
did_estimate = model.params['treatment:post']
print(f"DiD estimate: {did_estimate:.4f}")

The displayed DiD is a simple two-group/two-period design. Causal interpretation additionally requires parallel untreated trends, no anticipation/interference, appropriate sampling, and a defensible clustering level; this formula is not a general staggered-adoption estimator.

Raw lines 817 to 879
880## Best Practices
8821. **Always check assumptions**: Test before interpreting results
8832. **Report effect sizes**: Not just p-values
8843. **Use appropriate tests**: Match test to data type and distribution
8854. **Correct for multiple comparisons**: When conducting many tests
8865. **Check sample size**: Ensure adequate power
8876. **Visual inspection**: Plot data before testing
8887. **Report confidence intervals**: Along with point estimates
8898. **Consider alternatives**: Non-parametric when assumptions violated
8909. **Robust standard errors**: Use when heteroskedasticity/autocorrelation present
89110. **Document decisions**: Note which tests used and why

Best Practices

  1. Always check assumptions: Test before interpreting results
  2. Report effect sizes: Not just p-values
  3. Use appropriate tests: Match test to data type and distribution
  4. Correct for multiple comparisons: When conducting many tests
  5. Check sample size: Ensure adequate power
  6. Visual inspection: Plot data before testing
  7. Report confidence intervals: Along with point estimates
  8. Consider alternatives: Non-parametric when assumptions violated
  9. Robust standard errors: Use when heteroskedasticity/autocorrelation present
  10. Document decisions: Note which tests used and why
Raw lines 880 to 892
893## Common Pitfalls
8951. **Not checking test assumptions**: May invalidate results
8962. **Multiple testing without correction**: Inflated Type I error
8973. **Changing estimands after a normality test**: Rank tests do not generally test mean differences; choose the estimand and sampling assumptions explicitly
8984. **Ignoring heteroskedasticity**: Use robust SEs
8995. **Confusing statistical and practical significance**: Check effect sizes
9006. **Not reporting confidence intervals**: Only p-values insufficient
9017. **Using wrong test**: Match test to research question
9028. **Insufficient power**: Risk of Type II error (false negatives)
9039. **p-hacking**: Testing many specifications until significant
90410. **Overinterpreting p-values**: Remember limitations of NHST

Common Pitfalls

  1. Not checking test assumptions: May invalidate results
  2. Multiple testing without correction: Inflated Type I error
  3. Changing estimands after a normality test: Rank tests do not generally test mean differences; choose the estimand and sampling assumptions explicitly
  4. Ignoring heteroskedasticity: Use robust SEs
  5. Confusing statistical and practical significance: Check effect sizes
  6. Not reporting confidence intervals: Only p-values insufficient
  7. Using wrong test: Match test to research question
  8. Insufficient power: Risk of Type II error (false negatives)
  9. p-hacking: Testing many specifications until significant
  10. Overinterpreting p-values: Remember limitations of NHST
Raw lines 893 to 904