Skip to content

Commit 7d4c82b

Browse files
committed
implement plotting functionality
1 parent 28d7862 commit 7d4c82b

4 files changed

Lines changed: 452 additions & 3 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ IGLU_PYTHON extends beyond the capabilities of the original IGLU-R package by of
101101
| load_dexcom() | Load Timeseries from Dexcom device file (CGM reading converted into mg/dL)
102102
| **PLOT/VISUALISE CGM **
103103
| plot_daily() | Plot daily Glucose values for each day |
104+
| plot_statistics() | Plot median + quantile daily statistics |
104105

105106
# Installation
106107

iglu_python/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from .ea1c import ea1c
1111
from .episode_calculation import episode_calculation
1212
from .extension.load_data import load_dexcom, load_libre
13-
from .extension.plots import plot_daily
13+
from .extension.plots import plot_daily, plot_statistics
1414
from .gmi import gmi
1515
from .grade import grade
1616
from .grade_eugly import grade_eugly
@@ -87,6 +87,7 @@
8787
"modd",
8888
"pgs",
8989
"plot_daily",
90+
"plot_statistics",
9091
"process_data",
9192
"quantile_glu",
9293
"range_glu",

iglu_python/extension/plots.py

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44

55
import matplotlib.pyplot as plt
6+
import numpy as np
67
import pandas as pd
78

89

@@ -38,7 +39,7 @@ def plot_daily(cgm_timeseries: pd.Series, lower: int = 70, upper: int = 140) ->
3839
axes[i].plot(cgm_one_day.index, cgm_one_day.values)
3940
axes[i].set_title(f"Day: {day.strftime('%Y-%m-%d')}")
4041
axes[i].set_ylabel("Glucose (mg/dL)")
41-
axes[i].set_ylim(0, max(max(cgm_one_day.values), 300))
42+
axes[i].set_ylim(0, max(np.nanmax(cgm_one_day.values), 300))
4243

4344
# Fill area above upper limit and plot it in orange
4445
upper_array = [upper] * len(cgm_one_day.values)
@@ -66,3 +67,82 @@ def plot_daily(cgm_timeseries: pd.Series, lower: int = 70, upper: int = 140) ->
6667

6768
fig.tight_layout()
6869
return fig
70+
71+
def plot_statistics(cgm_timeseries: pd.Series, lower: int = 70, upper: int = 140) -> plt.Figure:
72+
"""
73+
Plot statistical representation of daily trends
74+
in the single 24h timeline, this will plot mean sample trends, 10%, +25% and 75% and 90% quantiles
75+
"""
76+
# check if cgm_timeseries is a pandas series
77+
if not isinstance(cgm_timeseries, pd.Series):
78+
raise AttributeError("cgm_timeseries must be a pandas series")
79+
80+
# check if cgm_timeseries is not a datetime index
81+
if not isinstance(cgm_timeseries.index, pd.DatetimeIndex):
82+
raise AttributeError("cgm_timeseries must have a datetime index")
83+
84+
# check if cgm_timeseries is not empty
85+
if len(cgm_timeseries) < 16:
86+
raise ValueError("cgm_timeseries is too short to plot statistics")
87+
88+
# get sampling frequency
89+
time_diffs = cgm_timeseries.index.to_series().diff()
90+
dt0 = int(time_diffs.mode().iloc[0].total_seconds() / 60)
91+
92+
93+
# Create time grid
94+
start_time = cgm_timeseries.index.min().floor("D")
95+
end_time = cgm_timeseries.index.max().ceil("D")
96+
time_grid = pd.date_range(start=start_time, end=end_time, freq=f"{dt0}min")
97+
# remove the last time point
98+
time_grid = time_grid[:-1]
99+
100+
# interpolate
101+
cgm_timeseries_interpolated = np.interp(
102+
(time_grid - start_time).total_seconds() / 60,
103+
(cgm_timeseries.index - start_time).total_seconds() / 60,
104+
cgm_timeseries.values,
105+
left=np.nan,
106+
right=np.nan,
107+
)
108+
109+
# reorganise as 2d array with rows as timepoints and columns as days
110+
# Reshape to days
111+
n_days = (end_time - start_time).days
112+
n_points_per_day = 24 * 60 // dt0
113+
cgm_timeseries_2d = cgm_timeseries_interpolated.reshape(n_days, n_points_per_day)
114+
115+
# one day time grid
116+
time_grid_one_day = time_grid[0:n_points_per_day]
117+
# get mean sample trends
118+
mean_sample_trends = np.nanmean(cgm_timeseries_2d, axis=0)
119+
120+
# get 10%, +25% and 75% and 90% quantiles
121+
quantiles = np.nanpercentile(cgm_timeseries_2d, [10, 25, 75, 90], axis=0)
122+
123+
# create figure and axes
124+
fig, ax = plt.subplots(figsize=(12, 6))
125+
126+
# plot mean sample trends
127+
ax.plot(time_grid_one_day,mean_sample_trends, color="orange", alpha=1, linewidth=3, label='Mean sample trends')
128+
129+
# plot quantiles
130+
ax.fill_between(time_grid_one_day, quantiles[0], quantiles[1], alpha=0.25, color="blue",label='10% quantile')
131+
ax.fill_between(time_grid_one_day, quantiles[1], mean_sample_trends, alpha=0.50, color="blue",label='25% quantile')
132+
ax.fill_between(time_grid_one_day, mean_sample_trends, quantiles[2], alpha=0.50, color="blue",label='75% quantile')
133+
ax.fill_between(time_grid_one_day, quantiles[2], quantiles[3], alpha=0.25, color="blue",label='90% quantile')
134+
135+
ax.axhline(y=upper, color="orange", linestyle="--", alpha=0.7, label=f"Hyper threshold ({upper} mg/dL)")
136+
ax.axhline(y=lower, color="green", linestyle="--", alpha=0.7, label=f"Hypo threshold ({lower} mg/dL)")
137+
138+
ax.set_ylim(min(30, np.nanmin(cgm_timeseries.values)), max(np.nanmax(cgm_timeseries.values), 300))
139+
ax.set_xlabel("Time (hours)")
140+
time_grid_one_day = pd.date_range(start=start_time, periods=24, freq="1h")
141+
ax.set_xticks(time_grid_one_day) # Show every hour from 0 to 24
142+
ax.set_xticklabels([f"{h.hour}" for h in time_grid_one_day]) # Format as HH:00
143+
ax.grid(True, alpha=0.3, linestyle="--")
144+
ax.legend()
145+
fig.tight_layout()
146+
147+
# plot the results
148+
return fig

0 commit comments

Comments
 (0)