Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 77 additions & 56 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,50 +19,75 @@ A significant focus of this project has been ensuring compatibility with the ori
This approach ensures that the Python implementation produces results consistent with the original R package.

## Unit Test Status
Unless noted, iglu-r test is considered successful if it achieves precision of 0.001

| Function | IGLU-R test compatibility | array/list/Series | TZ | Comments |
|----------|---------------------------|-------------------|----|----------|
| above_percent | ✅ | |||
| active_percent | ✅ |
| adrr | ✅ |
| auc| 🟡 (0.01 precision) | || see [auc_evaluation.ipynb](https://github.com/staskh/iglu_python/blob/main/notebooks/auc_evaluation.ipynb)|
| below_percent| ✅ |
| cogi | ✅ |
| conga | ✅ |
| cv_glu | ✅ |
| cv_measures | ✅ |
| ea1c | ✅ |
| episode_calculation | ✅| || |
| gmi | ✅ |
| grade_eugly | ✅ |
| grade_hyper | ✅ |
| grade_hypo | ✅ |
| grade | ✅ |
| gri | ✅ |
| gvp | ✅ |
| hbgi | ✅ |
| hyper_index | ✅ |
| hypo_index | ✅ |
| igc | ✅ |
| j_index | ✅ |
| lbgi | ✅ |
| mad_glu | ✅ |
| mag | ✅ | || IMHO, Original R implementation has an error |
| mage | ✅ | || See algorithm at [MAGE](https://irinagain.github.io/iglu/articles/MAGE.html) |
| mean_glu | ✅ |
| median_glu | ✅ |
| modd | ✅ |
| pgs | ✅ | || |
| quantile_glu | ✅ |
| range_glu | ✅ |
| roc | ✅ |
| sd_glu | ✅ |
| sd_measures | ✅ |
| sd_roc | ✅ | |||
| process_data | ✅ |
| summary_glu | ✅ |
| CGMS2DayByDay | ✅ |
The current version of IGLU-PYTHON is test-compatible with IGLU-R v4.2.2

Unless noted, IGLU-R test compatability is considered successful if it achieves precision of 0.001

| Function | Description | IGLU-R test compatibility | list /ndarray /Series input | TZ | Comments |
|----------|-------------|-------------|-------------------|----|----------|
| above_percent | percentage of values above target thresholds| ✅ |✅ returns Dict[str,float] |||
| active_percent | percentage of time CGM was active | ✅ | ✅ only Series(DatetimeIndex) returns Dict[str,float]|
| adrr | average daily risk range | ✅ |✅ only Series(DatetimeIndex) returns float |
| 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)|
| below_percent| percentage of values below target thresholds| ✅ | ✅ returns Dict[str,float]|
| cogi |Coefficient of Glucose Irregularity | ✅ | ✅ returns float
| conga | Continuous Overall Net Glycemic Action |✅ | ✅ only Series(DatetimeIndex) returns float
| cv_glu | Coefficient of Variation | ✅| ✅ returns float |
| cv_measures |Coefficient of Variation subtypes (CVmean and CVsd) |✅ |✅ only Series(DatetimeIndex) returns Dict[str,float]| |
| ea1c |estimated A1C (eA1C) values| ✅ | ✅ returns float |
| episode_calculation | Hypo/Hyperglycemic episodes with summary statistics| ✅| 🟡 always returns DataFrame(s)|| |
| gmi | Glucose Management Indicator | ✅ | ✅ returns float |
| grade_eugly |percentage of GRADE score attributable to target range| ✅ | ✅ returns float
| grade_hyper |percentage of GRADE score attributable to hyperglycemia| ✅ |✅ returns float
| grade_hypo |percentage of GRADE score attributable to hypoglycemia| ✅ |✅ returns float
| grade |mean GRADE score| ✅ | ✅ returns float |
| gri |Glycemia Risk Index | ✅ | ✅ returns float |
| gvp |Glucose Variability Percentage| ✅ | ✅ only Series(DatetimeIndex) returns float
| hbgi |High Blood Glucose Index| ✅ | ✅ returns float |
| hyper_index |Hyperglycemia Index| ✅ |✅ returns float |
| hypo_index |Hypoglycemia Index| ✅ |✅ returns float |
| igc |Index of Glycemic Control| ✅ |✅ returns float |
| in_range_percent |percentage of values within target ranges| ✅ | ✅ returns Dict[str,float]|
| iqr_glu |glucose level interquartile range|✅ |✅ returns float |
| j_index |J-Index score for glucose measurements| ✅ |✅ returns float |
| lbgi | Low Blood Glucose Index| ✅ |✅ returns float |
| m_value | M-value of Schlichtkrull et al | ✅ |✅ returns float |
| mad_glu | Median Absolute Deviation | ✅ |✅ returns float |
| mag | Mean Absolute Glucose| ✅ | ✅ only Series(DatetimeIndex) returns float ||| IMHO, Original R implementation has an error |
| mage | Mean Amplitude of Glycemic Excursions| ✅ |✅ only Series(DatetimeIndex) returns float || See algorithm at [MAGE](https://irinagain.github.io/iglu/articles/MAGE.html) |
| mean_glu | Mean glucose value | ✅ | ✅ returns float|
| median_glu |Median glucose value| ✅ |✅ returns float |
| modd | Mean of Daily Differences| ✅ | ✅ only Series(DatetimeIndex) returns float|
| pgs | Personal Glycemic State | ✅ |✅ only Series(DatetimeIndex) returns float| ||
| quantile_glu |glucose level quantiles| ✅ |✅ returns List[float] |
| range_glu |glucose level range| ✅ |✅ returns float|
| roc | Rate of Change| ✅ |🟡 always returns DataFrame|
| sd_glu | standard deviation of glucose values| ✅ | ✅ returns float
| sd_measures |various standard deviation subtypes| ✅ |✅ only Series(DatetimeIndex) returns Dict[str,float]|
| sd_roc | standard deviation of the rate of change| ✅ |✅ only Series(DatetimeIndex) returns float ||
| summary_glu | summary glucose level| ✅ |
| process_data | Data Pre-Processor | ✅ |
| CGMS2DayByDay |Interpolate glucose input| ✅ |

### Input & Output
The implementation maintains compatibility with the R version while following Python best practices. The metrics can be used as:

```Python
import iglu_python ias iglu

# With DataFrame input
result_df = iglu.cv_glu(data) # data should have 'id', 'time', and 'gl' columns
# Return DataFrame with "id' and column(s) with value(s)

# With Series input (some metrics require Series with DateTimeIndex)
result_float = iglu.cv_glu(glucose_series) # just glucose values
# returns a single float value

# Same with function that support list or ndarray
result_float = iglu.cv_glu(glucose_list) # list of glucose values
# returns a single float value

```

# Installation

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

# Calculate glucose metrics
mean_glucose = iglu.mean_glu(data)
cv = iglu.cv_glu(data)
time_in_range = iglu.active_percent(data, lltr=70, ultr=180)
active = iglu.active_percent(data)

print(f"Mean glucose: {mean_glucose}")
print(f"CV: {cv}")
print(f"Time in range (70-180 mg/dL): {time_in_range}%")
print(f"Mean glucose: {mean_glucose['mean'][0]}")
print(f"CV: {cv['CV'][0]}")
print(f"CGM active percent: {active['active_percent'][0]}%")
```

### Using with Time Series Data

```python
import pandas as pd
import numpy as np
import iglu_python as iglu
from datetime import datetime, timedelta

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

data = pd.DataFrame({
'id': ['Subject1'] * 288,
'time': timestamps,
'gl': glucose_values
})
data = pd.Series(glucose_values, index=timestamps)

# Calculate advanced metrics
mage = iglu.mage(data)
Expand Down
8 changes: 7 additions & 1 deletion iglu_python/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
from .below_percent import below_percent
from .cogi import cogi
from .conga import conga
from .cv_glu import cv_glu
from .cv_measures import cv_measures
from .ea1c import ea1c
from .episode_calculation import episode_calculation
from .gmi import gmi
from .grade import grade
from .grade_eugly import grade_eugly
from .grade_hyper import grade_hyper
Expand Down Expand Up @@ -37,7 +40,7 @@
from .sd_measures import sd_measures
from .sd_roc import sd_roc
from .summary_glu import summary_glu
from .utils import set_iglu_r_compatible, is_iglu_r_compatible, CGMS2DayByDay, check_data_columns, gd2d_to_df
from .utils import CGMS2DayByDay, check_data_columns, gd2d_to_df, is_iglu_r_compatible, set_iglu_r_compatible

__all__ = [
"above_percent",
Expand All @@ -49,13 +52,16 @@
"CGMS2DayByDay",
"cogi",
"conga",
"cv_glu",
"cv_measures",
"ea1c",
"episode_calculation",
"gd2d_to_df",
"grade",
"grade_eugly",
"grade_hyper",
"grade_hypo",
"gmi",
"gri",
"gvp",
"hbgi",
Expand Down
66 changes: 36 additions & 30 deletions iglu_python/above_percent.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
from typing import List, Union

import numpy as np
import pandas as pd

from .utils import check_data_columns


def above_percent(
data: Union[pd.DataFrame, pd.Series, list],
targets_above: List[int] = [140, 180, 250],
) -> pd.DataFrame:
data: Union[pd.DataFrame, pd.Series, list,np.ndarray],
targets_above: List[int] = None,
) -> pd.DataFrame|dict[str:float]:
"""
Calculate percentage of values above target thresholds.

Expand Down Expand Up @@ -58,22 +59,13 @@ def above_percent(
0 75.0 25.0
"""
# Handle Series input
if isinstance(data, (pd.Series, list)):
# Convert targets to float
targets_above = [int(t) for t in targets_above]
if targets_above is None:
targets_above = [140, 180, 250]
if isinstance(data, (pd.Series, list,np.ndarray)):
if isinstance(data, (list, np.ndarray)):
data = pd.Series(data)
return above_percent_single(data, targets_above)

# Calculate total non-NA readings
total_readings = len(data.dropna())
if total_readings == 0:
return pd.DataFrame(columns=[f"above_{t}" for t in targets_above])

# Calculate percentages for each target
percentages = {}
for target in targets_above:
above_count = len(data[data > target])
percentages[f"above_{target}"] = (above_count / total_readings) * 100

return pd.DataFrame([percentages])

# Handle DataFrame input
data = check_data_columns(data)
Expand All @@ -85,19 +77,33 @@ def above_percent(
# Process each subject
for subject in data["id"].unique():
subject_data = data[data["id"] == subject]
total_readings = len(subject_data.dropna(subset=["gl"]))

if total_readings == 0:
continue

# Calculate percentages for each target
percentages = {}
for target in targets_above:
above_count = len(subject_data[subject_data["gl"] > target])
percentages[f"above_{target}"] = (above_count / total_readings) * 100

percentages = above_percent_single(subject_data["gl"], targets_above)
percentages["id"] = subject
result.append(percentages)

# Convert to DataFrame
return pd.DataFrame(result)
df = pd.DataFrame(result)
df = df[['id'] + [col for col in df.columns if col != 'id']]
return df

def above_percent_single(data: pd.Series, targets_above: List[int] = None) -> dict[str:float]:
"""
Calculate percentage of values above target thresholds for a single series/subject.
"""
# Convert targets to float
if targets_above is None:
targets_above = [140, 180, 250]
targets_above = [int(t) for t in targets_above]

# Calculate total non-NA readings
total_readings = len(data.dropna())
if total_readings == 0:
return {f"above_{t}": 0 for t in targets_above}

# Calculate percentages for each target
percentages = {}
for target in targets_above:
above_count = len(data[data > target])
percentages[f"above_{target}"] = (above_count / total_readings) * 100

return percentages
Loading
Loading