diff --git a/README.md b/README.md index d7641a1..7e1ecdb 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ result_float = iglu.cv_glu(glucose_list) # list of glucose values ``` ## IGLU-R Compatibility Test Status -The current version of IGLU-PYTHON is test-compatible with IGLU-R v4.2.2 +The current version of IGLU-PYTHON is test-compatible with IGLU-R v4.3.0 (2025-07-12) Unless noted, IGLU-R test compatability is considered successful if it achieves precision of 0.001 @@ -73,7 +73,7 @@ Unless noted, IGLU-R test compatability is considered successful if it achieves | lbgi | Low Blood Glucose Index| ✅ |✅ returns float | | m_value | M-value of Schlichtkrull et al | ✅ |✅ returns float | | mad_glu | Median Absolute Deviation | ✅ |✅ returns float | -| mag | Mean Absolute Glucose| ✅ | ✅ only Series(DatetimeIndex) returns float ||| IMHO, Original R implementation has an error | +| mag | Mean Absolute Glucose| ✅ | ✅ only Series(DatetimeIndex) returns float ||| IMHO, Original R bug fixed in v4.3.0 | | mage | Mean Amplitude of Glycemic Excursions| ✅ |✅ only Series(DatetimeIndex) returns float || See algorithm at [MAGE](https://irinagain.github.io/iglu/articles/MAGE.html) | | mean_glu | Mean glucose value | ✅ | ✅ returns float| | median_glu |Median glucose value| ✅ |✅ returns float | diff --git a/iglu_python/mag.py b/iglu_python/mag.py index faee060..78525a3 100644 --- a/iglu_python/mag.py +++ b/iglu_python/mag.py @@ -3,12 +3,12 @@ import numpy as np import pandas as pd -from .utils import CGMS2DayByDay, check_data_columns, is_iglu_r_compatible +from .utils import CGMS2DayByDay, check_data_columns def mag( data: Union[pd.DataFrame, pd.Series], - n: int = 60, + n: int | None = None, # to match a new IGLU-R behavior dt0: Optional[int] = None, inter_gap: int = 45, tz: str = "", @@ -26,9 +26,10 @@ def mag( ---------- data : Union[pd.DataFrame, pd.Series] DataFrame with columns 'id', 'time', and 'gl', or a Series of glucose values - n : int, default=60 + n : int|None, default=None Integer giving the desired interval in minutes over which to calculate - the change in glucose. Default is 60 for hourly intervals. + the change in glucose. Default is None - will be automatically set to dt0 + (from data collection frequency). dt0 : Optional[int], default=None Time interval between measurements in minutes. If None, it will be automatically determined from the data. @@ -85,14 +86,20 @@ def mag( return out -def mag_single(gl: pd.Series, n: int = 60, dt0: Optional[int] = None, inter_gap: int = 45, tz: str = "") -> float: +def mag_single( + gl: pd.Series, + n: int | None = None, # to match a new IGLU-R behavior + dt0: Optional[int] = None, + inter_gap: int = 45, + tz: str = "", +) -> float: """Calculate MAG for a single subject""" # Convert data to day-by-day format data_ip = CGMS2DayByDay(gl, dt0=dt0, inter_gap=inter_gap, tz=tz) dt0_actual = data_ip[2] # Time between measurements in minutes # Ensure n is not less than data collection frequency - if n < dt0_actual: + if n is None or n < dt0_actual: n = dt0_actual # Calculate number of readings per interval @@ -108,27 +115,14 @@ def mag_single(gl: pd.Series, n: int = 60, dt0: Optional[int] = None, inter_gap: # Calculate absolute differences between readings n minutes apart lag = readings_per_interval - if is_iglu_r_compatible(): - idx = np.arange(0, len(gl_values), lag) - gl_values_idx = gl_values[idx] - diffs = gl_values_idx[1:] - gl_values_idx[:-1] - diffs = np.abs(diffs) - diffs = diffs[~np.isnan(diffs)] - # to be IGLU-R test compatible, imho they made error. - # has to be total_time_hours = ((len(diffs)) * n) / 60 - total_time_hours = ((len(gl_values_idx[~np.isnan(gl_values_idx)])) * n) / 60 - if total_time_hours == 0: - return 0.0 - mag = float(np.sum(diffs) / total_time_hours) - else: - diffs = gl_values[lag:] - gl_values[:-lag] - diffs = np.abs(diffs) - diffs = diffs[~np.isnan(diffs)] - - # Calculate MAG: sum of absolute differences divided by total time in hours - total_time_hours = ((len(diffs)) * n) / 60 - if total_time_hours == 0: - return 0.0 - mag = float(np.sum(diffs) / total_time_hours) + diffs = gl_values[lag:] - gl_values[:-lag] + diffs = np.abs(diffs) + diffs = diffs[~np.isnan(diffs)] + + # Calculate MAG: sum of absolute differences divided by total time in hours + total_time_hours = ((len(diffs)) * n) / 60 + if total_time_hours == 0: + return 0.0 + mag = float(np.sum(diffs) / total_time_hours) return mag diff --git a/iglu_r_discrepancies.ipynb b/iglu_r_discrepancies.ipynb index e31841f..035e1d1 100644 --- a/iglu_r_discrepancies.ipynb +++ b/iglu_r_discrepancies.ipynb @@ -10,7 +10,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 28, "metadata": {}, "outputs": [], "source": [ @@ -25,7 +25,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 29, "metadata": {}, "outputs": [ { @@ -35,7 +35,7 @@ "Python version: 3.11.10 (main, Oct 3 2024, 02:26:51) [Clang 14.0.6 ]\n", "R version: [1] \"R version 4.4.3 (2025-02-28)\"\n", "\n", - "iglu version: [1] ‘4.2.2’\n", + "iglu version: [1] ‘4.3.0’\n", "\n", "iglu_py version: 1.1.1\n", "rpy2 version: 3.6.0\n" @@ -61,7 +61,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 30, "metadata": {}, "outputs": [], "source": [ @@ -93,7 +93,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 31, "metadata": {}, "outputs": [ { @@ -190,7 +190,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 32, "metadata": {}, "outputs": [ { @@ -244,7 +244,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 33, "metadata": {}, "outputs": [ { @@ -287,7 +287,7 @@ "1 subject1 102.222222" ] }, - "execution_count": 7, + "execution_count": 33, "metadata": {}, "output_type": "execute_result" } @@ -315,7 +315,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 34, "metadata": {}, "outputs": [ { @@ -361,7 +361,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 35, "metadata": {}, "outputs": [ { @@ -451,7 +451,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 36, "metadata": {}, "outputs": [ { @@ -504,7 +504,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 37, "metadata": {}, "outputs": [ { @@ -626,7 +626,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 38, "metadata": {}, "outputs": [ { @@ -677,7 +677,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 39, "metadata": {}, "outputs": [ { @@ -799,7 +799,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 40, "metadata": {}, "outputs": [ { @@ -847,7 +847,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 41, "metadata": {}, "outputs": [], "source": [ @@ -858,7 +858,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 42, "metadata": {}, "outputs": [ { @@ -1026,7 +1026,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 43, "metadata": {}, "outputs": [ { @@ -1196,7 +1196,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 44, "metadata": {}, "outputs": [ { @@ -1366,7 +1366,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 45, "metadata": {}, "outputs": [ { diff --git a/notebooks/auc_evaluation.ipynb b/notebooks/auc_evaluation.ipynb index 280c5eb..3de3219 100644 --- a/notebooks/auc_evaluation.ipynb +++ b/notebooks/auc_evaluation.ipynb @@ -18,7 +18,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ @@ -51,7 +51,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 11, "metadata": {}, "outputs": [ { @@ -153,7 +153,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "metadata": {}, "outputs": [], "source": [ @@ -167,7 +167,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 13, "metadata": {}, "outputs": [ { @@ -177,7 +177,7 @@ "Python version: 3.11.10 (main, Oct 3 2024, 02:26:51) [Clang 14.0.6 ]\n", "R version: [1] \"R version 4.4.3 (2025-02-28)\"\n", "\n", - "iglu version: [1] ‘4.2.2’\n", + "iglu version: [1] ‘4.3.0’\n", "\n", "iglu_py version: 1.1.1\n", "rpy2 version: 3.6.0\n" @@ -209,7 +209,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 14, "metadata": {}, "outputs": [ { @@ -252,7 +252,7 @@ "1 subject1 102.222222" ] }, - "execution_count": 5, + "execution_count": 14, "metadata": {}, "output_type": "execute_result" } @@ -294,7 +294,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 15, "metadata": {}, "outputs": [ { @@ -410,7 +410,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 16, "metadata": {}, "outputs": [], "source": [ @@ -433,7 +433,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 17, "metadata": {}, "outputs": [ { @@ -476,7 +476,7 @@ "0 subject1 100.0" ] }, - "execution_count": 8, + "execution_count": 17, "metadata": {}, "output_type": "execute_result" } @@ -502,7 +502,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 18, "metadata": {}, "outputs": [ { diff --git a/notebooks/episode_calculation_evaluation.ipynb b/notebooks/episode_calculation_evaluation.ipynb index 8901393..c292cbd 100644 --- a/notebooks/episode_calculation_evaluation.ipynb +++ b/notebooks/episode_calculation_evaluation.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": 7, "metadata": {}, "outputs": [], "source": [ @@ -19,29 +19,9 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 8, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Error importing in API mode: ImportError(\"dlopen(/Users/staskh/Sandbox/iglu_python/.venv/lib/python3.11/site-packages/_rinterface_cffi_api.abi3.so, 0x0002): Library not loaded: /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRblas.dylib\\n Referenced from: <38886600-97A2-37BA-9F86-5263C9A3CF6D> /Users/staskh/Sandbox/iglu_python/.venv/lib/python3.11/site-packages/_rinterface_cffi_api.abi3.so\\n Reason: tried: '/Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRblas.dylib' (no such file), '/System/Volumes/Preboot/Cryptexes/OS/Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRblas.dylib' (no such file), '/Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRblas.dylib' (no such file)\")\n", - "Trying to import in ABI mode.\n", - "/Users/staskh/Sandbox/iglu_python/.venv/lib/python3.11/site-packages/rpy2/rinterface/__init__.py:1185: UserWarning: Environment variable \"PWD\" redefined by R and overriding existing variable. Current: \"/\", R: \"/Users/staskh/Sandbox/iglu_python/notebooks\"\n", - " warnings.warn(\n", - "/Users/staskh/Sandbox/iglu_python/.venv/lib/python3.11/site-packages/rpy2/rinterface/__init__.py:1185: UserWarning: Environment variable \"R_SESSION_TMPDIR\" redefined by R and overriding existing variable. Current: \"/var/folders/fr/9n81007x72d8q41sk2r0hsfm0000gn/T//RtmpyCrQ6k\", R: \"/var/folders/fr/9n81007x72d8q41sk2r0hsfm0000gn/T//Rtmp8dTCDC\"\n", - " warnings.warn(\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n" - ] - } - ], + "outputs": [], "source": [ "import sys\n", "from importlib.metadata import version\n", @@ -53,7 +33,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 9, "metadata": {}, "outputs": [ { @@ -63,7 +43,7 @@ "Python version: 3.11.10 (main, Oct 3 2024, 02:26:51) [Clang 14.0.6 ]\n", "R version: [1] \"R version 4.4.3 (2025-02-28)\"\n", "\n", - "iglu version: [1] ‘4.2.2’\n", + "iglu version: [1] ‘4.3.0’\n", "\n", "iglu_py version: 1.1.1\n", "rpy2 version: 3.6.0\n" @@ -82,7 +62,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ @@ -111,7 +91,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 11, "metadata": {}, "outputs": [ { @@ -263,7 +243,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 12, "metadata": {}, "outputs": [ { diff --git a/notebooks/mag_evaluation.ipynb b/notebooks/mag_evaluation.ipynb new file mode 100644 index 0000000..06f584e --- /dev/null +++ b/notebooks/mag_evaluation.ipynb @@ -0,0 +1,192 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Evaluation of hourly MAG (Mean Absolute Glucose) results\n", + "\n", + "Tested for sequence:\n", + "```\n", + " series_data = pd.Series([150, 160, 170, 180, 190, 200, 210, 220],\n", + " index=pd.date_range(start=\"2020-01-01 10:00:00\", periods=8, freq=\"5min\"))\n", + "\n", + " with n = 20 min\n", + "```\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "%reload_ext autoreload\n", + "%autoreload 2\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# IGLU/IGLU-PY results\n", + "\n", + "**NOTE:** IGLU reports AUC in mg.h/dL\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "from importlib.metadata import version\n", + "\n", + "import iglu_py\n", + "import pandas as pd\n", + "import rpy2.robjects as ro\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Python version: 3.11.10 (main, Oct 3 2024, 02:26:51) [Clang 14.0.6 ]\n", + "R version: [1] \"R version 4.4.3 (2025-02-28)\"\n", + "\n", + "iglu version: [1] ‘4.3.0’\n", + "\n", + "iglu_py version: 1.1.1\n", + "rpy2 version: 3.6.0\n" + ] + } + ], + "source": [ + "# Print versions for future references\n", + "print(f\"Python version: {sys.version}\")\n", + "print(f\"R version: {ro.r('R.version.string')}\")\n", + "iglu_version = str(ro.r('packageVersion(\"iglu\")'))\n", + "print(f\"iglu version: {iglu_version}\")\n", + "print(f\"iglu_py version: {version('iglu-py')}\")\n", + "print(f\"rpy2 version: {version('rpy2')}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Test on synthetic data\n", + "\n", + "- Samples - every 5 min\n", + "- duration - 1h\n", + "- values [80,120] repeated for sampling duration\n", + "\n", + "Expected hourly AUC = 100 mg.h/dL" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idMAG
1subject1120.0
\n", + "
" + ], + "text/plain": [ + " id MAG\n", + "1 subject1 120.0" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "#hours = 1\n", + "dt0 = 5\n", + "samples = 8\n", + "times = pd.date_range(start=\"2020-01-01 10:00:00\", periods=samples, freq=f\"{dt0}min\")\n", + "glucose_values = [150, 160, 170, 180, 190, 200, 210, 220]\n", + "n = 20\n", + "\n", + "syntheticdata = pd.DataFrame({\n", + " 'id': ['subject1'] * samples,\n", + " 'time': times,\n", + " 'gl': glucose_values\n", + "})\n", + "\n", + "synthetic_iglu_mag_results = iglu_py.mag(syntheticdata,n=20)\n", + "synthetic_iglu_mag_results" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Conclusion\n", + "\n", + "Both IGLU-R and IGLU-PYTHON return the same result" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.10" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/pyproject.toml b/pyproject.toml index 814f288..5c002e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "iglu_python" -version = "0.2.5" +version = "0.3.0" description = "Python implementation of the iglu package for continuous glucose monitoring data analysis" readme = "README.md" requires-python = ">=3.11" @@ -24,11 +24,11 @@ classifiers = [ "Topic :: Scientific/Engineering :: Medical Science Apps.", ] dependencies = [ - "numpy>=2.2.6", - "pandas>=2.2.3", - "tzlocal>=5.3.1", - "openpyxl >= 3.1.5", - "matplotlib >= 3.10.0" + "numpy", + "pandas", + "tzlocal", + "openpyxl", + "matplotlib" ] [project.urls] diff --git a/tests/expected_results.json b/tests/expected_results.json index 058fdb3..0ea41d8 100644 --- a/tests/expected_results.json +++ b/tests/expected_results.json @@ -2,7 +2,7 @@ "config": { "local_tz": "Asia/Jerusalem", "iglu-py": "1.1.1", - "iglu": "[1] \u20184.2.2\u2019\n", + "iglu": "[1] \u20184.3.0\u2019\n", "R": "R version 4.4.3 (2025-02-28)" }, "test_runs": [ @@ -38063,20 +38063,20 @@ "13": "Subject 1 day 14" }, "MAG": { - "0": 18.35339183824697, - "1": 7.094564973716318, - "2": 11.029943205706743, - "3": 8.57147328020327, - "4": 7.187591700228256, - "5": 37.92116666666667, - "6": 17.777173293695036, - "7": 12.605942028985508, - "8": 15.293194444444444, - "9": 15.561131433095312, - "10": 18.62293905734755, - "11": 11.107083333333337, - "12": 15.837678118982467, - "13": 18.702222222222225 + "0": 36.15460398508433, + "1": 14.258667946896772, + "2": 19.070347414485084, + "3": 19.164708581904637, + "4": 24.94086789606976, + "5": 49.2176223776224, + "6": 30.635745853500417, + "7": 21.72708583565148, + "8": 24.80140274619675, + "9": 22.345986511046682, + "10": 28.769434811901956, + "11": 28.198461432788235, + "12": 31.51326575765373, + "13": 33.56528301886793 } } }, @@ -221711,11 +221711,11 @@ "4": "Subject 5" }, "MAG": { - "0": 17.010838733213426, - "1": 18.915483886967873, - "2": 27.068617645321183, - "3": 16.665966515695413, - "4": 35.30224976273915 + "0": 27.0968407621454, + "1": 32.22537004615433, + "2": 36.248007541760046, + "3": 27.63983573875867, + "4": 47.31401255870158 } } }, diff --git a/tests/test_mag.py b/tests/test_mag.py index a60b7f0..cc737c4 100644 --- a/tests/test_mag.py +++ b/tests/test_mag.py @@ -108,7 +108,7 @@ def test_mag_series_input(): series_data = pd.Series([150, 160, 170, 180, 190, 200, 210, 220], index=pd.date_range(start="2020-01-01 10:00:00", periods=8, freq="5min")) result = iglu.mag(series_data,n=20) - expected = 60 + expected = 120 # to match fix in IGLU-R v4.3.0 assert isinstance(result, float) np.testing.assert_allclose(result, expected, rtol=1e-3)