Skip to content

Commit c2645cb

Browse files
committed
support for Series, list and ndarray -> return float or dict
1 parent 9c0ddc9 commit c2645cb

8 files changed

Lines changed: 163 additions & 115 deletions

File tree

README.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,11 @@ Unless noted, IGLU-R test compatability is considered successful if it achieves
4545
| gvp |Glucose Variability Percentage| ✅ | ✅ only Series(DatetimeIndex) returns float
4646
| hbgi |High Blood Glucose Index|| ✅ returns float |
4747
| hyper_index |Hyperglycemia Index||✅ returns float |
48-
| hyper_index |Hyperglycemia Index||✅ returns float |
49-
| hypo_index |Hypoglycemia Index||
50-
| igc |Index of Glycemic Control||
48+
| hypo_index |Hypoglycemia Index||✅ returns float |
49+
| igc |Index of Glycemic Control||✅ returns float |
5150
| in_range_percent |percentage of values within target ranges| ✅ | ✅ returns dict
52-
| iqr_glu |glucose level interquartile range||
53-
| j_index |J-Index score for glucose measurements||
51+
| iqr_glu |glucose level interquartile range||✅ returns float |
52+
| j_index |J-Index score for glucose measurements||✅ returns float |
5453
| lbgi | Low Blood Glucose Index||
5554
| m_value | M-value of Schlichtkrull et al ||
5655
| mad_glu | Median Absolute Deviation ||

iglu_python/igc.py

Lines changed: 31 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@
99

1010

1111
def igc(
12-
data: Union[pd.DataFrame, pd.Series],
12+
data: Union[pd.DataFrame, pd.Series, np.ndarray, list],
1313
LLTR: int = 80,
1414
ULTR: int = 140,
1515
a: float = 1.1,
1616
b: float = 2,
1717
c: int = 30,
1818
d: int = 30,
19-
) -> pd.DataFrame:
19+
) -> pd.DataFrame|float:
2020
"""
2121
Calculate Index of Glycemic Control (IGC).
2222
@@ -25,8 +25,8 @@ def igc(
2525
2626
Parameters
2727
----------
28-
data : Union[pd.DataFrame, pd.Series]
29-
DataFrame with columns 'id', 'time', and 'gl', or a Series of glucose values
28+
data : Union[pd.DataFrame, pd.Series, np.ndarray, list]
29+
DataFrame with columns 'id', 'time', and 'gl', or a Series of glucose values, or a numpy array or list of glucose values
3030
LLTR : int, default=80
3131
Lower Limit of Target Range, in mg/dL
3232
ULTR : int, default=140
@@ -42,10 +42,9 @@ def igc(
4242
4343
Returns
4444
-------
45-
pd.DataFrame
45+
pd.DataFrame|float
4646
DataFrame with 1 row for each subject, a column for subject id and a column
47-
for the IGC value. If a Series of glucose values is passed, then a DataFrame
48-
without the subject id is returned.
47+
for the IGC value. If a Series of glucose values is passed, then a float is returned.
4948
5049
References
5150
----------
@@ -73,40 +72,34 @@ def igc(
7372
0 0.106
7473
"""
7574
# Handle Series input
76-
is_vector = False
77-
if isinstance(data, (list, np.ndarray)):
78-
data = pd.Series(data)
79-
if isinstance(data, pd.Series):
80-
is_vector = True
81-
data = data.dropna()
82-
if len(data) == 0:
83-
return pd.DataFrame({"GVP": [np.nan]})
84-
85-
# Convert to DataFrame format for processing
86-
data = pd.DataFrame(
87-
{
88-
"id": ["subject1"] * len(data),
89-
"time": pd.date_range(
90-
start="2020-01-01", periods=len(data), freq="5min"
91-
),
92-
"gl": data.values,
93-
}
94-
)
75+
if isinstance(data, (pd.Series,list, np.ndarray)):
76+
if isinstance(data, (np.ndarray, list)):
77+
data = pd.Series(data)
78+
return igc_single(data, LLTR, ULTR, a, b, c, d)
9579

9680
# Check and prepare data
9781
data = check_data_columns(data)
9882

99-
# Calculate hyper_index and hypo_index
100-
out_hyper = hyper_index(data, ULTR=ULTR, a=a, c=c)
101-
out_hypo = hypo_index(data, LLTR=LLTR, b=b, d=d)
102-
103-
# Combine the indices
104-
out = pd.merge(out_hyper, out_hypo, on="id")
105-
out["IGC"] = out["hyper_index"] + out["hypo_index"]
106-
out = out[["id", "IGC"]]
83+
out = data.groupby('id').agg(
84+
IGC = ("gl", lambda x: igc_single(x, LLTR, ULTR, a, b, c, d))
85+
).reset_index()
86+
return out
10787

108-
# Remove id column if input was a Series
109-
if is_vector:
110-
out = out.drop("id", axis=1)
88+
def igc_single(
89+
gl: pd.Series,
90+
LLTR: int = 80,
91+
ULTR: int = 140,
92+
a: float = 1.1,
93+
b: float = 2,
94+
c: int = 30,
95+
d: int = 30
96+
) -> float:
97+
"""
98+
Calculate Index of Glycemic Control for a single subject.
99+
"""
100+
# Calculate hyper_index and hypo_index
101+
out_hyper = hyper_index(gl, ULTR=ULTR, a=a, c=c)
102+
out_hypo = hypo_index(gl, LLTR=LLTR, b=b, d=d)
111103

112-
return out
104+
out = out_hyper + out_hypo
105+
return out

iglu_python/iqr_glu.py

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from .utils import check_data_columns
77

88

9-
def iqr_glu(data: Union[pd.DataFrame, pd.Series]) -> pd.DataFrame:
9+
def iqr_glu(data: Union[pd.DataFrame, pd.Series, np.ndarray, list]) -> pd.DataFrame|float:
1010
"""
1111
Calculate glucose level interquartile range (IQR).
1212
@@ -15,15 +15,14 @@ def iqr_glu(data: Union[pd.DataFrame, pd.Series]) -> pd.DataFrame:
1515
1616
Parameters
1717
----------
18-
data : Union[pd.DataFrame, pd.Series]
19-
DataFrame with columns 'id', 'time', and 'gl', or a Series of glucose values
18+
data : Union[pd.DataFrame, pd.Series, np.ndarray, list]
19+
DataFrame with columns 'id', 'time', and 'gl', or a Series of glucose values, or a numpy array or list of glucose values
2020
2121
Returns
2222
-------
23-
pd.DataFrame
24-
DataFrame with columns:
25-
- id: subject identifier (if DataFrame input)
26-
- IQR: interquartile range of glucose values (75th percentile - 25th percentile)
23+
pd.DataFrame|float
24+
DataFrame with 1 row for each subject, a column for subject id and a column
25+
for the IQR value. If a Series of glucose values is passed, then a float is returned.
2726
2827
Examples
2928
--------
@@ -44,10 +43,15 @@ def iqr_glu(data: Union[pd.DataFrame, pd.Series]) -> pd.DataFrame:
4443
0 70.0
4544
"""
4645
# Handle Series input
47-
if isinstance(data, pd.Series):
46+
if isinstance(data, (pd.Series,list, np.ndarray)):
47+
if isinstance(data, (np.ndarray, list)):
48+
data = pd.Series(data)
49+
data = data.dropna()
50+
if len(data) == 0:
51+
return np.nan
4852
# Calculate IQR for Series
49-
iqr_val = np.percentile(data, 75) - np.percentile(data, 25)
50-
return pd.DataFrame({"IQR": [iqr_val]})
53+
iqr_val = iqr_glu_single(data)
54+
return iqr_val
5155

5256
# Handle DataFrame input
5357
data = check_data_columns(data)
@@ -57,8 +61,28 @@ def iqr_glu(data: Union[pd.DataFrame, pd.Series]) -> pd.DataFrame:
5761
data = data.dropna()
5862
result = (
5963
data.groupby("id")
60-
.agg(IQR=("gl", lambda x: np.percentile(x, 75) - np.percentile(x, 25)))
64+
.agg(IQR=("gl", lambda x: iqr_glu_single(x)))
6165
.reset_index()
6266
)
6367

6468
return result
69+
70+
def iqr_glu_single(
71+
gl: pd.Series,
72+
) -> float:
73+
"""
74+
Calculate glucose level interquartile range (IQR) for a single subject.
75+
76+
Parameters
77+
----------
78+
gl : pd.Series
79+
Series of glucose values
80+
81+
Returns
82+
"""
83+
gl = gl.dropna()
84+
if len(gl) == 0:
85+
return np.nan
86+
# Calculate IQR for Series
87+
iqr_val = np.percentile(gl, 75) - np.percentile(gl, 25)
88+
return iqr_val

iglu_python/j_index.py

Lines changed: 29 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
from typing import Union
22

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

56
from .utils import check_data_columns
67

78

8-
def j_index(data: Union[pd.DataFrame, pd.Series]) -> pd.DataFrame:
9+
def j_index(data: Union[pd.DataFrame, pd.Series, np.ndarray, list]) -> pd.DataFrame|float:
910
"""
1011
Calculate J-Index score for glucose measurements.
1112
@@ -15,15 +16,15 @@ def j_index(data: Union[pd.DataFrame, pd.Series]) -> pd.DataFrame:
1516
1617
Parameters
1718
----------
18-
data : Union[pd.DataFrame, pd.Series]
19-
DataFrame with columns 'id', 'time', and 'gl', or a Series of glucose values
19+
data : Union[pd.DataFrame, pd.Series, np.ndarray, list]
20+
DataFrame with columns 'id', 'time', and 'gl', or a Series of glucose values,
21+
or a numpy array or list of glucose values
2022
2123
Returns
2224
-------
23-
pd.DataFrame
25+
pd.DataFrame|float
2426
DataFrame with 1 row for each subject, a column for subject id and a column
25-
for J-Index value. If a Series of glucose values is passed, then a DataFrame
26-
without the subject id is returned.
27+
for J-Index value. If a Series of glucose values is passed, then a float is returned.
2728
2829
References
2930
----------
@@ -51,34 +52,30 @@ def j_index(data: Union[pd.DataFrame, pd.Series]) -> pd.DataFrame:
5152
0 1.5000
5253
"""
5354
# Handle Series input
54-
if isinstance(data, pd.Series):
55-
# Calculate mean and standard deviation
56-
mean_gl = data.mean()
57-
sd_gl = data.std()
58-
59-
# Calculate J-index
60-
j_index = 0.001 * (mean_gl + sd_gl) ** 2
61-
62-
return pd.DataFrame({"J_index": [j_index]})
55+
if isinstance(data, (pd.Series,list, np.ndarray)):
56+
if isinstance(data, (np.ndarray, list)):
57+
data = pd.Series(data)
58+
return j_index_single(data)
6359

6460
# Handle DataFrame input
6561
data = check_data_columns(data)
6662

67-
# Initialize result list
68-
result = []
69-
70-
# Process each subject
71-
for subject in data["id"].unique():
72-
subject_data = data[data["id"] == subject]
73-
74-
# Calculate mean and standard deviation
75-
mean_gl = subject_data["gl"].mean()
76-
sd_gl = subject_data["gl"].std()
63+
out = data.groupby('id').agg(
64+
J_index = ("gl", lambda x: j_index_single(x))
65+
).reset_index()
66+
return out
7767

78-
# Calculate J-index
79-
j_index = 0.001 * (mean_gl + sd_gl) ** 2
80-
81-
result.append({"id": subject, "J_index": j_index})
82-
83-
# Convert to DataFrame
84-
return pd.DataFrame(result)
68+
def j_index_single(gl: pd.Series) -> float:
69+
"""
70+
Calculate J-Index score for a single subject.
71+
"""
72+
gl = gl.dropna()
73+
if len(gl) == 0:
74+
return np.nan
75+
# Calculate mean and standard deviation
76+
mean_gl = gl.mean()
77+
sd_gl = gl.std()
78+
79+
# Calculate J-index
80+
j_index = 0.001 * (mean_gl + sd_gl) ** 2
81+
return j_index

tests/test_igc.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -106,15 +106,28 @@ def test_igc_series_input():
106106

107107
# Calculate IGC
108108
result = iglu.igc(data)
109+
expected = 1.453976
109110

110111
# Check output format
111-
assert isinstance(result, pd.DataFrame)
112-
assert "IGC" in result.columns
113-
assert "id" not in result.columns
114-
assert len(result) == 1
112+
assert isinstance(result, float)
113+
np.testing.assert_allclose(result, expected, rtol=1e-3)
114+
115+
def test_igc_list_input():
116+
"""Test IGC calculation with list input."""
117+
data = [150, 200, 180, 130, 190, 160]
118+
result = iglu.igc(data)
119+
expected = 1.453976
120+
assert isinstance(result, float)
121+
np.testing.assert_allclose(result, expected, rtol=1e-3)
122+
123+
def test_igc_numpy_array_input():
124+
"""Test IGC calculation with numpy array input."""
125+
data = np.array([150, 200, 180, 130, 190, 160])
126+
result = iglu.igc(data)
127+
expected = 1.453976
128+
assert isinstance(result, float)
129+
np.testing.assert_allclose(result, expected, rtol=1e-3)
115130

116-
# Check that IGC value is non-negative
117-
assert result["IGC"].iloc[0] >= 0
118131

119132

120133
def test_igc_custom_parameters():

tests/test_in_range_percent.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,35 @@ def test_in_range_percent_series_input():
119119
assert len(result) == 2
120120

121121
# Check that percentages are between 0 and 100
122-
assert (result["in_range_70_180"] >= 0) and (result["in_range_70_180"] <= 100)
123-
assert (result["in_range_63_140"] >= 0) and (result["in_range_63_140"] <= 100)
122+
np.testing.assert_allclose(result["in_range_70_180"], 83.33, rtol=1e-3)
123+
np.testing.assert_allclose(result["in_range_63_140"], 66.66, rtol=1e-3)
124124

125+
def test_in_range_percent_list_input():
126+
"""Test in_range_percent calculation with list input."""
127+
data = [80, 90, 100, 130, 190, 160]
128+
result = iglu.in_range_percent(data)
129+
assert isinstance(result, dict)
130+
assert "in_range_70_180" in result
131+
assert "in_range_63_140" in result
132+
assert len(result) == 2
133+
134+
# Check that percentages are between 0 and 100
135+
np.testing.assert_allclose(result["in_range_70_180"], 83.33, rtol=1e-3)
136+
np.testing.assert_allclose(result["in_range_63_140"], 66.66, rtol=1e-3)
125137

138+
def test_in_range_percent_numpy_array_input():
139+
"""Test in_range_percent calculation with numpy array input."""
140+
data = np.array([80, 90, 100, 130, 190, 160])
141+
result = iglu.in_range_percent(data)
142+
assert isinstance(result, dict)
143+
assert "in_range_70_180" in result
144+
assert "in_range_63_140" in result
145+
assert len(result) == 2
146+
147+
# Check that percentages are between 0 and 100
148+
np.testing.assert_allclose(result["in_range_70_180"], 83.33, rtol=1e-3)
149+
np.testing.assert_allclose(result["in_range_63_140"], 66.66, rtol=1e-3)
150+
126151
def test_in_range_percent_custom_targets():
127152
"""Test in_range_percent calculation with custom targets."""
128153
data = pd.DataFrame(

tests/test_iqr_glu.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -111,13 +111,19 @@ def test_iqr_glu_output_format():
111111
# Test with Series input
112112
series_data = pd.Series([150, 155, 160, 165, 140, 145])
113113
result_series = iglu.iqr_glu(series_data)
114-
assert isinstance(result_series, pd.DataFrame)
115-
assert "IQR" in result_series.columns
116-
assert len(result_series) == 1
117-
assert (
118-
result_series["IQR"].iloc[0] == 12.5
119-
) # 75th percentile (160) - 25th percentile (145)
120-
114+
assert isinstance(result_series, float)
115+
np.testing.assert_allclose(result_series, 12.5, rtol=1e-3)
116+
117+
list_data = [150, 155, 160, 165, 140, 145]
118+
result_list = iglu.iqr_glu(list_data)
119+
assert isinstance(result_list, float)
120+
np.testing.assert_allclose(result_list, 12.5, rtol=1e-3)
121+
122+
array_data = np.array([150, 155, 160, 165, 140, 145])
123+
result_array = iglu.iqr_glu(array_data)
124+
assert isinstance(result_array, float)
125+
np.testing.assert_allclose(result_array, 12.5, rtol=1e-3)
126+
121127
# Test with empty data
122128
empty_data = pd.DataFrame(columns=["id", "time", "gl"])
123129
with pytest.raises(ValueError):

0 commit comments

Comments
 (0)