Skip to content

Commit bfe687a

Browse files
authored
Merge pull request #4 from staskh/all_metrics
All metrics completed, support for series,list and arrays as input
2 parents 154db59 + cbf6ff0 commit bfe687a

85 files changed

Lines changed: 3606 additions & 2139 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 77 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -19,50 +19,75 @@ A significant focus of this project has been ensuring compatibility with the ori
1919
This approach ensures that the Python implementation produces results consistent with the original R package.
2020

2121
## Unit Test Status
22-
Unless noted, iglu-r test is considered successful if it achieves precision of 0.001
23-
24-
| Function | IGLU-R test compatibility | array/list/Series | TZ | Comments |
25-
|----------|---------------------------|-------------------|----|----------|
26-
| above_percent || |||
27-
| active_percent ||
28-
| adrr ||
29-
| auc| 🟡 (0.01 precision) | || see [auc_evaluation.ipynb](https://github.com/staskh/iglu_python/blob/main/notebooks/auc_evaluation.ipynb)|
30-
| below_percent||
31-
| cogi ||
32-
| conga ||
33-
| cv_glu ||
34-
| cv_measures ||
35-
| ea1c ||
36-
| episode_calculation || || |
37-
| gmi ||
38-
| grade_eugly ||
39-
| grade_hyper ||
40-
| grade_hypo ||
41-
| grade ||
42-
| gri ||
43-
| gvp ||
44-
| hbgi ||
45-
| hyper_index ||
46-
| hypo_index ||
47-
| igc ||
48-
| j_index ||
49-
| lbgi ||
50-
| mad_glu ||
51-
| mag || || IMHO, Original R implementation has an error |
52-
| mage || || See algorithm at [MAGE](https://irinagain.github.io/iglu/articles/MAGE.html) |
53-
| mean_glu ||
54-
| median_glu ||
55-
| modd ||
56-
| pgs || || |
57-
| quantile_glu ||
58-
| range_glu ||
59-
| roc ||
60-
| sd_glu ||
61-
| sd_measures ||
62-
| sd_roc || |||
63-
| process_data ||
64-
| summary_glu ||
65-
| CGMS2DayByDay ||
22+
The current version of IGLU-PYTHON is test-compatible with IGLU-R v4.2.2
23+
24+
Unless noted, IGLU-R test compatability is considered successful if it achieves precision of 0.001
25+
26+
| Function | Description | IGLU-R test compatibility | list /ndarray /Series input | TZ | Comments |
27+
|----------|-------------|-------------|-------------------|----|----------|
28+
| above_percent | percentage of values above target thresholds||✅ returns Dict[str,float] |||
29+
| active_percent | percentage of time CGM was active || ✅ only Series(DatetimeIndex) returns Dict[str,float]|
30+
| adrr | average daily risk range ||✅ only Series(DatetimeIndex) returns float |
31+
| auc| Area Under Curve | 🟡 (0.01 precision) |✅ only Series(DatetimeIndex) returns float || see [auc_evaluation.ipynb](https://github.com/staskh/iglu_python/blob/main/notebooks/auc_evaluation.ipynb)|
32+
| below_percent| percentage of values below target thresholds|| ✅ returns Dict[str,float]|
33+
| cogi |Coefficient of Glucose Irregularity | ✅ | ✅ returns float
34+
| conga | Continuous Overall Net Glycemic Action |✅ | ✅ only Series(DatetimeIndex) returns float
35+
| cv_glu | Coefficient of Variation || ✅ returns float |
36+
| cv_measures |Coefficient of Variation subtypes (CVmean and CVsd) ||✅ only Series(DatetimeIndex) returns Dict[str,float]| |
37+
| ea1c |estimated A1C (eA1C) values|| ✅ returns float |
38+
| episode_calculation | Hypo/Hyperglycemic episodes with summary statistics|| 🟡 always returns DataFrame(s)|| |
39+
| gmi | Glucose Management Indicator || ✅ returns float |
40+
| grade_eugly |percentage of GRADE score attributable to target range| ✅ | ✅ returns float
41+
| grade_hyper |percentage of GRADE score attributable to hyperglycemia| ✅ |✅ returns float
42+
| grade_hypo |percentage of GRADE score attributable to hypoglycemia| ✅ |✅ returns float
43+
| grade |mean GRADE score|| ✅ returns float |
44+
| gri |Glycemia Risk Index || ✅ returns float |
45+
| gvp |Glucose Variability Percentage| ✅ | ✅ only Series(DatetimeIndex) returns float
46+
| hbgi |High Blood Glucose Index|| ✅ returns float |
47+
| hyper_index |Hyperglycemia Index||✅ returns float |
48+
| hypo_index |Hypoglycemia Index||✅ returns float |
49+
| igc |Index of Glycemic Control||✅ returns float |
50+
| in_range_percent |percentage of values within target ranges|| ✅ returns Dict[str,float]|
51+
| iqr_glu |glucose level interquartile range||✅ returns float |
52+
| j_index |J-Index score for glucose measurements||✅ returns float |
53+
| lbgi | Low Blood Glucose Index||✅ returns float |
54+
| m_value | M-value of Schlichtkrull et al ||✅ returns float |
55+
| mad_glu | Median Absolute Deviation ||✅ returns float |
56+
| mag | Mean Absolute Glucose|| ✅ only Series(DatetimeIndex) returns float ||| IMHO, Original R implementation has an error |
57+
| mage | Mean Amplitude of Glycemic Excursions||✅ only Series(DatetimeIndex) returns float || See algorithm at [MAGE](https://irinagain.github.io/iglu/articles/MAGE.html) |
58+
| mean_glu | Mean glucose value || ✅ returns float|
59+
| median_glu |Median glucose value||✅ returns float |
60+
| modd | Mean of Daily Differences|| ✅ only Series(DatetimeIndex) returns float|
61+
| pgs | Personal Glycemic State ||✅ only Series(DatetimeIndex) returns float| ||
62+
| quantile_glu |glucose level quantiles||✅ returns List[float] |
63+
| range_glu |glucose level range||✅ returns float|
64+
| roc | Rate of Change||🟡 always returns DataFrame|
65+
| sd_glu | standard deviation of glucose values| ✅ | ✅ returns float
66+
| sd_measures |various standard deviation subtypes||✅ only Series(DatetimeIndex) returns Dict[str,float]|
67+
| sd_roc | standard deviation of the rate of change||✅ only Series(DatetimeIndex) returns float ||
68+
| summary_glu | summary glucose level||
69+
| process_data | Data Pre-Processor ||
70+
| CGMS2DayByDay |Interpolate glucose input||
71+
72+
### Input & Output
73+
The implementation maintains compatibility with the R version while following Python best practices. The metrics can be used as:
74+
75+
```Python
76+
import iglu_python ias iglu
77+
78+
# With DataFrame input
79+
result_df = iglu.cv_glu(data) # data should have 'id', 'time', and 'gl' columns
80+
# Return DataFrame with "id' and column(s) with value(s)
81+
82+
# With Series input (some metrics require Series with DateTimeIndex)
83+
result_float = iglu.cv_glu(glucose_series) # just glucose values
84+
# returns a single float value
85+
86+
# Same with function that support list or ndarray
87+
result_float = iglu.cv_glu(glucose_list) # list of glucose values
88+
# returns a single float value
89+
90+
```
6691

6792
# Installation
6893

@@ -92,36 +117,32 @@ import iglu_python as iglu
92117
# Optional: datetime index or 'time' column
93118
data = pd.DataFrame({
94119
'id': ['Subject1'] * 100,
95-
'time': pd.date_range(start='2023-01-01', periods=100, freq='5min')
96-
'gl': [120, 135, 140, 125, 110, ...], # glucose values in mg/dL
120+
'time': pd.date_range(start='2023-01-01', periods=100, freq='5min'),
121+
'gl': [120, 135, 140, 125, 110]*20 # glucose values in mg/dL
97122
})
98123

99124
# Calculate glucose metrics
100125
mean_glucose = iglu.mean_glu(data)
101126
cv = iglu.cv_glu(data)
102-
time_in_range = iglu.active_percent(data, lltr=70, ultr=180)
127+
active = iglu.active_percent(data)
103128

104-
print(f"Mean glucose: {mean_glucose}")
105-
print(f"CV: {cv}")
106-
print(f"Time in range (70-180 mg/dL): {time_in_range}%")
129+
print(f"Mean glucose: {mean_glucose['mean'][0]}")
130+
print(f"CV: {cv['CV'][0]}")
131+
print(f"CGM active percent: {active['active_percent'][0]}%")
107132
```
108133

109134
### Using with Time Series Data
110135

111136
```python
112137
import pandas as pd
138+
import numpy as np
113139
import iglu_python as iglu
114-
from datetime import datetime, timedelta
115140

116141
# Create time series data
117142
timestamps = pd.date_range(start='2023-01-01', periods=288, freq='5min')
118143
glucose_values = [120 + 20 * np.sin(i/48) + np.random.normal(0, 5) for i in range(288)]
119144

120-
data = pd.DataFrame({
121-
'id': ['Subject1'] * 288,
122-
'time': timestamps,
123-
'gl': glucose_values
124-
})
145+
data = pd.Series(glucose_values, index=timestamps)
125146

126147
# Calculate advanced metrics
127148
mage = iglu.mage(data)

iglu_python/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55
from .below_percent import below_percent
66
from .cogi import cogi
77
from .conga import conga
8+
from .cv_glu import cv_glu
9+
from .cv_measures import cv_measures
810
from .ea1c import ea1c
911
from .episode_calculation import episode_calculation
12+
from .gmi import gmi
1013
from .grade import grade
1114
from .grade_eugly import grade_eugly
1215
from .grade_hyper import grade_hyper
@@ -37,7 +40,7 @@
3740
from .sd_measures import sd_measures
3841
from .sd_roc import sd_roc
3942
from .summary_glu import summary_glu
40-
from .utils import set_iglu_r_compatible, is_iglu_r_compatible, CGMS2DayByDay, check_data_columns, gd2d_to_df
43+
from .utils import CGMS2DayByDay, check_data_columns, gd2d_to_df, is_iglu_r_compatible, set_iglu_r_compatible
4144

4245
__all__ = [
4346
"above_percent",
@@ -49,13 +52,16 @@
4952
"CGMS2DayByDay",
5053
"cogi",
5154
"conga",
55+
"cv_glu",
56+
"cv_measures",
5257
"ea1c",
5358
"episode_calculation",
5459
"gd2d_to_df",
5560
"grade",
5661
"grade_eugly",
5762
"grade_hyper",
5863
"grade_hypo",
64+
"gmi",
5965
"gri",
6066
"gvp",
6167
"hbgi",

iglu_python/above_percent.py

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
from typing import List, Union
22

3+
import numpy as np
34
import pandas as pd
45

56
from .utils import check_data_columns
67

78

89
def above_percent(
9-
data: Union[pd.DataFrame, pd.Series, list],
10-
targets_above: List[int] = [140, 180, 250],
11-
) -> pd.DataFrame:
10+
data: Union[pd.DataFrame, pd.Series, list,np.ndarray],
11+
targets_above: List[int] = None,
12+
) -> pd.DataFrame|dict[str:float]:
1213
"""
1314
Calculate percentage of values above target thresholds.
1415
@@ -58,22 +59,13 @@ def above_percent(
5859
0 75.0 25.0
5960
"""
6061
# Handle Series input
61-
if isinstance(data, (pd.Series, list)):
62-
# Convert targets to float
63-
targets_above = [int(t) for t in targets_above]
62+
if targets_above is None:
63+
targets_above = [140, 180, 250]
64+
if isinstance(data, (pd.Series, list,np.ndarray)):
65+
if isinstance(data, (list, np.ndarray)):
66+
data = pd.Series(data)
67+
return above_percent_single(data, targets_above)
6468

65-
# Calculate total non-NA readings
66-
total_readings = len(data.dropna())
67-
if total_readings == 0:
68-
return pd.DataFrame(columns=[f"above_{t}" for t in targets_above])
69-
70-
# Calculate percentages for each target
71-
percentages = {}
72-
for target in targets_above:
73-
above_count = len(data[data > target])
74-
percentages[f"above_{target}"] = (above_count / total_readings) * 100
75-
76-
return pd.DataFrame([percentages])
7769

7870
# Handle DataFrame input
7971
data = check_data_columns(data)
@@ -85,19 +77,33 @@ def above_percent(
8577
# Process each subject
8678
for subject in data["id"].unique():
8779
subject_data = data[data["id"] == subject]
88-
total_readings = len(subject_data.dropna(subset=["gl"]))
89-
90-
if total_readings == 0:
91-
continue
92-
93-
# Calculate percentages for each target
94-
percentages = {}
95-
for target in targets_above:
96-
above_count = len(subject_data[subject_data["gl"] > target])
97-
percentages[f"above_{target}"] = (above_count / total_readings) * 100
98-
80+
percentages = above_percent_single(subject_data["gl"], targets_above)
9981
percentages["id"] = subject
10082
result.append(percentages)
10183

10284
# Convert to DataFrame
103-
return pd.DataFrame(result)
85+
df = pd.DataFrame(result)
86+
df = df[['id'] + [col for col in df.columns if col != 'id']]
87+
return df
88+
89+
def above_percent_single(data: pd.Series, targets_above: List[int] = None) -> dict[str:float]:
90+
"""
91+
Calculate percentage of values above target thresholds for a single series/subject.
92+
"""
93+
# Convert targets to float
94+
if targets_above is None:
95+
targets_above = [140, 180, 250]
96+
targets_above = [int(t) for t in targets_above]
97+
98+
# Calculate total non-NA readings
99+
total_readings = len(data.dropna())
100+
if total_readings == 0:
101+
return {f"above_{t}": 0 for t in targets_above}
102+
103+
# Calculate percentages for each target
104+
percentages = {}
105+
for target in targets_above:
106+
above_count = len(data[data > target])
107+
percentages[f"above_{target}"] = (above_count / total_readings) * 100
108+
109+
return percentages

0 commit comments

Comments
 (0)