forecose 0.1.0__tar.gz
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-0.1.0/.gitignore +11 -0
- forecose-0.1.0/.vscode/settings.json +3 -0
- forecose-0.1.0/PKG-INFO +48 -0
- forecose-0.1.0/README.md +36 -0
- forecose-0.1.0/pyproject.toml +19 -0
- forecose-0.1.0/src/forecose/__init__.py +5 -0
- forecose-0.1.0/src/forecose/forecose.py +116 -0
- forecose-0.1.0/tests/test_forecast.py +64 -0
forecose-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+
```
|
forecose-0.1.0/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
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.
|
|
2
|
+
|
|
3
|
+
> All modelling and forecasting is performed locally on your device. The only external connections made are with:
|
|
4
|
+
> - Dexcom Share API: fetching CGM readings following the `pydexcom` approach.
|
|
5
|
+
> - HuggingFace: one-time download of the forecasting model weights on the first run.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
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).
|
|
9
|
+
|
|
10
|
+
`pip install pydexcom`
|
|
11
|
+
|
|
12
|
+
2. Initialise `pydexcom` with your Dexcom credentials (below shows the simplist route, refere to pydexcom for further instruction).
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
>>> from pydexcom import Dexcom
|
|
16
|
+
>>> dexcom = Dexcom(username="username", password="password")
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
3. Generate a prediction.
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
>>> from forecose import DexcomForecast
|
|
23
|
+
>>> forecaster = DexcomForecast.from_dexcom(
|
|
24
|
+
dexcom=dexcom, # pull recent readings from your active 'Dexcom' session
|
|
25
|
+
context_len=288, # uses prior day's readings as context
|
|
26
|
+
horizon=12 # predicts the next hour
|
|
27
|
+
)
|
|
28
|
+
>>> predictions = forecaster.forecast()
|
|
29
|
+
>>> print(predictions.head())
|
|
30
|
+
timestamp predicted_glucose q10 q25 q50 q75 q90
|
|
31
|
+
0 2026-06-25 11:01:19.199000+01:00 8.243370 8.244899 8.125346 8.214749 8.266071 8.309934
|
|
32
|
+
1 2026-06-25 11:06:19.199000+01:00 8.050682 8.073329 7.738788 8.018662 8.208736 8.303193
|
|
33
|
+
2 2026-06-25 11:11:19.199000+01:00 7.897943 7.879324 7.332723 7.783697 8.082028 8.256586
|
|
34
|
+
3 2026-06-25 11:16:19.199000+01:00 7.767045 7.738261 6.965607 7.621467 8.026337 8.236394
|
|
35
|
+
4 2026-06-25 11:21:19.199000+01:00 7.615633 7.667328 6.668064 7.442780 7.972524 8.216294
|
|
36
|
+
```
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling", "hatch-vcs"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "forecose"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{name = "Alexander Sadler"}
|
|
10
|
+
]
|
|
11
|
+
description = "A time-series forecasting extension for pydexcom using Google's TimesFM"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.9"
|
|
14
|
+
dependencies = [
|
|
15
|
+
"numpy",
|
|
16
|
+
"pandas",
|
|
17
|
+
"pydexcom",
|
|
18
|
+
"timesfm[torch]>=2.0.0"
|
|
19
|
+
]
|
|
@@ -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,64 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from unittest.mock import patch, MagicMock
|
|
5
|
+
from forecose import DexcomForecast
|
|
6
|
+
|
|
7
|
+
@pytest.fixture
|
|
8
|
+
def sample_cgm_data():
|
|
9
|
+
"""Sample CGM data for prior 24 hours."""
|
|
10
|
+
return np.random.uniform(4.0, 10.0, size=288)
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def mock_timesfm_model():
|
|
14
|
+
"""
|
|
15
|
+
Mocks the TimesFM model to return dummy arrays.
|
|
16
|
+
"""
|
|
17
|
+
with patch("forecose.forecose.timesfm.TimesFM_2p5_200M_torch.from_pretrained") as mock_model:
|
|
18
|
+
instance = mock_model.return_value
|
|
19
|
+
mock_point = np.array([[5.5] * 12])
|
|
20
|
+
mock_quantiles = np.zeros((1, 12, 10))
|
|
21
|
+
|
|
22
|
+
# quantiles
|
|
23
|
+
mock_quantiles[0, :, 0] = 4.0
|
|
24
|
+
mock_quantiles[0, :, 1] = 4.5
|
|
25
|
+
mock_quantiles[0, :, 4] = 5.0
|
|
26
|
+
mock_quantiles[0, :, 7] = 6.5
|
|
27
|
+
mock_quantiles[0, :, 8] = 7.0
|
|
28
|
+
|
|
29
|
+
instance.forecast.return_value = (mock_point, mock_quantiles)
|
|
30
|
+
yield instance
|
|
31
|
+
|
|
32
|
+
def test_forecaster_structure(sample_cgm_data, mock_timesfm_model):
|
|
33
|
+
"""Verifies the forecast method returns the correct pd.DataFrame structure."""
|
|
34
|
+
|
|
35
|
+
last_time = pd.Timestamp.now(tz="UTC")
|
|
36
|
+
interval = pd.Timedelta(minutes=5)
|
|
37
|
+
|
|
38
|
+
forecaster = DexcomForecast(
|
|
39
|
+
cgm_history=sample_cgm_data,
|
|
40
|
+
last_timestamp=last_time,
|
|
41
|
+
sampling_interval=interval,
|
|
42
|
+
context_len=288,
|
|
43
|
+
horizon=12
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
df = forecaster.forecast()
|
|
47
|
+
|
|
48
|
+
assert len(df) == 12
|
|
49
|
+
|
|
50
|
+
assert list(df.columns) == ["timestamp", "predicted_glucose", "q10", "q25", "q50", "q75", "q90"]
|
|
51
|
+
|
|
52
|
+
assert df["timestamp"].iloc[0] == last_time + interval
|
|
53
|
+
assert df["timestamp"].iloc[1] == last_time + (2 * interval)
|
|
54
|
+
|
|
55
|
+
def test_forecaster_empty_history():
|
|
56
|
+
"""Verifies the model blocks execution if no data is passed from the Dexcom Share API."""
|
|
57
|
+
forecaster = DexcomForecast(
|
|
58
|
+
cgm_history=np.array([]),
|
|
59
|
+
last_timestamp=pd.Timestamp.now(tz="UTC"),
|
|
60
|
+
sampling_interval=pd.Timedelta(minutes=5)
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
with pytest.raises(ValueError, match="No historical data provided"):
|
|
64
|
+
forecaster.forecast()
|