Show HN: I have trained 15 blood glucose forecasting models on my own data
Caution
Research and educational use only - not a medical device. T1DMAI is a research artifact. It is not clinically validated, has no regulatory clearance, and its output is a forecast of research signals, not medical advice. It must not be used to make medical, diagnostic, or treatment decisions, to calculate or adjust insulin doses, or to manage diabetes in any way. For medical guidance consult a qualified healthcare professional. The software is provided "as is", without warranty of any kind (see LICENSE).
Note
Most of the code in this repo was written by AI. If you don't like that, feel free to leave.
This repo is a zoo. Here I train & test many blood glucose forecasting models (including my own) on both my own data and metabonet (public).
My model is a small transformer that forecasts blood glucose 2 hours ahead. I've made two variants: D16 DILATE and D16 MSE which use DILATE and MSE loss for training, respectively. D16 refers to the width of the model (16).
They are both trained on synthetic traces, and then a LoRA adapter is added for light fine-tuning, but I also test them without the adapter.
Feast thine eyes on some of their forecasts (these have been trained on synthetic traces, then fine-tuned with a LoRA adapter on my training split):
Background
I have type 1 diabetes and I wanna know if I can train a BG forecasting model to help me with my daily management. To this end I've made some auxiliary tools:
T1DMSIMcreates synthetic CGM traces since UVA/Padova was too slow on my laptop.T1DMDROIDruns the models on my phone, also has some neat functionality like reading BG from multiple CGMs at once.
For evaluation, I use the recommendations by Jacobs et al., IEEE Reviews in Biomedical Engineering, 2024 [1]. It's a nice paper that you should probably read.
Main results
All models suck. XGBoost is the best one but my own model comes close. More details below:
- On my data, D16 DILATE + LoRA comes 3rd of 26 (22.8), behind XGBoost (22.2) and LightGBM (22.4). Wilcoxon puts it level with both at 60 and 90 min, behind them at 30 and 120.
- On MetaboNet, D16 DILATE + LoRA comes 9th (34.6) but has the lowest RMSE of all models at 90 min. XGBoost leads at 30, 60 and 120.
- At 60 min a 3771 parameter LoRA adapter helps both models on both datasets: 26.1 to 22.8 and 27.1 to 23.5 on my data, 36.6 to 34.6 and 36.7 to 35.1 on MetaboNet. At 120 min on MetaboNet it makes D16 DILATE worse (48.0 to 50.4).
- On MetaboNet, LoRA also fixes the bands. The 90% band holds the truth 75-78% of the time at 60 min with sim only and 81-85% with LoRA. On my data both are 83-85%.
- T1DMAI + LoRA reacts earliest on my data: 40-50 min time gain at 60 min, against 30 for XGBoost.
- It also catches the most lows on my data: 40-47% at 60 min, against 29% for XGBoost. On MetaboNet every low-RMSE model misses nearly all lows (XGBoost 0%, T1DMAI 6-8%), while linear extrapolation catches 58%.
- LightGBM matches XGBoost within 0.2 mg/dL everywhere. An LSTM with 15 hours of history comes 3rd on MetaboNet (33.6 at 60 min), against 35.9 for the 2 hour one.
- Zero-shot Chronos-2 and TimesFM 2.5 beat zero-shot Chronos-Bolt by 1.4-2.7 mg/dL at 60 min on both datasets, but trail fine-tuned Chronos-Bolt.
Models
| Model | Family | Description |
|---|---|---|
| Persistence | Baseline | Last reading carried forward. |
| Linear extrapolation | Baseline | Straight line through the last few readings. |
| AR (ridge) | Classical | Linear regression on recent glucose. |
| ARX (ridge) | Classical | Linear regression on recent glucose, insulin and carbs. |
| ARIMA | Classical | Glucose only. |
| SARIMAX | Classical | With insulin and carbs. |
| Kalman filter | Classical | Damped local linear trend. |
| SVR | Classical | Glucose, insulin and carbs. |
| Sparse GP | Classical | 1000 inducing points. |
| XGBoost | Classical | Glucose, insulin and carbs [18]. |
| LightGBM | Classical | Same features, depth, rate and rounds as XGBoost [19]. |
| LSTM | Deep | 2 hours of history [5]. |
| LSTM (15 h) | Deep | Two layers of 128 over 15 hours, as in MetaboNet-Bench [3]. |
| GluNet | Deep | Dilated causal convolutions over 4 hours [6]. |
| PatchTST | Deep | [13] |
| N-HiTS | Deep | [14] |
| Chronos-Bolt tiny and mini | Foundation | 9M and 21M parameters, glucose only, zero-shot and fine-tuned [15]. |
| Chronos-2 | Foundation | 120M parameters, zero-shot. Past insulin and carbs; on my data also the future ones [16]. |
| TimesFM 2.5 | Foundation | 200M parameters, zero-shot. Glucose only on MetaboNet; on my data future insulin and carbs through in-context regression [17]. |
| T1DMAI D16 DILATE and D16 MSE | T1DMAI | 31k parameters. Glucose, carbs and insulin. |
Numbers in brackets point to References.
Everything except the baselines and the foundation models was trained from scratch on each dataset's training split. T1DMAI comes in two versions: sim only is the checkpoint in models/, and sim + LoRA adds a frozen-base rank 1 adapter [23] (3771 parameters) trained on the real training split.
T1DMAI details:
| Architecture | encoder-only transformer, 30 min patches, one attention head, RoPE [26], SwiGLU |
| Size | 16 layers of width 16, 31k parameters |
| Loss | 7 quantile pinball loss plus a median term (DILATE [25] on soft-DTW [24], or MSE) |
| Inputs | CGM in a risk-scaled space, carb absorption curve, insulin action curve, mask bit |
| Output | 7 quantiles (5% to 95%) for every 5 minute step of the next 2 hours |
| History | up to 7 days |
| Simulator training | 1M simulated people, 7 days each, 10000 steps of batch 512 |
| Runtime | ExecuTorch or LiteRT, on the phone in T1DMDROID |
More in docs/USAGE.md and ARCHITECTURE.md.
Data
My data. 115 days from T1DMDROID: days 1 to 80 for training, 81 to 115 for testing. Note that the model sees future inputs like insulin & carbs. I should probably also train the adapters while withholding those inputs.
| Source | Subjects | Person-days | Forecasts | CGM coverage % | Mean mg/dL | CV % | 180 % | > 250 % |
|---|---|---|---|---|---|---|---|---|
| t1dmdroid | 1 | 35 | 9764 | 97 | 130 | 28 | 0.4 | 3.0 |
MetaboNet [2]. Official split, 279 test people from 9 studies. Blind, because most of them use closed-loop pumps whose future insulin reacts to glucose the model hasn't seen.
| Source | Subjects | Person-days | Forecasts | CGM coverage % | Mean mg/dL | CV % | 180 % | > 250 % |
|---|---|---|---|---|---|---|---|---|
| AZT1D | 4 | 131 | 775 | 99 | 141 | 30 | 0.3 | 1.3 |
| BrisT1D | 2 | 312 | 1953 | 77 | 144 | 33 | 0.2 | 1.8 |
| CTR3 | 5 | 1157 | 1052 | 51 | 141 | 38 | 0.7 | 3.5 |
| HUPA-UCM | 4 | 315 | 896 | 100 | 132 | 36 | 0.9 | 6.8 |
| IOBP2 | 62 | 4737 | 14653 | 84 | 159 | 37 | 0.4 | 2.0 |
| Loop | 159 | 47438 | 203538 | 93 | 139 | 40 | 0.7 | 3.9 |
| PEDAP | 6 | 2070 | 6321 | 85 | 158 | 38 | 0.4 | 2.4 |
| ReplaceBG | 35 | 7967 | 35683 | 87 | 158 | 41 | 1.0 | 4.2 |
| ShanghaiT1DM | 2 | 19 | 28 | 33 | 159 | 39 | 1.0 | 3.8 |
| All | 279 | 64146 | 264899 | 90 | 143 | 40 | 0.7 | 3.7 |
Method
| My data | MetaboNet |
|---|---|
| Forecasts | every 5 min of the test days | every 10th row of the leaderboard template |
| Horizons | 30, 60, 90, 120 min | 30, 60, 90, 120 min |
| Training windows, other models | 60000 | 351000 (200000 for fine-tuned Chronos) |
| LoRA steps | 2000 | 6000 |
- LoRA checkpoints are picked by mean RMSE on the last 10 days of each subject's training data.
- All models forecast at the same moments.
- 95% intervals: bootstrap over people (MetaboNet) or days (my data).
- Significance: Wilcoxon signed-rank on paired absolute errors against the best model, Bonferroni corrected.
- Clinical error grids: DTS [20], Clarke [22], CG-EGA [21].
| Model | Parameter count | Microseconds per forecast | History used |
|---|---|---|---|
| Persistence | 0 | 0.0042 | 5 min |
| Linear extrapolation | 0 | 0.033 | 30 min |
| AR (ridge) | 648 | 0.13 | 2 h |
| SARIMAX | 8,088 | 0.21 | 8 h |
| ARIMA | 4,632 | 0.27 | 4 h |
| ARX (ridge) | 1,116 | 0.36 | 6 h |
| Kalman filter | 864 | 0.48 | 3 h |
| SVR | 69,772 | 4.2 | 6 h |
| Sparse GP | 66,134 | 6.1 | 6 h |
| N-HiTS | 417,772 | 6.5 | 6 h |
| LSTM | 103,192 | 33 | 2 h |
| GluNet | 65,368 | 71 | 4 h |
| PatchTST | 182,266 | 74 | 6 h |
| XGBoost | 2,692,860 | 506 | 6 h |
| Chronos-Bolt tiny (fine-tuned) | 8,669,901 | 1,346 | 24 h |
| Chronos-Bolt tiny (zero-shot) | 8,669,901 | 1,361 | 24 h |
| Chronos-Bolt mini (zero-shot) | 21,256,141 | 3,871 | 24 h |
| Chronos-Bolt mini (fine-tuned) | 21,256,141 | 3,889 | 24 h |
| T1DMAI D16 DILATE | 31,253 | 5,679 | 168 h |
| T1DMAI D16 MSE | 31,253 | 7,342 | 168 h |
Limitations
- My data is N=1 over 35 days.
- The test sets differ a lot (in range 89% vs 75%, CV 28% vs 40%), so don't compare numbers across them.
- Lows are rare: 292 forecasts on my data, 8347 on MetaboNet.
- T1DMAI fell back to the last reading on 0.7% of my forecasts and 0.4% on MetaboNet, including all 28 ShanghaiT1DM ones. The other models never fall back.
- The bands are raw quantiles, no conformal correction.
- The sparse GP's own uncertainty isn't shown. Its noise fit hit the bottom of its grid, giving a band about 1 mg/dL wide.
- Time gain is only computed for my data.
- Unlogged meals count as "no meal".
- CPU timings are single runs on one laptop core and varied by up to 45% between runs.
- LightGBM, LSTM (15 h), Chronos-2 and TimesFM 2.5 have no CPU timing yet.
Terms
| Term | Meaning |
|---|---|
| Non-blind | The model gets the carbs and insulin logged after the forecast moment. |
| Blind | It doesn't. |
| Time gain | Horizon minus the lag of peak correlation between forecast and CGM. |
| Example forecasts | Median case of: falls into a low, rises over 40 mg/dL after a meal, calm nights. |
| After a meal | At least 5 g of carbs logged in the 2 hours before. |
| Parameter count | Stored weights and constants in the exported model. |
Full tables
A * marks the lowest RMSE and the models not significantly worse than it.
My data:
| Model | Family | RMSE 30 | RMSE 60 | RMSE 90 | RMSE 120 | gRMSE 60 | MARD 30 | MARD 60 | MARD 120 | ME 60 | MRE 60 % | TG 30 | TG 60 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| XGBoost | Classical | 14.7 * | 22.2 * | 26.9 * | 31.1 * | 24.2 | 8.5 | 13.4 | 19.8 | +0.8 | +3.1 | 15 | 30 |
| LightGBM | Classical | 14.7 * | 22.4 * | 27.0 * | 31.0 * | 24.4 | 8.5 | 13.4 | 19.8 | +0.9 | +3.2 | 15 | 30 |
| T1DMAI D16 DILATE (sim + LoRA) | T1DMAI | 15.2 | 22.8 * | 27.4 * | 31.7 | 24.7 | 8.8 | 13.5 | 20.3 | -1.6 | +1.2 | 15 | 40 |
| SVR | Classical | 15.1 * | 23.0 | 27.8 | 31.6 | 24.9 | 8.5 | 13.6 | 20.0 | -0.7 | +1.8 | 10 | 25 |
| LSTM (15 h) | Deep | 15.2 | 23.2 | 28.3 | 32.5 | 24.9 | 8.9 | 14.3 | 21.5 | +2.0 | +4.0 | 10 | 25 |
| PatchTST | Deep | 15.1 | 23.3 | 28.9 | 33.1 | 25.0 | 8.9 | 14.3 | 21.8 | +1.3 | +3.3 | 10 | 25 |
| Sparse GP | Classical | 15.4 | 23.5 | 28.3 | 32.1 | 25.6 | 9.1 | 14.4 | 21.0 | +0.8 | +3.4 | 10 | 25 |
| T1DMAI D16 MSE (sim + LoRA) | T1DMAI | 15.1 | 23.5 | 28.4 | 32.4 | 25.4 | 8.7 | 13.9 | 20.3 | -0.2 | +2.0 | 20 | 50 |
| GluNet | Deep | 15.4 | 23.6 | 29.6 | 34.4 | 25.3 | 8.9 | 14.3 | 22.3 | +1.0 | +2.8 | 10 | 25 |
| ARX (ridge) | Classical | 15.7 | 24.1 | 28.6 | 31.6 | 26.3 | 9.4 | 15.1 | 21.2 | +1.0 | +4.0 | 10 | 15 |
| LSTM | Deep | 15.4 | 24.3 | 30.6 | 35.8 | 25.7 | 8.9 | 14.6 | 22.9 | +0.2 | +1.9 | 10 | 20 |
| N-HiTS | Deep | 15.8 | 25.0 | 31.4 | 36.0 | 26.6 | 9.5 | 15.5 | 23.3 | +2.0 | +3.5 | 15 | 30 |
| T1DMAI D16 DILATE (sim only) | T1DMAI | 16.2 | 26.1 | 33.2 | 39.9 | 28.6 | 9.6 | 16.0 | 25.2 | +0.6 | +3.9 | 10 | 15 |
| T1DMAI D16 MSE (sim only) | T1DMAI | 16.8 | 27.1 | 33.9 | 37.5 | 29.8 | 9.9 | 16.8 | 24.4 | +1.2 | +4.8 | 10 | 15 |
| AR (ridge) | Classical | 17.2 | 27.3 | 32.3 | 34.5 | 30.3 | 10.3 | 17.2 | 23.0 | -0.1 | +4.1 | 5 | 5 |
| Chronos-Bolt tiny (fine-tuned) | Foundation | 17.3 | 27.3 | 33.0 | 36.0 | 30.0 | 9.8 | 16.4 | 23.4 | -2.2 | +1.7 | 5 | 10 |
| Chronos-Bolt mini (fine-tuned) | Foundation | 17.3 | 27.3 | 32.9 | 36.0 | 29.9 | 9.8 | 16.5 | 23.6 | -1.5 | +2.2 | 5 | 10 |
| TimesFM 2.5 (zero-shot) | Foundation | 18.4 | 29.1 | 35.8 | 39.4 | 31.5 | 10.5 | 17.4 | 25.1 | -1.5 | +2.0 | 5 | 5 |
| Chronos-2 (zero-shot) | Foundation | 17.8 | 29.4 | 36.4 | 40.0 | 31.8 | 10.2 | 17.7 | 25.5 | -1.0 | +2.5 | 5 | 10 |
| SARIMAX | Classical | 17.7 | 29.8 | 38.6 | 45.4 | 31.6 | 10.2 | 17.9 | 28.8 | -3.5 | -0.6 | 5 | 5 |
| Chronos-Bolt tiny (zero-shot) | Foundation | 18.7 | 30.8 | 38.3 | 42.0 | 32.9 | 10.8 | 18.5 | 27.1 | +0.8 | +3.5 | 5 | 5 |
| Chronos-Bolt mini (zero-shot) | Foundation | 18.7 | 30.9 | 38.3 | 42.4 | 32.9 | 10.7 | 18.5 | 27.2 | +1.0 | +3.6 | 5 | 10 |
| Kalman filter | Classical | 18.5 | 31.3 | 39.6 | 45.0 | 33.2 | 10.6 | 18.7 | 28.6 | +0.2 | +2.7 | 0 | 5 |
| ARIMA | Classical | 18.6 | 31.4 | 39.7 | 45.1 | 33.3 | 10.7 | 18.8 | 28.7 | +0.2 | +2.7 | 0 | 5 |
| Persistence | Baseline | 20.0 | 32.1 | 39.7 | 44.7 | 34.2 | 11.7 | 19.4 | 28.5 | +0.2 | +3.3 | 0 | 0 |
| Linear extrapolation | Baseline | 24.3 | 48.9 | 71.0 | 89.2 | 50.1 | 13.3 | 27.9 | 56.4 | +0.1 | -0.3 | 10 | 15 |
| Model | DTS A 30 | DTS A 60 | DTS A 120 | DTS A+B 120 | Clarke A 60 | Clarke A+B 120 | CG-EGA hypo AP 30 | hypo EP 60 | eu EP 60 | hyper EP 60 |
|---|---|---|---|---|---|---|---|---|---|---|
| XGBoost | 91.5 | 78.3 | 62.8 | 96.9 | 78.6 | 96.6 | 77.7 | 51.9 | 0.0 | 0.1 |
| LightGBM | 91.4 | 78.0 | 62.7 | 96.9 | 78.4 | 96.6 | 77.7 | 50.3 | 0.0 | 0.3 |
| T1DMAI D16 DILATE (sim + LoRA) | 90.7 | 78.0 | 60.7 | 96.8 | 78.3 | 97.0 | 83.2 | 38.4 | 0.0 | 0.1 |
| SVR | 91.3 | 77.5 | 61.3 | 97.3 | 77.8 | 97.1 | 83.5 | 47.4 | 0.0 | 0.8 |
| LSTM (15 h) | 91.0 | 76.0 | 58.3 | 97.2 | 76.3 | 96.7 | 82.6 | 48.7 | 0.0 | 0.1 |
| PatchTST | 90.9 | 75.0 | 57.6 | 96.5 | 75.3 | 96.9 | 82.3 | 48.1 | 0.0 | 0.4 |
| Sparse GP | 89
Comments
No comments yet. Start the discussion.