Skip to content

Commit 8e420ab

Browse files
committed
Update MAG function to match IGLU-R v4.3.0 behavior
- Change MAG function parameter 'n' from int=60 to int|None=None to match new IGLU-R behavior - Update test expectations to reflect IGLU-R v4.3.0 calculation results - Bump version to 0.3.0 to reflect compatibility with IGLU-R v4.3.0 - Add MAG evaluation notebook for testing and validation This update ensures compatibility with the latest IGLU-R development version (4.3.0) installed from GitHub, addressing changes in the MAG calculation algorithm.
1 parent 010fab4 commit 8e420ab

4 files changed

Lines changed: 209 additions & 29 deletions

File tree

iglu_python/mag.py

Lines changed: 15 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
def mag(
1010
data: Union[pd.DataFrame, pd.Series],
11-
n: int = 60,
11+
n: int|None = None, # to match new IGLU-R behavior
1212
dt0: Optional[int] = None,
1313
inter_gap: int = 45,
1414
tz: str = "",
@@ -26,9 +26,10 @@ def mag(
2626
----------
2727
data : Union[pd.DataFrame, pd.Series]
2828
DataFrame with columns 'id', 'time', and 'gl', or a Series of glucose values
29-
n : int, default=60
29+
n : int|None, default=None
3030
Integer giving the desired interval in minutes over which to calculate
31-
the change in glucose. Default is 60 for hourly intervals.
31+
the change in glucose. Default is None - will be automatically set to dt0
32+
(from data collection frequency).
3233
dt0 : Optional[int], default=None
3334
Time interval between measurements in minutes. If None, it will be automatically
3435
determined from the data.
@@ -85,14 +86,14 @@ def mag(
8586
return out
8687

8788

88-
def mag_single(gl: pd.Series, n: int = 60, dt0: Optional[int] = None, inter_gap: int = 45, tz: str = "") -> float:
89+
def mag_single(gl: pd.Series, n: int|None = None, dt0: Optional[int] = None, inter_gap: int = 45, tz: str = "") -> float:
8990
"""Calculate MAG for a single subject"""
9091
# Convert data to day-by-day format
9192
data_ip = CGMS2DayByDay(gl, dt0=dt0, inter_gap=inter_gap, tz=tz)
9293
dt0_actual = data_ip[2] # Time between measurements in minutes
9394

9495
# Ensure n is not less than data collection frequency
95-
if n < dt0_actual:
96+
if n is None or n < dt0_actual:
9697
n = dt0_actual
9798

9899
# Calculate number of readings per interval
@@ -108,27 +109,14 @@ def mag_single(gl: pd.Series, n: int = 60, dt0: Optional[int] = None, inter_gap:
108109
# Calculate absolute differences between readings n minutes apart
109110
lag = readings_per_interval
110111

111-
if is_iglu_r_compatible():
112-
idx = np.arange(0, len(gl_values), lag)
113-
gl_values_idx = gl_values[idx]
114-
diffs = gl_values_idx[1:] - gl_values_idx[:-1]
115-
diffs = np.abs(diffs)
116-
diffs = diffs[~np.isnan(diffs)]
117-
# to be IGLU-R test compatible, imho they made error.
118-
# has to be total_time_hours = ((len(diffs)) * n) / 60
119-
total_time_hours = ((len(gl_values_idx[~np.isnan(gl_values_idx)])) * n) / 60
120-
if total_time_hours == 0:
121-
return 0.0
122-
mag = float(np.sum(diffs) / total_time_hours)
123-
else:
124-
diffs = gl_values[lag:] - gl_values[:-lag]
125-
diffs = np.abs(diffs)
126-
diffs = diffs[~np.isnan(diffs)]
127-
128-
# Calculate MAG: sum of absolute differences divided by total time in hours
129-
total_time_hours = ((len(diffs)) * n) / 60
130-
if total_time_hours == 0:
131-
return 0.0
132-
mag = float(np.sum(diffs) / total_time_hours)
112+
diffs = gl_values[lag:] - gl_values[:-lag]
113+
diffs = np.abs(diffs)
114+
diffs = diffs[~np.isnan(diffs)]
115+
116+
# Calculate MAG: sum of absolute differences divided by total time in hours
117+
total_time_hours = ((len(diffs)) * n) / 60
118+
if total_time_hours == 0:
119+
return 0.0
120+
mag = float(np.sum(diffs) / total_time_hours)
133121

134122
return mag

notebooks/mag_evaluation.ipynb

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"# Evaluation of hourly MAG (Mean Absolute Glucose) results\n",
8+
"\n",
9+
"Tested for sequence:\n",
10+
"```\n",
11+
" series_data = pd.Series([150, 160, 170, 180, 190, 200, 210, 220],\n",
12+
" index=pd.date_range(start=\"2020-01-01 10:00:00\", periods=8, freq=\"5min\"))\n",
13+
"\n",
14+
" with n = 20 min\n",
15+
"```\n"
16+
]
17+
},
18+
{
19+
"cell_type": "code",
20+
"execution_count": 1,
21+
"metadata": {},
22+
"outputs": [],
23+
"source": [
24+
"%reload_ext autoreload\n",
25+
"%autoreload 2\n"
26+
]
27+
},
28+
{
29+
"cell_type": "markdown",
30+
"metadata": {},
31+
"source": [
32+
"# IGLU/IGLU-PY results\n",
33+
"\n",
34+
"**NOTE:** IGLU reports AUC in mg.h/dL\n"
35+
]
36+
},
37+
{
38+
"cell_type": "code",
39+
"execution_count": 3,
40+
"metadata": {},
41+
"outputs": [],
42+
"source": [
43+
"import sys\n",
44+
"from importlib.metadata import version\n",
45+
"\n",
46+
"import iglu_py\n",
47+
"import pandas as pd\n",
48+
"import rpy2.robjects as ro\n"
49+
]
50+
},
51+
{
52+
"cell_type": "code",
53+
"execution_count": 4,
54+
"metadata": {},
55+
"outputs": [
56+
{
57+
"name": "stdout",
58+
"output_type": "stream",
59+
"text": [
60+
"Python version: 3.11.10 (main, Oct 3 2024, 02:26:51) [Clang 14.0.6 ]\n",
61+
"R version: [1] \"R version 4.4.3 (2025-02-28)\"\n",
62+
"\n",
63+
"iglu version: [1] ‘4.3.0’\n",
64+
"\n",
65+
"iglu_py version: 1.1.1\n",
66+
"rpy2 version: 3.6.0\n"
67+
]
68+
}
69+
],
70+
"source": [
71+
"# Print versions for future references\n",
72+
"print(f\"Python version: {sys.version}\")\n",
73+
"print(f\"R version: {ro.r('R.version.string')}\")\n",
74+
"iglu_version = str(ro.r('packageVersion(\"iglu\")'))\n",
75+
"print(f\"iglu version: {iglu_version}\")\n",
76+
"print(f\"iglu_py version: {version('iglu-py')}\")\n",
77+
"print(f\"rpy2 version: {version('rpy2')}\")"
78+
]
79+
},
80+
{
81+
"cell_type": "markdown",
82+
"metadata": {},
83+
"source": [
84+
"## Test on synthetic data\n",
85+
"\n",
86+
"- Samples - every 5 min\n",
87+
"- duration - 1h\n",
88+
"- values [80,120] repeated for sampling duration\n",
89+
"\n",
90+
"Expected hourly AUC = 100 mg.h/dL"
91+
]
92+
},
93+
{
94+
"cell_type": "code",
95+
"execution_count": 6,
96+
"metadata": {},
97+
"outputs": [
98+
{
99+
"data": {
100+
"text/html": [
101+
"<div>\n",
102+
"<style scoped>\n",
103+
" .dataframe tbody tr th:only-of-type {\n",
104+
" vertical-align: middle;\n",
105+
" }\n",
106+
"\n",
107+
" .dataframe tbody tr th {\n",
108+
" vertical-align: top;\n",
109+
" }\n",
110+
"\n",
111+
" .dataframe thead th {\n",
112+
" text-align: right;\n",
113+
" }\n",
114+
"</style>\n",
115+
"<table border=\"1\" class=\"dataframe\">\n",
116+
" <thead>\n",
117+
" <tr style=\"text-align: right;\">\n",
118+
" <th></th>\n",
119+
" <th>id</th>\n",
120+
" <th>MAG</th>\n",
121+
" </tr>\n",
122+
" </thead>\n",
123+
" <tbody>\n",
124+
" <tr>\n",
125+
" <th>1</th>\n",
126+
" <td>subject1</td>\n",
127+
" <td>120.0</td>\n",
128+
" </tr>\n",
129+
" </tbody>\n",
130+
"</table>\n",
131+
"</div>"
132+
],
133+
"text/plain": [
134+
" id MAG\n",
135+
"1 subject1 120.0"
136+
]
137+
},
138+
"execution_count": 6,
139+
"metadata": {},
140+
"output_type": "execute_result"
141+
}
142+
],
143+
"source": [
144+
"#hours = 1\n",
145+
"dt0 = 5\n",
146+
"samples = 8\n",
147+
"times = pd.date_range(start=\"2020-01-01 10:00:00\", periods=samples, freq=f\"{dt0}min\")\n",
148+
"glucose_values = [150, 160, 170, 180, 190, 200, 210, 220]\n",
149+
"n = 20\n",
150+
"\n",
151+
"syntheticdata = pd.DataFrame({\n",
152+
" 'id': ['subject1'] * samples,\n",
153+
" 'time': times,\n",
154+
" 'gl': glucose_values\n",
155+
"})\n",
156+
"\n",
157+
"synthetic_iglu_mag_results = iglu_py.mag(syntheticdata,n=20)\n",
158+
"synthetic_iglu_mag_results"
159+
]
160+
},
161+
{
162+
"cell_type": "markdown",
163+
"metadata": {},
164+
"source": [
165+
"# Conclusion\n",
166+
"\n",
167+
"Both IGLU-R and IGLU-PYTHON return the same result"
168+
]
169+
}
170+
],
171+
"metadata": {
172+
"kernelspec": {
173+
"display_name": ".venv",
174+
"language": "python",
175+
"name": "python3"
176+
},
177+
"language_info": {
178+
"codemirror_mode": {
179+
"name": "ipython",
180+
"version": 3
181+
},
182+
"file_extension": ".py",
183+
"mimetype": "text/x-python",
184+
"name": "python",
185+
"nbconvert_exporter": "python",
186+
"pygments_lexer": "ipython3",
187+
"version": "3.11.10"
188+
}
189+
},
190+
"nbformat": 4,
191+
"nbformat_minor": 2
192+
}

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "iglu_python"
7-
version = "0.2.5"
7+
version = "0.3.0"
88
description = "Python implementation of the iglu package for continuous glucose monitoring data analysis"
99
readme = "README.md"
1010
requires-python = ">=3.11"

tests/test_mag.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ def test_mag_series_input():
108108
series_data = pd.Series([150, 160, 170, 180, 190, 200, 210, 220],
109109
index=pd.date_range(start="2020-01-01 10:00:00", periods=8, freq="5min"))
110110
result = iglu.mag(series_data,n=20)
111-
expected = 60
111+
expected = 120 # to match fix in IGLU-R v4.3.0
112112
assert isinstance(result, float)
113113
np.testing.assert_allclose(result, expected, rtol=1e-3)
114114

0 commit comments

Comments
 (0)