Skip to content

Commit 9fad0bf

Browse files
committed
refactor: consolidate shared components and reorganize schema modules
- Move common functions (risk categorization, calibration, gompertz model) to helpers.py - Consolidate schema modules into algorithm-specific files (phenoage.py, score2.py) - Rename coefficient classes for clarity and consistency - Update imports across all compute modules to use new schema structure - Remove redundant schema files (core.py, markers.py, units.py)
1 parent 30324a0 commit 9fad0bf

9 files changed

Lines changed: 265 additions & 271 deletions

File tree

vitals/biomarkers/helpers.py

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
from collections.abc import Callable
22
from pathlib import Path
3-
from typing import Any, TypedDict, TypeVar
3+
from typing import Any, Literal, TypeAlias, TypedDict, TypeVar
44

5+
import numpy as np
56
from pydantic import BaseModel
67

7-
from vitals.schemas.units import PhenoageUnits, Score2DiabetesUnits, Score2Units
8+
from vitals.schemas import phenoage, score2
89

10+
RiskCategory: TypeAlias = Literal["Low to moderate", "High", "Very high"]
911
Biomarkers = TypeVar("Biomarkers", bound=BaseModel)
10-
Units = PhenoageUnits | Score2Units | Score2DiabetesUnits
12+
Units = phenoage.Units | score2.Units | score2.UnitsDiabetes
1113

1214

1315
class ConversionInfo(TypedDict):
@@ -198,3 +200,57 @@ def extract_biomarkers_from_json(
198200
extracted_values[field_name] = value
199201

200202
return biomarker_class(**extracted_values)
203+
204+
205+
def determine_risk_category(age: float, calibrated_risk: float) -> RiskCategory:
206+
"""
207+
Determine cardiovascular risk category based on age and calibrated risk percentage.
208+
209+
Args:
210+
age: Patient's age in years
211+
calibrated_risk: Calibrated 10-year CVD risk as a percentage
212+
213+
Returns:
214+
Risk stratification category
215+
"""
216+
if age < 50:
217+
if calibrated_risk < 2.5:
218+
return "Low to moderate"
219+
elif calibrated_risk < 7.5:
220+
return "High"
221+
else:
222+
return "Very high"
223+
else: # age 50-69
224+
if calibrated_risk < 5:
225+
return "Low to moderate"
226+
elif calibrated_risk < 10:
227+
return "High"
228+
else:
229+
return "Very high"
230+
231+
232+
def apply_calibration(uncalibrated_risk: float, scale1: float, scale2: float) -> float:
233+
"""
234+
Apply regional calibration to uncalibrated risk estimate.
235+
236+
Args:
237+
uncalibrated_risk: Raw risk estimate from the Cox model
238+
scale1: First calibration scale parameter
239+
scale2: Second calibration scale parameter
240+
241+
Returns:
242+
Calibrated 10-year CVD risk as a percentage
243+
"""
244+
return float(
245+
(1 - np.exp(-np.exp(scale1 + scale2 * np.log(-np.log(1 - uncalibrated_risk)))))
246+
* 100
247+
)
248+
249+
250+
def gompertz_mortality_model(weighted_risk_score: float) -> float:
251+
params = phenoage.Gompertz()
252+
return 1 - np.exp(
253+
-np.exp(weighted_risk_score)
254+
* (np.exp(120 * params.lambda_) - 1)
255+
/ params.lambda_
256+
)

vitals/phenoage/compute.py

Lines changed: 7 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,9 @@
11
from pathlib import Path
22

33
import numpy as np
4-
from pydantic import BaseModel
54

65
from vitals.biomarkers import helpers
7-
from vitals.schemas.markers import PhenoageMarkers
8-
from vitals.schemas.units import PhenoageUnits
9-
10-
11-
class LinearModel(BaseModel):
12-
"""
13-
Coefficients used to calculate the PhenoAge from Levine et al 2018
14-
"""
15-
16-
intercept: float = -19.9067
17-
albumin: float = -0.0336
18-
creatinine: float = 0.0095
19-
glucose: float = 0.1953
20-
log_crp: float = 0.0954
21-
lymphocyte_percent: float = -0.0120
22-
mean_cell_volume: float = 0.0268
23-
red_cell_distribution_width: float = 0.3306
24-
alkaline_phosphatase: float = 0.00188
25-
white_blood_cell_count: float = 0.0554
26-
age: float = 0.0804
27-
28-
29-
class Gompertz(BaseModel):
30-
"""
31-
Parameters of the Gompertz distribution for PhenoAge computation
32-
"""
33-
34-
lambda_: float = 0.0192
35-
coef1: float = 141.50225
36-
coef2: float = -0.00553
37-
coef3: float = 0.090165
38-
39-
40-
def __gompertz_mortality_model(weighted_risk_score: float) -> float:
41-
__params = Gompertz()
42-
return 1 - np.exp(
43-
-np.exp(weighted_risk_score)
44-
* (np.exp(120 * __params.lambda_) - 1)
45-
/ __params.lambda_
46-
)
6+
from vitals.schemas.phenoage import Gompertz, LinearModel, Markers, Units
477

488

499
def biological_age(filepath: str | Path) -> tuple[float, float, float]:
@@ -57,14 +17,14 @@ def biological_age(filepath: str | Path) -> tuple[float, float, float]:
5717
# Extract biomarkers from JSON file
5818
biomarkers = helpers.extract_biomarkers_from_json(
5919
filepath=filepath,
60-
biomarker_class=PhenoageMarkers,
61-
biomarker_units=PhenoageUnits(),
20+
biomarker_class=Markers,
21+
biomarker_units=Units(),
6222
)
6323

6424
age = biomarkers.age
6525
coef = LinearModel()
6626

67-
if isinstance(biomarkers, PhenoageMarkers):
27+
if isinstance(biomarkers, Markers):
6828
weighted_risk_score = (
6929
coef.intercept
7030
+ (coef.albumin * biomarkers.albumin)
@@ -81,7 +41,9 @@ def biological_age(filepath: str | Path) -> tuple[float, float, float]:
8141
+ (coef.white_blood_cell_count * biomarkers.white_blood_cell_count)
8242
+ (coef.age * biomarkers.age)
8343
)
84-
gompertz = __gompertz_mortality_model(weighted_risk_score=weighted_risk_score)
44+
gompertz = helpers.gompertz_mortality_model(
45+
weighted_risk_score=weighted_risk_score
46+
)
8547
model = Gompertz()
8648
pred_age = (
8749
model.coef1 + np.log(model.coef2 * np.log(1 - gompertz)) / model.coef3
@@ -90,17 +52,3 @@ def biological_age(filepath: str | Path) -> tuple[float, float, float]:
9052
return (age, pred_age, accl_age)
9153
else:
9254
raise ValueError(f"Invalid biomarker class used: {biomarkers}")
93-
94-
95-
# if __name__ == "__main__":
96-
# from pathlib import Path
97-
# input_dir = Path("tests/outputs")
98-
# output_dir = Path("tests/outputs")
99-
100-
# for input_file in input_dir.glob("*.json"):
101-
# if "patient" not in str(input_file):
102-
# continue
103-
104-
# # Update biomarker data
105-
# age, pred_age, accl_age = biological_age(str(input_file))
106-
# print(f"Chrono Age: {age} ::: Predicted Age: {pred_age} ::: Accel {accl_age}")

vitals/schemas/core.py

Lines changed: 0 additions & 80 deletions
This file was deleted.

vitals/schemas/markers.py

Lines changed: 0 additions & 42 deletions
This file was deleted.

vitals/schemas/phenoage.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
from pydantic import BaseModel
2+
3+
4+
class Markers(BaseModel):
5+
"""Processed PhenoAge biomarkers with standardized units."""
6+
7+
albumin: float
8+
creatinine: float
9+
glucose: float
10+
crp: float
11+
lymphocyte_percent: float
12+
mean_cell_volume: float
13+
red_cell_distribution_width: float
14+
alkaline_phosphatase: float
15+
white_blood_cell_count: float
16+
age: float
17+
18+
19+
class Units(BaseModel):
20+
"""
21+
The expected unit to be used for phenoage computation
22+
"""
23+
24+
albumin: str = "g/L"
25+
creatinine: str = "umol/L"
26+
glucose: str = "mmol/L"
27+
crp: str = "mg/dL"
28+
lymphocyte_percent: str = "%"
29+
mean_cell_volume: str = "fL"
30+
red_cell_distribution_width: str = "%"
31+
alkaline_phosphatase: str = "U/L"
32+
white_blood_cell_count: str = "1000 cells/uL"
33+
age: str = "years"
34+
35+
36+
class LinearModel(BaseModel):
37+
"""
38+
Coefficients used to calculate the PhenoAge from Levine et al 2018
39+
"""
40+
41+
intercept: float = -19.9067
42+
albumin: float = -0.0336
43+
creatinine: float = 0.0095
44+
glucose: float = 0.1953
45+
log_crp: float = 0.0954
46+
lymphocyte_percent: float = -0.0120
47+
mean_cell_volume: float = 0.0268
48+
red_cell_distribution_width: float = 0.3306
49+
alkaline_phosphatase: float = 0.00188
50+
white_blood_cell_count: float = 0.0554
51+
age: float = 0.0804
52+
53+
54+
class Gompertz(BaseModel):
55+
"""
56+
Parameters of the Gompertz distribution for PhenoAge computation
57+
"""
58+
59+
lambda_: float = 0.0192
60+
coef1: float = 141.50225
61+
coef2: float = -0.00553
62+
coef3: float = 0.090165

0 commit comments

Comments
 (0)