glucose360 0.0.1__py3-none-any.whl → 0.0.2__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.
- glucose360/__init__.py +5 -0
- glucose360/config.ini +10 -0
- glucose360/event_colors.json +47 -0
- glucose360/events.py +45 -43
- glucose360/features.py +314 -24
- glucose360/main.py +13 -0
- glucose360/plots.py +537 -493
- glucose360/preprocessing.py +594 -557
- glucose360-0.0.2.dist-info/METADATA +177 -0
- glucose360-0.0.2.dist-info/RECORD +13 -0
- {glucose360-0.0.1.dist-info → glucose360-0.0.2.dist-info}/WHEEL +1 -1
- glucose360-0.0.2.dist-info/licenses/LICENSE +339 -0
- glucose360-0.0.1.dist-info/LICENSE +0 -674
- glucose360-0.0.1.dist-info/METADATA +0 -34
- glucose360-0.0.1.dist-info/RECORD +0 -10
- {glucose360-0.0.1.dist-info → glucose360-0.0.2.dist-info}/top_level.txt +0 -0
glucose360/plots.py
CHANGED
@@ -1,494 +1,538 @@
|
|
1
|
-
import pandas as pd
|
2
|
-
import numpy as np
|
3
|
-
import
|
4
|
-
import configparser
|
5
|
-
import json
|
6
|
-
|
7
|
-
import plotly.express as px
|
8
|
-
from plotly.subplots import make_subplots
|
9
|
-
import plotly.graph_objects as go
|
10
|
-
|
11
|
-
import
|
12
|
-
|
13
|
-
from
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
config
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
:
|
37
|
-
:
|
38
|
-
:
|
39
|
-
:
|
40
|
-
:
|
41
|
-
:
|
42
|
-
:
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
)
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
:
|
60
|
-
:
|
61
|
-
:
|
62
|
-
:
|
63
|
-
:
|
64
|
-
:
|
65
|
-
:
|
66
|
-
:
|
67
|
-
:
|
68
|
-
:
|
69
|
-
:
|
70
|
-
:
|
71
|
-
:
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
data =
|
76
|
-
data[
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
with open(
|
86
|
-
color_dict = json.load(colors_file)
|
87
|
-
colors = list(color_dict.values())
|
88
|
-
color_map = {event_type: colors[i] for i, event_type in enumerate(event_types)}
|
89
|
-
|
90
|
-
fig = make_subplots(rows=len(days), cols=2 if show_events else 1, column_widths=[0.66,0.34] if show_events else None,
|
91
|
-
specs=[[{"type":"scatter"}, {"type":"table"}] for _ in range(len(days))] if show_events else None,
|
92
|
-
horizontal_spacing=0.01 if show_events else None,
|
93
|
-
vertical_spacing=0.05 if show_events else None)
|
94
|
-
|
95
|
-
num_events = 0
|
96
|
-
|
97
|
-
for idx, (day, dataset) in enumerate(data.groupby("Day"), start=1):
|
98
|
-
fig.add_trace(go.Scatter(x=dataset[TIME],y=dataset[GLUCOSE],mode='lines+markers',name=str(day), showlegend=False), row=idx, col=1)
|
99
|
-
fig.update_xaxes(
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
fig.
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
:param
|
158
|
-
:type
|
159
|
-
:param
|
160
|
-
:type
|
161
|
-
:param
|
162
|
-
:type
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
if
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
:
|
199
|
-
|
200
|
-
|
201
|
-
""
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
"""
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
:param
|
240
|
-
:type
|
241
|
-
:param
|
242
|
-
:type
|
243
|
-
:param
|
244
|
-
:type
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
fig.
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
:param
|
315
|
-
:type
|
316
|
-
:param
|
317
|
-
:type
|
318
|
-
:param
|
319
|
-
:type
|
320
|
-
:param
|
321
|
-
:type
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
""
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
if
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
for
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
1
|
+
import pandas as pd
|
2
|
+
import numpy as np
|
3
|
+
import glucose360.preprocessing as pp
|
4
|
+
import configparser
|
5
|
+
import json
|
6
|
+
from importlib import resources
|
7
|
+
import plotly.express as px
|
8
|
+
from plotly.subplots import make_subplots
|
9
|
+
import plotly.graph_objects as go
|
10
|
+
import os
|
11
|
+
from glucose360.preprocessing import load_config
|
12
|
+
from glucose360.features import percent_time_in_range, percent_time_in_tight_range, mean, CV, GMI
|
13
|
+
from pathlib import Path
|
14
|
+
|
15
|
+
# Initialize config at module level
|
16
|
+
config = load_config()
|
17
|
+
INTERVAL = int(config["variables"]["interval"])
|
18
|
+
ID = config['variables']['id']
|
19
|
+
GLUCOSE = config['variables']['glucose']
|
20
|
+
TIME = config['variables']['time']
|
21
|
+
BEFORE = config['variables']['before']
|
22
|
+
AFTER = config['variables']['after']
|
23
|
+
TYPE = config['variables']['type']
|
24
|
+
DESCRIPTION = config['variables']['description']
|
25
|
+
|
26
|
+
def daily_plot_all(
|
27
|
+
df: pd.DataFrame,
|
28
|
+
events: pd.DataFrame = None,
|
29
|
+
save: str = None,
|
30
|
+
height: int = 2000
|
31
|
+
):
|
32
|
+
"""Graphs daily plots for all of the patients in the given DataFrame.
|
33
|
+
Also saves these plots as PDFs and HTMLs if passed a valid path.
|
34
|
+
|
35
|
+
:param df: the DataFrame (following package guidelines) containing the CGM data to plot
|
36
|
+
:type df: 'pandas.DataFrame'
|
37
|
+
:param events: a DataFrame containing any events to mark on the daily plots, defaults to None
|
38
|
+
:type events: 'pandas.DataFrame', optional
|
39
|
+
:param save: path of the location where the saved PDF and HTML versions of the plots are saved, defaults to None
|
40
|
+
:type save: str, optional
|
41
|
+
:param height: the height (in pixels) of the resulting plot(s), defaults to 2000
|
42
|
+
:type height: int, optional
|
43
|
+
"""
|
44
|
+
for id, data in df.groupby(ID):
|
45
|
+
daily_plot(data, id, height, events, save)
|
46
|
+
|
47
|
+
def daily_plot(
|
48
|
+
df: pd.DataFrame,
|
49
|
+
id: str,
|
50
|
+
height: int = 2000,
|
51
|
+
events: pd.DataFrame = None,
|
52
|
+
save: bool = False,
|
53
|
+
app: bool = False
|
54
|
+
):
|
55
|
+
"""Graphs a daily (time-series) plot for the given patient within the given DataFrame.
|
56
|
+
Also saves this plot as both a PDF and HTML file if passed a valid path.
|
57
|
+
|
58
|
+
:param df: the DataFrame (following package guidelines) containing the CGM data to plot
|
59
|
+
:type df: 'pandas.DataFrame'
|
60
|
+
:param id: the identification of the patient whose CGM data to plot within the given DataFrame
|
61
|
+
:type id: str
|
62
|
+
:param events: a DataFrame containing any events to mark on the daily plots, defaults to None
|
63
|
+
:type events: 'pandas.DataFrame', optional
|
64
|
+
:param save: path of the location where the saved PDF and HTML versions of the plot are saved, defaults to None
|
65
|
+
:type save: str, optional
|
66
|
+
:param height: the height (in pixels) of the resulting plot, defaults to 2000
|
67
|
+
:type height: int, optional
|
68
|
+
:param app: boolean indicating whether to return the Plotly figure instead of rendering it (used mainly within the web application), defaults to False
|
69
|
+
:type app: bool, optional
|
70
|
+
:return: None if app is False, otherwise the Plotly figure
|
71
|
+
:rtype: 'plotly.graph_objects.Figure' | None
|
72
|
+
"""
|
73
|
+
show_events = not (events is None or events[events[ID] == id].empty)
|
74
|
+
data = df.loc[id].copy()
|
75
|
+
data[TIME] = pd.to_datetime(data[TIME])
|
76
|
+
data["Day"] = data[TIME].dt.date
|
77
|
+
|
78
|
+
days = data["Day"].unique().astype(str).tolist()
|
79
|
+
offset = pd.Timedelta(hours=1, minutes=30)
|
80
|
+
|
81
|
+
rendered_types = []
|
82
|
+
if show_events:
|
83
|
+
event_types = events[TYPE].unique()
|
84
|
+
colors_path = Path(__file__).parent / 'event_colors.json'
|
85
|
+
with open(colors_path) as colors_file:
|
86
|
+
color_dict = json.load(colors_file)
|
87
|
+
colors = list(color_dict.values())
|
88
|
+
color_map = {event_type: colors[i] for i, event_type in enumerate(event_types)}
|
89
|
+
|
90
|
+
fig = make_subplots(rows=len(days), cols=2 if show_events else 1, column_widths=[0.66,0.34] if show_events else None,
|
91
|
+
specs=[[{"type":"scatter"}, {"type":"table"}] for _ in range(len(days))] if show_events else None,
|
92
|
+
horizontal_spacing=0.01 if show_events else None,
|
93
|
+
vertical_spacing=0.05 if show_events else None)
|
94
|
+
|
95
|
+
num_events = 0
|
96
|
+
|
97
|
+
for idx, (day, dataset) in enumerate(data.groupby("Day"), start=1):
|
98
|
+
fig.add_trace(go.Scatter(x=dataset[TIME].apply(lambda dt: dt.isoformat()),y=dataset[GLUCOSE],mode='lines+markers',name=str(day), showlegend=False), row=idx, col=1)
|
99
|
+
fig.update_xaxes(
|
100
|
+
range=[pd.Timestamp(day) - offset, pd.Timestamp(day) + pd.Timedelta(days=1) + offset],
|
101
|
+
row=idx, col=1,
|
102
|
+
tickfont=dict(size=20),
|
103
|
+
title=dict(font=dict(size=35))
|
104
|
+
)
|
105
|
+
fig.update_yaxes(
|
106
|
+
title=dict(text="Glucose Value (mg/dL)"),
|
107
|
+
tickfont=dict(size=20),
|
108
|
+
row=idx,
|
109
|
+
col=1
|
110
|
+
)
|
111
|
+
|
112
|
+
if show_events:
|
113
|
+
day_events = events[(events[TIME].dt.date == day) & (events[ID] == id)].sort_values(TIME)
|
114
|
+
num_events = max(num_events, day_events.shape[0])
|
115
|
+
if not day_events.empty:
|
116
|
+
table_body = [day_events[TIME].dt.time.astype(str).tolist(), day_events[DESCRIPTION].tolist()]
|
117
|
+
fig.add_trace(
|
118
|
+
go.Table(
|
119
|
+
columnwidth=[25,25],
|
120
|
+
header=dict(values = [["<b>Time</b>"], ["<b>Description</b>"]],font=dict(size=22)),
|
121
|
+
cells=dict(values=table_body,align=['left', 'left'],font=dict(size=20))
|
122
|
+
), row=idx, col=2
|
123
|
+
)
|
124
|
+
|
125
|
+
for _, row in day_events.drop_duplicates(subset=[TIME, TYPE]).iterrows():
|
126
|
+
already_rendered = (row[TYPE] in rendered_types)
|
127
|
+
if (not already_rendered): rendered_types.append(row[TYPE])
|
128
|
+
fig.add_shape(go.layout.Shape(
|
129
|
+
type="line", yref="y domain",
|
130
|
+
x0=pd.to_datetime(row[TIME]), y0=0,
|
131
|
+
x1=pd.to_datetime(row[TIME]), y1=1,
|
132
|
+
line_color=color_map[row[TYPE]], line_dash="dash",
|
133
|
+
name=row[TYPE], legendgroup=row[TYPE], showlegend=(not already_rendered)), row=idx, col=1)
|
134
|
+
|
135
|
+
fig.update_yaxes(range=[min(np.min(data[GLUCOSE]), 60) - 10, max(np.max(data[GLUCOSE]), 180) + 10])
|
136
|
+
fig.update_layout(
|
137
|
+
title=dict(text=f"Daily Plot for {id}", font=dict(size=40)),
|
138
|
+
height=height,
|
139
|
+
showlegend=False
|
140
|
+
)
|
141
|
+
|
142
|
+
if save:
|
143
|
+
path = os.path.join(save, f"{id}_daily_plot")
|
144
|
+
fig.write_image(path + ".pdf", width=1500, height=height)
|
145
|
+
fig.write_image(path + ".png", width=1500, height=height)
|
146
|
+
fig.write_image(path + ".jpeg", width=1500, height=height)
|
147
|
+
fig.write_html(path +".html")
|
148
|
+
|
149
|
+
if app: return fig
|
150
|
+
fig.show()
|
151
|
+
|
152
|
+
def event_plot_all(df: pd.DataFrame, id: str, events: pd.DataFrame, type: str, save: bool = False):
|
153
|
+
"""Graphs all event plots of a certain type for the given patient within the given DataFrame.
|
154
|
+
|
155
|
+
:param df: the DataFrame (following package guidelines) containing the CGM data to plot
|
156
|
+
:type df: 'pandas.DataFrame'
|
157
|
+
:param id: the identification of the patient whose CGM data to plot within the given DataFrame
|
158
|
+
:type id: str
|
159
|
+
:param events: a DataFrame containing events to be plotted, defaults to None
|
160
|
+
:type events: 'pandas.DataFrame'
|
161
|
+
:param type: the type of events to plot
|
162
|
+
:type type: str
|
163
|
+
"""
|
164
|
+
relevant_events = events[(events[ID] == id) & (events[TYPE] == type)]
|
165
|
+
for index, event in relevant_events.iterrows():
|
166
|
+
event_plot(df, id, event, relevant_events, save=save)
|
167
|
+
|
168
|
+
def event_plot(df: pd.DataFrame, id: str, event: pd.Series, events: pd.DataFrame = None, save: bool = False, app: bool = False):
|
169
|
+
"""Graphs an event plot for the given patient within the given DataFrame.
|
170
|
+
|
171
|
+
:param df: the DataFrame (following package guidelines) containing the CGM data to plot
|
172
|
+
:type df: 'pandas.DataFrame'
|
173
|
+
:param id: the identification of the patient whose CGM data to plot within the given DataFrame
|
174
|
+
:type id: str
|
175
|
+
:param event: the event to be displayed
|
176
|
+
:type event: 'pandas.Series'
|
177
|
+
:param events: a DataFrame containing any extra events to be marked within the event plot, defaults to None
|
178
|
+
:type events: 'pandas.DataFrame', optional
|
179
|
+
:param save: boolean indicating whether to save the plot as a file, defaults to False
|
180
|
+
:type save: bool, optional
|
181
|
+
:param app: boolean indicating whether to return the Plotly figure instead of rendering it (used mainly within the web application), defaults to False
|
182
|
+
:type app: bool, optional
|
183
|
+
:return: None if app is False, otherwise the Plotly figure
|
184
|
+
:rtype: 'plotly.graph_objects.Figure' | None
|
185
|
+
"""
|
186
|
+
if event[ID] != id: raise Exception("Given event does not match the 'id' given.")
|
187
|
+
data = df.loc[id].copy()
|
188
|
+
event = event.copy()
|
189
|
+
event[TIME] = pd.to_datetime(event[TIME])
|
190
|
+
before = event[TIME] - pd.Timedelta(minutes=event[BEFORE])
|
191
|
+
after = event[TIME] + pd.Timedelta(minutes=event[AFTER])
|
192
|
+
|
193
|
+
data["Day"] = data[TIME].dt.date
|
194
|
+
subplot_figs = [go.Scatter(x=dataset[TIME].apply(lambda dt: dt.isoformat()),y=dataset[GLUCOSE],mode='lines+markers', name=str(day)) for day, dataset in data.groupby("Day")]
|
195
|
+
fig = go.Figure(data=subplot_figs, layout=go.Layout(title=dict(text=f"Event Plot for {id}", font=dict(size=40)), legend=dict(font=dict(size=20))))
|
196
|
+
|
197
|
+
event_data = events[events[ID] == id] if events is not None else pd.DataFrame()
|
198
|
+
if not event_data.empty: create_event_lines(fig, event_data)
|
199
|
+
|
200
|
+
fig.update_xaxes(type="date", range=[before, after], tickfont=dict(size=20), title=dict(font=dict(size=35)))
|
201
|
+
fig.update_yaxes(title_text="Glucose Value (mg/dL)", tickfont=dict(size=20), title=dict(font=dict(size=35)))
|
202
|
+
|
203
|
+
if save:
|
204
|
+
path = os.path.join(save, f"{id}_event_plot")
|
205
|
+
fig.write_image(path + ".png", width=1500, height=1000)
|
206
|
+
fig.write_html(path +".html")
|
207
|
+
|
208
|
+
if app: return fig
|
209
|
+
fig.show()
|
210
|
+
|
211
|
+
def create_event_lines(fig: go.Figure, events: pd.DataFrame):
|
212
|
+
"""Marks vertical lines within the given plotly Figure for the given events
|
213
|
+
|
214
|
+
:param fig: the plotly figure to mark vertical lines in
|
215
|
+
:type fig: plotly.graph_objects.Figure
|
216
|
+
:param events: Pandas DataFrame containing vevents to mark
|
217
|
+
:type events: pandas.DataFrame''
|
218
|
+
"""
|
219
|
+
event_types = events[TYPE].unique()
|
220
|
+
events[TIME] = pd.to_datetime(events[TIME])
|
221
|
+
colors_path = Path(__file__).parent / 'event_colors.json'
|
222
|
+
with open(colors_path) as colors_file:
|
223
|
+
color_dict = json.load(colors_file)
|
224
|
+
colors = list(color_dict.values())
|
225
|
+
color_map = {event_type: colors[i] for i, event_type in enumerate(event_types)}
|
226
|
+
|
227
|
+
rendered_types = []
|
228
|
+
for event in events.itertuples():
|
229
|
+
time = getattr(event, TIME)
|
230
|
+
type = getattr(event, TYPE)
|
231
|
+
already_rendered = (type in rendered_types)
|
232
|
+
if (not already_rendered): rendered_types.append(type)
|
233
|
+
fig.add_vline(x=time, line_width=3, line_dash="dash", line_color=color_map[type], name=type, legendgroup=type, showlegend=(not already_rendered))
|
234
|
+
|
235
|
+
def weekly_plot_all(df: pd.DataFrame, save: str = None, height: int = 1000):
|
236
|
+
"""Graphs weekly plots for all of the patients within the given DataFrame.
|
237
|
+
Also saves these plots as PDF and HTML files if passed a valid path.
|
238
|
+
|
239
|
+
:param df: the DataFrame (following package guidelines) containing the CGM data to plot
|
240
|
+
:type df: 'pandas.DataFrame'
|
241
|
+
:param save: path of the location where the saved PDF and HTML versions of the plots are saved, defaults to None
|
242
|
+
:type save: str, optional
|
243
|
+
:param height: the height (in pixels) of the resulting plot(s), defaults to 1000
|
244
|
+
:type height: int, optional
|
245
|
+
"""
|
246
|
+
for id, data in df.groupby(ID):
|
247
|
+
weekly_plot(data, id, height)
|
248
|
+
|
249
|
+
def weekly_plot(df: pd.DataFrame, id: str, save: str = None, height: int = 1000, app = False):
|
250
|
+
"""Graphs a weekly (time-series) plot for the given patient within the given DataFrame.
|
251
|
+
Also saves this plot as a PDF and HTML if passed a valid path.
|
252
|
+
|
253
|
+
:param df: the DataFrame (following package guidelines) containing the CGM data to plot
|
254
|
+
:type df: 'pandas.DataFrame'
|
255
|
+
:param id: the identification of the patient whose CGM data to plot within the given DataFrame
|
256
|
+
:type id: str
|
257
|
+
:param save: path of the location where the saved PDF and HTML versions of the plots are saved, defaults to None
|
258
|
+
:type save: str, optional
|
259
|
+
:param height: the height (in pixels) of the resulting plot, defaults to 1000
|
260
|
+
:type height: int, optional
|
261
|
+
:param app: boolean indicating whether to return the Plotly figure instead of rendering it (used mainly within the web application), defaults to False
|
262
|
+
:type app: bool, optional
|
263
|
+
:return: None if app is False, otherwise the Plotly figure
|
264
|
+
:rtype: 'plotly.graph_objects.Figure' | None
|
265
|
+
"""
|
266
|
+
data = df.loc[id].reset_index().copy()
|
267
|
+
data.set_index(TIME, inplace=True)
|
268
|
+
|
269
|
+
weekly_data = data.groupby(pd.Grouper(freq='W'))
|
270
|
+
weekly_dfs = [group for _, group in weekly_data]
|
271
|
+
|
272
|
+
fig = make_subplots(rows=len(weekly_dfs), cols=1)
|
273
|
+
for week_index in range(len(weekly_dfs)):
|
274
|
+
week = weekly_dfs[week_index].reset_index()
|
275
|
+
fig.add_trace(
|
276
|
+
go.Scatter(
|
277
|
+
x=week[TIME].apply(lambda dt: dt.isoformat()),
|
278
|
+
y=week[GLUCOSE],
|
279
|
+
mode='lines+markers',
|
280
|
+
), row=(week_index+1), col=1
|
281
|
+
)
|
282
|
+
fig.update_yaxes(title_text="Glucose Value (mg/dL)", row=(week_index+1), col=1)
|
283
|
+
|
284
|
+
if len(weekly_dfs) > 1:
|
285
|
+
offset_before = pd.Timedelta(hours=10)
|
286
|
+
offset_after = pd.Timedelta(hours=10)
|
287
|
+
first_end = pd.Timestamp(weekly_dfs[1].reset_index()[TIME].dt.date.iloc[0])
|
288
|
+
first_start = first_end - pd.Timedelta(weeks=1)
|
289
|
+
last_start = pd.Timestamp(weekly_dfs[-1].reset_index()[TIME].dt.date.iloc[0])
|
290
|
+
last_end = last_start + pd.Timedelta(weeks=1)
|
291
|
+
fig.update_xaxes(range=[first_start - offset_before, first_end + offset_after], row=1, col=1)
|
292
|
+
fig.update_xaxes(range=[last_start - offset_before, last_end + offset_after], row=len(weekly_dfs), col=1)
|
293
|
+
fig.update_yaxes(range=[min(np.min(data[GLUCOSE]), 60) - 10, max(np.max(data[GLUCOSE]), 180) + 10],
|
294
|
+
tickfont=dict(size=30),
|
295
|
+
title=dict(text="Glucose Value<br>(mg/dL)", font=dict(size=35)))
|
296
|
+
fig.update_xaxes(tickformat="%B %d, %Y <br> (%a)", tickfont=dict(size=30), title=dict(font=dict(size=35)))
|
297
|
+
|
298
|
+
fig.update_layout(title=dict(text=f"Weekly Plot for {id}", font=dict(size=30)), height=height, showlegend=False)
|
299
|
+
|
300
|
+
if save:
|
301
|
+
path = os.path.join(save, f"{id}_weekly_plot")
|
302
|
+
fig.write_image(path + ".pdf", width=1500, height=height)
|
303
|
+
fig.write_image(path + ".png", width=1500, height=height)
|
304
|
+
fig.write_image(path + ".jpeg", width=1500, height=height)
|
305
|
+
fig.write_html(path +".html")
|
306
|
+
|
307
|
+
if app: return fig
|
308
|
+
fig.show()
|
309
|
+
|
310
|
+
def spaghetti_plot_all(df: pd.DataFrame, chunk_day: bool = False, save: str = None, height: int = 600):
|
311
|
+
"""Graphs spaghetti plots for all patients within the given DataFrame.
|
312
|
+
Also saves these plots as PDF and HTML files if passed a valid path.
|
313
|
+
|
314
|
+
:param df: the DataFrame (following package guidelines) containing the CGM data to plot
|
315
|
+
:type df: 'pandas.DataFrame'
|
316
|
+
:param chunk_day: boolean indicating whether to create separate subplots based on whether the data occurred on a weekday or during the weekend
|
317
|
+
:type chunk_day: bool, optional
|
318
|
+
:param save: path of the location where the saved PDF and HTML versions of the plots are saved, defaults to None
|
319
|
+
:type save: str, optional
|
320
|
+
:param height: the height (in pixels) of the resulting plot(s), defaults to 600
|
321
|
+
:type height: int, optional
|
322
|
+
"""
|
323
|
+
for id, data in df.groupby(ID):
|
324
|
+
spaghetti_plot(df=data, id=id, chunk_day=chunk_day, save=save, height=height)
|
325
|
+
|
326
|
+
def spaghetti_plot(df: pd.DataFrame, id: str, chunk_day: bool = False, save: str = None, height: int = 600, app=False):
|
327
|
+
"""Graphs a spaghetti plot for the given patient within the given DataFrame.
|
328
|
+
Also saves this plot as both a PDF and HTML file if passed a valid path.
|
329
|
+
|
330
|
+
:param df: the DataFrame (following package guidelines) containing the CGM data to plot
|
331
|
+
:type df: 'pandas.DataFrame'
|
332
|
+
:param id: the identification of the patient whose CGM data to plot within the given DataFrame
|
333
|
+
:type id: str
|
334
|
+
:param chunk_day: boolean indicating whether to create separate subplots based on whether the data occurred on a weekday or during the weekend
|
335
|
+
:type chunk_day: bool, optional
|
336
|
+
:param save: path of the location where the saved PDF and HTML versions of the plots are saved, defaults to None
|
337
|
+
:type save: str, optional
|
338
|
+
:param height: the height (in pixels) of the resulting plot(s), defaults to 600
|
339
|
+
:type height: int, optional
|
340
|
+
:param app: boolean indicating whether to return the Plotly figure instead of rendering it (used mainly within the web application), defaults to False
|
341
|
+
:type app: bool, optional
|
342
|
+
:return: None if app is False, otherwise the Plotly figure
|
343
|
+
:rtype: 'plotly.graph_objects.Figure' | None
|
344
|
+
"""
|
345
|
+
data = df.loc[id].copy()
|
346
|
+
data["Day"] = data[TIME].dt.date
|
347
|
+
times = data[TIME] - data[TIME].dt.normalize()
|
348
|
+
data["Time"] = (pd.to_datetime(["1/1/1970" for i in range(data[TIME].size)]) + times)
|
349
|
+
data["Time"] = data["Time"].apply(lambda dt: dt.isoformat())
|
350
|
+
data.sort_values(by=[TIME], inplace=True)
|
351
|
+
|
352
|
+
fig = px.line(data, x="Time", y=GLUCOSE, color="Day", title=f"Spaghetti Plot for {id}", height=height, facet_col="Day Chunking" if chunk_day else None)
|
353
|
+
fig.update_xaxes(
|
354
|
+
tickformat="%H:%M:%S",
|
355
|
+
title_text="Time",
|
356
|
+
tickfont_size=20,
|
357
|
+
title=dict(font=dict(size=35))
|
358
|
+
)
|
359
|
+
fig.update_yaxes(
|
360
|
+
title_text="Glucose Value (mg/dL)",
|
361
|
+
tickfont_size=20,
|
362
|
+
title=dict(font=dict(size=35)),
|
363
|
+
row=1,
|
364
|
+
col=1
|
365
|
+
)
|
366
|
+
fig.update_annotations(font_size=35)
|
367
|
+
fig.update_layout(title=dict(font=dict(size=40)), legend=dict(font=dict(size=30)))
|
368
|
+
|
369
|
+
if save:
|
370
|
+
path = os.path.join(save, f"{id}_spaghetti_plot")
|
371
|
+
fig.write_image(path + ".pdf", width=1500, height=height)
|
372
|
+
fig.write_image(path + ".png", width=1500, height=height)
|
373
|
+
fig.write_image(path + ".jpeg", width=1500, height=height)
|
374
|
+
fig.write_html(path +".html")
|
375
|
+
|
376
|
+
if app: return fig
|
377
|
+
fig.show()
|
378
|
+
|
379
|
+
def AGP_plot_all(df: pd.DataFrame, height: int = 600, save: str = None):
|
380
|
+
"""Graphs AGP-report style plots for all of the patients within the given DataFrame.
|
381
|
+
Also saves these plots as both PDF and HTML files if passed a valid path.
|
382
|
+
|
383
|
+
:param df: the DataFrame (following package guidelines) containing the CGM data to plot
|
384
|
+
:type df: 'pandas.DataFrame'
|
385
|
+
:param save: path of the location where the saved PDF and HTML versions of the plot are saved, defaults to None
|
386
|
+
:type save: str, optional
|
387
|
+
:param height: the height (in pixels) of the resulting plot, defaults to 600
|
388
|
+
:type height: int, optional
|
389
|
+
"""
|
390
|
+
for id, data in df.groupby(ID):
|
391
|
+
AGP_plot(data, id, height)
|
392
|
+
|
393
|
+
|
394
|
+
def AGP_plot(df: pd.DataFrame, id: str, save: str = None, height: int = 600, app=False):
|
395
|
+
"""Graphs AGP-report style plots for the given patient within the given DataFrame.
|
396
|
+
Also saves this plot as both a PDF and HTML file if passed a valid path.
|
397
|
+
|
398
|
+
:param df: the DataFrame (following package guidelines) containing the CGM data to plot
|
399
|
+
:type df: 'pandas.DataFrame'
|
400
|
+
:param id: the identification of the patient whose CGM data to plot within the given DataFrame
|
401
|
+
:type id: str
|
402
|
+
:param save: path of the location where the saved PDF and HTML versions of the plot are saved, defaults to None
|
403
|
+
:type save: str, optional
|
404
|
+
:param height: the height (in pixels) of the resulting plot, defaults to 600
|
405
|
+
:type height: int, optional
|
406
|
+
:param app: boolean indicating whether to return the Plotly figure instead of rendering it (used mainly within the web application), defaults to False
|
407
|
+
:type app: bool, optional
|
408
|
+
:return: None if app is False, otherwise the Plotly figure
|
409
|
+
:rtype: 'plotly.graph_objects.Figure' | None
|
410
|
+
"""
|
411
|
+
config.read('config.ini')
|
412
|
+
interval = int(config["variables"]["interval"])
|
413
|
+
if interval > 5:
|
414
|
+
raise Exception("Data needs to have measurement intervals at most 5 minutes long")
|
415
|
+
|
416
|
+
data = df.loc[id].copy()
|
417
|
+
data.reset_index(inplace=True)
|
418
|
+
|
419
|
+
data[[TIME, GLUCOSE, ID]] = pp._resample_data(data[[TIME, GLUCOSE, ID]])
|
420
|
+
times = data[TIME] - data[TIME].dt.normalize()
|
421
|
+
# need to be in a DateTime format so plots can tell how to scale the x axis labels below
|
422
|
+
data["Time"] = (pd.to_datetime(["1/1/1970" for i in range(data[TIME].size)]) + times)
|
423
|
+
|
424
|
+
data.set_index("Time", inplace=True)
|
425
|
+
|
426
|
+
agp_data = pd.DataFrame()
|
427
|
+
for time, measurements in data.groupby("Time"):
|
428
|
+
metrics = {
|
429
|
+
"Time": time,
|
430
|
+
"5th": measurements[GLUCOSE].quantile(0.05),
|
431
|
+
"25th": measurements[GLUCOSE].quantile(0.25),
|
432
|
+
"Median": measurements[GLUCOSE].median(),
|
433
|
+
"75th": measurements[GLUCOSE].quantile(0.75),
|
434
|
+
"95th": measurements[GLUCOSE].quantile(0.95),
|
435
|
+
}
|
436
|
+
agp_data = pd.concat([agp_data, pd.DataFrame.from_records([metrics])])
|
437
|
+
|
438
|
+
agp_data.sort_values(by=["Time"], inplace=True)
|
439
|
+
agp_data["Time"] = agp_data["Time"].apply(lambda dt: dt.isoformat())
|
440
|
+
|
441
|
+
fig = go.Figure()
|
442
|
+
fig.add_trace(go.Scatter(name="5th", x=agp_data["Time"], y=agp_data["5th"], line=dict(color="#869FCE")))
|
443
|
+
fig.add_trace(go.Scatter(name="25th", x=agp_data["Time"], y=agp_data["25th"], fill="tonexty", fillcolor="#C9D4E9", line=dict(color="#97A8CB")))
|
444
|
+
fig.add_trace(go.Scatter(name="Median",x=agp_data["Time"], y=agp_data["Median"], fill="tonexty", fillcolor="#97A8CB", line=dict(color="#183260")))
|
445
|
+
fig.add_trace(go.Scatter(name="75th", x=agp_data["Time"], y=agp_data["75th"], fill="tonexty", fillcolor="#97A8CB", line=dict(color="#97A8CB")))
|
446
|
+
fig.add_trace(go.Scatter(name="95th", x=agp_data["Time"], y=agp_data["95th"], fill="tonexty", fillcolor="#C9D4E9", line=dict(color="#869FCE")))
|
447
|
+
|
448
|
+
fig.add_hline(y=70, line_color="lime")
|
449
|
+
fig.add_hline(y=140, line_color="lime")
|
450
|
+
fig.add_hline(y=180, line_color="green")
|
451
|
+
fig.update_layout(title={"text": f"AGP Plot for {id}", "font": {"size":30}}, height=height, yaxis_range = [35,405],
|
452
|
+
legend=dict(font=dict(size=40), itemsizing='constant'))
|
453
|
+
fig.update_xaxes(tickformat="%H:%M:%S", title_text="Time", tickfont_size=30, title_font_size=40)
|
454
|
+
fig.update_yaxes(title_text="Glucose Value<br>(mg/dL)", tickfont_size=30, title_font_size=35)
|
455
|
+
|
456
|
+
if app: return fig
|
457
|
+
fig.show()
|
458
|
+
|
459
|
+
def AGP_report(df: pd.DataFrame, id: str, path: str = None):
|
460
|
+
"""Creates an AGP-report for the given patient within the given DataFrame.
|
461
|
+
Also saves this plot as a PDF file if passed a valid path.
|
462
|
+
|
463
|
+
:param df: the DataFrame (following package guidelines) containing the CGM data to report on
|
464
|
+
:type df: 'pandas.DataFrame'
|
465
|
+
:param id: the identification of the patient whose CGM data to report on
|
466
|
+
:type id: str
|
467
|
+
:param path: path of the location where the saved PDF version of the plot is saved, defaults to None
|
468
|
+
:type path: str, optional
|
469
|
+
:return: the AGP-report in string form if path is False, otherwise None
|
470
|
+
:rtype: str | None
|
471
|
+
"""
|
472
|
+
fig = make_subplots(rows = 1, cols = 2, specs=[[{"type": "table"}, {"type": "bar"}]])
|
473
|
+
|
474
|
+
patient_data = df.loc[id]
|
475
|
+
TIR = {"< 54 mg/dL": percent_time_in_range(patient_data, 0, 53),
|
476
|
+
"54 - 69 mg/dL": percent_time_in_range(patient_data, 54, 69),
|
477
|
+
"70 - 140 mg/dL": percent_time_in_tight_range(patient_data),
|
478
|
+
"141 - 180 mg/dL": percent_time_in_range(patient_data, 141, 180),
|
479
|
+
"181 - 250 mg/dL": percent_time_in_range(patient_data, 181, 250),
|
480
|
+
"> 250 mg/dL": percent_time_in_range(patient_data, 251, 400)}
|
481
|
+
|
482
|
+
COLORS = {"< 54 mg/dL": "rgba(151,34,35,255)",
|
483
|
+
"54 - 69 mg/dL": "rgba(229,43,23,255)",
|
484
|
+
"70 - 140 mg/dL": "#00ff00",
|
485
|
+
"141 - 180 mg/dL": "rgba(82,173,79,255)",
|
486
|
+
"181 - 250 mg/dL": "rgba(250,192,3,255)",
|
487
|
+
"> 250 mg/dL": "rgba(241,136,64,255)"}
|
488
|
+
|
489
|
+
for key, value in TIR.items():
|
490
|
+
fig.add_trace(go.Bar(name=key, x=["TIR"], y=[value],
|
491
|
+
marker=dict(color=COLORS[key]),
|
492
|
+
text=[round(value, 2)],
|
493
|
+
textposition="inside",
|
494
|
+
textfont=dict(size=35)), row=1, col=2)
|
495
|
+
fig.update_layout(barmode='stack', height=800, font=dict(size=35),
|
496
|
+
legend=dict(font=dict(size=35)))
|
497
|
+
|
498
|
+
fig.update_xaxes(title_text="Time in Range", title_font=dict(size=35), tickfont=dict(size=35), row=1, col=2)
|
499
|
+
fig.update_yaxes(title_text="Percentage (%)", title_font=dict(size=35), tickfont=dict(size=35), row=1, col=2)
|
500
|
+
|
501
|
+
ave_glucose = mean(patient_data)
|
502
|
+
gmi = GMI(patient_data)
|
503
|
+
cv = CV(patient_data)
|
504
|
+
|
505
|
+
days = patient_data[TIME].dt.date
|
506
|
+
table_body = [["ID:", f"{len(days.unique())} Days:", "Average Glucose (mg/dL):", "Glucose Management Indicator:", "Glucose Variability:"],
|
507
|
+
[id, f"{days.iloc[0]} to {days.iloc[-1]}", str(round(ave_glucose, 2)), str(round(gmi, 2)), str(round(cv, 2))]]
|
508
|
+
fig.add_trace(go.Table(cells=dict(values=table_body,align=['left', 'center'],font=dict(size=35),height=100)), row=1, col=1)
|
509
|
+
|
510
|
+
agp = AGP_plot(df, id, app=True)
|
511
|
+
weekly = weekly_plot(df, id, height=1000, app=True)
|
512
|
+
weekly.update_layout(margin=dict(l=150))
|
513
|
+
weekly.update_yaxes(title=dict(text="Glucose Value<br>(mg/dL)", font=dict(size=35)))
|
514
|
+
|
515
|
+
header_html = fig.to_html(full_html=False, include_plotlyjs="cdn")
|
516
|
+
agp_html = agp.to_html(full_html=False, include_plotlyjs=False)
|
517
|
+
weekly_html = weekly.to_html(full_html=False, include_plotlyjs=False)
|
518
|
+
|
519
|
+
html_template = f"""
|
520
|
+
<!DOCTYPE html>
|
521
|
+
<html>
|
522
|
+
<head>
|
523
|
+
<title>AGP Report for {id}</title>
|
524
|
+
</head>
|
525
|
+
<body>
|
526
|
+
<h1 style="font-size: 48px; text-align: center;"> AGP Report: Continuous Glucose Monitoring </h1>
|
527
|
+
{header_html}
|
528
|
+
{agp_html}
|
529
|
+
{weekly_html}
|
530
|
+
</body>
|
531
|
+
</html>
|
532
|
+
"""
|
533
|
+
|
534
|
+
if path:
|
535
|
+
with open(os.path.join(path, f"{id}_AGP_Report.html"), "w") as f:
|
536
|
+
f.write(html_template)
|
537
|
+
|
494
538
|
return html_template
|