Skip to content

Commit 22988f1

Browse files
committed
correct CKF-EPI using 2021 guidelines, add tests
1 parent f72ba2d commit 22988f1

File tree

2 files changed

+190
-34
lines changed

2 files changed

+190
-34
lines changed

src/medimetry/renal.py

Lines changed: 59 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,8 @@ def mdrd(creatinine: float, age: int, gender: Gender, race: EthnicalRace = Ethni
6767
"""
6868
assert creatinine > 0, "Creatinine must be positive"
6969
assert age > 0, "Age must be positive"
70-
assert gender in (
71-
Gender.MALE,
72-
Gender.FEMALE,
73-
), "Gender must be Gender.MALE|Gender.FEMALE"
70+
if not isinstance(gender, Gender):
71+
raise TypeError("Gender must be Gender.MALE or Gender.FEMALE")
7472

7573
# Base MDRD formula: 175 x (creatinine)^-1.154 x (age)^-0.203
7674
egfr = 175 * (creatinine**-1.154) * (age**-0.203)
@@ -86,46 +84,73 @@ def mdrd(creatinine: float, age: int, gender: Gender, race: EthnicalRace = Ethni
8684
return egfr
8785

8886

89-
def ckd_epi(creatinine: float, age: int, gender: Gender, race: EthnicalRace = EthnicalRace.OTHER) -> float:
87+
def ckd_epi(creatinine: float, age: int, gender: Gender, cystatin_c: float | None = None) -> float:
9088
"""
91-
Calculate eGFR using CKD-EPI (Chronic Kidney Disease Epidemiology Collaboration) formula.
89+
Calculate eGFR using 2021 CKD-EPI equation (Chronic Kidney Disease Epidemiology Collaboration)
90+
formula, which does not use a "race" as parameter.
91+
92+
Sources:
93+
https://www.kidney.org/ckd-epi-creatinine-equation-2021
94+
https://www.kidney.org/ckd-epi-creatinine-cystatin-equation-2021
95+
96+
For the same creatinine value, the 2021 equation will estimate a slightly-too-low
97+
GFR for Black patients and a slightly-too-high GFR for non-Black patients.
9298
9399
Args:
94100
creatinine (float): Serum creatinine in mg/dl
95101
age (int): Age in years
96102
gender (Gender): Gender (Gender.MALE or Gender.FEMALE)
97-
race (EthnicalRace): Race (EthnicalRace.AFRICAN_AMERICAN or EthnicalRace.OTHER)
103+
cystain_c (optional): Cystatin c (mg/dl) - if given, it will be considered for
104+
the calculation
98105
99106
Returns:
100107
float: Estimated GFR in mL/min/1.73m²
101108
"""
102109
assert creatinine > 0, "Creatinine must be positive"
103110
assert age > 0, "Age must be positive"
104-
assert gender in (
105-
Gender.MALE,
106-
Gender.FEMALE,
107-
), "Gender must be Gender.MALE|Gender.FEMALE"
108-
109-
# Define kappa and alpha based on sex
110-
if gender == Gender.FEMALE:
111-
kappa = 0.7
112-
alpha = -0.329
113-
gender_factor = 1.018
111+
if not isinstance(gender, Gender):
112+
raise TypeError("Gender must be of type 'Gender'")
113+
if cystatin_c is not None:
114+
assert cystatin_c > 0, "Cystatin C must be positive, if given"
115+
116+
if cystatin_c is None:
117+
# Formula WITHOUT Cystatin C
118+
119+
# Define kappa and alpha based on gender
120+
if gender == Gender.FEMALE:
121+
kappa = 0.7
122+
alpha = -0.241
123+
gender_factor = 1.012
124+
else:
125+
kappa = 0.9
126+
alpha = -0.302
127+
gender_factor = 1.0
128+
129+
# Calculate min and max terms
130+
cr_kappa_ratio = creatinine / kappa
131+
132+
# Base CKD-EPI formula
133+
return 142 * min(cr_kappa_ratio, 1.0) ** alpha * max(cr_kappa_ratio, 1.0) ** -1.2 * (0.9938**age) * gender_factor
114134
else:
115-
kappa = 0.9
116-
alpha = -0.411
117-
gender_factor = 1.0
118-
119-
# Calculate min and max terms
120-
cr_kappa_ratio = creatinine / kappa
121-
min_term = min(cr_kappa_ratio, 1.0) ** alpha
122-
max_term = max(cr_kappa_ratio, 1.0) ** -1.209
123-
124-
# Base CKD-EPI formula
125-
egfr = 141 * min_term * max_term * (0.993**age) * gender_factor
126-
127-
# Apply race correction factor
128-
if race == EthnicalRace.AFRICAN_AMERICAN:
129-
egfr *= 1.159
130-
131-
return egfr
135+
# Formula WITH Cystatin C
136+
# Define kappa and alpha based on gender
137+
if gender == Gender.FEMALE:
138+
kappa = 0.7
139+
alpha = -0.219
140+
gender_factor = 0.963
141+
else:
142+
kappa = 0.9
143+
alpha = -0.144
144+
gender_factor = 1.0
145+
146+
cr_kappa_ratio = creatinine / kappa
147+
148+
return (
149+
135
150+
* min(cr_kappa_ratio, 1.0) ** alpha
151+
* max(cr_kappa_ratio, 1.0) ** -0.544
152+
* min(cystatin_c / 0.8, 1.0) ** -0.323
153+
* max(cystatin_c / 0.8, 1.0) ** -0.778
154+
* (0.9961**age)
155+
* gender_factor
156+
)

tests/test_cdk_epi.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import pytest
2+
3+
from medimetry import Gender
4+
from medimetry.renal import ckd_epi
5+
6+
7+
def assert_in_range(result, expected):
8+
"""Helper for asserting that a result is within a certain range of 0.1."""
9+
assert abs(result - expected) < 0.1
10+
11+
12+
def test_ckd_epi_male_normal_creatinine():
13+
"""Test CKD-EPI formula for male patient with normal creatinine levels."""
14+
# 50-year-old male, creatinine 1.0 mg/dl
15+
result = ckd_epi(creatinine=1.0, age=50, gender=Gender.MALE)
16+
expected = 91.6
17+
assert_in_range(result, expected)
18+
19+
20+
def test_ckd_epi_male_elevated_creatinine():
21+
result = ckd_epi(creatinine=2.0, age=50, gender=Gender.MALE)
22+
expected = 40.0
23+
assert_in_range(result, expected)
24+
25+
26+
def test_ckd_epi_female_normal_creatinine():
27+
"""Test CKD-EPI formula for female patient with normal creatinine levels."""
28+
# 50-year-old female, creatinine 0.8 mg/dl
29+
result = ckd_epi(creatinine=0.8, age=50, gender=Gender.FEMALE)
30+
expected = 89.7
31+
assert_in_range(result, expected)
32+
33+
34+
def test_ckd_epi_female_elevated_creatinine():
35+
"""Test CKD-EPI formula for female patient with elevated creatinine levels."""
36+
# 60-year-old female, creatinine 2.5 mg/dl
37+
result = ckd_epi(creatinine=2.5, age=60, gender=Gender.FEMALE)
38+
expected = 21.4
39+
assert_in_range(result, expected)
40+
41+
42+
def test_ckd_epi_male_with_cystatin_c():
43+
"""Test CKD-EPI formula for male patient with cystatin C."""
44+
# 60-year-old male, creatinine 1.2 mg/dl, cystatin C 1.0 mg/dl
45+
result = ckd_epi(creatinine=1.2, age=60, gender=Gender.MALE, cystatin_c=1.0)
46+
expected = 76.8
47+
assert_in_range(result, expected)
48+
49+
50+
def test_ckd_epi_female_with_cystatin_c():
51+
"""Test CKD-EPI formula for female patient with cystatin C."""
52+
# 50-year-old female, creatinine 0.9 mg/dl, cystatin C 1.1 mg/dl
53+
result = ckd_epi(creatinine=0.9, age=50, gender=Gender.FEMALE, cystatin_c=1.1)
54+
expected = 72.8
55+
assert_in_range(result, expected)
56+
57+
58+
def test_ckd_epi_zero_creatinine():
59+
"""Test that AssertionError is raised for zero creatinine."""
60+
with pytest.raises(AssertionError, match="Creatinine must be positive"):
61+
ckd_epi(creatinine=0.0, age=50, gender=Gender.MALE)
62+
63+
64+
def test_ckd_epi_negative_creatinine():
65+
"""Test that AssertionError is raised for negative creatinine."""
66+
with pytest.raises(AssertionError, match="Creatinine must be positive"):
67+
ckd_epi(creatinine=-0.5, age=50, gender=Gender.MALE)
68+
69+
70+
def test_ckd_epi_zero_age():
71+
"""Test that AssertionError is raised for zero age."""
72+
with pytest.raises(AssertionError, match="Age must be positive"):
73+
ckd_epi(creatinine=1.0, age=0, gender=Gender.MALE)
74+
75+
76+
def test_ckd_epi_negative_age():
77+
"""Test that AssertionError is raised for negative age."""
78+
with pytest.raises(AssertionError, match="Age must be positive"):
79+
ckd_epi(creatinine=1.0, age=-5, gender=Gender.MALE)
80+
81+
82+
def test_ckd_epi_invalid_gender():
83+
"""Test that AssertionError is raised for invalid gender."""
84+
with pytest.raises(TypeError, match="Gender must be of type 'Gender'"):
85+
ckd_epi(creatinine=1.0, age=50, gender="invalid")
86+
87+
88+
def test_ckd_epi_zero_cystatin_c():
89+
"""Test that AssertionError is raised for zero cystatin C."""
90+
with pytest.raises(AssertionError, match="Cystatin C must be positive, if given"):
91+
ckd_epi(creatinine=1.0, age=50, gender=Gender.MALE, cystatin_c=0.0)
92+
93+
94+
def test_ckd_epi_negative_cystatin_c():
95+
"""Test that AssertionError is raised for negative cystatin C."""
96+
with pytest.raises(AssertionError, match="Cystatin C must be positive, if given"):
97+
ckd_epi(creatinine=1.0, age=50, gender=Gender.MALE, cystatin_c=-0.5)
98+
99+
100+
def test_ckd_epi_very_low_creatinine():
101+
"""Test CKD-EPI formula with very low creatinine values below kappa threshold."""
102+
# Female with very low creatinine (0.5 mg/dl, below kappa=0.7)
103+
result = ckd_epi(creatinine=0.5, age=30, gender=Gender.FEMALE)
104+
expected = 129.3
105+
assert_in_range(result, expected)
106+
107+
# Male with very low creatinine (0.6 mg/dl, below kappa=0.9)
108+
result = ckd_epi(creatinine=0.6, age=30, gender=Gender.MALE)
109+
expected = 133.1
110+
assert_in_range(result, expected)
111+
112+
113+
def test_ckd_epi_very_high_creatinine():
114+
"""Test CKD-EPI formula with very high creatinine values above kappa threshold."""
115+
# Female with very high creatinine (2.5 mg/dl, above kappa=0.7)
116+
result = ckd_epi(creatinine=6.0, age=80, gender=Gender.FEMALE)
117+
expected = 6.7
118+
assert_in_range(result, expected)
119+
120+
# Male with very high creatinine (3.0 mg/dl, above kappa=0.9)
121+
result = ckd_epi(creatinine=6.0, age=80, gender=Gender.MALE)
122+
expected = 8.8
123+
assert_in_range(result, expected)
124+
125+
126+
def test_ckd_epi_elderly_patient():
127+
"""Test CKD-EPI formula for elderly patient over 80 years."""
128+
# 85-year-old female, creatinine 1.2 mg/dl
129+
result = ckd_epi(creatinine=1.2, age=85, gender=Gender.FEMALE)
130+
expected = 44.3
131+
assert_in_range(result, expected)

0 commit comments

Comments
 (0)