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/features.py
CHANGED
@@ -4,11 +4,12 @@ import configparser
|
|
4
4
|
from multiprocessing import Pool
|
5
5
|
import os
|
6
6
|
from scipy.integrate import trapezoid
|
7
|
+
from importlib import resources
|
8
|
+
from glucose360.preprocessing import load_config
|
7
9
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
config.read(config_path)
|
10
|
+
# Initialize config at module level
|
11
|
+
config = load_config()
|
12
|
+
INTERVAL = int(config["variables"]["interval"])
|
12
13
|
ID = config['variables']['id']
|
13
14
|
GLUCOSE = config['variables']['glucose']
|
14
15
|
TIME = config['variables']['time']
|
@@ -266,6 +267,19 @@ def HBGI(df: pd.DataFrame) -> float:
|
|
266
267
|
BG = np.maximum(0, BG_formula(df[GLUCOSE]))
|
267
268
|
return np.mean(10 * (BG ** 2))
|
268
269
|
|
270
|
+
def BGRI(df: pd.DataFrame) -> float:
|
271
|
+
"""Calculates the Blood Glucose Risk Index (BGRI).
|
272
|
+
|
273
|
+
BGRI is the overall glucose-risk score obtained by summing
|
274
|
+
the Low Blood Glucose Index (LBGI) and the High Blood Glucose
|
275
|
+
Index (HBGI).
|
276
|
+
|
277
|
+
:param df: Pandas DataFrame with pre-processed CGM data
|
278
|
+
:return: BGRI value
|
279
|
+
"""
|
280
|
+
return LBGI(df) + HBGI(df)
|
281
|
+
|
282
|
+
|
269
283
|
def COGI(df: pd.DataFrame) -> float:
|
270
284
|
"""Calculates the Continuous Glucose Monitoring Index (COGI) for the given CGM trace.
|
271
285
|
|
@@ -341,6 +355,42 @@ def GRADE_hyper(df: pd.DataFrame) -> float:
|
|
341
355
|
df_GRADE = GRADE_formula(df)
|
342
356
|
return np.sum(df_GRADE[df_GRADE[GLUCOSE] > 140]["GRADE"]) / np.sum(df_GRADE["GRADE"]) * 100
|
343
357
|
|
358
|
+
def GRADE_eugly_absolute(df: pd.DataFrame) -> float:
|
359
|
+
"""Returns the absolute GRADE contribution from euglycemic values.
|
360
|
+
|
361
|
+
This function sums the GRADE scores for readings in the target
|
362
|
+
glucose range (70–140 mg/dL) without normalizing to the total GRADE.
|
363
|
+
|
364
|
+
:param df: a Pandas DataFrame containing preprocessed CGM data
|
365
|
+
:type df: 'pandas.DataFrame'
|
366
|
+
:return: the absolute euglycemic GRADE for the given CGM trace
|
367
|
+
:rtype: float
|
368
|
+
"""
|
369
|
+
df_GRADE = GRADE_formula(df)
|
370
|
+
return np.sum(df_GRADE[(df_GRADE[GLUCOSE] >= 70) & (df_GRADE[GLUCOSE] <= 140)]["GRADE"])
|
371
|
+
|
372
|
+
def GRADE_hypo_absolute(df: pd.DataFrame) -> float:
|
373
|
+
"""Returns the absolute GRADE contribution from hypoglycemic values.
|
374
|
+
|
375
|
+
:param df: a Pandas DataFrame containing preprocessed CGM data
|
376
|
+
:type df: 'pandas.DataFrame'
|
377
|
+
:return: the absolute hypoglycemic GRADE for the given CGM trace
|
378
|
+
:rtype: float
|
379
|
+
"""
|
380
|
+
df_GRADE = GRADE_formula(df)
|
381
|
+
return np.sum(df_GRADE[df_GRADE[GLUCOSE] < 70]["GRADE"])
|
382
|
+
|
383
|
+
def GRADE_hyper_absolute(df: pd.DataFrame) -> float:
|
384
|
+
"""Returns the absolute GRADE contribution from hyperglycemic values.
|
385
|
+
|
386
|
+
:param df: a Pandas DataFrame containing preprocessed CGM data
|
387
|
+
:type df: 'pandas.DataFrame'
|
388
|
+
:return: the absolute hyperglycemic GRADE for the given CGM trace
|
389
|
+
:rtype: float
|
390
|
+
"""
|
391
|
+
df_GRADE = GRADE_formula(df)
|
392
|
+
return np.sum(df_GRADE[df_GRADE[GLUCOSE] > 140]["GRADE"])
|
393
|
+
|
344
394
|
def GRADE(df: pd.DataFrame) -> float:
|
345
395
|
"""Calculates the Glycaemic Risk Assessment Diabetes Equation (GRADE) for the given CGM trace.
|
346
396
|
|
@@ -416,15 +466,27 @@ def hypo_index(df: pd.DataFrame, limit: int = 80, b: float = 2, d: float = 30) -
|
|
416
466
|
BG = df[GLUCOSE].dropna()
|
417
467
|
return np.sum(np.power(limit - BG[BG < limit], b)) / (BG.size * d)
|
418
468
|
|
419
|
-
def IGC(df: pd.DataFrame) -> float:
|
469
|
+
def IGC(df: pd.DataFrame, hyper_limit: int = 140, hypo_limit: int = 80, hyper_a: float = 1.1, hyper_c: float = 30, hypo_b: float = 2, hypo_d: float = 30) -> float:
|
420
470
|
"""Calculates the Index of Glycemic Control (IGC) for the given CGM trace.
|
421
471
|
|
422
472
|
:param df: a Pandas DataFrame containing preprocessed CGM data
|
423
473
|
:type df: 'pandas.DataFrame'
|
474
|
+
:param hyper_limit: upper limit of target range (above which would hyperglycemia), defaults to 140 mg/dL
|
475
|
+
:type hyper_limit: int, optional
|
476
|
+
:param hypo_limit: lower limit of target range (above which would hypoglycemia), defaults to 80 mg/dL
|
477
|
+
:type hypo_limit: int, optional
|
478
|
+
:param hyper_a: exponent utilized for Hyperglycemia Index calculation, defaults to 1.1
|
479
|
+
:type hyper_a: float, optional
|
480
|
+
:param hyper_c: constant to help scale Hyperglycemia Index the same as other metrics (e.g. LBGI, HBGI, and GRADE), defaults to 30
|
481
|
+
:type hyper_c: float, optional
|
482
|
+
:param hypo_b: exponent utilized for Hypoglycemia Index calculation, defaults to 2
|
483
|
+
:type hypo_b: float, optional
|
484
|
+
:param hypo_d: constant to help scale Hypoglycemia Index the same as other metrics (e.g. LBGI, HBGI, and GRADE), defaults to 30
|
485
|
+
:type hypo_d: float, optional
|
424
486
|
:return: the IGC for the given CGM trace
|
425
487
|
:rtype: float
|
426
488
|
"""
|
427
|
-
return hyper_index(df) + hypo_index(df)
|
489
|
+
return hyper_index(df, hyper_limit, hyper_a, hyper_c) + hypo_index(df, hypo_limit, hypo_b, hypo_d)
|
428
490
|
|
429
491
|
def j_index(df: pd.DataFrame) -> float:
|
430
492
|
"""Calculates the J-Index for the given CGM trace.
|
@@ -446,9 +508,7 @@ def CONGA(df: pd.DataFrame, n: int = 24) -> float:
|
|
446
508
|
:return: the CONGA for the given CGM trace
|
447
509
|
:rtype: float
|
448
510
|
"""
|
449
|
-
|
450
|
-
interval = int(config["variables"]["interval"])
|
451
|
-
period = n * (60 / interval)
|
511
|
+
period = n * (60 / INTERVAL)
|
452
512
|
return np.std(df[GLUCOSE].diff(periods=period))
|
453
513
|
|
454
514
|
# lag is in days
|
@@ -462,10 +522,7 @@ def MODD(df: pd.DataFrame, lag: int = 1) -> float:
|
|
462
522
|
:return: the MODD for the given CGM trace
|
463
523
|
:rtype: float
|
464
524
|
"""
|
465
|
-
|
466
|
-
interval = int(config["variables"]["interval"])
|
467
|
-
period = lag * 24 * (60 / interval)
|
468
|
-
|
525
|
+
period = lag * 24 * (60 / INTERVAL)
|
469
526
|
return np.mean(np.abs(df[GLUCOSE].diff(periods=period)))
|
470
527
|
|
471
528
|
def mean_absolute_differences(df: pd.DataFrame) -> float:
|
@@ -504,6 +561,7 @@ def MAG(df: pd.DataFrame) -> float:
|
|
504
561
|
|
505
562
|
def MAGE(df: pd.DataFrame, short_ma: int = 5, long_ma: int = 32, max_gap: int = 180) -> float:
|
506
563
|
"""Calculates the Mean Amplitude of Glycemic Excursions (MAGE) for the given CGM trace.
|
564
|
+
Algorithm for calculating MAGE is based on iglu's implementation (please cite their papers found in the README).
|
507
565
|
|
508
566
|
:param df: a Pandas DataFrame containing preprocessed CGM data
|
509
567
|
:type df: 'pandas.DataFrame'
|
@@ -518,16 +576,13 @@ def MAGE(df: pd.DataFrame, short_ma: int = 5, long_ma: int = 32, max_gap: int =
|
|
518
576
|
"""
|
519
577
|
data = df.reset_index(drop=True)
|
520
578
|
|
521
|
-
config.read('config.ini')
|
522
|
-
interval = int(config["variables"]["interval"])
|
523
|
-
|
524
579
|
missing = data[GLUCOSE].isnull()
|
525
580
|
# create groups of consecutive missing values
|
526
581
|
groups = missing.ne(missing.shift()).cumsum()
|
527
582
|
# group by the created groups and count the size of each group, then apply it where values are missing
|
528
583
|
size_of_groups = data.groupby([groups, missing])[GLUCOSE].transform('size').where(missing, 0)
|
529
584
|
# filter groups where size is greater than 0 and take their indexes
|
530
|
-
indexes = size_of_groups[size_of_groups.diff() > (max_gap /
|
585
|
+
indexes = size_of_groups[size_of_groups.diff() > (max_gap / INTERVAL)].index.tolist()
|
531
586
|
|
532
587
|
if not indexes: # no gaps in data larger than max_gap
|
533
588
|
return MAGE_helper(df, short_ma, long_ma)
|
@@ -544,7 +599,7 @@ def MAGE(df: pd.DataFrame, short_ma: int = 5, long_ma: int = 32, max_gap: int =
|
|
544
599
|
|
545
600
|
def MAGE_helper(df: pd.DataFrame, short_ma: int = 5, long_ma: int = 32) -> float:
|
546
601
|
"""Calculates the Mean Amplitude of Glycemic Excursions (MAGE) for a specific segment of a CGM trace.
|
547
|
-
Algorithm for calculating MAGE is based on iglu's implementation, and this method is a helper for the MAGE() function.
|
602
|
+
Algorithm for calculating MAGE is based on iglu's implementation (please cite their papers found in the README), and this method is a helper for the MAGE() function.
|
548
603
|
|
549
604
|
:param df: a Pandas DataFrame containing preprocessed CGM data without significant gaps (as defined in the MAGE() function)
|
550
605
|
:type df: 'pandas.DataFrame'
|
@@ -573,8 +628,9 @@ def MAGE_helper(df: pd.DataFrame, short_ma: int = 5, long_ma: int = 32) -> float
|
|
573
628
|
averages["MA_Long"] = averages[GLUCOSE].rolling(window=long_ma, min_periods=1).mean()
|
574
629
|
|
575
630
|
# fill in leading NaNs due to moving average calculation
|
576
|
-
averages
|
577
|
-
averages
|
631
|
+
averages.loc[:(short_ma - 2), "MA_Short"] = averages.at[short_ma - 1, "MA_Short"]
|
632
|
+
averages.loc[:(long_ma - 2), "MA_Long"] = averages.at[long_ma - 1, "MA_Long"]
|
633
|
+
|
578
634
|
averages["DELTA_SL"] = averages["MA_Short"] - averages["MA_Long"]
|
579
635
|
|
580
636
|
# get crossing points
|
@@ -694,12 +750,10 @@ def ROC(df: pd.DataFrame, timedelta: int = 15) -> pd.Series:
|
|
694
750
|
:return: a Pandas Series with the rate of change in glucose values at every data point
|
695
751
|
:rtype: 'pandas.Series'
|
696
752
|
"""
|
697
|
-
|
698
|
-
interval = int(config["variables"]["interval"])
|
699
|
-
if timedelta < interval:
|
753
|
+
if timedelta < INTERVAL:
|
700
754
|
raise Exception("Given timedelta must be greater than resampling interval.")
|
701
755
|
|
702
|
-
positiondelta = round(timedelta /
|
756
|
+
positiondelta = round(timedelta / INTERVAL)
|
703
757
|
return df[GLUCOSE].diff(periods=positiondelta) / timedelta
|
704
758
|
|
705
759
|
def number_readings(df: pd.DataFrame):
|
@@ -957,6 +1011,231 @@ def nocturnal_auc(df: pd.DataFrame) -> float:
|
|
957
1011
|
|
958
1012
|
return np.nan if not daily_aucs else np.mean(daily_aucs)
|
959
1013
|
|
1014
|
+
def daily_range_helper(df: pd.DataFrame) -> float:
|
1015
|
+
"""
|
1016
|
+
Mean of the daily glucose ranges (max – min) across all complete days.
|
1017
|
+
Helper function for Q-Score.
|
1018
|
+
|
1019
|
+
Returns NaN if no valid data are available.
|
1020
|
+
"""
|
1021
|
+
valid = df.dropna(subset=[GLUCOSE]).copy()
|
1022
|
+
if valid.empty:
|
1023
|
+
return np.nan
|
1024
|
+
|
1025
|
+
valid['date'] = valid[TIME].dt.date
|
1026
|
+
day_ranges = valid.groupby('date')[GLUCOSE].apply(lambda x: x.max() - x.min())
|
1027
|
+
return day_ranges.mean()
|
1028
|
+
|
1029
|
+
def Q_score(df: pd.DataFrame, hyper_limit: int = 180) -> float:
|
1030
|
+
"""
|
1031
|
+
Composite Q-Score.
|
1032
|
+
|
1033
|
+
Q = 8
|
1034
|
+
+ (MBG − 140.4) / 30.6
|
1035
|
+
+ (daily_range − 135) / 52.2
|
1036
|
+
+ (t_hypo − 0.6) / 1.2
|
1037
|
+
+ (t_hyper − 6.2) / 5.7
|
1038
|
+
+ (MODD − 32.4) / 16.2
|
1039
|
+
|
1040
|
+
All glucose values and constants are in mg/dL; t_hypo and t_hyper are
|
1041
|
+
hours/day spent < 70 mg/dL and > 160 mg/dL, respectively.
|
1042
|
+
|
1043
|
+
:param df: a Pandas DataFrame containing preprocessed CGM data
|
1044
|
+
:type df: 'pandas.DataFrame'
|
1045
|
+
:param hyper_limit: upper limit of target range (above which would hyperglycemia), defaults to 180 mg/dL, previously 160 mg/dL
|
1046
|
+
:type hyper_limit: int, optional
|
1047
|
+
:return: the Q-Score for the given CGM trace
|
1048
|
+
:rtype: float
|
1049
|
+
"""
|
1050
|
+
mbg = mean(df)
|
1051
|
+
drange = daily_range_helper(df)
|
1052
|
+
modd = MODD(df)
|
1053
|
+
|
1054
|
+
# Convert %-of-time to hours/day
|
1055
|
+
t_hypo = percent_time_below_range(df) / 100 * 24
|
1056
|
+
t_hyper = percent_time_above_range(df, limit=hyper_limit) / 100 * 24
|
1057
|
+
|
1058
|
+
# If any component is missing, return NaN
|
1059
|
+
if np.isnan([mbg, drange, modd, t_hypo, t_hyper]).any():
|
1060
|
+
return np.nan
|
1061
|
+
|
1062
|
+
return (
|
1063
|
+
8
|
1064
|
+
+ (mbg - 140.4) / 30.6
|
1065
|
+
+ (drange - 135) / 52.2
|
1066
|
+
+ (t_hypo - 0.6) / 1.2
|
1067
|
+
+ (t_hyper - 6.2) / 5.7
|
1068
|
+
+ (modd - 32.4) / 16.2
|
1069
|
+
)
|
1070
|
+
|
1071
|
+
def _n_days(df: pd.DataFrame) -> float:
|
1072
|
+
"""
|
1073
|
+
Decimal number of (calendar) days covered by the dataframe.
|
1074
|
+
Any partial day counts fractionally; e.g. 36 h = 1.5 days.
|
1075
|
+
"""
|
1076
|
+
t0, t1 = df[TIME].min(), df[TIME].max()
|
1077
|
+
if pd.isna(t0) or pd.isna(t1):
|
1078
|
+
return np.nan
|
1079
|
+
return (t1 - t0).total_seconds() / 86_400.0
|
1080
|
+
|
1081
|
+
def _axis_linear(val: float,
|
1082
|
+
ref: float,
|
1083
|
+
max_val: float,
|
1084
|
+
r0: float = 14.0,
|
1085
|
+
r_max: float = 76.0) -> float:
|
1086
|
+
"""
|
1087
|
+
Maps ref → r0 and max_val → r_max with linear scaling.
|
1088
|
+
Values ≤ ref are clamped to r0; values ≥ max_val are clamped to r_max.
|
1089
|
+
"""
|
1090
|
+
if np.isnan(val):
|
1091
|
+
return np.nan
|
1092
|
+
if val <= ref:
|
1093
|
+
return r0
|
1094
|
+
if val >= max_val:
|
1095
|
+
return r_max
|
1096
|
+
return r0 + (r_max - r0) * (val - ref) / (max_val - ref)
|
1097
|
+
|
1098
|
+
|
1099
|
+
def _auc_above_helper(df: pd.DataFrame, thr: int = 180) -> float:
|
1100
|
+
"""Total AUC (mg · min dL⁻¹) **above** *thr* over the *full* time axis."""
|
1101
|
+
bg = df[[TIME, GLUCOSE]].dropna().copy()
|
1102
|
+
if bg.empty:
|
1103
|
+
return 0.0
|
1104
|
+
excess = np.maximum(bg[GLUCOSE] - thr, 0.0)
|
1105
|
+
if excess.eq(0).all():
|
1106
|
+
return 0.0
|
1107
|
+
t = (bg[TIME] - bg[TIME].iloc[0]).dt.total_seconds() / 60.0 # minutes
|
1108
|
+
return trapezoid(excess, x=t)
|
1109
|
+
|
1110
|
+
|
1111
|
+
def _auc_below_helper(df: pd.DataFrame, thr: int = 70) -> float:
|
1112
|
+
"""Total AUC (mg · min dL⁻¹) **below** *thr* over the *full* time axis."""
|
1113
|
+
bg = df[[TIME, GLUCOSE]].dropna().copy()
|
1114
|
+
if bg.empty:
|
1115
|
+
return 0.0
|
1116
|
+
deficit = np.maximum(thr - bg[GLUCOSE], 0.0)
|
1117
|
+
if deficit.eq(0).all():
|
1118
|
+
return 0.0
|
1119
|
+
t = (bg[TIME] - bg[TIME].iloc[0]).dt.total_seconds() / 60.0
|
1120
|
+
return trapezoid(deficit, x=t)
|
1121
|
+
|
1122
|
+
|
1123
|
+
def _intensity_helper(time_min: float, auc: float) -> float:
|
1124
|
+
"""Euclidean intensity of an excursion (minutes, mg · min dL⁻¹)."""
|
1125
|
+
return float(np.hypot(time_min, auc))
|
1126
|
+
|
1127
|
+
|
1128
|
+
|
1129
|
+
_GP_MAX = {
|
1130
|
+
"HbA1c": 14.0,
|
1131
|
+
"Mean": 250.0,
|
1132
|
+
"SD": 150.0,
|
1133
|
+
"TAbove": 1440.0,
|
1134
|
+
"AUCAbove": 432_000.0
|
1135
|
+
}
|
1136
|
+
|
1137
|
+
|
1138
|
+
def glucose_pentagon(df: pd.DataFrame,
|
1139
|
+
hbA1c: float | None = None,
|
1140
|
+
hyper_thr: int = 160) -> dict[str, float]:
|
1141
|
+
|
1142
|
+
days = _n_days(df)
|
1143
|
+
if np.isnan(days) or days == 0:
|
1144
|
+
return {"Glucose_Pentagon_Area": np.nan,
|
1145
|
+
"Glucose_Pentagon_GRP": np.nan}
|
1146
|
+
|
1147
|
+
# ----- core metrics -------------------------------------------------
|
1148
|
+
a1c = hbA1c if hbA1c is not None else eA1c(df)
|
1149
|
+
mean_glu = mean(df)
|
1150
|
+
sd_glu = SD(df)
|
1151
|
+
|
1152
|
+
# convert to *minutes per day* and *AUC per day*
|
1153
|
+
t_above = (percent_time_above_range(df, hyper_thr) / 100 * 1440)
|
1154
|
+
auc_above = _auc_above_helper(df, hyper_thr) / days
|
1155
|
+
|
1156
|
+
axes = np.array([
|
1157
|
+
_axis_linear(a1c, 5.5, _GP_MAX["HbA1c"]),
|
1158
|
+
_axis_linear(mean_glu, 90.0, _GP_MAX["Mean"]),
|
1159
|
+
_axis_linear(sd_glu, 10.0, _GP_MAX["SD"]),
|
1160
|
+
_axis_linear(t_above, 0.0, _GP_MAX["TAbove"]),
|
1161
|
+
_axis_linear(auc_above, 0.0, _GP_MAX["AUCAbove"])
|
1162
|
+
])
|
1163
|
+
|
1164
|
+
theta = np.deg2rad(72)
|
1165
|
+
area = 0.5 * np.sum(axes * np.roll(axes, -1) * np.sin(theta))
|
1166
|
+
grp = area / 466.0
|
1167
|
+
|
1168
|
+
return {"Glucose_Pentagon_Area": area,
|
1169
|
+
"Glucose_Pentagon_GRP": grp}
|
1170
|
+
|
1171
|
+
|
1172
|
+
|
1173
|
+
_CGP_MAX = {
|
1174
|
+
"Mean": 250.0,
|
1175
|
+
"CV": 60.0,
|
1176
|
+
"ToR": 1440.0,
|
1177
|
+
"Intensity": 432_002.0
|
1178
|
+
}
|
1179
|
+
|
1180
|
+
def _axis_cgp(val: float, kind: str, r0: float = 14.0, r_max: float = 76.0) -> float:
|
1181
|
+
"""
|
1182
|
+
Returns the radial length (mm) for the selected CGP axis,
|
1183
|
+
with hard clamping to the range [14, 76] to prevent
|
1184
|
+
negative or oversized radii.
|
1185
|
+
"""
|
1186
|
+
if np.isnan(val):
|
1187
|
+
return np.nan
|
1188
|
+
match kind:
|
1189
|
+
case "Mean":
|
1190
|
+
r = ((val - 90.)*0.0217)**2.63 + 14.
|
1191
|
+
case "CV":
|
1192
|
+
r = (val - 17.)*0.92 + 14.
|
1193
|
+
case "ToR":
|
1194
|
+
r = (val*0.00614)**1.581 + 14.
|
1195
|
+
case "IntHyper":
|
1196
|
+
r = (val*0.000115)**1.51 + 14.
|
1197
|
+
case "IntHypo":
|
1198
|
+
r = np.exp(val*0.00057) + 13.
|
1199
|
+
case _:
|
1200
|
+
raise ValueError(kind)
|
1201
|
+
|
1202
|
+
return max(r0, min(r_max,r))
|
1203
|
+
|
1204
|
+
def comprehensive_glucose_pentagon(df: pd.DataFrame) -> dict[str, float]:
|
1205
|
+
|
1206
|
+
days = _n_days(df)
|
1207
|
+
if np.isnan(days) or days == 0:
|
1208
|
+
return {"Comprehensive_Glucose_Pentagon_Area": np.nan,
|
1209
|
+
"Comprehensive_Glucose_Pentagon_PGR": np.nan}
|
1210
|
+
|
1211
|
+
mean_glu = mean(df)
|
1212
|
+
cv_ = CV(df)
|
1213
|
+
|
1214
|
+
tir_min = percent_time_in_range(df) / 100 * 1440 * days
|
1215
|
+
tor_min = (1440*days - tir_min) / days
|
1216
|
+
|
1217
|
+
t_above = percent_time_above_range(df, 180) / 100 * 1440
|
1218
|
+
t_below = percent_time_below_range(df, 70) / 100 * 1440
|
1219
|
+
auc_above = _auc_above_helper(df, 180) / days
|
1220
|
+
auc_below = _auc_below_helper(df, 70) / days
|
1221
|
+
|
1222
|
+
int_hyper = _intensity_helper(t_above, auc_above)
|
1223
|
+
int_hypo = _intensity_helper(t_below, auc_below)
|
1224
|
+
|
1225
|
+
axes = np.array([
|
1226
|
+
_axis_cgp(mean_glu, "Mean"),
|
1227
|
+
_axis_cgp(cv_, "CV"),
|
1228
|
+
_axis_cgp(tor_min, "ToR"),
|
1229
|
+
_axis_cgp(int_hyper, "IntHyper"),
|
1230
|
+
_axis_cgp(int_hypo, "IntHypo")
|
1231
|
+
])
|
1232
|
+
|
1233
|
+
theta = np.deg2rad(72)
|
1234
|
+
area = 0.5 * np.sum(axes * np.roll(axes, -1) * np.sin(theta))
|
1235
|
+
pgr = area / 466.0
|
1236
|
+
|
1237
|
+
return {"Comprehensive_Glucose_Pentagon_Area": area,
|
1238
|
+
"Comprehensive_Glucose_Pentagon_PGR": pgr}
|
960
1239
|
|
961
1240
|
def compute_features(id: str, data: pd.DataFrame) -> dict[str, any]:
|
962
1241
|
"""Calculates statistics and metrics for a single patient within the given DataFrame
|
@@ -969,11 +1248,16 @@ def compute_features(id: str, data: pd.DataFrame) -> dict[str, any]:
|
|
969
1248
|
:rtype: dict[str, any]
|
970
1249
|
"""
|
971
1250
|
summary = summary_stats(data)
|
1251
|
+
gp = glucose_pentagon(data)
|
1252
|
+
cgp = comprehensive_glucose_pentagon(data)
|
972
1253
|
|
973
1254
|
features = {
|
974
1255
|
ID: id,
|
975
1256
|
"ADRR": ADRR(data),
|
1257
|
+
"BGRI": BGRI(data),
|
976
1258
|
"COGI": COGI(data),
|
1259
|
+
"Comprehensive Glucose Pentagon Area": cgp["Comprehensive_Glucose_Pentagon_Area"],
|
1260
|
+
"Comprehensive Glucose Pentagon PGR": cgp["Comprehensive_Glucose_Pentagon_PGR"],
|
977
1261
|
"CONGA": CONGA(data),
|
978
1262
|
"CV": CV(data),
|
979
1263
|
"Daytime AUC": auc_daytime(data),
|
@@ -981,10 +1265,15 @@ def compute_features(id: str, data: pd.DataFrame) -> dict[str, any]:
|
|
981
1265
|
"FBG": FBG(data),
|
982
1266
|
"First Quartile": summary[1],
|
983
1267
|
"GMI": GMI(data),
|
1268
|
+
"Glucose Pentagon Area": gp["Glucose_Pentagon_Area"],
|
1269
|
+
"Glucose Pentagon GRP": gp["Glucose_Pentagon_GRP"],
|
984
1270
|
"GRADE": GRADE(data),
|
985
1271
|
"GRADE (euglycemic)": GRADE_eugly(data),
|
1272
|
+
"GRADE (euglycemic absolute)": GRADE_eugly_absolute(data),
|
986
1273
|
"GRADE (hyperglycemic)": GRADE_hyper(data),
|
1274
|
+
"GRADE (hyperglycemic absolute)": GRADE_hyper_absolute(data),
|
987
1275
|
"GRADE (hypoglycemic)": GRADE_hypo(data),
|
1276
|
+
"GRADE (hypoglycemic absolute)": GRADE_hypo_absolute(data),
|
988
1277
|
"GRI": GRI(data),
|
989
1278
|
"GVP": GVP(data),
|
990
1279
|
"HBGI": HBGI(data),
|
@@ -1021,6 +1310,7 @@ def compute_features(id: str, data: pd.DataFrame) -> dict[str, any]:
|
|
1021
1310
|
"Percent Time in Hypoglycemia (level 2)": percent_time_in_level_2_hypoglycemia(data),
|
1022
1311
|
"Percent Time In Range (70-180)": percent_time_in_range(data),
|
1023
1312
|
"Percent Time In Tight Range (70-140)": percent_time_in_tight_range(data),
|
1313
|
+
"Q-Score": Q_score(data),
|
1024
1314
|
"SD": SD(data),
|
1025
1315
|
"Third Quartile": summary[3],
|
1026
1316
|
}
|
glucose360/main.py
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
import pandas as pd
|
2
|
+
from glucose360.preprocessing import *
|
3
|
+
from glucose360.features import *
|
4
|
+
from glucose360.events import *
|
5
|
+
from glucose360.plots import *
|
6
|
+
|
7
|
+
def main():
|
8
|
+
df = import_data("datasets")
|
9
|
+
for index, row in create_features(df).iterrows():
|
10
|
+
print(row)
|
11
|
+
|
12
|
+
if __name__ == "__main__":
|
13
|
+
main()
|