--- title: "Logistic Regression Reference" task: "" lineage_type: import upstream_source: https://github.com/mims-harvard/ToolUniverse/blob/e2520a96/skills/tooluniverse-statistical-modeling/references/logistic_regression.md upstream_sha: e2520a96 imported_at: 2026-06-26 prompt_class: prompt upstream_changes: accepted author: upstream validated: false --- # Logistic Regression Reference Complete guide to binary logistic regression for biomedical data analysis. ## Binary Logistic Regression ### Basic Model ```python import pandas as pd import numpy as np import statsmodels.api as sm import statsmodels.formula.api as smf # Load data df = pd.read_csv('clinical_data.csv') # Method 1: Formula API (recommended) model = smf.logit('disease ~ exposure + age + sex', data=df).fit(disp=0) # Method 2: Matrix API X = sm.add_constant(df[['exposure', 'age', 'sex']]) y = df['disease'] model = sm.Logit(y, X).fit(disp=0) # Print summary print(model.summary()) ``` ### Extracting Odds Ratios ```python # Coefficients (log odds) coefs = model.params print("Log odds (coefficients):") print(coefs) # Odds ratios (exponentiate coefficients) odds_ratios = np.exp(model.params) print("\nOdds ratios:") print(odds_ratios) # 95% Confidence intervals conf_int = model.conf_int() conf_int_exp = np.exp(conf_int) # Pretty print for var in model.params.index: or_val = odds_ratios[var] ci_lower = conf_int_exp.loc[var, 0] ci_upper = conf_int_exp.loc[var, 1] p_val = model.pvalues[var] print(f"\n{var}:") print(f" OR: {or_val:.4f}") print(f" 95% CI: ({ci_lower:.4f}, {ci_upper:.4f})") print(f" p-value: {p_val:.6f}") print(f" Significant: {'Yes' if p_val < 0.05 else 'No'}") ``` ### Model Fit Statistics ```python # Pseudo R-squared print(f"McFadden's R²: {model.prsquared:.4f}") # Log-likelihood print(f"Log-likelihood: {model.llf:.4f}") # AIC/BIC print(f"AIC: {model.aic:.2f}") print(f"BIC: {model.bic:.2f}") # Number of observations print(f"N: {int(model.nobs)}") ``` ## Categorical Predictors ### Manual Dummy Coding ```python # Create dummy variables df_encoded = pd.get_dummies(df, columns=['treatment_group'], drop_first=True, dtype=int) # Fit model model = smf.logit('disease ~ treatment_group_B + treatment_group_C + age', data=df_encoded).fit(disp=0) ``` ### Formula with Categorical Variables ```python # statsmodels handles categorical automatically with C() model = smf.logit('disease ~ C(treatment_group) + age', data=df).fit(disp=0) # Set reference level model = smf.logit('disease ~ C(treatment_group, Treatment("Control")) + age', data=df).fit(disp=0) ``` ## Interaction Terms ### Two-Way Interactions ```python # Interaction between continuous and binary model = smf.logit('disease ~ exposure * age + sex', data=df).fit(disp=0) # The interaction term is 'exposure:age' interaction_coef = model.params['exposure:age'] interaction_or = np.exp(interaction_coef) print(f"Interaction OR: {interaction_or:.4f}") ``` ### Interpreting Interactions ```python # Main effects + interaction # Model: logit(p) = β0 + β1*exposure + β2*age + β3*exposure*age # OR for exposure depends on age: # OR(exposure) = exp(β1 + β3*age) # Example: OR at age=30 vs age=50 beta_exposure = model.params['exposure'] beta_interaction = model.params['exposure:age'] or_age30 = np.exp(beta_exposure + beta_interaction * 30) or_age50 = np.exp(beta_exposure + beta_interaction * 50) print(f"OR (exposure) at age 30: {or_age30:.4f}") print(f"OR (exposure) at age 50: {or_age50:.4f}") ``` ## Adjusted vs Unadjusted Analysis ### Percentage Reduction in OR ```python # Unadjusted (crude) model model_crude = smf.logit('disease ~ exposure', data=df).fit(disp=0) or_crude = np.exp(model_crude.params['exposure']) # Adjusted model model_adj = smf.logit('disease ~ exposure + age + sex + bmi', data=df).fit(disp=0) or_adj = np.exp(model_adj.params['exposure']) # Calculate percentage reduction pct_reduction = (or_crude - or_adj) / or_crude * 100 print(f"Crude OR: {or_crude:.4f}") print(f"Adjusted OR: {or_adj:.4f}") print(f"Percentage reduction: {pct_reduction:.1f}%") # Interpretation if pct_reduction > 10: print("Strong confounding detected") elif pct_reduction > 5: print("Moderate confounding") else: print("Minimal confounding") ``` ## Model Comparison ### Likelihood Ratio Test ```python from scipy import stats # Nested models model_reduced = smf.logit('disease ~ exposure', data=df).fit(disp=0) model_full = smf.logit('disease ~ exposure + age + sex + bmi', data=df).fit(disp=0) # LR test statistic lr_stat = -2 * (model_reduced.llf - model_full.llf) df_diff = model_full.df_model - model_reduced.df_model p_value = stats.chi2.sf(lr_stat, df_diff) print(f"LR statistic: {lr_stat:.4f}") print(f"df: {df_diff}") print(f"p-value: {p_value:.6f}") if p_value < 0.05: print("Full model provides significantly better fit") else: print("Additional predictors not significant") ``` ### AIC/BIC Comparison ```python # Fit multiple models models = { 'Model 1': smf.logit('disease ~ exposure', data=df).fit(disp=0), 'Model 2': smf.logit('disease ~ exposure + age', data=df).fit(disp=0), 'Model 3': smf.logit('disease ~ exposure + age + sex', data=df).fit(disp=0), } # Compare print("Model Comparison:") for name, m in models.items(): print(f"\n{name}:") print(f" AIC: {m.aic:.2f}") print(f" BIC: {m.bic:.2f}") print(f" Pseudo R²: {m.prsquared:.4f}") # Best model (lowest AIC) best_model = min(models.items(), key=lambda x: x[1].aic) print(f"\nBest model (by AIC): {best_model[0]}") ``` ## Prediction ### Predicted Probabilities ```python # Get predicted probabilities for existing data df['predicted_prob'] = model.predict(df) # Predict for new data new_data = pd.DataFrame({ 'exposure': [1], 'age': [45], 'sex': ['M'] }) pred_prob = model.predict(new_data) print(f"Predicted probability: {pred_prob[0]:.4f}") ``` ### Classification ```python # Binary classification with 0.5 threshold df['predicted_class'] = (model.predict(df) > 0.5).astype(int) # Confusion matrix from sklearn.metrics import confusion_matrix, classification_report cm = confusion_matrix(df['disease'], df['predicted_class']) print("Confusion Matrix:") print(cm) # Accuracy, precision, recall print("\nClassification Report:") print(classification_report(df['disease'], df['predicted_class'])) ``` ## Diagnostics ### Influential Observations ```python # Cook's distance from statsmodels.stats.outliers_influence import OLSInfluence # Get influence measures influence = model.get_influence() # Standardized residuals std_resid = influence.resid_studentized # Plot influential points import matplotlib.pyplot as plt plt.scatter(range(len(std_resid)), std_resid) plt.axhline(y=2, color='r', linestyle='--') plt.axhline(y=-2, color='r', linestyle='--') plt.xlabel('Observation') plt.ylabel('Studentized Residual') plt.title('Influential Observations') plt.show() ``` ### Hosmer-Lemeshow Test ```python # Goodness of fit test from statsmodels.stats.diagnostic import _diagnostic_hl # Not directly available in statsmodels # Use manual implementation def hosmer_lemeshow_test(y_true, y_pred, g=10): """Hosmer-Lemeshow goodness of fit test.""" data = pd.DataFrame({'y': y_true, 'pred': y_pred}) data['decile'] = pd.qcut(data['pred'], g, duplicates='drop') obs = data.groupby('decile')['y'].agg(['sum', 'count']) exp = data.groupby('decile')['pred'].agg(['sum', 'count']) hl_stat = ((obs['sum'] - exp['sum'])**2 / (exp['sum'] * (1 - exp['sum']/exp['count']))).sum() p_value = stats.chi2.sf(hl_stat, g-2) return hl_stat, p_value hl_stat, hl_p = hosmer_lemeshow_test(df['disease'], model.predict(df)) print(f"Hosmer-Lemeshow: χ²={hl_stat:.4f}, p={hl_p:.6f}") ``` ## Common Issues ### Separation (Perfect Prediction) **Problem**: One predictor perfectly predicts the outcome. **Solution**: Use Firth logistic regression (penalized likelihood): ```python # This requires logistf package (not in standard statsmodels) # Alternative: Remove the problematic predictor or use regularization # Check for separation print("Value counts by predictor:") print(pd.crosstab(df['exposure'], df['disease'])) # If separation detected, try Ridge logistic from sklearn.linear_model import LogisticRegression lr = LogisticRegression(penalty='l2', C=1.0) lr.fit(df[['exposure', 'age', 'sex']], df['disease']) ``` ### Convergence Failure **Problem**: Model doesn't converge (common with small samples or collinearity). **Solution**: Increase max iterations or check for collinearity: ```python # Increase iterations model = smf.logit('disease ~ exposure + age + sex', data=df).fit(disp=0, maxiter=200) # Check for collinearity from statsmodels.stats.outliers_influence import variance_inflation_factor X = df[['exposure', 'age', 'sex']].copy() X = pd.get_dummies(X, drop_first=True, dtype=float) X['const'] = 1 for i, col in enumerate(X.columns[:-1]): vif = variance_inflation_factor(X.values, i) print(f"{col}: VIF={vif:.2f}") ``` ### Quasi-Complete Separation **Problem**: Predictor almost perfectly separates outcomes. **Symptoms**: Very large coefficients (>10), very large standard errors. **Solution**: Use regularization or remove problematic predictor. ## Reporting Template ```python def report_logistic_regression(model): """Generate publication-quality report.""" report = [] report.append("=== Logistic Regression Results ===\n") report.append(f"N = {int(model.nobs)}") report.append(f"Pseudo R² = {model.prsquared:.4f}") report.append(f"AIC = {model.aic:.2f}\n") report.append("Odds Ratios (95% CI):\n") ors = np.exp(model.params) ci = np.exp(model.conf_int()) for var in model.params.index: if var == 'Intercept': continue or_val = ors[var] ci_lower = ci.loc[var, 0] ci_upper = ci.loc[var, 1] p_val = model.pvalues[var] sig = "*" if p_val < 0.05 else "" report.append(f" {var}: OR={or_val:.4f} ({ci_lower:.4f}-{ci_upper:.4f}), p={p_val:.4f}{sig}") return "\n".join(report) print(report_logistic_regression(model)) ```