forecose 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- forecose/__init__.py +5 -0
- forecose/forecose.py +116 -0
- forecose-0.1.0.dist-info/METADATA +48 -0
- forecose-0.1.0.dist-info/RECORD +5 -0
- forecose-0.1.0.dist-info/WHEEL +4 -0
forecose/__init__.py
ADDED
forecose/forecose.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Dexcom prediction class."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
import timesfm
|
|
8
|
+
|
|
9
|
+
from pydexcom import Dexcom
|
|
10
|
+
|
|
11
|
+
class DexcomForecast:
|
|
12
|
+
"""Class for a time-series forecasting model construction of CGM data."""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
cgm_history: np.ndarray,
|
|
17
|
+
last_timestamp: pd.Timestamp,
|
|
18
|
+
sampling_interval: pd.Timedelta,
|
|
19
|
+
context_len: int = 288,
|
|
20
|
+
horizon: int = 12
|
|
21
|
+
) -> None:
|
|
22
|
+
"""
|
|
23
|
+
Compiles the TimesFM model.
|
|
24
|
+
"""
|
|
25
|
+
self.context_len = context_len
|
|
26
|
+
self.horizon = horizon
|
|
27
|
+
|
|
28
|
+
self.cgm_history = cgm_history[-self.context_len:]
|
|
29
|
+
self.last_timestamp = last_timestamp
|
|
30
|
+
self.sampling_interval = sampling_interval
|
|
31
|
+
|
|
32
|
+
# initalise and compile the TimesFM model
|
|
33
|
+
self.model = timesfm.TimesFM_2p5_200M_torch.from_pretrained(
|
|
34
|
+
"google/timesfm-2.5-200m-pytorch"
|
|
35
|
+
)
|
|
36
|
+
self.model.compile(
|
|
37
|
+
timesfm.ForecastConfig(
|
|
38
|
+
max_context=self.context_len,
|
|
39
|
+
max_horizon=self.horizon,
|
|
40
|
+
normalize_inputs=True,
|
|
41
|
+
use_continuous_quantile_head=True,
|
|
42
|
+
infer_is_positive=True,
|
|
43
|
+
fix_quantile_crossing=True,
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def from_dexcom(
|
|
49
|
+
cls,
|
|
50
|
+
dexcom: Dexcom,
|
|
51
|
+
context_len: int = 288,
|
|
52
|
+
horizon: int = 12
|
|
53
|
+
):
|
|
54
|
+
"""Pull data from an active pydexcom session."""
|
|
55
|
+
if not isinstance(dexcom, Dexcom):
|
|
56
|
+
raise TypeError("Expected an object of type pydexcom.Dexcom.")
|
|
57
|
+
|
|
58
|
+
# extract data
|
|
59
|
+
readings = dexcom.get_glucose_readings(
|
|
60
|
+
minutes=1440, max_count=context_len
|
|
61
|
+
)
|
|
62
|
+
if not readings:
|
|
63
|
+
raise RuntimeError("No readings returned from Dexcom Share API.")
|
|
64
|
+
|
|
65
|
+
# process data structures
|
|
66
|
+
df = pd.DataFrame(
|
|
67
|
+
[{"Time": r.datetime, "Glucose": r.mmol_l} for r in reversed(readings)]
|
|
68
|
+
)
|
|
69
|
+
df["Time"] = pd.to_datetime(df["Time"], utc=True).dt.tz_convert("Europe/London")
|
|
70
|
+
df = df.sort_values("Time").reset_index(drop=True)
|
|
71
|
+
|
|
72
|
+
cgm_history = df["Glucose"].to_numpy(dtype=float)
|
|
73
|
+
last_timestamp = df["Time"].iloc[-1]
|
|
74
|
+
sampling_interval = pd.Timedelta(df["Time"].diff().median())
|
|
75
|
+
|
|
76
|
+
# return instance
|
|
77
|
+
return cls(
|
|
78
|
+
cgm_history=cgm_history,
|
|
79
|
+
last_timestamp=last_timestamp,
|
|
80
|
+
sampling_interval=sampling_interval,
|
|
81
|
+
context_len=context_len,
|
|
82
|
+
horizon=horizon
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def forecast(self) -> pd.DataFrame:
|
|
86
|
+
"""Returns point and quantile forecasts of time series data."""
|
|
87
|
+
if len(self.cgm_history) == 0:
|
|
88
|
+
raise ValueError("No historical data provided for forecasting.")
|
|
89
|
+
|
|
90
|
+
# run inference
|
|
91
|
+
point_forecast, quantile_forecast = self.model.forecast(
|
|
92
|
+
horizon=self.horizon,
|
|
93
|
+
inputs=[self.cgm_history],
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
point_vals = point_forecast[0]
|
|
97
|
+
quant_vals = quantile_forecast[0]
|
|
98
|
+
|
|
99
|
+
future_timestamps = pd.date_range(
|
|
100
|
+
start=self.last_timestamp + self.sampling_interval,
|
|
101
|
+
periods=self.horizon,
|
|
102
|
+
freq=self.sampling_interval,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# compile
|
|
106
|
+
forecast_df = pd.DataFrame({
|
|
107
|
+
"timestamp": future_timestamps,
|
|
108
|
+
"predicted_glucose": np.clip(point_vals, *(2.2, 22.2)),
|
|
109
|
+
"q10": np.clip(quant_vals[:, 0], *(2.2, 22.2)),
|
|
110
|
+
"q25": np.clip(quant_vals[:, 1], *(2.2, 22.2)),
|
|
111
|
+
"q50": np.clip(quant_vals[:, 4], *(2.2, 22.2)),
|
|
112
|
+
"q75": np.clip(quant_vals[:, 7], *(2.2, 22.2)),
|
|
113
|
+
"q90": np.clip(quant_vals[:, 8], *(2.2, 22.2)),
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
return forecast_df
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: forecose
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A time-series forecasting extension for pydexcom using Google's TimesFM
|
|
5
|
+
Author: Alexander Sadler
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Requires-Dist: numpy
|
|
8
|
+
Requires-Dist: pandas
|
|
9
|
+
Requires-Dist: pydexcom
|
|
10
|
+
Requires-Dist: timesfm[torch]>=2.0.0
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
A time-series forecasting extension for [pydexcom](https://github.com/gagebenne/pydexcom) using Google's [TimesFM](https://github.com/google-research/timesfm). Used to predict immediate, short term blood glucose readings.
|
|
14
|
+
|
|
15
|
+
> All modelling and forecasting is performed locally on your device. The only external connections made are with:
|
|
16
|
+
> - Dexcom Share API: fetching CGM readings following the `pydexcom` approach.
|
|
17
|
+
> - HuggingFace: one-time download of the forecasting model weights on the first run.
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
1. Ensure that you have installed the pydexcom package and [enabled the Share service](https://provider.dexcom.com/education-research/cgm-education-use/videos/setting-dexcom-share-and-follow) within your [Dexcom G7 / G6 / G5 / G4](https://www.dexcom.com/apps).
|
|
21
|
+
|
|
22
|
+
`pip install pydexcom`
|
|
23
|
+
|
|
24
|
+
2. Initialise `pydexcom` with your Dexcom credentials (below shows the simplist route, refere to pydexcom for further instruction).
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
>>> from pydexcom import Dexcom
|
|
28
|
+
>>> dexcom = Dexcom(username="username", password="password")
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
3. Generate a prediction.
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
>>> from forecose import DexcomForecast
|
|
35
|
+
>>> forecaster = DexcomForecast.from_dexcom(
|
|
36
|
+
dexcom=dexcom, # pull recent readings from your active 'Dexcom' session
|
|
37
|
+
context_len=288, # uses prior day's readings as context
|
|
38
|
+
horizon=12 # predicts the next hour
|
|
39
|
+
)
|
|
40
|
+
>>> predictions = forecaster.forecast()
|
|
41
|
+
>>> print(predictions.head())
|
|
42
|
+
timestamp predicted_glucose q10 q25 q50 q75 q90
|
|
43
|
+
0 2026-06-25 11:01:19.199000+01:00 8.243370 8.244899 8.125346 8.214749 8.266071 8.309934
|
|
44
|
+
1 2026-06-25 11:06:19.199000+01:00 8.050682 8.073329 7.738788 8.018662 8.208736 8.303193
|
|
45
|
+
2 2026-06-25 11:11:19.199000+01:00 7.897943 7.879324 7.332723 7.783697 8.082028 8.256586
|
|
46
|
+
3 2026-06-25 11:16:19.199000+01:00 7.767045 7.738261 6.965607 7.621467 8.026337 8.236394
|
|
47
|
+
4 2026-06-25 11:21:19.199000+01:00 7.615633 7.667328 6.668064 7.442780 7.972524 8.216294
|
|
48
|
+
```
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
forecose/__init__.py,sha256=1sZANKnJHKphkWyyBaMovicQrkh6BiuRh1oNW8K3YoU,76
|
|
2
|
+
forecose/forecose.py,sha256=8hInzDuxa_jrWKnjNT00KdgLlC5tSDxJFNmZQijm1ss,3875
|
|
3
|
+
forecose-0.1.0.dist-info/METADATA,sha256=Vy8lOQdWpeHRYrsTkw5PfAwi2JRYJy6TwHXgOvNKfPE,2403
|
|
4
|
+
forecose-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
5
|
+
forecose-0.1.0.dist-info/RECORD,,
|