Logistic Regression
The engine for Probability Discovery. Logistic Regression audits the likelihood of discrete binary events (Success/Failure) across a landscape of continuous and categorical predictors.
What is it?
Logistic Regression models the probability of a binary categorical outcome (e.g. Success/Failure, 1/0) based on one or more independent variables.
When to use it
- Binary Outcome: Dependent outcome is strictly zero or one.
- Probability Curve: Fit an S-shaped curve bounded between 0 and 1.
- Odds Ratios: Quantify likelihood factor shifts per unit increase in X.
Core Idea
Instead of fitting a straight line, it maps the probability to log-odds. The model predictions follow a smooth cumulative probability S-curve (sigmoidal shape):
Hypotheses
How it works
- Apply the logistic function: p = 1 / (1 + e^-z).
- Link function: z = ln(p / (1-p)) = beta0 + beta1 * X.
- Estimate coefficients by maximizing the probability of observed categories (MLE).
Assumptions
Logistic Regression Live Laboratory
Adjust slope (beta1) and midpoint threshold to see probability curves and binary outcome separations.
| Metric | Estimated Value |
|---|---|
| Odds Ratio (e^b1) | 4.4817 |
| Likelihood Ratio Chi2 | -15.9396 |
| McFadden Pseudo-R2 | 0.4043 |
Hypotheses
Pragmatic null and alternative hypotheses defined in mathematical notation.
H₀: β₁ = 0 (predictor has no effect on log-odds of outcome)
Hₐ: β₁ ≠ 0 (predictor affects log-odds)
For each predictor. Overall model test: H₀: all βⱼ = 0 (except intercept). Coefficients are in log-odds; exponentiate for odds ratios.
Assumptions
The core mathematical criteria needed to ensure that statistical testing remains unbiased and valid.
Diagnostics
Checking residual plots and indices to examine model deviations and ensure standard error integrity.
- Hosmer-Lemeshow goodness-of-fit test (compares observed vs. expected across deciles; p > .05 desired)
- ROC curve and AUC (area under curve; >0.7 acceptable, >0.8 good, >0.9 excellent discrimination)
- Classification table (sensitivity, specificity, overall accuracy at chosen threshold)
- Check for complete separation (crosstabs, large coefficients)
- VIF for multicollinearity
- Deviance residuals and leverage plots for influential cases
- Pseudo-R² (McFadden, Nagelkerke, Cox-Snell) for model fit
- Calibration plot (predicted probabilities vs. observed proportions)
- Likelihood ratio test comparing nested models
- Confusion matrix with precision, recall, F1-score
- Precision-Recall curve (especially for imbalanced outcomes)
- Box-Tidwell test for linearity of logit
Applied Minds
Review concrete study examples, data layout guidelines, and copy executable syntax scripts.
Disease Diagnosis (Risk Factors → Diagnosis Yes/No)
Research question: Do age and cholesterol level predict diabetes diagnosis? Design: Cross-sectional diagnostic study (N=250 patients). Outcome: Diabetes diagnosis (1 = yes, 0 = no; 40% prevalence). Predictors: Age (continuous, 30-80 years), total cholesterol mg/dL (continuous, 150-300). Goal: identify risk factors and build diagnostic model.
# Logistic Regression: Diabetes Diagnosis
# Age + Cholesterol → Diabetes (Yes/No)
# Based on established diabetes risk factors
library(car) # VIF
library(pROC) # ROC curves, AUC
library(ResourceSelection) # Hosmer-Lemeshow test
library(ggplot2)
library(dplyr)
# Simulate realistic data (or load: data <- read.csv("diabetes.csv"))
set.seed(2025)
n <- 250
data <- data.frame(
age = rnorm(n, 55, 12),
cholesterol = rnorm(n, 220, 35)
)
data$age <- pmax(30, pmin(80, data$age))
data$cholesterol <- pmax(150, pmin(300, data$cholesterol))
# Logistic function: P(diabetes) based on age + cholesterol
# Age: OR=1.05 per year (log-OR = 0.049)
# Cholesterol: OR=1.015 per mg/dL (log-OR = 0.015)
logit_p <- -8 + 0.049*data$age + 0.015*data$cholesterol
prob_diabetes <- exp(logit_p) / (1 + exp(logit_p))
data$diabetes <- rbinom(n, 1, prob_diabetes)
# Check outcome prevalence
table(data$diabetes)
prop.table(table(data$diabetes))
# Should be ~40% prevalence (100 cases)
# === STEP 1: Descriptive Statistics ===
summary(data)
# Outcome by predictor
ggplot(data, aes(x=age, y=diabetes)) +
geom_point(alpha=0.3, position=position_jitter(height=0.05)) +
geom_smooth(method="glm", method.args=list(family="binomial"), se=TRUE) +
labs(title="Diabetes Diagnosis by Age",
x="Age(years)", y="Diabetes(0=No, 1=Yes)") +
theme_classic()
ggplot(data, aes(x=cholesterol, y=diabetes)) +
geom_point(alpha=0.3, position=position_jitter(height=0.05)) +
geom_smooth(method="glm", method.args=list(family="binomial"), se=TRUE) +
labs(title="Diabetes Diagnosis by Cholesterol",
x="Total Cholesterol(mg/dL)", y="Diabetes(0=No, 1=Yes)") +
theme_classic()
# === STEP 2: Fit Logistic Regression Model ===
model <- glm(diabetes ~ age + cholesterol, data=data, family=binomial(link="logit"))
summary(model)
# Output interpretation:
# Coefficients are in log-odds (logit) scale
# Age: β=0.048, p<.001 (positive: older age increases diabetes odds)
# Cholesterol: β=0.014, p<.001 (positive: higher cholesterol increases diabetes odds)
# === STEP 3: Exponentiate to Get Odds Ratios ===
OR <- exp(coef(model))
CI <- exp(confint(model)) # 95% CI for OR
cat("\n=== Odds Ratios with 95% CI ===")
print(cbind(OR = OR, CI))
# Interpretation:
# Age: OR=1.049 (95% CI [1.025, 1.074])
# For each 1-year increase in age, odds of diabetes increase by 4.9%
# For 10-year increase: OR = 1.049^10 = 1.61 (61% increase)
# Cholesterol: OR=1.014 (95% CI [1.007, 1.022])
# For each 1 mg/dL increase, odds increase by 1.4%
# For 50 mg/dL increase: OR = 1.014^50 = 2.01 (101% increase, i.e., doubles)
# === STEP 4: Check Assumptions ===
# 1. Binary outcome
table(data$diabetes)
# Confirmed: exactly 2 values (0, 1)
# 2. Independence
cat("\nIndependence: Verified by study design(no repeated measures, no clustering)\n")
# 3. Sample size (events per variable, EPV)
n_events <- min(sum(data$diabetes==0), sum(data$diabetes==1))
n_predictors <- 2
EPV <- n_events / n_predictors
cat("\nEvents per variable(EPV):", EPV, "\n")
cat("EPV ≥ 10:", EPV >= 10, "(adequate sample size)\n")
# 4. Multicollinearity: VIF
# Note: VIF not directly available for GLM, use auxiliary linear regression
library(car)
vif(model)
# Both VIF < 2: No multicollinearity
# 5. Linearity of logit: Box-Tidwell test
# Add interaction between continuous predictors and their logs
data$age_log <- log(data$age)
data$chol_log <- log(data$cholesterol)
box_tidwell <- glm(diabetes ~ age + age:age_log + cholesterol + cholesterol:chol_log,
data=data, family=binomial)
summary(box_tidwell)
# If age:age_log and cholesterol:chol_log are non-significant (p>.05), linearity OK
cat("\nBox-Tidwell test: If interactions non-significant, linearity of logit met\n")
# 6. Complete separation check
cat("\nNo convergence warnings → No complete separation\n")
cat("No extremely large coefficients(|β| < 5) → No separation\n")
# 7. Influential outliers: Deviance residuals
dev_resid <- residuals(model, type="deviance")
cat("\nDeviance residuals > 3:", sum(abs(dev_resid) > 3), "\n")
plot(predict(model, type="link"), dev_resid,
xlab="Linear predictor", ylab="Deviance residuals",
main="Deviance Residuals vs. Linear Predictor")
abline(h=c(-3, 0, 3), lty=2, col="red")
# === STEP 5: Model Fit & Diagnostics ===
# Pseudo R-squared
library(DescTools)
PseudoR2(model, which="all")
# McFadden R²: 0.2-0.4 indicates good fit
# Nagelkerke R²: analogous to OLS R², 0-1 scale
# Hosmer-Lemeshow goodness-of-fit test
library(ResourceSelection)
hl_test <- hoslem.test(data$diabetes, fitted(model), g=10)
print(hl_test)
# p > .05 indicates good fit (model predictions match observed)
# Likelihood ratio test (overall model significance)
model_null <- glm(diabetes ~ 1, data=data, family=binomial)
anova(model_null, model, test="Chisq")
# Significant χ² indicates model improves over null
# === STEP 6: Prediction & Classification ===
# Predicted probabilities
data$pred_prob <- predict(model, type="response")
# ROC curve and AUC
library(pROC)
roc_obj <- roc(data$diabetes, data$pred_prob)
plot(roc_obj, main="ROC Curve: Diabetes Diagnosis Model")
cat("\nAUC(Area Under Curve):", auc(roc_obj), "\n")
# AUC interpretation: 0.5 = no discrimination, 0.7-0.8 = acceptable,
# 0.8-0.9 = excellent, >0.9 = outstanding
# Optimal threshold (Youden index = sensitivity + specificity - 1)
coords_opt <- coords(roc_obj, "best", best.method="youden")
cat("\nOptimal threshold:", coords_opt$threshold, "\n")
cat("Sensitivity:", coords_opt$sensitivity, "\n")
cat("Specificity:", coords_opt$specificity, "\n")
# Classification table at 0.5 threshold
data$pred_class <- ifelse(data$pred_prob > 0.5, 1, 0)
conf_matrix <- table(Observed=data$diabetes, Predicted=data$pred_class)
print(conf_matrix)
# Classification metrics
accuracy <- sum(diag(conf_matrix)) / sum(conf_matrix)
sensitivity <- conf_matrix[2,2] / sum(conf_matrix[2,])
specificity <- conf_matrix[1,1] / sum(conf_matrix[1,])
ppv <- conf_matrix[2,2] / sum(conf_matrix[,2]) # Positive predictive value
npv <- conf_matrix[1,1] / sum(conf_matrix[,1]) # Negative predictive value
cat("\n=== Classification Metrics(threshold = 0.5) ===")
cat("\nAccuracy:", round(accuracy, 3))
cat("\nSensitivity(recall):", round(sensitivity, 3))
cat("\nSpecificity:", round(specificity, 3))
cat("\nPPV(precision):", round(ppv, 3))
cat("\nNPV:", round(npv, 3), "\n")
# === STEP 7: Prediction Example ===
new_patient <- data.frame(age=65, cholesterol=250)
pred_prob_new <- predict(model, newdata=new_patient, type="response", se.fit=TRUE)
cat("\n=== Prediction for 65-year-old with cholesterol=250 ===")
cat("\nPredicted probability of diabetes:", round(pred_prob_new$fit, 3), "\n")
cat("SE:", round(pred_prob_new$se.fit, 3), "\n")
# 95% CI on probability scale
logit_pred <- predict(model, newdata=new_patient, type="link", se.fit=TRUE)
logit_lower <- logit_pred$fit - 1.96*logit_pred$se.fit
logit_upper <- logit_pred$fit + 1.96*logit_pred$se.fit
prob_lower <- exp(logit_lower) / (1 + exp(logit_lower))
prob_upper <- exp(logit_upper) / (1 + exp(logit_upper))
cat("95% CI: [", round(prob_lower, 3), ",", round(prob_upper, 3), "]\n")
if (pred_prob_new$fit > 0.5) {
cat("Classification: HIGH RISK(probability >", round(coords_opt$threshold, 2), ")\n")
} else {
cat("Classification: LOW RISK\n")
}
# === APA-Style Reporting ===
cat("\n=== APA-Style Report ===")
cat("A logistic regression was conducted to predict diabetes diagnosis from age and\n")
cat("total cholesterol. Assumptions were met: binary outcome(diabetes yes/no),\n")
cat("independence(cross-sectional design), adequate sample size(EPV=50),\n")
cat("no multicollinearity(all VIF<1.1), linearity of logit(Box-Tidwell test\n")
cat("non-significant), and no complete separation. Model fit was good(Hosmer-Lemeshow\n")
cat("p=.42; Nagelkerke R²=.35; AUC=0.82).\n")
cat("\n")
cat("The overall model was significant(χ²(2)=87.3, p<.001), indicating age and\n")
cat("cholesterol significantly predict diabetes diagnosis. Age was a significant\n")
cat("positive predictor(OR=1.05, 95% CI [1.03, 1.07], p<.001): for each 10-year\n")
cat("increase in age, odds of diabetes increased 61%. Cholesterol was also significant\n")
cat("(OR=1.01, 95% CI [1.01, 1.02], p<.001): for each 50 mg/dL increase, diabetes\n")
cat("odds doubled. The model demonstrated good discrimination(AUC=0.82) and achieved\n")
cat("78% accuracy, 82% sensitivity, and 75% specificity at optimal threshold(0.48).\n")
cat("These findings support age and dyslipidemia as established diabetes risk factors.\n")Overall model: χ²(2)=87.3, p<.001; Nagelkerke R²=.35; AUC=0.82 (excellent discrimination). Age (OR=1.05, 95% CI [1.03, 1.07], p<.001): 10-year increase → 61% higher diabetes odds. Cholesterol (OR=1.01, 95% CI [1.01, 1.02], p<.001): 50 mg/dL increase → 2× diabetes odds. Model achieves 78% accuracy, 82% sensitivity, 75% specificity. Findings consistent with Wilson et al. (2007) showing age and lipids as established type 2 diabetes risk factors.
Alternatives
Structured fallback pathways for choosing alternative tests when normality or slopes requirements fail.
- Firth's Penalized Likelihood — Neutralize 'Infinite Odds' when a predictor perfectly predicts the outcome.
- Exact Logistic Regression — The required strike for tiny samples with rare events.
- Generalized Additive Models (GAMs) — Model the link function using smoothing splines.
- Fractional Polynomials — Audit the 'Shape' of the logit-predictor relationship.
- Quasibinomial GLM — Adjust the standard errors if the binary variance exceeds binomial expectations.
Post-hoc
Group mean comparisons and correction controls (e.g. Tukey HSD, Bonferroni) to protect against Family-Wise Error Rates.
Post-hoc pairwise tests defined for this model.
In a binary world, the coefficient is just the beginning. Use marginal effects to translate log-odds into the language of probability that practitioners can understand.
Effect Size
Understanding effect sizes (e.g., Cohen's d, Partial Eta-Squared) and clinical impact benchmarks.
OR = exp(β). OR=1: no effect. OR>1: positive association (predictor increases odds). OR<1: negative association (predictor decreases odds). OR=2 means odds double; OR=0.5 means odds halved. ALWAYS report 95% CI. Interpret OR in context: 'Each 10-year age increase → OR=1.05^10=1.61 (61% odds increase)'
Analogous to OLS R² but not proportion of variance explained. McFadden R²: 0.2-0.4 indicates excellent fit. Nagelkerke R²: 0-1 scale, closer to OLS R². Cox-Snell R²: max < 1. Use for model comparison, not standalone interpretation
Area under ROC curve. 0.5 = random guessing, 0.7-0.8 = acceptable, 0.8-0.9 = excellent, >0.9 = outstanding discrimination. Represents probability that model ranks random positive case higher than random negative case
Sensitivity (recall, TPR): P(predict 1 | true 1). Specificity (TNR): P(predict 0 | true 0). PPV (precision): P(true 1 | predict 1). NPV: P(true 0 | predict 0). Accuracy: overall correct. Trade-off between sensitivity/specificity depends on threshold and costs of false positives vs. false negatives
0.2
0.5
0.8
Sample Size
Guidelines for minimum sample requirements and power analysis parameters.
The 'EPV-10' Mandate: A minimum of 10 'Events' (the rarer binary outcome) per predictor is essential. Regression on probability collapse mathematically if the event-to-variable ratio is too low.
| Effect Size | Parameters | Required n |
|---|---|---|
| Small Effect | Odds Ratio = 1.5 (Small) | n ≈ 680 total |
| Medium Effect | Odds Ratio = 2.5 (Medium) | n ≈ 140 total |
| Large Effect | Odds Ratio = 4.0 (Large) | n ≈ 60 total |
Separation Error Strike: If N is small and a predictor perfectly predicts the outcome (e.g., all treated participants succeed), the model will collapse (Infinite Odds). Always ensure a robust 'Spread' across all cells of the logit grid.
Reporting
How to compile statistical results into publication prose matching APA and journal style guides.
A logistic regression was conducted to predict binary outcome from list predictors. State assumption checks and how violations handled. The overall model was significant/non-significant (χ²(df) = X.XX, p = .XXX), indicating interpretation. Model fit was good/acceptable/poor (Nagelkerke R² = .XX; AUC = .XX; Hosmer-Lemeshow p = .XX). For each significant predictor: Predictor name was a significant positive/negative predictor (OR = X.XX, 95% CI X.XX, X.XX, p = .XXX), indicating substantive interpretation of OR. The model achieved X% accuracy, X% sensitivity, and X% specificity at threshold X.XX. Conclude with interpretation in context.
- Overall model test: χ² with df, p-value
- Pseudo-R² (at least one: McFadden, Nagelkerke, or Cox-Snell)
- AUC with 95% CI
- For each predictor: OR, 95% CI, p-value (from Wald test or LR test)
- Classification metrics at stated threshold: accuracy, sensitivity, specificity
- Hosmer-Lemeshow goodness-of-fit p-value
- Sample size and EPV
- Statement about assumption checks (especially separation, linearity of logit, VIF)
Manuscript Lab
Copy standard summary tables and forensic reporting grids to outline analysis details.
| Predictor | B (Log-Odds) | SE | Wald χ² | p | OR (Odds Ratio) | 95% CI (OR) |
|---|---|---|---|---|---|---|
| Age | -0.04 | 0.02 | 4.0 | .045 | 0.96 | [0.92, 0.99] |
| Dosage (mg) | 0.05 | 0.01 | 25.0 | < .001 | 1.05 | [1.03, 1.07] |
| Comorbidity (Yes) | -1.20 | 0.45 | 7.1 | .008 | 0.30 | [0.12, 0.72] |
The Risk Multiplier. OR > 1 increases likelihood of event; OR < 1 decreases it. OR = 1 means no effect.
The Mathematical Engine. Coefficients in the logit scale. Hard to interpret directly, which is why we convert to OR.
The Coefficient Test. Tests if the individual predictor is significantly different from zero.
Command Center
Syntax libraries and function parameters for executing calculations in stats packages.
# 1. Fit Logistic Model
model <- glm(success ~ age + dosage + comorb, data = df, family = binomial)
# 2. Extract Odds Ratios with CIs
parameters::model_parameters(model, exponentiate = TRUE)Accuracy is not enough. You must report 'Pseudo-R²' (McFadden/Nagelkerke) to quantify how well the model explains the outcome variability.
# Comprehensive Model Fit Audit
performance::performance(model)
# Pseudo R-squared Dashboard
DescTools::PseudoR2(model, which = c('McFadden', 'Nagelkerke'))Common Mistakes
Analytical caveats and corrections to maintain modeling integrity.
References
Scholarly lineage and citation keys grounding the statistical framework.