Medical AI · Machine learning · Lesson 3 of 9
A logistic-regression risk score
Reviewed by Dr. Ram Kumar G, MBBS, MD (Internal Medicine), MPH (USA), on · Last updated
- Intermediate
- Technical focus
- About 35 minutes
- Includes exercise
Before this lesson: Building a cohort and avoiding data leakage, Diagnostic accuracy metrics: sensitivity, specificity, PPV, ROC and calibration
In short
Logistic regression models the log-odds of an outcome as a weighted sum of predictors, giving a probability for each patient and an odds ratio for each predictor. For many clinical prediction problems with tabular data it performs as well as more complex machine-learning methods, is easy to check and explain, and can be turned into a simple points score. Building one well means choosing predictors in advance, having enough outcome events, and checking discrimination and calibration on held-out data.
You will learn to
- Fit a logistic-regression risk model in scikit-learn and interpret its odds ratios
- Evaluate it on held-out data with AUROC, calibration slope and intercept
- Convert the model into an integer points score and check what that costs
- Plan sample size and predictor choice before modelling
Why start with logistic regression
Logistic regression estimates the probability of a binary outcome, such as developing type 2 diabetes within five years, from a set of predictors. Each coefficient is the change in log-odds per unit of a predictor, and its exponent is an odds ratio.
A systematic review of 71 studies found no performance benefit, on average, of machine-learning methods over logistic regression for clinical prediction models (Christodoulou et al., 2019). More flexible models can help with very large datasets, images or strong interactions, but logistic regression is the baseline any new model should beat.
A synthetic cohort
The code simulates 5,000 adults with five predictors and a five-year diabetes outcome. The 'true' relationships are set in the code, so we can check whether the model recovers them. These numbers are invented for teaching and must not be used as clinical risk estimates.
import numpy as np
import pandas as pd
rng = np.random.default_rng(5)
n = 5000
df = pd.DataFrame({
"age": rng.integers(30, 75, n),
"bmi": rng.normal(27, 4.5, n).round(1),
"fasting_glucose": rng.normal(100, 10, n).round(), # mg/dL
"family_history": rng.binomial(1, 0.3, n),
"hypertension": rng.binomial(1, 0.35, n),
})
logit = (-17.5 + 0.03 * df.age + 0.10 * df.bmi + 0.11 * df.fasting_glucose
+ 0.6 * df.family_history + 0.4 * df.hypertension)
df["diabetes_5y"] = rng.binomial(1, 1 / (1 + np.exp(-logit)))
print(f"{n} synthetic adults, 5-year incidence {df.diabetes_5y.mean():.1%}")
# 5000 synthetic adults, 5-year incidence 19.4%Fitting the model and reading odds ratios
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
predictors = ["age", "bmi", "fasting_glucose", "family_history", "hypertension"]
train, test = train_test_split(df, test_size=0.3, random_state=0, stratify=df.diabetes_5y)
model = LogisticRegression(penalty=None, max_iter=1000)
model.fit(train[predictors], train.diabetes_5y)
odds_ratios = pd.Series(np.exp(model.coef_[0]), index=predictors)
print(odds_ratios.round(2))
# age 1.03
# bmi 1.09
# fasting_glucose 1.12
# family_history 2.04
# hypertension 1.65- Odds ratios are per unit: each extra year of age multiplies the odds by 1.03, each extra mg/dL of fasting glucose by 1.12. Per unit, continuous predictors look small; per 10 mg/dL of glucose the odds ratio is about 1.12¹⁰ ≈ 3.1.
- penalty=None gives the classical unpenalised model. scikit-learn applies L2 penalisation by default, which shrinks coefficients; that is often sensible for prediction but changes the odds ratios.
- As the previous lesson explained, these odds ratios describe associations within this model, not the effect of changing a patient's BMI or glucose.
Checking discrimination and calibration
from sklearn.metrics import roc_auc_score
p = model.predict_proba(test[predictors])[:, 1]
print(f"Test AUROC {roc_auc_score(test.diabetes_5y, p):.3f}")
# Calibration slope and intercept: regress the outcome on the model's log-odds
log_odds = np.log(p / (1 - p)).reshape(-1, 1)
recal = LogisticRegression(penalty=None).fit(log_odds, test.diabetes_5y)
print(f"Calibration slope {recal.coef_[0][0]:.2f}, intercept {recal.intercept_[0]:.2f}")
print(f"Mean predicted risk {p.mean():.1%}, observed {test.diabetes_5y.mean():.1%}")
# Test AUROC 0.793
# Calibration slope 0.99, intercept 0.04
# Mean predicted risk 18.7%, observed 19.4%- A calibration slope near 1 means predictions are neither too extreme nor too modest; below 1 usually signals overfitting.
- Here the model is well calibrated because the test data come from the same simulation. In a new population, calibration often shifts and may need updating.
- The usual intercept-only calibration check fixes the slope at 1. This simple two-parameter version is enough for learning; the BMC Medicine paper in the sources gives the full approach.
From model to points score
Many clinical scores turn regression coefficients into integer points, so they can be added up without a computer. Each predictor's points are its coefficient, per clinically meaningful step, divided by a reference amount, here the coefficient for 10 years of age.
coef = pd.Series(model.coef_[0], index=predictors)
steps = {"age": 10, "bmi": 5, "fasting_glucose": 10, "family_history": 1, "hypertension": 1}
unit = coef["age"] * 10
points = {k: round(coef[k] * s / unit) for k, s in steps.items()}
print(points)
score = ((test.age - 30) / 10 * points["age"]
+ (test.bmi - 25) / 5 * points["bmi"]
+ (test.fasting_glucose - 90) / 10 * points["fasting_glucose"]
+ test.family_history * points["family_history"]
+ test.hypertension * points["hypertension"])
print(f"Points score AUROC {roc_auc_score(test.diabetes_5y, score):.3f}")
# {'age': 1, 'bmi': 2, 'fasting_glucose': 4, 'family_history': 2, 'hypertension': 2}
# Points score AUROC 0.793- Rounding to integer points cost nothing measurable in AUROC here, because the rounded weights keep almost the same ranking of patients.
- A points total ranks patients but is not a probability. To report risk, map each total back to the model's predicted probability or to observed risk in a validation set.
- Real scores often also bin continuous predictors (for example BMI 25–29.9, 30–34.9), which loses more information; check AUROC and calibration after any simplification.
| Predictor | Step | Points |
|---|---|---|
| Age | Per 10 years above 30 | 1 |
| BMI | Per 5 kg/m² above 25 | 2 |
| Fasting glucose | Per 10 mg/dL above 90 | 4 |
| Family history of diabetes | Present | 2 |
| Hypertension | Present | 2 |
Planning before modelling
Most of the quality of a risk model is decided before any code runs.
- Choose candidate predictors from clinical knowledge and the literature, defined as they will be available at time zero, rather than by screening many variables for significance.
- Check the sample size. Riley and colleagues' method calculates the number of participants and outcome events needed for a given number of predictor parameters and expected performance; the older rule of '10 events per variable' is often too low.
- Keep continuous predictors continuous where possible, and consider splines for non-linear relationships, rather than splitting them into categories early.
- Plan how missing data will be handled, and how the model will be validated internally (bootstrap or cross-validation) and externally.
- Report the model following TRIPOD+AI, including the full equation so others can validate it.
Teaching models are not clinical tools
- The model in this lesson is fitted to invented data. Validated diabetes risk scores exist for many populations, and any score used with patients must be validated for that population.
Exercise
Develop, evaluate and simplify a logistic-regression risk model on synthetic data, and test how it holds up when the population changes.
Data: Synthetic adults generated in the notebook (no real patient data) (Generated in the notebook)
- Reproduce the model, odds ratios, test AUROC and calibration slope and intercept.
- Draw a calibration plot with 10 risk groups and a decision curve for thresholds from 5% to 40%.
- Simulate a 'new population' with higher average BMI and a different baseline risk, then measure AUROC and calibration there. Recalibrate the intercept and compare.
- Build the points score, map each total to observed risk in the test set, and present it as a small lookup table.
- Fit an L2-penalised version and a gradient-boosted model, and compare their AUROC and calibration with the unpenalised model.
The runnable notebook and worked solution will be published soon. You can work through the steps with the code on this page in the meantime.
Key takeaways
- Logistic regression is the baseline for tabular clinical prediction; more complex models must beat it.
- Odds ratios are per unit and describe associations within the model, not causal effects.
- Always check calibration as well as AUROC, and expect calibration to shift in new populations.
- Choose predictors and sample size before modelling, and report with TRIPOD+AI.
Terms used: AUROC (area under the ROC curve), Calibration, Cross-validation, Logistic regression, Odds ratio
Related: Building a cohort and avoiding data leakage, Diagnostic accuracy metrics, Type 2 diabetes
FAQ
Is machine learning better than logistic regression for clinical prediction?
Often not, for typical tabular clinical data. A systematic review found no average performance benefit of machine learning over logistic regression. Flexible models can help with very large datasets, images or complex interactions.
What does an odds ratio of 2.04 mean?
That the odds of the outcome are about twice as high for patients with that characteristic, holding the other predictors in the model constant. It is an association, not proof of cause.
Why does scikit-learn's logistic regression give different coefficients from statistics software?
scikit-learn applies L2 penalisation by default, which shrinks coefficients. Use penalty=None to match classical unpenalised logistic regression.
How many patients do I need to build a risk model?
It depends on the number of predictor parameters, the outcome rate and the expected model performance. Use a formal sample-size calculation such as Riley and colleagues' method rather than a rule of thumb.
Can I turn any model into a points score?
Points scores come naturally from linear models such as logistic regression. Simplifying always risks some loss of performance, so check discrimination and calibration of the final score.
Sources
- Christodoulou E, et al. A systematic review shows no performance benefit of machine learning over logistic regression for clinical prediction models. J Clin Epidemiol 2019;110:12–22. doi:10.1016/j.jclinepi.2019.02.004
- Riley RD, et al. Calculating the sample size required for developing a clinical prediction model. BMJ 2020;368:m441. doi:10.1136/bmj.m441
- Collins GS, et al. TRIPOD+AI statement: updated guidance for reporting clinical prediction models that use regression or machine learning methods. BMJ 2024;385:e078378. doi:10.1136/bmj-2023-078378
- Van Calster B, et al. Calibration: the Achilles heel of predictive analytics. BMC Medicine 2019;17:230. doi:10.1186/s12916-019-1466-7
- Steyerberg EW. Clinical Prediction Models: A Practical Approach to Development, Validation, and Updating. 2nd ed. Springer, 2019. doi:10.1007/978-3-030-16399-0
- scikit-learn documentation. LogisticRegression
Educational content only. This material teaches methods; it is not clinical advice. Code, models and examples here are not validated medical devices and must not be used to make decisions about real patients without proper clinical validation, governance and any required regulatory approval. Examples use synthetic or openly licensed de-identified data only. Never paste identifiable patient information into public AI tools. Mentions of models, tools or vendors are for identification, not endorsement.