11 KiB
11 KiB
title, task, lineage_type, upstream_source, upstream_sha, imported_at, prompt_class, upstream_changes, author, validated
| title | task | lineage_type | upstream_source | upstream_sha | imported_at | prompt_class | upstream_changes | author | validated |
|---|---|---|---|---|---|---|---|---|---|
| Linear Models and Mixed-Effects Reference | import | https://github.com/mims-harvard/ToolUniverse/blob/e2520a96/skills/tooluniverse-statistical-modeling/references/linear_models.md | e2520a96 | 2026-06-26 | prompt | accepted | upstream | false |
Linear Models and Mixed-Effects Reference
Complete guide to linear regression and mixed-effects models.
Ordinary Least Squares (OLS) Regression
Basic Linear Regression
import pandas as pd
import statsmodels.api as sm
import statsmodels.formula.api as smf
# Method 1: Formula API (recommended)
model = smf.ols('outcome ~ predictor1 + predictor2 + age', data=df).fit()
# Method 2: Matrix API
X = sm.add_constant(df[['predictor1', 'predictor2', 'age']])
y = df['outcome']
model = sm.OLS(y, X).fit()
# Print summary
print(model.summary())
Interpreting Coefficients
# Extract coefficients
coefs = model.params
se = model.bse
t_vals = model.tvalues
p_vals = model.pvalues
ci = model.conf_int()
for var in coefs.index:
print(f"\n{var}:")
print(f" Coefficient: {coefs[var]:.4f}")
print(f" Std Error: {se[var]:.4f}")
print(f" t-value: {t_vals[var]:.4f}")
print(f" p-value: {p_vals[var]:.6f}")
print(f" 95% CI: ({ci.loc[var, 0]:.4f}, {ci.loc[var, 1]:.4f})")
Model Fit Statistics
# R-squared
print(f"R-squared: {model.rsquared:.4f}")
print(f"Adjusted R-squared: {model.rsquared_adj:.4f}")
# F-statistic
print(f"F-statistic: {model.fvalue:.4f}")
print(f"F-test p-value: {model.f_pvalue:.6f}")
# AIC/BIC
print(f"AIC: {model.aic:.2f}")
print(f"BIC: {model.bic:.2f}")
# Root mean squared error
rmse = np.sqrt(model.mse_resid)
print(f"RMSE: {rmse:.4f}")
Diagnostics
Residual Normality
from scipy import stats as scipy_stats
# Shapiro-Wilk test
residuals = model.resid
sw_stat, sw_p = scipy_stats.shapiro(residuals)
print(f"Shapiro-Wilk test:")
print(f" Statistic: {sw_stat:.4f}")
print(f" p-value: {sw_p:.6f}")
if sw_p > 0.05:
print(" ✅ Residuals appear normally distributed")
else:
print(" ⚠️ Residuals may not be normally distributed")
Homoscedasticity (Equal Variance)
from statsmodels.stats.diagnostic import het_breuschpagan
# Breusch-Pagan test
bp_stat, bp_p, _, _ = het_breuschpagan(residuals, model.model.exog)
print(f"\nBreusch-Pagan test:")
print(f" Statistic: {bp_stat:.4f}")
print(f" p-value: {bp_p:.6f}")
if bp_p > 0.05:
print(" ✅ Homoscedasticity assumption met")
else:
print(" ⚠️ Heteroscedasticity detected")
print(" Consider: robust standard errors, log transformation, or WLS")
Autocorrelation
from statsmodels.stats.stattools import durbin_watson
# Durbin-Watson test
dw = durbin_watson(residuals)
print(f"\nDurbin-Watson statistic: {dw:.4f}")
if 1.5 < dw < 2.5:
print(" ✅ No autocorrelation detected")
else:
print(" ⚠️ Possible autocorrelation")
Multicollinearity (VIF)
from statsmodels.stats.outliers_influence import variance_inflation_factor
X = model.model.exog
vif_data = []
for i in range(X.shape[1]):
var_name = model.model.exog_names[i]
if var_name != 'Intercept':
vif = variance_inflation_factor(X, i)
vif_data.append({'variable': var_name, 'VIF': vif})
vif_df = pd.DataFrame(vif_data)
print("\nVariance Inflation Factors:")
print(vif_df)
# Rule of thumb: VIF > 10 indicates multicollinearity
if (vif_df['VIF'] > 10).any():
print("\n⚠️ High multicollinearity detected (VIF > 10)")
print("Consider: removing correlated predictors or PCA")
Linear Mixed-Effects Models (LMM)
When to Use LMM
Use LMM when:
- Repeated measures (same subject measured multiple times)
- Nested/clustered data (patients within hospitals)
- Hierarchical structure (students within schools)
- Need to model between-subject and within-subject variation
Random Intercept Model
import statsmodels.formula.api as smf
# Random intercepts for subjects
model = smf.mixedlm('outcome ~ treatment + time',
data=df,
groups=df['subject_id'])
fit = model.fit(reml=True)
print(fit.summary())
Random Slope Model
# Random slopes for time (subjects have different time trends)
model = smf.mixedlm('outcome ~ treatment + time',
data=df,
groups=df['subject_id'],
re_formula='~time')
fit = model.fit(reml=True)
Fixed Effects Interpretation
# Extract fixed effects
fe_params = fit.fe_params
fe_pvalues = fit.pvalues
fe_ci = fit.conf_int()
print("Fixed Effects:")
for var in fe_params.index:
coef = fe_params[var]
p_val = fe_pvalues[var]
ci_lower = fe_ci.loc[var, 0]
ci_upper = fe_ci.loc[var, 1]
print(f"\n{var}:")
print(f" Coefficient: {coef:.4f}")
print(f" 95% CI: ({ci_lower:.4f}, {ci_upper:.4f})")
print(f" p-value: {p_val:.6f}")
Random Effects Variance
# Group (subject) variance
cov_re = fit.cov_re
if hasattr(cov_re, 'values'):
group_var = cov_re.iloc[0, 0]
else:
group_var = float(cov_re)
# Residual variance
resid_var = fit.scale
print(f"\nRandom Effects Variance:")
print(f" Between-subject (Group): {group_var:.4f}")
print(f" Within-subject (Residual): {resid_var:.4f}")
Intraclass Correlation Coefficient (ICC)
# ICC: proportion of variance due to grouping
icc = group_var / (group_var + resid_var)
print(f"\nICC: {icc:.4f}")
print(f"Interpretation: {icc*100:.1f}% of variance is between subjects")
if icc > 0.1:
print(" ✅ Substantial clustering - LMM appropriate")
else:
print(" ⚠️ Low clustering - OLS may be sufficient")
Complete Example: Longitudinal Study
import pandas as pd
import numpy as np
import statsmodels.formula.api as smf
# Simulated longitudinal data
np.random.seed(42)
n_subjects = 50
n_timepoints = 4
data = []
for subject_id in range(n_subjects):
treatment = np.random.binomial(1, 0.5)
baseline = np.random.normal(100, 10)
for time in range(n_timepoints):
# Treatment effect + time trend + random noise
outcome = baseline + treatment * 5 + time * 2 + np.random.normal(0, 3)
data.append({
'subject_id': subject_id,
'treatment': treatment,
'time': time,
'outcome': outcome
})
df = pd.DataFrame(data)
print("=== Longitudinal Analysis ===\n")
# 1. OLS (ignoring repeated measures - WRONG)
print("1. OLS (incorrect for repeated measures):\n")
ols_model = smf.ols('outcome ~ treatment + time', data=df).fit()
print(f"Treatment effect: {ols_model.params['treatment']:.4f}")
print(f"p-value: {ols_model.pvalues['treatment']:.6f}")
print(f"R-squared: {ols_model.rsquared:.4f}\n")
# 2. LMM with random intercepts (correct)
print("2. LMM with random intercepts (correct):\n")
lmm_model = smf.mixedlm('outcome ~ treatment + time',
data=df,
groups=df['subject_id'])
lmm_fit = lmm_model.fit(reml=True)
print(f"Treatment effect: {lmm_fit.fe_params['treatment']:.4f}")
print(f"p-value: {lmm_fit.pvalues['treatment']:.6f}")
# ICC
group_var = float(lmm_fit.cov_re.iloc[0, 0])
resid_var = float(lmm_fit.scale)
icc = group_var / (group_var + resid_var)
print(f"ICC: {icc:.4f}\n")
# 3. LMM with random slopes (allow different time trends)
print("3. LMM with random slopes:\n")
lmm_slopes = smf.mixedlm('outcome ~ treatment + time',
data=df,
groups=df['subject_id'],
re_formula='~time')
lmm_slopes_fit = lmm_slopes.fit(reml=True)
print(f"Treatment effect: {lmm_slopes_fit.fe_params['treatment']:.4f}")
print(f"p-value: {lmm_slopes_fit.pvalues['treatment']:.6f}\n")
# Model comparison
print("4. Model Comparison:")
print(f"Random intercepts AIC: {lmm_fit.aic:.2f}")
print(f"Random slopes AIC: {lmm_slopes_fit.aic:.2f}")
if lmm_slopes_fit.aic < lmm_fit.aic:
print("Random slopes model preferred (lower AIC)")
else:
print("Random intercepts model sufficient")
Weighted Least Squares (WLS)
When to Use WLS
Use WLS when:
- Heteroscedasticity detected
- Known different precision across observations
- Want to downweight outliers
# Estimate weights from variance model
# Common approach: inverse variance weighting
residuals_sq = model.resid ** 2
weights = 1 / residuals_sq
# Fit WLS
wls_model = sm.WLS(y, X, weights=weights).fit()
print(wls_model.summary())
Robust Standard Errors
Heteroscedasticity-Consistent Standard Errors
# Use robust standard errors (HC3)
model_robust = model.get_robustcov_results(cov_type='HC3')
print("Robust Standard Errors:")
print(model_robust.summary())
Generalized Estimating Equations (GEE)
Alternative to LMM for Clustered Data
import statsmodels.api as sm
from statsmodels.genmod.generalized_estimating_equations import GEE
from statsmodels.genmod.families import Gaussian
from statsmodels.genmod.cov_struct import Exchangeable
# GEE for continuous outcome
gee_model = GEE.from_formula('outcome ~ treatment + time',
groups='subject_id',
data=df,
family=Gaussian(),
cov_struct=Exchangeable())
gee_fit = gee_model.fit()
print(gee_fit.summary())
GEE vs LMM:
- GEE: Population-averaged effects, requires weaker assumptions
- LMM: Subject-specific effects, models random effects explicitly
- Use GEE when interested in marginal effects, LMM when interested in individual trajectories
Polynomial and Spline Models
Polynomial Regression
# Add polynomial terms
df['age_sq'] = df['age'] ** 2
df['age_cube'] = df['age'] ** 3
model = smf.ols('outcome ~ age + age_sq + age_cube', data=df).fit()
Natural Cubic Splines
from patsy import dmatrix
# Create spline basis
spline_basis = dmatrix("bs(age, df=4, degree=3)", df, return_type='dataframe')
X_spline = pd.concat([spline_basis, df[['treatment']]], axis=1)
y = df['outcome']
model_spline = sm.OLS(y, X_spline).fit()
Reporting Template
def report_linear_model(model):
"""Generate publication-ready linear model report."""
report = []
report.append("=== Linear Regression Results ===\n")
report.append(f"N = {int(model.nobs)}")
report.append(f"R² = {model.rsquared:.4f}")
report.append(f"Adjusted R² = {model.rsquared_adj:.4f}")
report.append(f"F({model.df_model:.0f}, {model.df_resid:.0f}) = {model.fvalue:.4f}, p = {model.f_pvalue:.6f}\n")
report.append("Coefficients:\n")
report.append("Variable | Coef | SE | t | p | 95% CI")
report.append("---------|------|----|----|---|-------")
for var in model.params.index:
coef = model.params[var]
se = model.bse[var]
t_val = model.tvalues[var]
p_val = model.pvalues[var]
ci = model.conf_int().loc[var]
sig = "*" if p_val < 0.05 else ""
report.append(f"{var} | {coef:.4f} | {se:.4f} | {t_val:.4f} | {p_val:.6f}{sig} | ({ci[0]:.4f}, {ci[1]:.4f})")
return "\n".join(report)
print(report_linear_model(model))