519 lines
14 KiB
Markdown
519 lines
14 KiB
Markdown
---
|
|
title: "Statistical Question Patterns"
|
|
task: ""
|
|
lineage_type: import
|
|
upstream_source: https://github.com/mims-harvard/ToolUniverse/blob/e2520a96/skills/tooluniverse-statistical-modeling/references/common_patterns.md
|
|
upstream_sha: e2520a96
|
|
imported_at: 2026-06-26
|
|
prompt_class: prompt
|
|
upstream_changes: accepted
|
|
author: upstream
|
|
validated: false
|
|
---
|
|
|
|
# Statistical Question Patterns
|
|
|
|
Common statistical question patterns with solutions.
|
|
|
|
## Pattern 1: Odds Ratio from Binary Logistic Regression
|
|
|
|
**Question format**: "What is the odds ratio of [outcome] associated with [exposure]?"
|
|
|
|
**Example**: "What is the odds ratio of disease associated with exposure to chemical X?"
|
|
|
|
**Solution**:
|
|
```python
|
|
import statsmodels.formula.api as smf
|
|
import numpy as np
|
|
|
|
# Fit logistic regression
|
|
model = smf.logit('disease ~ exposure + age + sex', data=df).fit(disp=0)
|
|
|
|
# Extract odds ratio
|
|
or_exposure = np.exp(model.params['exposure'])
|
|
ci = np.exp(model.conf_int())
|
|
ci_lower = ci.loc['exposure', 0]
|
|
ci_upper = ci.loc['exposure', 1]
|
|
p_val = model.pvalues['exposure']
|
|
|
|
# Answer
|
|
print(f"Odds Ratio: {or_exposure:.4f}")
|
|
print(f"95% CI: ({ci_lower:.4f}, {ci_upper:.4f})")
|
|
print(f"p-value: {p_val:.6f}")
|
|
```
|
|
|
|
---
|
|
|
|
## Pattern 2: Odds Ratio from Ordinal Logistic Regression
|
|
|
|
**Question format**: "What is the odds ratio of [ordinal outcome] associated with [exposure] in ordinal logistic regression?"
|
|
|
|
**Example**: "What is the odds ratio of COVID-19 severity associated with BCG vaccination?"
|
|
|
|
**Solution**:
|
|
```python
|
|
from statsmodels.miscmodels.ordinal_model import OrderedModel
|
|
import pandas as pd
|
|
import numpy as np
|
|
|
|
# Set up ordered outcome
|
|
severity_order = ['Mild', 'Moderate', 'Severe']
|
|
df['severity'] = pd.Categorical(df['severity'], categories=severity_order, ordered=True)
|
|
y = df['severity'].cat.codes
|
|
|
|
# Prepare predictors
|
|
X = pd.get_dummies(df[['bcg_vaccination', 'age', 'sex']], drop_first=True, dtype=float)
|
|
|
|
# Fit ordinal logit
|
|
model = OrderedModel(y, X, distr='logit').fit(method='bfgs', disp=0)
|
|
|
|
# Extract OR for BCG vaccination (first predictor)
|
|
or_bcg = np.exp(model.params[0])
|
|
ci = np.exp(model.conf_int())
|
|
ci_lower = ci.iloc[0, 0]
|
|
ci_upper = ci.iloc[0, 1]
|
|
p_val = model.pvalues[0]
|
|
|
|
# Answer
|
|
print(f"Odds Ratio (BCG): {or_bcg:.4f}")
|
|
print(f"95% CI: ({ci_lower:.4f}, {ci_upper:.4f})")
|
|
print(f"p-value: {p_val:.6f}")
|
|
```
|
|
|
|
---
|
|
|
|
## Pattern 3: Percentage Reduction in Odds Ratio
|
|
|
|
**Question format**: "What is the percentage reduction in odds ratio for [outcome] after adjusting for [confounders]?"
|
|
|
|
**Example**: "What is the percentage reduction in OR for disease after adjusting for age and sex?"
|
|
|
|
**Solution**:
|
|
```python
|
|
# Unadjusted 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', data=df).fit(disp=0)
|
|
or_adj = np.exp(model_adj.params['exposure'])
|
|
|
|
# Percentage reduction
|
|
pct_reduction = (or_crude - or_adj) / or_crude * 100
|
|
|
|
# Answer
|
|
print(f"Crude OR: {or_crude:.4f}")
|
|
print(f"Adjusted OR: {or_adj:.4f}")
|
|
print(f"Percentage reduction: {pct_reduction:.1f}%")
|
|
```
|
|
|
|
---
|
|
|
|
## Pattern 4: Hazard Ratio from Cox Regression
|
|
|
|
**Question format**: "What is the hazard ratio for [exposure] in a Cox proportional hazards model?"
|
|
|
|
**Example**: "What is the hazard ratio for treatment in a Cox model adjusting for age and stage?"
|
|
|
|
**Solution**:
|
|
```python
|
|
from lifelines import CoxPHFitter
|
|
|
|
# Fit Cox model
|
|
cph = CoxPHFitter()
|
|
cph.fit(df[['time', 'event', 'treatment', 'age', 'stage']],
|
|
duration_col='time', event_col='event')
|
|
|
|
# Extract HR
|
|
hr_treatment = cph.hazard_ratios_['treatment']
|
|
summary = cph.summary
|
|
ci_lower = summary.loc['treatment', 'exp(coef) lower 95%']
|
|
ci_upper = summary.loc['treatment', 'exp(coef) upper 95%']
|
|
p_val = summary.loc['treatment', 'p']
|
|
|
|
# Answer
|
|
print(f"Hazard Ratio: {hr_treatment:.4f}")
|
|
print(f"95% CI: ({ci_lower:.4f}, {ci_upper:.4f})")
|
|
print(f"p-value: {p_val:.6f}")
|
|
print(f"Concordance: {cph.concordance_index_:.4f}")
|
|
```
|
|
|
|
---
|
|
|
|
## Pattern 5: Kaplan-Meier Survival Estimate
|
|
|
|
**Question format**: "What is the Kaplan-Meier survival estimate at time T?"
|
|
|
|
**Example**: "What is the 5-year survival probability in the treatment group?"
|
|
|
|
**Solution**:
|
|
```python
|
|
from lifelines import KaplanMeierFitter
|
|
|
|
# Subset treatment group
|
|
treatment_df = df[df['treatment'] == 1]
|
|
|
|
# Fit KM
|
|
kmf = KaplanMeierFitter()
|
|
kmf.fit(treatment_df['time'], treatment_df['event'])
|
|
|
|
# Survival at 5 years (60 months)
|
|
survival_5yr = kmf.predict(60)
|
|
|
|
# Answer
|
|
print(f"5-year survival probability: {survival_5yr:.4f}")
|
|
print(f"Median survival time: {kmf.median_survival_time_:.1f} months")
|
|
```
|
|
|
|
---
|
|
|
|
## Pattern 6: Interaction Effect
|
|
|
|
**Question format**: "What is the odds ratio associated with the interaction between [A] and [B]?"
|
|
|
|
**Example**: "What is the OR for the interaction between treatment and biomarker status?"
|
|
|
|
**Solution**:
|
|
```python
|
|
# Fit model with interaction
|
|
model = smf.logit('outcome ~ treatment * biomarker + age', data=df).fit(disp=0)
|
|
|
|
# Interaction term
|
|
interaction_coef = model.params['treatment:biomarker']
|
|
interaction_or = np.exp(interaction_coef)
|
|
interaction_p = model.pvalues['treatment:biomarker']
|
|
|
|
# Answer
|
|
print(f"Interaction OR: {interaction_or:.4f}")
|
|
print(f"p-value: {interaction_p:.6f}")
|
|
|
|
# Interpretation
|
|
if interaction_p < 0.05:
|
|
print("Significant interaction: effect of treatment varies by biomarker status")
|
|
```
|
|
|
|
---
|
|
|
|
## Pattern 7: Linear Regression Coefficient
|
|
|
|
**Question format**: "What is the coefficient for [predictor] in a linear regression model?"
|
|
|
|
**Example**: "What is the coefficient for BMI in a linear regression of blood pressure?"
|
|
|
|
**Solution**:
|
|
```python
|
|
import statsmodels.formula.api as smf
|
|
|
|
# Fit OLS
|
|
model = smf.ols('blood_pressure ~ bmi + age + sex', data=df).fit()
|
|
|
|
# Extract coefficient
|
|
coef_bmi = model.params['bmi']
|
|
ci = model.conf_int()
|
|
ci_lower = ci.loc['bmi', 0]
|
|
ci_upper = ci.loc['bmi', 1]
|
|
p_val = model.pvalues['bmi']
|
|
|
|
# Answer
|
|
print(f"Coefficient (BMI): {coef_bmi:.4f}")
|
|
print(f"95% CI: ({ci_lower:.4f}, {ci_upper:.4f})")
|
|
print(f"p-value: {p_val:.6f}")
|
|
print(f"R-squared: {model.rsquared:.4f}")
|
|
|
|
# Interpretation
|
|
print(f"Interpretation: Each 1-unit increase in BMI is associated with {coef_bmi:.2f} mmHg change in blood pressure")
|
|
```
|
|
|
|
---
|
|
|
|
## Pattern 8: Mixed-Effects Model Coefficient
|
|
|
|
**Question format**: "What is the coefficient for [predictor] in a mixed-effects model with random intercepts for [grouping]?"
|
|
|
|
**Example**: "What is the treatment effect in a mixed model with random intercepts for patient?"
|
|
|
|
**Solution**:
|
|
```python
|
|
import statsmodels.formula.api as smf
|
|
|
|
# Fit LMM
|
|
model = smf.mixedlm('outcome ~ treatment + time', data=df, groups=df['patient_id']).fit(reml=True)
|
|
|
|
# Extract fixed effect
|
|
coef_treatment = model.fe_params['treatment']
|
|
se = model.bse_fe['treatment']
|
|
p_val = model.pvalues['treatment']
|
|
ci = model.conf_int()
|
|
ci_lower = ci.loc['treatment', 0]
|
|
ci_upper = ci.loc['treatment', 1]
|
|
|
|
# Answer
|
|
print(f"Coefficient (treatment): {coef_treatment:.4f}")
|
|
print(f"95% CI: ({ci_lower:.4f}, {ci_upper:.4f})")
|
|
print(f"p-value: {p_val:.6f}")
|
|
|
|
# ICC
|
|
group_var = float(model.cov_re.iloc[0, 0])
|
|
resid_var = float(model.scale)
|
|
icc = group_var / (group_var + resid_var)
|
|
print(f"ICC: {icc:.4f}")
|
|
```
|
|
|
|
---
|
|
|
|
## Pattern 9: Model Comparison with AIC/BIC
|
|
|
|
**Question format**: "Which model has better fit according to AIC?"
|
|
|
|
**Example**: "Compare models with and without interaction term using AIC"
|
|
|
|
**Solution**:
|
|
```python
|
|
# Fit models
|
|
model1 = smf.logit('outcome ~ A + B', data=df).fit(disp=0)
|
|
model2 = smf.logit('outcome ~ A * B', data=df).fit(disp=0)
|
|
|
|
# Compare
|
|
print("Model 1 (no interaction):")
|
|
print(f" AIC: {model1.aic:.2f}")
|
|
print(f" BIC: {model1.bic:.2f}")
|
|
|
|
print("\nModel 2 (with interaction):")
|
|
print(f" AIC: {model2.aic:.2f}")
|
|
print(f" BIC: {model2.bic:.2f}")
|
|
|
|
# Answer
|
|
if model2.aic < model1.aic:
|
|
print("\nModel 2 preferred (lower AIC)")
|
|
else:
|
|
print("\nModel 1 preferred (lower AIC)")
|
|
```
|
|
|
|
---
|
|
|
|
## Pattern 10: Proportional Odds Assumption Test
|
|
|
|
**Question format**: "Is the proportional odds assumption met?"
|
|
|
|
**Example**: "Test if proportional odds assumption holds for ordinal severity model"
|
|
|
|
**Solution**:
|
|
```python
|
|
import statsmodels.api as sm
|
|
|
|
# Fit binary logits at each cutpoint
|
|
severity_order = ['Mild', 'Moderate', 'Severe']
|
|
df['severity'] = pd.Categorical(df['severity'], categories=severity_order, ordered=True)
|
|
y_codes = df['severity'].cat.codes
|
|
|
|
X = pd.get_dummies(df[['exposure', 'age']], drop_first=True, dtype=float)
|
|
X_const = sm.add_constant(X)
|
|
|
|
# Fit at each cutpoint
|
|
coef_by_cutpoint = {}
|
|
for k in range(len(severity_order) - 1):
|
|
y_binary = (y_codes > k).astype(int)
|
|
model = sm.Logit(y_binary, X_const).fit(disp=0)
|
|
coef_by_cutpoint[k] = model.params['exposure']
|
|
|
|
# Check if coefficients similar
|
|
coefs = list(coef_by_cutpoint.values())
|
|
coef_range = max(coefs) - min(coefs)
|
|
|
|
print(f"Coefficients by cutpoint: {coefs}")
|
|
print(f"Range: {coef_range:.4f}")
|
|
|
|
# Answer
|
|
if coef_range < 0.5:
|
|
print("Proportional odds assumption likely satisfied")
|
|
else:
|
|
print("Proportional odds assumption may be violated")
|
|
```
|
|
|
|
---
|
|
|
|
## Pattern 11: Log-Rank Test
|
|
|
|
**Question format**: "Is there a significant difference in survival between groups?"
|
|
|
|
**Example**: "Test if survival differs between treatment and control groups"
|
|
|
|
**Solution**:
|
|
```python
|
|
from lifelines.statistics import logrank_test
|
|
|
|
# Split by group
|
|
treatment_group = df['treatment'] == 1
|
|
control_group = df['treatment'] == 0
|
|
|
|
# Log-rank test
|
|
result = logrank_test(
|
|
df.loc[treatment_group, 'time'],
|
|
df.loc[control_group, 'time'],
|
|
df.loc[treatment_group, 'event'],
|
|
df.loc[control_group, 'event']
|
|
)
|
|
|
|
# Answer
|
|
print(f"Log-rank test statistic: {result.test_statistic:.4f}")
|
|
print(f"p-value: {result.p_value:.6f}")
|
|
|
|
if result.p_value < 0.05:
|
|
print("Survival curves are significantly different")
|
|
else:
|
|
print("No significant difference in survival")
|
|
```
|
|
|
|
---
|
|
|
|
## Pattern 12: R-squared Interpretation
|
|
|
|
**Question format**: "What is the R-squared of the model?"
|
|
|
|
**Example**: "What proportion of variance is explained by the linear model?"
|
|
|
|
**Solution**:
|
|
```python
|
|
model = smf.ols('outcome ~ predictor1 + predictor2 + age', data=df).fit()
|
|
|
|
# R-squared
|
|
r2 = model.rsquared
|
|
adj_r2 = model.rsquared_adj
|
|
|
|
# Answer
|
|
print(f"R-squared: {r2:.4f}")
|
|
print(f"Adjusted R-squared: {adj_r2:.4f}")
|
|
print(f"Interpretation: {r2*100:.1f}% of variance in outcome is explained by predictors")
|
|
```
|
|
|
|
---
|
|
|
|
## Pattern 13: Concordance Index Interpretation
|
|
|
|
**Question format**: "What is the concordance index of the Cox model?"
|
|
|
|
**Example**: "How well does the Cox model discriminate between patients?"
|
|
|
|
**Solution**:
|
|
```python
|
|
# Already fitted cph model
|
|
c_index = cph.concordance_index_
|
|
|
|
# Answer
|
|
print(f"Concordance index: {c_index:.4f}")
|
|
|
|
# Interpretation
|
|
if c_index > 0.7:
|
|
print("Good discrimination (C > 0.7)")
|
|
elif c_index > 0.6:
|
|
print("Acceptable discrimination (0.6 < C < 0.7)")
|
|
else:
|
|
print("Poor discrimination (C < 0.6)")
|
|
```
|
|
|
|
---
|
|
|
|
## Pattern 14: Coefficient Change After Adjustment
|
|
|
|
**Question format**: "How does the coefficient change after adjusting for confounders?"
|
|
|
|
**Example**: "Compare exposure coefficient before and after adjusting for age"
|
|
|
|
**Solution**:
|
|
```python
|
|
# Unadjusted
|
|
model_crude = smf.ols('outcome ~ exposure', data=df).fit()
|
|
coef_crude = model_crude.params['exposure']
|
|
|
|
# Adjusted
|
|
model_adj = smf.ols('outcome ~ exposure + age + sex', data=df).fit()
|
|
coef_adj = model_adj.params['exposure']
|
|
|
|
# Change
|
|
absolute_change = coef_adj - coef_crude
|
|
pct_change = (coef_adj - coef_crude) / coef_crude * 100
|
|
|
|
# Answer
|
|
print(f"Crude coefficient: {coef_crude:.4f}")
|
|
print(f"Adjusted coefficient: {coef_adj:.4f}")
|
|
print(f"Absolute change: {absolute_change:.4f}")
|
|
print(f"Percentage change: {pct_change:.1f}%")
|
|
```
|
|
|
|
---
|
|
|
|
## Pattern 15: Stratified Analysis
|
|
|
|
**Question format**: "What is the odds ratio stratified by [variable]?"
|
|
|
|
**Example**: "What is the OR for exposure separately in men and women?"
|
|
|
|
**Solution**:
|
|
```python
|
|
# Stratify by sex
|
|
results_by_sex = {}
|
|
|
|
for sex in ['M', 'F']:
|
|
df_subset = df[df['sex'] == sex]
|
|
model = smf.logit('outcome ~ exposure + age', data=df_subset).fit(disp=0)
|
|
|
|
or_exposure = np.exp(model.params['exposure'])
|
|
ci = np.exp(model.conf_int())
|
|
|
|
results_by_sex[sex] = {
|
|
'OR': or_exposure,
|
|
'CI_lower': ci.loc['exposure', 0],
|
|
'CI_upper': ci.loc['exposure', 1],
|
|
'p_value': model.pvalues['exposure']
|
|
}
|
|
|
|
# Answer
|
|
for sex, result in results_by_sex.items():
|
|
print(f"\n{sex}:")
|
|
print(f" OR: {result['OR']:.4f}")
|
|
print(f" 95% CI: ({result['CI_lower']:.4f}, {result['CI_upper']:.4f})")
|
|
print(f" p-value: {result['p_value']:.6f}")
|
|
```
|
|
|
|
---
|
|
|
|
## Quick Reference Table
|
|
|
|
| Pattern | Model Type | Key Output | Formula Example |
|
|
|---------|------------|------------|-----------------|
|
|
| 1 | Binary Logistic | Odds Ratio | `logit('y ~ x + z')` |
|
|
| 2 | Ordinal Logistic | Odds Ratio | `OrderedModel(y, X)` |
|
|
| 3 | Logistic (2 models) | % Reduction | Compare crude vs adjusted |
|
|
| 4 | Cox PH | Hazard Ratio | `cph.fit(..., duration_col, event_col)` |
|
|
| 5 | Kaplan-Meier | Survival Prob | `kmf.predict(time)` |
|
|
| 6 | Logistic + Interaction | Interaction OR | `'y ~ A * B'` |
|
|
| 7 | Linear Regression | Coefficient | `ols('y ~ x + z')` |
|
|
| 8 | Mixed-Effects | Fixed Effect | `mixedlm(..., groups=...)` |
|
|
| 9 | Model Comparison | AIC/BIC | Compare `.aic` values |
|
|
| 10 | Ordinal Assumption | PO Test | Binary logits at cutpoints |
|
|
| 11 | Survival Comparison | Log-rank | `logrank_test(...)` |
|
|
| 12 | Linear Regression | R² | `.rsquared` |
|
|
| 13 | Cox PH | C-index | `.concordance_index_` |
|
|
| 14 | Any Regression | Coef Change | Compare models |
|
|
| 15 | Stratified Analysis | Stratum-specific OR | Subset + fit |
|
|
|
|
---
|
|
|
|
## Common Mistakes to Avoid
|
|
|
|
1. **Forgetting to exponentiate**: Logistic/Cox coefficients are log-odds/log-hazards. Must use `np.exp()` for ORs/HRs.
|
|
|
|
2. **Wrong variable type**: Ordinal outcomes need `OrderedModel`, not `Logit`.
|
|
|
|
3. **Missing confounders**: Always check if question specifies "adjusting for" variables.
|
|
|
|
4. **Interpretation direction**: HR > 1 = worse outcome (higher hazard), OR > 1 = higher odds.
|
|
|
|
5. **Precision**: Round to requested decimal places (typically 4 for ORs/HRs, 6 for p-values).
|
|
|
|
6. **CI extraction**: Use `conf_int()` method, not `conf_int` attribute.
|
|
|
|
7. **Formula syntax**: Interactions use `*`, not `+`. E.g., `'y ~ A * B'` includes A, B, and A:B.
|
|
|
|
8. **Duration/event cols**: Cox models require explicit `duration_col` and `event_col` parameters.
|