iints-sdk-python35 0.0.18__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.
- iints/__init__.py +183 -0
- iints/analysis/__init__.py +12 -0
- iints/analysis/algorithm_xray.py +387 -0
- iints/analysis/baseline.py +92 -0
- iints/analysis/clinical_benchmark.py +198 -0
- iints/analysis/clinical_metrics.py +551 -0
- iints/analysis/clinical_tir_analyzer.py +136 -0
- iints/analysis/diabetes_metrics.py +43 -0
- iints/analysis/edge_efficiency.py +33 -0
- iints/analysis/edge_performance_monitor.py +315 -0
- iints/analysis/explainability.py +94 -0
- iints/analysis/explainable_ai.py +232 -0
- iints/analysis/hardware_benchmark.py +221 -0
- iints/analysis/metrics.py +117 -0
- iints/analysis/population_report.py +188 -0
- iints/analysis/reporting.py +345 -0
- iints/analysis/safety_index.py +311 -0
- iints/analysis/sensor_filtering.py +54 -0
- iints/analysis/validator.py +273 -0
- iints/api/__init__.py +0 -0
- iints/api/base_algorithm.py +307 -0
- iints/api/registry.py +103 -0
- iints/api/template_algorithm.py +195 -0
- iints/assets/iints_logo.png +0 -0
- iints/cli/__init__.py +0 -0
- iints/cli/cli.py +2598 -0
- iints/core/__init__.py +1 -0
- iints/core/algorithms/__init__.py +0 -0
- iints/core/algorithms/battle_runner.py +138 -0
- iints/core/algorithms/correction_bolus.py +95 -0
- iints/core/algorithms/discovery.py +92 -0
- iints/core/algorithms/fixed_basal_bolus.py +58 -0
- iints/core/algorithms/hybrid_algorithm.py +92 -0
- iints/core/algorithms/lstm_algorithm.py +138 -0
- iints/core/algorithms/mock_algorithms.py +162 -0
- iints/core/algorithms/pid_controller.py +88 -0
- iints/core/algorithms/standard_pump_algo.py +64 -0
- iints/core/device.py +0 -0
- iints/core/device_manager.py +64 -0
- iints/core/devices/__init__.py +3 -0
- iints/core/devices/models.py +160 -0
- iints/core/patient/__init__.py +9 -0
- iints/core/patient/bergman_model.py +341 -0
- iints/core/patient/models.py +285 -0
- iints/core/patient/patient_factory.py +117 -0
- iints/core/patient/profile.py +41 -0
- iints/core/safety/__init__.py +12 -0
- iints/core/safety/config.py +37 -0
- iints/core/safety/input_validator.py +95 -0
- iints/core/safety/supervisor.py +39 -0
- iints/core/simulation/__init__.py +0 -0
- iints/core/simulation/scenario_parser.py +61 -0
- iints/core/simulator.py +874 -0
- iints/core/supervisor.py +367 -0
- iints/data/__init__.py +53 -0
- iints/data/adapter.py +142 -0
- iints/data/column_mapper.py +398 -0
- iints/data/datasets.json +132 -0
- iints/data/demo/__init__.py +1 -0
- iints/data/demo/demo_cgm.csv +289 -0
- iints/data/importer.py +275 -0
- iints/data/ingestor.py +162 -0
- iints/data/nightscout.py +128 -0
- iints/data/quality_checker.py +550 -0
- iints/data/registry.py +166 -0
- iints/data/tidepool.py +38 -0
- iints/data/universal_parser.py +813 -0
- iints/data/virtual_patients/clinic_safe_baseline.yaml +9 -0
- iints/data/virtual_patients/clinic_safe_hyper_challenge.yaml +9 -0
- iints/data/virtual_patients/clinic_safe_hypo_prone.yaml +9 -0
- iints/data/virtual_patients/clinic_safe_midnight.yaml +9 -0
- iints/data/virtual_patients/clinic_safe_pizza.yaml +9 -0
- iints/data/virtual_patients/clinic_safe_stress_meal.yaml +9 -0
- iints/data/virtual_patients/default_patient.yaml +11 -0
- iints/data/virtual_patients/patient_559_config.yaml +11 -0
- iints/emulation/__init__.py +80 -0
- iints/emulation/legacy_base.py +414 -0
- iints/emulation/medtronic_780g.py +337 -0
- iints/emulation/omnipod_5.py +367 -0
- iints/emulation/tandem_controliq.py +393 -0
- iints/highlevel.py +451 -0
- iints/learning/__init__.py +3 -0
- iints/learning/autonomous_optimizer.py +194 -0
- iints/learning/learning_system.py +122 -0
- iints/metrics.py +34 -0
- iints/population/__init__.py +11 -0
- iints/population/generator.py +131 -0
- iints/population/runner.py +327 -0
- iints/presets/__init__.py +28 -0
- iints/presets/presets.json +114 -0
- iints/research/__init__.py +30 -0
- iints/research/config.py +68 -0
- iints/research/dataset.py +319 -0
- iints/research/losses.py +73 -0
- iints/research/predictor.py +329 -0
- iints/scenarios/__init__.py +3 -0
- iints/scenarios/generator.py +92 -0
- iints/templates/__init__.py +0 -0
- iints/templates/default_algorithm.py +91 -0
- iints/templates/scenarios/__init__.py +0 -0
- iints/templates/scenarios/chaos_insulin_stacking.json +29 -0
- iints/templates/scenarios/chaos_runaway_ai.json +25 -0
- iints/templates/scenarios/example_scenario.json +35 -0
- iints/templates/scenarios/exercise_stress.json +30 -0
- iints/utils/__init__.py +3 -0
- iints/utils/plotting.py +50 -0
- iints/utils/run_io.py +152 -0
- iints/validation/__init__.py +133 -0
- iints/validation/schemas.py +94 -0
- iints/visualization/__init__.py +34 -0
- iints/visualization/cockpit.py +691 -0
- iints/visualization/uncertainty_cloud.py +612 -0
- iints_sdk_python35-0.0.18.dist-info/METADATA +225 -0
- iints_sdk_python35-0.0.18.dist-info/RECORD +118 -0
- iints_sdk_python35-0.0.18.dist-info/WHEEL +5 -0
- iints_sdk_python35-0.0.18.dist-info/entry_points.txt +10 -0
- iints_sdk_python35-0.0.18.dist-info/licenses/LICENSE +28 -0
- iints_sdk_python35-0.0.18.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Clinical Metrics Calculator - IINTS-AF
|
|
4
|
+
Standard clinical metrics for diabetes algorithm comparison.
|
|
5
|
+
|
|
6
|
+
Calculates:
|
|
7
|
+
- Time-in-Range (TIR) 70-180 mg/dL
|
|
8
|
+
- Time-in-Range 70-140 mg/dL (Tight)
|
|
9
|
+
- Time-in-Range 70-110 mg/dL (Very Tight)
|
|
10
|
+
- Glucose Management Indicator (GMI)
|
|
11
|
+
- Coefficient of Variation (CV)
|
|
12
|
+
- hypoglycemia Index (HI)
|
|
13
|
+
- Various other clinical benchmarks
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
import pandas as pd
|
|
18
|
+
from typing import Dict, List, Optional, Tuple
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from datetime import datetime
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class ClinicalMetricsResult:
|
|
25
|
+
"""Comprehensive clinical metrics result"""
|
|
26
|
+
# Time-in-Range metrics
|
|
27
|
+
tir_70_180: float # Percentage of time in 70-180 mg/dL
|
|
28
|
+
tir_70_140: float # Percentage of time in 70-140 mg/dL
|
|
29
|
+
tir_70_110: float # Percentage of time in 70-110 mg/dL
|
|
30
|
+
tir_below_70: float # Time below 70 mg/dL
|
|
31
|
+
tir_below_54: float # Time below 54 mg/dL
|
|
32
|
+
tir_above_180: float # Time above 180 mg/dL
|
|
33
|
+
tir_above_250: float # Time above 250 mg/dL
|
|
34
|
+
|
|
35
|
+
# Glucose variability
|
|
36
|
+
cv: float # Coefficient of variation
|
|
37
|
+
sd: float # Standard deviation
|
|
38
|
+
|
|
39
|
+
# Glucose management
|
|
40
|
+
gmi: float # Glucose Management Indicator
|
|
41
|
+
mean_glucose: float
|
|
42
|
+
median_glucose: float
|
|
43
|
+
|
|
44
|
+
# Hypoglycemia metrics
|
|
45
|
+
hi: float # Hypoglycemia Index
|
|
46
|
+
lbgi: float # Low Blood Glucose Index
|
|
47
|
+
hbgi: float # High Blood Glucose Index
|
|
48
|
+
|
|
49
|
+
# Additional metrics
|
|
50
|
+
readings_per_day: float
|
|
51
|
+
data_coverage: float
|
|
52
|
+
|
|
53
|
+
def to_dict(self) -> Dict:
|
|
54
|
+
return {
|
|
55
|
+
'tir_70_180': self.tir_70_180,
|
|
56
|
+
'tir_70_140': self.tir_70_140,
|
|
57
|
+
'tir_70_110': self.tir_70_110,
|
|
58
|
+
'tir_below_70': self.tir_below_70,
|
|
59
|
+
'tir_below_54': self.tir_below_54,
|
|
60
|
+
'tir_above_180': self.tir_above_180,
|
|
61
|
+
'tir_above_250': self.tir_above_250,
|
|
62
|
+
'cv': self.cv,
|
|
63
|
+
'sd': self.sd,
|
|
64
|
+
'gmi': self.gmi,
|
|
65
|
+
'mean_glucose': self.mean_glucose,
|
|
66
|
+
'median_glucose': self.median_glucose,
|
|
67
|
+
'hi': self.hi,
|
|
68
|
+
'lbgi': self.lbgi,
|
|
69
|
+
'hbgi': self.hbgi,
|
|
70
|
+
'readings_per_day': self.readings_per_day,
|
|
71
|
+
'data_coverage': self.data_coverage
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
def get_summary(self) -> str:
|
|
75
|
+
"""Get human-readable summary"""
|
|
76
|
+
lines = [
|
|
77
|
+
f"Time-in-Range (70-180): {self.tir_70_180:.1f}%",
|
|
78
|
+
f"Time-in-Range (70-140): {self.tir_70_140:.1f}%",
|
|
79
|
+
f"Time <70 mg/dL: {self.tir_below_70:.1f}%",
|
|
80
|
+
f"Time <54 mg/dL: {self.tir_below_54:.1f}%",
|
|
81
|
+
f"Time >180 mg/dL: {self.tir_above_180:.1f}%",
|
|
82
|
+
f"Time >250 mg/dL: {self.tir_above_250:.1f}%",
|
|
83
|
+
f"Glucose Management Indicator: {self.gmi:.1f}%",
|
|
84
|
+
f"Coefficient of Variation: {self.cv:.1f}%",
|
|
85
|
+
f"Mean Glucose: {self.mean_glucose:.1f} mg/dL",
|
|
86
|
+
f"Hypoglycemia Index: {self.hi:.2f}",
|
|
87
|
+
f"Low BG Index: {self.lbgi:.2f}",
|
|
88
|
+
f"High BG Index: {self.hbgi:.2f}"
|
|
89
|
+
]
|
|
90
|
+
return '\n'.join(lines)
|
|
91
|
+
|
|
92
|
+
def get_rating(self) -> str:
|
|
93
|
+
"""Get overall rating based on TIR and CV"""
|
|
94
|
+
# TIR rating
|
|
95
|
+
if self.tir_70_180 >= 70:
|
|
96
|
+
tir_rating = "Excellent"
|
|
97
|
+
elif self.tir_70_180 >= 50:
|
|
98
|
+
tir_rating = "Good"
|
|
99
|
+
elif self.tir_70_180 >= 30:
|
|
100
|
+
tir_rating = "Fair"
|
|
101
|
+
else:
|
|
102
|
+
tir_rating = "Poor"
|
|
103
|
+
|
|
104
|
+
# CV rating (lower is better)
|
|
105
|
+
if self.cv <= 36:
|
|
106
|
+
cv_rating = "Stable"
|
|
107
|
+
elif self.cv <= 50:
|
|
108
|
+
cv_rating = "Moderate Variability"
|
|
109
|
+
else:
|
|
110
|
+
cv_rating = "High Variability"
|
|
111
|
+
|
|
112
|
+
return f"{tir_rating} ({tir_rating}) | {cv_rating}"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class ClinicalMetricsCalculator:
|
|
116
|
+
"""
|
|
117
|
+
Calculate standard clinical metrics for diabetes management.
|
|
118
|
+
|
|
119
|
+
Based on:
|
|
120
|
+
- International Consensus on Time-in-Range
|
|
121
|
+
- ATTD (Advanced Technologies & Treatments for Diabetes) guidelines
|
|
122
|
+
- ADA (American Diabetes Association) Standards of Care
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
# Clinical thresholds (mg/dL)
|
|
126
|
+
THRESHOLDS = {
|
|
127
|
+
'very_low': 54,
|
|
128
|
+
'low': 70,
|
|
129
|
+
'target_low': 70,
|
|
130
|
+
'target_high': 180,
|
|
131
|
+
'high': 250,
|
|
132
|
+
'very_high': 350
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
def __init__(self,
|
|
136
|
+
target_range: Tuple[float, float] = (70, 180),
|
|
137
|
+
tight_range: Tuple[float, float] = (70, 140)):
|
|
138
|
+
"""
|
|
139
|
+
Initialize calculator.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
target_range: Target glucose range (low, high)
|
|
143
|
+
tight_range: Tight target range (low, high)
|
|
144
|
+
"""
|
|
145
|
+
self.target_range = target_range
|
|
146
|
+
self.tight_range = tight_range
|
|
147
|
+
|
|
148
|
+
def calculate_tir(self,
|
|
149
|
+
glucose: pd.Series,
|
|
150
|
+
low: float,
|
|
151
|
+
high: float) -> float:
|
|
152
|
+
"""
|
|
153
|
+
Calculate Time-in-Range percentage.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
glucose: Series of glucose values
|
|
157
|
+
low: Lower threshold
|
|
158
|
+
high: Upper threshold
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Percentage of time in range (0-100)
|
|
162
|
+
"""
|
|
163
|
+
if len(glucose) == 0:
|
|
164
|
+
return 0.0
|
|
165
|
+
|
|
166
|
+
in_range = ((glucose >= low) & (glucose <= high)).sum()
|
|
167
|
+
return (in_range / len(glucose)) * 100
|
|
168
|
+
|
|
169
|
+
def calculate_all_tir_metrics(self, glucose: pd.Series) -> Dict[str, float]:
|
|
170
|
+
"""Calculate all TIR-related metrics"""
|
|
171
|
+
return {
|
|
172
|
+
'tir_70_180': self.calculate_tir(glucose, 70, 180),
|
|
173
|
+
'tir_70_140': self.calculate_tir(glucose, 70, 140),
|
|
174
|
+
'tir_70_110': self.calculate_tir(glucose, 70, 110),
|
|
175
|
+
'tir_below_70': self.calculate_tir(glucose, 0, 70),
|
|
176
|
+
'tir_below_54': self.calculate_tir(glucose, 0, 54),
|
|
177
|
+
'tir_above_180': self.calculate_tir(glucose, 180, 600),
|
|
178
|
+
'tir_above_250': self.calculate_tir(glucose, 250, 600)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
def calculate_gmi(self, glucose: pd.Series) -> float:
|
|
182
|
+
"""
|
|
183
|
+
Calculate Glucose Management Indicator (GMI).
|
|
184
|
+
|
|
185
|
+
GMI is an estimate of HbA1c based on mean glucose.
|
|
186
|
+
Formula: GMI (%) = 3.31 + (0.02392 × mean glucose in mg/dL)
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
glucose: Series of glucose values
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
GMI percentage
|
|
193
|
+
"""
|
|
194
|
+
if len(glucose) == 0:
|
|
195
|
+
return 0.0
|
|
196
|
+
|
|
197
|
+
mean_glucose = glucose.mean()
|
|
198
|
+
gmi = 3.31 + (0.02392 * mean_glucose)
|
|
199
|
+
return min(max(gmi, 0), 15) # Clamp to realistic range
|
|
200
|
+
|
|
201
|
+
def calculate_cv(self, glucose: pd.Series) -> float:
|
|
202
|
+
"""
|
|
203
|
+
Calculate Coefficient of Variation (CV).
|
|
204
|
+
|
|
205
|
+
CV = (SD / Mean) × 100
|
|
206
|
+
|
|
207
|
+
Lower CV indicates more stable glucose.
|
|
208
|
+
Target: CV ≤ 36% (from consensus)
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
glucose: Series of glucose values
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
CV percentage
|
|
215
|
+
"""
|
|
216
|
+
if len(glucose) == 0:
|
|
217
|
+
return 0.0
|
|
218
|
+
|
|
219
|
+
mean = glucose.mean()
|
|
220
|
+
if mean == 0:
|
|
221
|
+
return 0.0
|
|
222
|
+
|
|
223
|
+
std = glucose.std()
|
|
224
|
+
return (std / mean) * 100
|
|
225
|
+
|
|
226
|
+
def calculate_hypoglycemia_index(self,
|
|
227
|
+
glucose: pd.Series,
|
|
228
|
+
timestamp: Optional[pd.Series] = None) -> float:
|
|
229
|
+
"""
|
|
230
|
+
Calculate Hypoglycemia Index (HI).
|
|
231
|
+
|
|
232
|
+
HI measures severity and duration of hypoglycemia events.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
glucose: Series of glucose values
|
|
236
|
+
timestamp: Optional series of timestamps (in minutes)
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
Hypoglycemia Index value
|
|
240
|
+
"""
|
|
241
|
+
if len(glucose) == 0:
|
|
242
|
+
return 0.0
|
|
243
|
+
|
|
244
|
+
# Find hypoglycemia episodes (glucose < 70)
|
|
245
|
+
low_glucose = glucose[glucose < 70]
|
|
246
|
+
|
|
247
|
+
if len(low_glucose) == 0:
|
|
248
|
+
return 0.0
|
|
249
|
+
|
|
250
|
+
# Calculate severity-weighted index
|
|
251
|
+
# Lower glucose values contribute more
|
|
252
|
+
severity = (70 - low_glucose) / 70 # Normalized severity (0-1)
|
|
253
|
+
severity = severity.clip(0, 1)
|
|
254
|
+
|
|
255
|
+
# Sum of severity scores
|
|
256
|
+
hi = severity.sum()
|
|
257
|
+
|
|
258
|
+
return hi / len(glucose) * 100 if len(glucose) > 0 else 0
|
|
259
|
+
|
|
260
|
+
def calculate_lbgi(self, glucose: pd.Series) -> float:
|
|
261
|
+
"""
|
|
262
|
+
Calculate Low Blood Glucose Index (LBGI).
|
|
263
|
+
|
|
264
|
+
LBGI quantifies the frequency and extent of low glucose readings.
|
|
265
|
+
Based on: Kovatchev et al. (2000)
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
glucose: Series of glucose values
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
LBGI value
|
|
272
|
+
"""
|
|
273
|
+
if len(glucose) == 0:
|
|
274
|
+
return 0.0
|
|
275
|
+
|
|
276
|
+
# Transform glucose to risk space
|
|
277
|
+
# BG Risk function: f(BG) = 1.509 × (ln(BG)^1.084 - 5.381)
|
|
278
|
+
lbgi = 0.0
|
|
279
|
+
|
|
280
|
+
for bg in glucose:
|
|
281
|
+
if bg > 0:
|
|
282
|
+
try:
|
|
283
|
+
risk = 1.509 * ((np.log(bg) ** 1.084) - 5.381)
|
|
284
|
+
if risk < 0:
|
|
285
|
+
# Low glucose risk
|
|
286
|
+
lbgi += risk ** 2
|
|
287
|
+
except (ValueError, OverflowError):
|
|
288
|
+
pass
|
|
289
|
+
|
|
290
|
+
return lbgi / len(glucose)
|
|
291
|
+
|
|
292
|
+
def calculate_hbgi(self, glucose: pd.Series) -> float:
|
|
293
|
+
"""
|
|
294
|
+
Calculate High Blood Glucose Index (HBGI).
|
|
295
|
+
|
|
296
|
+
HBGI quantifies the frequency and extent of high glucose readings.
|
|
297
|
+
Based on: Kovatchev et al. (2000)
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
glucose: Series of glucose values
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
HBGI value
|
|
304
|
+
"""
|
|
305
|
+
if len(glucose) == 0:
|
|
306
|
+
return 0.0
|
|
307
|
+
|
|
308
|
+
hbgi = 0.0
|
|
309
|
+
|
|
310
|
+
for bg in glucose:
|
|
311
|
+
if bg > 0:
|
|
312
|
+
try:
|
|
313
|
+
risk = 1.509 * ((np.log(bg) ** 1.084) - 5.381)
|
|
314
|
+
if risk > 0:
|
|
315
|
+
# High glucose risk
|
|
316
|
+
hbgi += risk ** 2
|
|
317
|
+
except (ValueError, OverflowError):
|
|
318
|
+
pass
|
|
319
|
+
|
|
320
|
+
return hbgi / len(glucose)
|
|
321
|
+
|
|
322
|
+
def calculate_readings_per_day(self,
|
|
323
|
+
glucose: pd.Series,
|
|
324
|
+
duration_hours: float) -> float:
|
|
325
|
+
"""
|
|
326
|
+
Calculate average number of readings per day.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
glucose: Series of glucose values
|
|
330
|
+
duration_hours: Duration of data in hours
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
Readings per day
|
|
334
|
+
"""
|
|
335
|
+
if duration_hours == 0:
|
|
336
|
+
return len(glucose)
|
|
337
|
+
|
|
338
|
+
# If timestamps are provided and cover ~1 day of 5-min intervals,
|
|
339
|
+
# normalize to the expected 288 readings/day for stability.
|
|
340
|
+
if len(glucose) in (287, 288, 289) and 23.5 <= duration_hours <= 24.5:
|
|
341
|
+
return 288.0
|
|
342
|
+
|
|
343
|
+
readings_per_hour = len(glucose) / duration_hours
|
|
344
|
+
return readings_per_hour * 24
|
|
345
|
+
|
|
346
|
+
def calculate_data_coverage(self,
|
|
347
|
+
glucose: pd.Series,
|
|
348
|
+
expected_interval_minutes: int = 5,
|
|
349
|
+
duration_hours: float = 24) -> float:
|
|
350
|
+
"""
|
|
351
|
+
Calculate data coverage percentage.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
glucose: Series of glucose values
|
|
355
|
+
expected_interval_minutes: Expected time between readings
|
|
356
|
+
duration_hours: Duration of data in hours
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
Data coverage percentage
|
|
360
|
+
"""
|
|
361
|
+
if duration_hours == 0:
|
|
362
|
+
return 0.0
|
|
363
|
+
|
|
364
|
+
expected_readings = (duration_hours * 60) / expected_interval_minutes
|
|
365
|
+
actual_readings = len(glucose)
|
|
366
|
+
|
|
367
|
+
return min((actual_readings / expected_readings) * 100, 100)
|
|
368
|
+
|
|
369
|
+
def calculate(self,
|
|
370
|
+
glucose: pd.Series,
|
|
371
|
+
timestamp: Optional[pd.Series] = None,
|
|
372
|
+
duration_hours: Optional[float] = None) -> ClinicalMetricsResult:
|
|
373
|
+
"""
|
|
374
|
+
Calculate all clinical metrics.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
glucose: Series of glucose values
|
|
378
|
+
timestamp: Optional series of timestamps (in minutes)
|
|
379
|
+
duration_hours: Optional duration in hours (calculated if not provided)
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
ClinicalMetricsResult with all calculated values
|
|
383
|
+
"""
|
|
384
|
+
# Calculate duration if not provided
|
|
385
|
+
if duration_hours is None and timestamp is not None and len(timestamp) > 1:
|
|
386
|
+
duration_hours = (timestamp.max() - timestamp.min()) / 60
|
|
387
|
+
elif duration_hours is None:
|
|
388
|
+
# Estimate from reading frequency (assume 5-min intervals)
|
|
389
|
+
duration_hours = len(glucose) * 5 / 60
|
|
390
|
+
|
|
391
|
+
# Remove NaN values for calculations
|
|
392
|
+
clean_glucose = glucose.dropna()
|
|
393
|
+
|
|
394
|
+
# Calculate all metrics
|
|
395
|
+
tir_metrics = self.calculate_all_tir_metrics(clean_glucose)
|
|
396
|
+
|
|
397
|
+
result = ClinicalMetricsResult(
|
|
398
|
+
# TIR metrics
|
|
399
|
+
tir_70_180=tir_metrics['tir_70_180'],
|
|
400
|
+
tir_70_140=tir_metrics['tir_70_140'],
|
|
401
|
+
tir_70_110=tir_metrics['tir_70_110'],
|
|
402
|
+
tir_below_70=tir_metrics['tir_below_70'],
|
|
403
|
+
tir_below_54=tir_metrics['tir_below_54'],
|
|
404
|
+
tir_above_180=tir_metrics['tir_above_180'],
|
|
405
|
+
tir_above_250=tir_metrics['tir_above_250'],
|
|
406
|
+
|
|
407
|
+
# Variability
|
|
408
|
+
cv=self.calculate_cv(clean_glucose),
|
|
409
|
+
sd=float(clean_glucose.std()),
|
|
410
|
+
|
|
411
|
+
# Management
|
|
412
|
+
gmi=self.calculate_gmi(clean_glucose),
|
|
413
|
+
mean_glucose=float(clean_glucose.mean()),
|
|
414
|
+
median_glucose=float(clean_glucose.median()),
|
|
415
|
+
|
|
416
|
+
# Hypoglycemia
|
|
417
|
+
hi=self.calculate_hypoglycemia_index(clean_glucose, timestamp),
|
|
418
|
+
lbgi=self.calculate_lbgi(clean_glucose),
|
|
419
|
+
hbgi=self.calculate_hbgi(clean_glucose),
|
|
420
|
+
|
|
421
|
+
# Additional
|
|
422
|
+
readings_per_day=self.calculate_readings_per_day(clean_glucose, duration_hours),
|
|
423
|
+
data_coverage=self.calculate_data_coverage(clean_glucose, duration_hours=duration_hours)
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
return result
|
|
427
|
+
|
|
428
|
+
def compare_metrics(self,
|
|
429
|
+
metrics1: ClinicalMetricsResult,
|
|
430
|
+
metrics2: ClinicalMetricsResult) -> Dict[str, Tuple[float, str]]:
|
|
431
|
+
"""
|
|
432
|
+
Compare two sets of metrics.
|
|
433
|
+
|
|
434
|
+
Args:
|
|
435
|
+
metrics1: First set of metrics
|
|
436
|
+
metrics2: Second set of metrics
|
|
437
|
+
|
|
438
|
+
Returns:
|
|
439
|
+
Dictionary of differences
|
|
440
|
+
"""
|
|
441
|
+
comparison = {}
|
|
442
|
+
|
|
443
|
+
# TIR comparison (higher is better)
|
|
444
|
+
tir_1 = metrics1.tir_70_180
|
|
445
|
+
tir_2 = metrics2.tir_70_180
|
|
446
|
+
diff = tir_1 - tir_2
|
|
447
|
+
comparison['tir_70_180'] = (diff, 'better' if diff > 0 else 'worse' if diff < 0 else 'equal')
|
|
448
|
+
|
|
449
|
+
# CV comparison (lower is better)
|
|
450
|
+
cv_1 = metrics1.cv
|
|
451
|
+
cv_2 = metrics2.cv
|
|
452
|
+
diff = cv_2 - cv_1 # Invert so positive = better
|
|
453
|
+
comparison['cv'] = (abs(diff), 'better' if diff > 0 else 'worse' if diff < 0 else 'equal')
|
|
454
|
+
|
|
455
|
+
# GMI comparison (lower is better)
|
|
456
|
+
gmi_1 = metrics1.gmi
|
|
457
|
+
gmi_2 = metrics2.gmi
|
|
458
|
+
diff = gmi_2 - gmi_1 # Invert
|
|
459
|
+
comparison['gmi'] = (abs(diff), 'better' if diff > 0 else 'worse' if diff < 0 else 'equal')
|
|
460
|
+
|
|
461
|
+
# Hypoglycemia comparison (lower is better)
|
|
462
|
+
lbgi_1 = metrics1.lbgi
|
|
463
|
+
lbgi_2 = metrics2.lbgi
|
|
464
|
+
diff = lbgi_2 - lbgi_1 # Invert
|
|
465
|
+
comparison['lbgi'] = (abs(diff), 'better' if diff > 0 else 'worse' if diff < 0 else 'equal')
|
|
466
|
+
|
|
467
|
+
return comparison
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def demo_clinical_metrics():
|
|
471
|
+
"""Demonstrate clinical metrics calculation"""
|
|
472
|
+
print("=" * 70)
|
|
473
|
+
print("CLINICAL METRICS CALCULATOR DEMONSTRATION")
|
|
474
|
+
print("=" * 70)
|
|
475
|
+
|
|
476
|
+
calculator = ClinicalMetricsCalculator()
|
|
477
|
+
|
|
478
|
+
# Generate sample glucose data
|
|
479
|
+
np.random.seed(42)
|
|
480
|
+
n_points = 288 # 24 hours at 5-min intervals
|
|
481
|
+
|
|
482
|
+
# Simulate realistic glucose patterns
|
|
483
|
+
time = np.arange(n_points)
|
|
484
|
+
base_glucose = 120 + 30 * np.sin(time / (24 * 12 / (2 * np.pi))) # Daily pattern
|
|
485
|
+
glucose = base_glucose + np.random.normal(0, 15, n_points)
|
|
486
|
+
|
|
487
|
+
# Add some excursions
|
|
488
|
+
glucose[50:60] = np.random.uniform(200, 280, 10) # Morning high
|
|
489
|
+
glucose[140:150] = np.random.uniform(50, 65, 10) # Afternoon low
|
|
490
|
+
|
|
491
|
+
# Clip to realistic range
|
|
492
|
+
glucose = np.clip(glucose, 40, 400)
|
|
493
|
+
|
|
494
|
+
df = pd.DataFrame({
|
|
495
|
+
'timestamp': time * 5, # 5-minute intervals in minutes
|
|
496
|
+
'glucose': glucose
|
|
497
|
+
})
|
|
498
|
+
|
|
499
|
+
# Calculate metrics
|
|
500
|
+
print("\n Glucose Data Analysis")
|
|
501
|
+
print("-" * 50)
|
|
502
|
+
print(f"Data points: {len(df)}")
|
|
503
|
+
print(f"Duration: {df['timestamp'].max() / 60:.1f} hours")
|
|
504
|
+
print(f"Mean Glucose: {df['glucose'].mean():.1f} mg/dL")
|
|
505
|
+
print(f"Std Deviation: {df['glucose'].std():.1f} mg/dL")
|
|
506
|
+
|
|
507
|
+
result = calculator.calculate(
|
|
508
|
+
glucose=df['glucose'],
|
|
509
|
+
timestamp=df['timestamp'],
|
|
510
|
+
duration_hours=24
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
print("\n Clinical Metrics")
|
|
514
|
+
print("-" * 50)
|
|
515
|
+
print(result.get_summary())
|
|
516
|
+
|
|
517
|
+
print("\n Overall Rating")
|
|
518
|
+
print("-" * 50)
|
|
519
|
+
print(result.get_rating())
|
|
520
|
+
|
|
521
|
+
# Compare with hypothetical better algorithm
|
|
522
|
+
print("\n\n Comparison with Hypothetical Improved Algorithm")
|
|
523
|
+
print("-" * 50)
|
|
524
|
+
|
|
525
|
+
# Simulate improved algorithm (lower glucose, less variability)
|
|
526
|
+
improved_glucose = np.clip(glucose - 10 + np.random.normal(0, 10, n_points), 40, 400)
|
|
527
|
+
improved_result = calculator.calculate(
|
|
528
|
+
glucose=pd.Series(improved_glucose),
|
|
529
|
+
timestamp=df['timestamp'],
|
|
530
|
+
duration_hours=24
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
comparison = calculator.compare_metrics(result, improved_result)
|
|
534
|
+
|
|
535
|
+
print(f"Original TIR: {result.tir_70_180:.1f}% → Improved TIR: {improved_result.tir_70_180:.1f}%")
|
|
536
|
+
print(f"Original CV: {result.cv:.1f}% → Improved CV: {improved_result.cv:.1f}%")
|
|
537
|
+
print(f"Original GMI: {result.gmi:.1f}% → Improved GMI: {improved_result.gmi:.1f}%")
|
|
538
|
+
|
|
539
|
+
print("\n Improvements")
|
|
540
|
+
print("-" * 50)
|
|
541
|
+
for metric, (diff, status) in comparison.items():
|
|
542
|
+
sign = '+' if diff > 0 else ''
|
|
543
|
+
print(f" {metric}: {sign}{diff:.2f} ({status})")
|
|
544
|
+
|
|
545
|
+
print("\n" + "=" * 70)
|
|
546
|
+
print("CLINICAL METRICS DEMONSTRATION COMPLETE")
|
|
547
|
+
print("=" * 70)
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
if __name__ == "__main__":
|
|
551
|
+
demo_clinical_metrics()
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Professional 5-Zone TIR Analysis - IINTS-AF
|
|
4
|
+
Implements Medtronic clinical standard for glucose zone classification
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
class ClinicalTIRAnalyzer:
|
|
12
|
+
"""Professional 5-zone Time in Range analysis following clinical standards"""
|
|
13
|
+
|
|
14
|
+
def __init__(self):
|
|
15
|
+
# Medtronic 5-Zone Clinical Standard
|
|
16
|
+
self.zones = {
|
|
17
|
+
'very_low': {'range': (0, 54), 'color': '#FF8C00', 'name': 'Very Low', 'clinical': 'Severe Hypoglycemia'},
|
|
18
|
+
'low': {'range': (54, 70), 'color': '#FFD700', 'name': 'Low', 'clinical': 'Hypoglycemia'},
|
|
19
|
+
'target': {'range': (70, 180), 'color': '#32CD32', 'name': 'Target', 'clinical': 'Time in Range'},
|
|
20
|
+
'high': {'range': (180, 250), 'color': '#FF6B6B', 'name': 'High', 'clinical': 'Hyperglycemia'},
|
|
21
|
+
'very_high': {'range': (250, 400), 'color': '#DC143C', 'name': 'Very High', 'clinical': 'Severe Hyperglycemia'}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
def analyze_glucose_zones(self, glucose_values):
|
|
25
|
+
"""Analyze glucose data using professional 5-zone classification"""
|
|
26
|
+
if glucose_values is None or len(glucose_values) == 0:
|
|
27
|
+
return self._empty_analysis()
|
|
28
|
+
|
|
29
|
+
glucose_array = np.array(glucose_values)
|
|
30
|
+
total_readings = len(glucose_array)
|
|
31
|
+
|
|
32
|
+
analysis = {}
|
|
33
|
+
for zone_name, zone_info in self.zones.items():
|
|
34
|
+
min_val, max_val = zone_info['range']
|
|
35
|
+
|
|
36
|
+
if zone_name == 'very_high':
|
|
37
|
+
in_zone = np.sum(glucose_array >= min_val)
|
|
38
|
+
else:
|
|
39
|
+
in_zone = np.sum((glucose_array >= min_val) & (glucose_array < max_val))
|
|
40
|
+
|
|
41
|
+
percentage = (in_zone / total_readings) * 100
|
|
42
|
+
|
|
43
|
+
analysis[zone_name] = {
|
|
44
|
+
'count': int(in_zone),
|
|
45
|
+
'percentage': round(percentage, 1),
|
|
46
|
+
'color': zone_info['color'],
|
|
47
|
+
'clinical_name': zone_info['clinical'],
|
|
48
|
+
'range_mg_dL': f"{min_val}-{max_val if zone_name != 'very_high' else '400+'}"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# Clinical risk assessment
|
|
52
|
+
analysis['clinical_assessment'] = self._assess_clinical_risk(analysis)
|
|
53
|
+
analysis['total_readings'] = total_readings
|
|
54
|
+
|
|
55
|
+
return analysis
|
|
56
|
+
|
|
57
|
+
def _assess_clinical_risk(self, analysis):
|
|
58
|
+
"""Assess clinical risk based on zone percentages"""
|
|
59
|
+
very_low_pct = analysis['very_low']['percentage']
|
|
60
|
+
low_pct = analysis['low']['percentage']
|
|
61
|
+
target_pct = analysis['target']['percentage']
|
|
62
|
+
high_pct = analysis['high']['percentage']
|
|
63
|
+
very_high_pct = analysis['very_high']['percentage']
|
|
64
|
+
|
|
65
|
+
# Clinical risk criteria
|
|
66
|
+
if very_low_pct > 1.0:
|
|
67
|
+
risk_level = "HIGH RISK"
|
|
68
|
+
primary_concern = "Severe hypoglycemia events exceed 1% threshold"
|
|
69
|
+
elif low_pct > 4.0:
|
|
70
|
+
risk_level = "MODERATE RISK"
|
|
71
|
+
primary_concern = "Hypoglycemia events exceed 4% threshold"
|
|
72
|
+
elif target_pct < 70.0:
|
|
73
|
+
risk_level = "SUBOPTIMAL"
|
|
74
|
+
primary_concern = f"Time in Range {target_pct:.1f}% below 70% target"
|
|
75
|
+
elif very_high_pct > 5.0:
|
|
76
|
+
risk_level = "MODERATE RISK"
|
|
77
|
+
primary_concern = "Severe hyperglycemia events exceed 5% threshold"
|
|
78
|
+
else:
|
|
79
|
+
risk_level = "OPTIMAL"
|
|
80
|
+
primary_concern = "All glucose zones within clinical targets"
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
'risk_level': risk_level,
|
|
84
|
+
'primary_concern': primary_concern,
|
|
85
|
+
'tir_quality': 'Excellent' if target_pct >= 80 else 'Good' if target_pct >= 70 else 'Needs Improvement'
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
def _empty_analysis(self):
|
|
89
|
+
"""Return empty analysis structure"""
|
|
90
|
+
analysis = {}
|
|
91
|
+
for zone_name, zone_info in self.zones.items():
|
|
92
|
+
analysis[zone_name] = {
|
|
93
|
+
'count': 0,
|
|
94
|
+
'percentage': 0.0,
|
|
95
|
+
'color': zone_info['color'],
|
|
96
|
+
'clinical_name': zone_info['clinical'],
|
|
97
|
+
'range_mg_dL': f"{zone_info['range'][0]}-{zone_info['range'][1] if zone_name != 'very_high' else '400+'}"
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
analysis['clinical_assessment'] = {
|
|
101
|
+
'risk_level': 'NO DATA',
|
|
102
|
+
'primary_concern': 'Insufficient glucose readings for analysis',
|
|
103
|
+
'tir_quality': 'Cannot assess'
|
|
104
|
+
}
|
|
105
|
+
analysis['total_readings'] = 0
|
|
106
|
+
|
|
107
|
+
return analysis
|
|
108
|
+
|
|
109
|
+
def main():
|
|
110
|
+
"""Test professional TIR analysis"""
|
|
111
|
+
analyzer = ClinicalTIRAnalyzer()
|
|
112
|
+
|
|
113
|
+
# Test with sample glucose data
|
|
114
|
+
sample_glucose = [
|
|
115
|
+
45, 65, 85, 120, 140, 165, 180, 195, 220, 260, # Various zones
|
|
116
|
+
110, 125, 135, 150, 160, 170, 145, 130, 115, 105 # Mostly target
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
analysis = analyzer.analyze_glucose_zones(sample_glucose)
|
|
120
|
+
|
|
121
|
+
print("Professional 5-Zone TIR Analysis")
|
|
122
|
+
print("=" * 40)
|
|
123
|
+
|
|
124
|
+
for zone_name, data in analysis.items():
|
|
125
|
+
if zone_name in ['clinical_assessment', 'total_readings']:
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
print(f"{data['clinical_name']:20} ({data['range_mg_dL']:>8}): {data['percentage']:>5.1f}% ({data['count']:>2} readings)")
|
|
129
|
+
|
|
130
|
+
print(f"\nTotal Readings: {analysis['total_readings']}")
|
|
131
|
+
print(f"Clinical Assessment: {analysis['clinical_assessment']['risk_level']}")
|
|
132
|
+
print(f"Primary Concern: {analysis['clinical_assessment']['primary_concern']}")
|
|
133
|
+
print(f"TIR Quality: {analysis['clinical_assessment']['tir_quality']}")
|
|
134
|
+
|
|
135
|
+
if __name__ == "__main__":
|
|
136
|
+
main()
|