Medical AI · Applied case studies: diabetes & metabolic health · Lesson 1 of 5
Forecasting glucose from CGM data
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: ECG and physiological time-series models, Building a cohort and avoiding data leakage
In short
Continuous glucose monitors record glucose every few minutes, and forecasting the next 30 to 60 minutes can warn of lows before they happen. On a synthetic trace, a learned model forecast 30 minutes ahead slightly more accurately than simply assuming glucose stays the same, but simple baselines are hard to beat. Average error is not the point clinically: what matters is how many lows are predicted early enough to act, and how many false alarms people must put up with.
You will learn to
- Summarise CGM data with standard metrics such as time in range
- Build 30-minute glucose forecasts with persistence, trend and regression models
- Evaluate forecasts with RMSE and MARD on a temporal split
- Judge low-glucose prediction by lows caught in advance and false alerts
CGM data and standard metrics
CGM sensors measure interstitial glucose every 1 to 15 minutes, giving hundreds of readings a day. An international consensus (Battelino et al., 2019) defined standard targets for most adults with type 1 or type 2 diabetes: more than 70% of readings between 70 and 180 mg/dL, less than 4% below 70 mg/dL, less than 1% below 54 mg/dL and less than 25% above 180 mg/dL.
The simulation below produces a crude 14-day trace with meals and occasional overnight lows. It is far simpler than real physiology; open simulators based on the UVA/Padova model, and datasets such as OhioT1DM (available under a data use agreement), are better for serious work.
import numpy as np
def synthetic_cgm(days=14, seed=0):
"""A crude 5-minute CGM trace: baseline, meal rises, slow decay, overnight dips and sensor noise."""
rng = np.random.default_rng(seed)
steps = days * 288 # 288 readings per day
glucose = np.empty(steps)
g, meal_effect = 130.0, 0.0
for t in range(steps):
minute = (t % 288) * 5
if minute in (8 * 60, 13 * 60, 20 * 60) and rng.random() < 0.95:
meal_effect += rng.uniform(60, 160) # a meal of varying size
if minute == 3 * 60 and rng.random() < 0.4:
meal_effect -= rng.uniform(120, 170) # an occasional overnight low
meal_effect *= 0.965 # absorbed and handled over a few hours
g += 0.03 * (125 - g) + 0.04 * meal_effect + rng.normal(0, 1.5)
glucose[t] = g
return np.clip(glucose + rng.normal(0, 4, steps), 40, 400) # sensor noise and range limits
cgm = synthetic_cgm()
in_range = np.mean((cgm >= 70) & (cgm <= 180))
print(f"{len(cgm)} readings over 14 days; mean {cgm.mean():.0f} mg/dL; "
f"time in range {in_range:.0%}; below 70: {np.mean(cgm < 70):.1%}; above 180: {np.mean(cgm > 180):.0%}")
# 4032 readings over 14 days; mean 152 mg/dL; time in range 79%; below 70: 2.9%; above 180: 18%Three forecasting methods
Each forecast uses the last 30 minutes (six readings) to predict the reading 30 minutes ahead. Training uses days 1 to 10 and testing days 11 to 14, so the model is always tested on the future, not on randomly mixed time points, which would leak information.
from sklearn.linear_model import Ridge
HISTORY, HORIZON = 6, 6 # use the last 30 minutes to predict 30 minutes ahead
X = np.array([cgm[t - HISTORY:t] for t in range(HISTORY, len(cgm) - HORIZON)])
y = np.array([cgm[t + HORIZON - 1] for t in range(HISTORY, len(cgm) - HORIZON)])
split = 10 * 288 - HISTORY # train on days 1-10, test on days 11-14 (temporal split)
X_tr, X_te, y_tr, y_te = X[:split], X[split:], y[:split], y[split:]
forecasts = {
"Persistence (last value)": X_te[:, -1],
"Linear trend (last 15 min)": X_te[:, -1] + (X_te[:, -1] - X_te[:, -4]) / 3 * HORIZON,
"Ridge regression on last 30 min": Ridge(alpha=1.0).fit(X_tr, y_tr).predict(X_te),
}
for name, pred in forecasts.items():
rmse = np.sqrt(np.mean((pred - y_te) ** 2))
mard = np.mean(np.abs(pred - y_te) / y_te)
print(f"{name:<32} RMSE {rmse:5.1f} mg/dL MARD {mard:5.1%}")
# Persistence (last value) RMSE 10.2 mg/dL MARD 5.6%
# Linear trend (last 15 min) RMSE 16.7 mg/dL MARD 9.3%
# Ridge regression on last 30 min RMSE 9.3 mg/dL MARD 5.1%- Persistence, assuming glucose will not change, is a strong baseline here, as it often is for short horizons. Simple trend extrapolation is worse because it amplifies sensor noise.
- Ridge regression is slightly more accurate overall. MARD, the mean absolute relative difference, is also used to describe sensor accuracy, so make clear which you are reporting.
Do the forecasts catch lows?
low = y_te < 70 # a low 30 minutes later
print(f"{low.sum()} test time points are followed by a low (< 70 mg/dL) 30 minutes later")
for name, pred in forecasts.items():
alert = pred < 80 # alert if the forecast is below 80 mg/dL
sens = np.mean(alert[low])
false_alerts_per_day = np.sum(alert & ~low) / 4 # four test days
print(f"{name:<32} lows caught {sens:4.0%} false alerts per day {false_alerts_per_day:4.1f}")
# 56 test time points are followed by a low (< 70 mg/dL) 30 minutes later
# Persistence (last value) lows caught 93% false alerts per day 9.0
# Linear trend (last 15 min) lows caught 82% false alerts per day 9.8
# Ridge regression on last 30 min lows caught 89% false alerts per day 6.5- These numbers flatter the models: most of the 'lows caught' were already low when the alert fired, which is no warning at all.
Lows predicted in advance
new_low = low & (X_te[:, -1] >= 70) # not low yet, but will be in 30 minutes
print(f"{new_low.sum()} of these lows start after a normal current reading")
for name, pred in forecasts.items():
print(f"{name:<32} new lows predicted in advance {np.mean((pred < 80)[new_low]):4.0%}")
# 16 of these lows start after a normal current reading
# Persistence (last value) new lows predicted in advance 75%
# Linear trend (last 15 min) new lows predicted in advance 69%
# Ridge regression on last 30 min new lows predicted in advance 62%- Only 16 lows began after a normal reading in four test days, too few to tell the methods apart; the differences here could easily reverse with more data.
- The model with the lowest RMSE predicted the fewest of these lows in advance. A forecast can be accurate on average and still miss the events that matter.
- Real evaluations use many patients and weeks of data, report lows predicted with enough warning, false alerts per day, and clinical error grids (Clarke or Parkes) that weight errors by their danger.
Teaching models, not clinical tools
- Data in these case studies are simulated, and the models are fitted to them for learning. They must not be used to make decisions about real patients.
- Validated tools exist for several of these problems; any tool used in care must be validated for its population and meet regulatory requirements.
Exercise
Forecast glucose from CGM data and evaluate forecasts by their clinical usefulness.
Data: Synthetic CGM traces generated in the notebook; optional simglucose (open-source UVA/Padova 2008 implementation) or OhioT1DM under its data use agreement (Generated in the notebook; external sources under their own terms)
- Reproduce the forecasts and metrics, then extend the horizon to 60 minutes and compare.
- Generate traces for ten synthetic people and evaluate across people, splitting by person as well as time.
- Add features for time of day and minutes since the last meal, and see whether advance prediction of lows improves.
- Plot forecasts on a Clarke error grid and count points in the dangerous zones.
- Choose an alert threshold that balances lows predicted in advance against false alerts per day, and justify it.
Open the notebook in ColabView on GitHubWorked solution coming soon
Key takeaways
- CGM data are summarised with standard metrics such as time in range.
- Simple baselines such as persistence are hard to beat at short horizons; always report them.
- Split by time (and by person) to avoid leakage.
- Judge low-glucose forecasting by lows caught in advance and false alerts, not RMSE alone.
Terms used: Data leakage, Sensitivity (recall)
Related: Continuous glucose monitoring (CGM), ECG and physiological time-series models, Insulin therapy
FAQ
What is time in range?
The share of CGM readings between 70 and 180 mg/dL. For most adults with diabetes, the international consensus target is more than 70% of the time.
Can AI predict low blood sugar before it happens?
Forecasting models can warn of some lows 30 to 60 minutes ahead, and some commercial CGM systems include predictive alerts. Accuracy varies, and false alarms are a real burden.
What is MARD?
The mean absolute relative difference between two sets of glucose values, used both for sensor accuracy and for forecast error. Lower is better.
Why use a temporal split?
Because forecasting must work on future data. Randomly mixing time points lets the model learn from moments right next to the ones it is tested on, which overstates accuracy.
Can these models adjust insulin?
Not these teaching models. Automated insulin delivery systems use validated algorithms within regulated devices; insulin decisions belong to people with diabetes and their care teams.
Sources
- Battelino T, et al. Clinical targets for continuous glucose monitoring data interpretation: recommendations from the International Consensus on Time in Range. Diabetes Care 2019;42(8):1593–1603. doi:10.2337/dci19-0028
- Clarke WL, et al. Evaluating clinical accuracy of systems for self-monitoring of blood glucose. Diabetes Care 1987;10(5):622–628. doi:10.2337/diacare.10.5.622
- Parkes JL, et al. A new consensus error grid to evaluate the clinical significance of inaccuracies in the measurement of blood glucose. Diabetes Care 2000;23(8):1143–1148. doi:10.2337/diacare.23.8.1143
- Dalla Man C, et al. The UVA/PADOVA type 1 diabetes simulator: new features. J Diabetes Sci Technol 2014;8(1):26–34. doi:10.1177/1932296813514502
- Marling C, Bunescu R. The OhioT1DM dataset for blood glucose level prediction: update 2020. CEUR Workshop Proceedings 2020;2675:71–74
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.