Same structural model as one-way ANOVA — only the variance estimator changes. Per-group variances σj2 yield Welch-Satterthwaite adjusted degrees of freedom for each coefficient’s t-test.
# Welch df should differ from standard n-k df, confirming Satterthwaite adjustment
n_k = len(penguins) - len(m_welch.params)
welch_dfs = [float(m_welch.params["df"][i]) for i in range(1, len(m_welch.params))]
assert all(df != n_k for df in welch_dfs), f"Expected Welch-adjusted df (not {n_k}), got {welch_dfs}"
# At least one species effect should be significant
welch_ps = [float(m_welch.params["p_value"][i]) for i in range(1, len(m_welch.params))]
assert any(p < 0.05 for p in welch_ps), f"Expected at least one significant species effect, got p-values {welch_ps}"
scipy reports the Kruskal-Wallis H statistic (χ²-distributed); bossanova reports an F-ratio on ranks. The test statistics differ but both test H₀: all group locations equal and yield equivalent p-values.
bn_f_kw = float(m_kw.effects["f_ratio"][0])
bn_p_kw = float(m_kw.effects["p_value"][0])
# Both methods should strongly reject (large species difference in body mass)
assert bn_p_kw < 1e-10, f"Expected highly significant bossanova p-value, got {bn_p_kw}"
assert scipy_kw.pvalue < 1e-10, f"Expected highly significant scipy p-value, got {scipy_kw.pvalue}"
# Normal approximations should be in the same ballpark
assert np.isclose(bn_p_kw, scipy_kw.pvalue, rtol=0.5), f"p-value gap too large: {bn_p_kw} vs {scipy_kw.pvalue}"
Each classical F corresponds to a joint test on the corresponding GLM coefficients. The interaction (αγ)ij tests whether the effect of one factor depends on the level of the other.
The group effect β1 is the adjusted mean difference after controlling for the covariate xi. ANCOVA reduces residual variance and removes confounding by the covariate.
# Compare species controlling for flipper length
m_ancova = model("body_mass_g ~ species + flipper_length_mm", penguins).fit().infer()
m_ancova.params.select("term", "estimate", "p_value")
Loading...
# All terms should be significant (species and flipper length both predict body mass)
for i in range(1, len(m_ancova.params)):
assert float(m_ancova.params["p_value"][i]) < 0.05, f"Expected significant term at row {i}"
# Flipper length coefficient should be positive (longer flippers → heavier penguins)
flipper_row = next(i for i in range(len(m_ancova.params)) if "flipper" in str(m_ancova.params["term"][i]))
assert float(m_ancova.params["estimate"][flipper_row]) > 0, "Expected positive flipper length coefficient"
# ANCOVA R² should exceed species-only model (covariate reduces residual variance)
m_species_only = model("body_mass_g ~ species", penguins).fit()
r2_ancova = float(m_ancova.diagnostics["rsquared"][0])
r2_species = float(m_species_only.diagnostics["rsquared"][0])
assert r2_ancova > r2_species, f"ANCOVA R² ({r2_ancova}) should exceed species-only ({r2_species})"