diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 5bf8ce5..2c3a718 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -17,14 +17,14 @@ jobs: # github.event.pull_request.user.login == 'external-contributor' || # github.event.pull_request.user.login == 'new-developer' || # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' - + runs-on: ubuntu-latest permissions: contents: read pull-requests: read issues: read id-token: write - + steps: - name: Checkout repository uses: actions/checkout@v4 @@ -39,7 +39,7 @@ jobs: # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4) # model: "claude-opus-4-20250514" - + # Direct prompt for automated review (no @claude mention needed) direct_prompt: | Please review this pull request and provide feedback on: @@ -48,12 +48,12 @@ jobs: - Performance considerations - Security concerns - Test coverage - + Be constructive and helpful in your feedback. # Optional: Use sticky comments to make Claude reuse the same comment on subsequent pushes to the same PR # use_sticky_comment: true - + # Optional: Customize review based on file types # direct_prompt: | # Review this PR focusing on: @@ -61,18 +61,17 @@ jobs: # - For API endpoints: Security, input validation, and error handling # - For React components: Performance, accessibility, and best practices # - For tests: Coverage, edge cases, and test quality - + # Optional: Different prompts for different authors # direct_prompt: | - # ${{ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' && + # ${{ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' && # 'Welcome! Please review this PR from a first-time contributor. Be encouraging and provide detailed explanations for any suggestions.' || # 'Please provide a thorough code review focusing on our coding standards and best practices.' }} - + # Optional: Add specific tools for running tests or linting # allowed_tools: "Bash(npm run test),Bash(npm run lint),Bash(npm run typecheck)" - + # Optional: Skip review for certain conditions # if: | # !contains(github.event.pull_request.title, '[skip-review]') && # !contains(github.event.pull_request.title, '[WIP]') - diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 64a3e5b..fab4b58 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -39,26 +39,25 @@ jobs: # This is an optional setting that allows Claude to read CI results on PRs additional_permissions: | actions: read - + # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4) # model: "claude-opus-4-20250514" - + # Optional: Customize the trigger phrase (default: @claude) # trigger_phrase: "/claude" - + # Optional: Trigger when specific user is assigned to an issue # assignee_trigger: "claude-bot" - + # Optional: Allow Claude to run specific commands # allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)" - + # Optional: Add custom instructions for Claude to customize its behavior for your project # custom_instructions: | # Follow our coding standards # Ensure all new code has tests # Use TypeScript for new files - + # Optional: Custom environment variables for Claude # claude_env: | # NODE_ENV: test - diff --git a/.gitignore b/.gitignore index 390141f..dd30b9b 100755 --- a/.gitignore +++ b/.gitignore @@ -211,4 +211,3 @@ __marimo__/ CLAUDE.md AGENTS.md .aider* -specs diff --git a/specs/coding_style.md b/specs/coding_style.md new file mode 100644 index 0000000..70dec02 --- /dev/null +++ b/specs/coding_style.md @@ -0,0 +1,112 @@ +# Python Coding Style Specification + +## Core Principles + +### 1. Favor Simplicity Over Complexity +- **Always choose the simple, straightforward solution** over complex or "sophisticated" alternatives +- **Avoid over-engineering** - resist the urge to build elaborate abstractions unless clearly needed +- **No premature optimization** - especially avoid blind optimization without measurement +- **Use simple building blocks** that can be composed elegantly rather than complex features +- **Principle**: If there are two ways to solve a problem, choose the one that is easier to understand + +### 2. Clarity is Key +- **Readable code beats clever code** - optimize for the reader, not the writer +- **Use clear, descriptive names** for variables, functions, and classes +- **Format code for maximal scanning ease** - use whitespace and structure intentionally +- **Document intent and organization** with comments and docstrings where helpful +- **Reduce cognitive load** - code should express intent clearly at a glance +- **Principle**: The easier your code is to understand immediately, the better it is + +### 3. Write Pythonic Code +- **Follow Python community standards and idioms** for naming, formatting, and programming paradigms +- **Cooperate with the language** rather than fighting it +- **Leverage Python features** like generators, itertools, collections, and functional programming +- **Write code that looks like Python wrote it** - use established patterns and conventions +- **Examples of Pythonic patterns**: + - List comprehensions over explicit loops when appropriate + - Context managers (`with` statements) for resource management + - Generator expressions for memory efficiency + - `enumerate()` instead of manual indexing + - `zip()` for parallel iteration + +### 4. Don't Repeat Yourself (DRY) +- **Avoid code duplication** to make code more maintainable and extendable +- **Use functions and modules** to encapsulate common logic in single authoritative locations +- **Consider inheritance** to avoid duplicate code between related classes +- **Leverage language features** like default arguments, variable argument lists (`*args`, `**kwargs`), and parameter unpacking +- **Eliminate duplication through abstraction** - but don't abstract too early + +### 5. Focus on Readability First +- **PEP8 is a guide, not a law** - readability trumps mechanical adherence to style rules +- **Make code as easy to understand as possible** - this is the ultimate goal +- **Deliberately violate guidelines** if it makes specific code more readable +- **Consider the human reader** first when making formatting and style decisions +- **Principle**: Rules serve readability, not the other way around + +### 6. Embrace Conventions +- **Follow established conventions** to eliminate trivial decision-making +- **Use PEP8 as a baseline** but prioritize readability when there's conflict +- **Establish consistent patterns** in your codebase for common tasks: + - Variable naming patterns + - Exception handling approaches + - Logging configuration + - Import organization +- **Consistency enables focus** - familiar patterns let readers focus on logic rather than parsing + +## Specific Implementation Guidelines + +### Naming Conventions +- **Variables and functions**: `snake_case` +- **Classes**: `PascalCase` +- **Constants**: `UPPER_SNAKE_CASE` +- **Private attributes**: `_single_leading_underscore` +- **Choose descriptive names** that clearly indicate purpose and content + +### Code Structure +- **Organize imports** in this order: standard library, third-party, local imports +- **Use blank lines** to separate logical sections +- **Keep functions focused** on a single responsibility +- **Prefer composition over inheritance** when appropriate +- **Write functions that do one thing well** + +### Documentation +- **Write docstrings** for modules, classes, and functions that aren't immediately obvious +- **Use comments** to explain why, not what +- **Keep comments up to date** with code changes +- **Focus on intent** rather than implementation details + +### Error Handling +- **Use specific exception types** rather than generic `Exception` +- **Follow the "easier to ask for forgiveness than permission" (EAFP) principle** +- **Handle errors at the appropriate level** - don't catch exceptions you can't handle meaningfully + +### Performance and Optimization +- **Write clear code first** - optimize only when necessary and after measurement +- **Use appropriate data structures** for the task +- **Leverage built-in functions** and library functions when they're clearer +- **Profile before optimizing** - don't guess where bottlenecks are + +## Code Review Checklist + +When generating or reviewing Python code, ensure: +- [ ] The simplest solution that works is chosen +- [ ] Names clearly communicate purpose +- [ ] Code is easily scannable and readable +- [ ] Pythonic patterns are used appropriately +- [ ] No unnecessary duplication exists +- [ ] Conventions are followed consistently +- [ ] Comments explain intent where needed +- [ ] Error handling is appropriate +- [ ] The code would be easy for another developer to understand and maintain + +## Decision Framework + +When faced with coding choices, ask: +1. **Is this the simplest solution that works?** +2. **Will this be clear to someone reading it in 6 months?** +3. **Am I using Python idioms appropriately?** +4. **Am I duplicating logic that could be abstracted?** +5. **Does this follow our established conventions?** +6. **Is this optimized for readability?** + +The answer to all these questions should be "yes" for beautiful Python code. diff --git a/specs/score2.md b/specs/score2.md new file mode 100644 index 0000000..20ae2c3 --- /dev/null +++ b/specs/score2.md @@ -0,0 +1,135 @@ +# CVD Risk Prediction Formula + +## Overview + +This formula calculates the 10-year risk of cardiovascular disease (CVD) using a sex-specific Cox proportional hazards model. The model incorporates multiple risk factors with specific transformations and interaction terms to provide personalized risk estimates. + +**Target Population**: European patients aged 40-69 years without prior CVD or diabetes. + +## Model Coefficients and Baseline Survival + +The model coefficients and baseline survival to calculate 10-year risk of CVD are as follows: + +| Risk Factor | Transformation | Male | Female | +|-------------|----------------|------|--------| +| Age, years | cage = (age - 60)/5 | 0.3742 | 0.4648 | +| Smoking | current = 1, other = 0 | 0.6012 | 0.7744 | +| SBP, mm Hg | csbp = (sbp - 120)/20 | 0.2777 | 0.3131 | +| Total cholesterol, mmol/L | ctchol = tchol - 6 | 0.1458 | 0.1002 | +| HDL cholesterol, mmol/L | chdl = (hdl - 1.3)/0.5 | -0.2698 | -0.2606 | +| Smoking*age interaction | smoking*cage | -0.0755 | -0.1088 | +| SBP*age interaction | csbp*cage | -0.0255 | -0.0277 | +| Total cholesterol*age interaction | ctchol*cage | -0.0281 | -0.0226 | +| HDL cholesterol*age interaction | chdl*cage | 0.0426 | 0.0613 | +| **Baseline survival** | | **0.9605** | **0.9776** | + +## Risk Calculation Formula + +The uncalibrated 10-year risk of CVD is calculated by the following: + +**10-year risk = 1 - (baseline survival)^exp(x)** + +where **x = Σ[β*(transformed variables)]** + +## Regional Calibration + +The region and sex-specific scales to calculate calibrated 10-year risk are as follows: + +| Risk Region | Male Scale 1 | Male Scale 2 | Female Scale 1 | Female Scale 2 | +|-------------|--------------|--------------|----------------|----------------| +| Low | -0.5699 | 0.7476 | -0.7380 | 0.7019 | +| Moderate | -0.1565 | 0.8009 | -0.3143 | 0.7701 | +| High | 0.3207 | 0.9360 | 0.5710 | 0.9369 | +| Very high | 0.5836 | 0.8294 | 0.9412 | 0.8329 | + +### Calibrated Risk Calculation Formula + +The calibrated 10-year risk of CVD is calculated by the following: + +**Calibrated 10-year risk, % = [1 - exp(-exp(scale1 + scale2*ln(-ln(1 - 10-year risk))))] * 100** + +### Regional Risk Classification + +- **Belgium**: Classified as a **Low Risk** region +- For initial development, use the Low Risk calibration scales: + - Males: Scale 1 = -0.5699, Scale 2 = 0.7476 + - Females: Scale 1 = -0.7380, Scale 2 = 0.7019 + +## Model Components Explained + +### Risk Factor Transformations + +1. **Age (cage)**: Centered at 60 years and scaled by 5-year intervals + - `cage = (age - 60)/5` + +2. **Smoking**: Binary indicator + - `current = 1, other = 0` + +3. **Systolic Blood Pressure (csbp)**: Centered at 120 mmHg and scaled by 20 mmHg intervals + - `csbp = (sbp - 120)/20` + +4. **Total Cholesterol (ctchol)**: Centered at 6 mmol/L + - `ctchol = tchol - 6` + +5. **HDL Cholesterol (chdl)**: Centered at 1.3 mmol/L and scaled by 0.5 mmol/L intervals + - `chdl = (hdl - 1.3)/0.5` + +### Interaction Terms + +The model includes four age interaction terms that capture how the effect of risk factors changes with age: + +1. **Smoking × Age**: `smoking × cage` +2. **SBP × Age**: `csbp × cage` +3. **Total Cholesterol × Age**: `ctchol × cage` +4. **HDL Cholesterol × Age**: `chdl × cage` + +### Sex-Specific Differences + +- **Females** generally have higher baseline survival (0.9776 vs 0.9605) +- **Smoking** has a stronger effect in females (0.7744 vs 0.6012) +- **Age** has a stronger effect in females (0.4648 vs 0.3742) +- **SBP** has a slightly stronger effect in females (0.3131 vs 0.2777) +- **HDL cholesterol** protective effect is similar between sexes + +## Implementation Workflow + +1. **Calculate uncalibrated risk** using the base formula with model coefficients +2. **Apply regional calibration** using the appropriate scales for the patient's location and sex +3. **Output calibrated percentage** as the final 10-year CVD risk estimate + +## Implementation Notes + +1. **Input Units**: + - Age: years + - SBP: mmHg + - Total cholesterol: mmol/L + - HDL cholesterol: mmol/L + - Smoking: binary (1 = current smoker, 0 = other) + +2. **Output**: 10-year CVD risk as a percentage (0-100%) + +3. **Model Type**: Cox proportional hazards model with sex-specific coefficients and regional calibration + +4. **Default Region**: Belgium (Low Risk region) for initial application development + +## Risk Stratification + +### Age-Specific Risk Categories + +#### Patients <50 years old + +- Low to moderate risk: <2.5% +- High risk: 2.5% to <7.5% +- Very high risk: ≥7.5% + +#### Patients 50-69 years old + +- Low to moderate risk: <5% +- High risk: 5% to <10% +- Very high risk: ≥10% + +### Treatment Recommendations + +- **Low to moderate risk**: Risk factor treatment plan generally not recommended. +- **High risk**: Risk factor treatment plan should be considered (i.e., blood pressure and LDL-C control). +- **Very high risk**: Risk factor treatment plan should be recommended (i.e., blood pressure and LDL-C control). diff --git a/specs/score2_diabetes.md b/specs/score2_diabetes.md new file mode 100644 index 0000000..b5ed851 --- /dev/null +++ b/specs/score2_diabetes.md @@ -0,0 +1,184 @@ +# SCORE2-Diabetes Algorithm Implementation Specification + +## Overview + +This specification is based on the FORMULA section from the SCORE2-Diabetes calculator. The model calculates 10-year risk of CVD using specific coefficients, baseline survival values, and regional calibration scales. + +## Model Coefficients Table + +The following coefficients are used to calculate 10-year risk of CVD: + +| Risk Factor | Transformation | Male | Female | +|-------------|----------------|------|--------| +| Age, years | cage = (age - 60)/5 | 0.5368 | 0.6624 | +| Smoking | current = 1, other = 0 | 0.4774 | 0.6139 | +| SBP, mm Hg | csbp = (sbp - 120)/20 | 0.1322 | 0.1421 | +| Diabetes | yes = 1, no = 0 | 0.6457 | 0.8096 | +| Total cholesterol, mmol/L | ctchol = tchol - 6 | 0.1102 | 0.1127 | +| HDL cholesterol, mmol/L | chdl = (hdl - 1.3)/0.5 | -0.1087 | -0.1568 | +| Smoking*age interaction | smoking*cage | -0.0672 | -0.1122 | +| SBP*age interaction | csbp*cage | -0.0268 | -0.0167 | +| Diabetes*age interaction | diabetes*cage | -0.0983 | -0.1272 | +| Total cholesterol*age interaction | ctchol*cage | -0.0181 | -0.0200 | +| HDL cholesterol*age interaction | chdl*cage | 0.0095 | 0.0186 | +| Age at diabetes diagnosis, years | cagediab = diabetes*(agediab - 50)/5 | -0.0998 | -0.1180 | +| HbA1c, mmol/mol | ca1c = (a1c - 31)/9.34 | 0.0955 | 0.1173 | +| eGFR | cegfr = (ln(egfr) - 4.5)/0.15 | -0.0591 | -0.0640 | +| eGFR² | cegfr² | 0.0058 | 0.0062 | +| HbA1c*age interaction | ca1c*cage | -0.0134 | -0.0196 | +| eGFR*age interaction | cegfr*cage | 0.0115 | 0.0169 | +| **Baseline survival** | | **0.9605** | **0.9776** | + +## Variable Transformations + +### Age Transformation +- `cage = (age - 60)/5` + +### Systolic Blood Pressure Transformation +- `csbp = (sbp - 120)/20` + +### Diabetes Status +- `diabetes = 1` if patient has diabetes, `0` otherwise + +### Total Cholesterol Transformation +- `ctchol = tchol - 6` + +### HDL Cholesterol Transformation +- `chdl = (hdl - 1.3)/0.5` + +### Age at Diabetes Diagnosis Transformation +- `cagediab = diabetes*(agediab - 50)/5` + +### HbA1c Transformation +- `ca1c = (a1c - 31)/9.34` + +### eGFR Transformations +- `cegfr = (ln(egfr) - 4.5)/0.15` +- `cegfr² = cegfr²` (squared term) + +### Smoking Transformation +- `smoking = 1` if current smoker, `0` otherwise + +## Initial Risk Calculation Formula + +The initial 10-year risk of CVD is calculated using: + +``` +10-year risk = [1 - (baseline survival)^exp(x)] +``` + +Where: +- `x = Σ[β*(transformed variables)]` +- The sum includes all coefficients multiplied by their corresponding transformed variables + +## Regional Calibration Scales + +The region and sex-specific scales to calculate calibrated 10-year risk are as follows: + +| Risk Region | Male Scale 1 | Male Scale 2 | Female Scale 1 | Female Scale 2 | +|-------------|--------------|--------------|----------------|----------------| +| Low | -0.5699 | 0.7476 | -0.7380 | 0.7019 | +| Moderate | -0.1565 | 0.8009 | -0.3143 | 0.7701 | +| High | 0.3207 | 0.9360 | 0.5710 | 0.9369 | +| Very high | 0.5836 | 0.8294 | 0.9412 | 0.8329 | + +## Calibrated Risk Calculation Formula + +The calibrated 10-year risk of CVD is calculated by the following: + +``` +Calibrated 10-year risk, % = [1 - exp(-exp(scale1 + scale2*ln(-ln(1 - 10-year risk))))] * 100 +``` + +Where: +- `scale1` and `scale2` are the region and sex-specific calibration values from the table above +- `10-year risk` is the initial risk calculated using the baseline survival formula + +## Linear Predictor Calculation + +The linear predictor (x) is calculated as the sum of: + +1. **Main Effects:** + - Age coefficient × cage + - Smoking coefficient × smoking + - SBP coefficient × csbp + - Diabetes coefficient × diabetes + - Total cholesterol coefficient × ctchol + - HDL cholesterol coefficient × chdl + - Age at diabetes diagnosis coefficient × cagediab + - HbA1c coefficient × ca1c + - eGFR coefficient × cegfr + - eGFR² coefficient × cegfr² + +2. **Age Interaction Terms:** + - Smoking*age coefficient × smoking × cage + - SBP*age coefficient × csbp × cage + - Diabetes*age coefficient × diabetes × cage + - Total cholesterol*age coefficient × ctchol × cage + - HDL cholesterol*age coefficient × chdl × cage + - HbA1c*age coefficient × ca1c × cage + - eGFR*age coefficient × cegfr × cage + +## Risk Stratification + +### Patients <50 years old +- **Low to moderate risk:** <2.5% +- **High risk:** 2.5% to <7.5% +- **Very high risk:** ≥7.5% + +### Patients 50-69 years old +- **Low to moderate risk:** <5% +- **High risk:** 5% to <10% +- **Very high risk:** ≥10% + +## Treatment Recommendations + +### Low to moderate risk +Risk factor treatment plan generally not recommended. + +### High risk +Risk factor treatment plan should be considered (i.e., blood pressure and LDL-C control). + +### Very high risk +Risk factor treatment plan should be recommended (i.e., blood pressure and LDL-C control, along with addition of SGLT2-i or GLP1-RA if not already taking). + +## Implementation Requirements + +### Input Parameters Required +1. Age (years) +2. Sex (Male/Female) +3. Smoking status (current smoker yes/no) +4. Systolic blood pressure (mm Hg) +5. Diabetes status (yes/no) +6. Total cholesterol (mmol/L) +7. HDL cholesterol (mmol/L) +8. Age at diabetes diagnosis (years) +9. HbA1c (mmol/mol) +10. eGFR (estimated glomerular filtration rate) +11. Geographic risk region (Low/Moderate/High/Very high) + +### Calculation Steps +1. Transform all input variables using the specified transformations +2. Calculate the linear predictor (x) using sex-specific coefficients +3. Calculate initial 10-year risk using sex-specific baseline survival +4. Apply regional calibration using the calibrated risk formula and region/sex-specific scales +5. Determine risk stratification category based on age and calculated risk percentage +6. Provide appropriate treatment recommendations based on risk category + +### Output Requirements +1. **Calibrated 10-year cardiovascular disease risk** as a percentage +2. **Risk stratification category** (Low to moderate/High/Very high) +3. **Treatment recommendation** based on risk category + +## Baseline Survival Values + +- **Male baseline survival:** 0.9605 +- **Female baseline survival:** 0.9776 + +## Notes +- All coefficients are sex-specific (different values for males and females) +- The model includes both main effects and age interaction terms +- eGFR requires both linear and quadratic terms +- Regional calibration is applied using a two-step process: initial risk calculation followed by regional calibration +- Risk stratification thresholds differ by age group (<50 vs 50-69 years) +- Treatment recommendations are tied directly to risk stratification categories diff --git a/tests/outputs/test__output__patient_01.json b/tests/inputs/phenoage/test__input__patient_01.json similarity index 100% rename from tests/outputs/test__output__patient_01.json rename to tests/inputs/phenoage/test__input__patient_01.json diff --git a/tests/outputs/test__output__patient_02.json b/tests/inputs/phenoage/test__input__patient_02.json similarity index 100% rename from tests/outputs/test__output__patient_02.json rename to tests/inputs/phenoage/test__input__patient_02.json diff --git a/tests/outputs/test__output__patient_03.json b/tests/inputs/phenoage/test__input__patient_03.json similarity index 100% rename from tests/outputs/test__output__patient_03.json rename to tests/inputs/phenoage/test__input__patient_03.json diff --git a/tests/outputs/test__output__patient_04.json b/tests/inputs/phenoage/test__input__patient_04.json similarity index 100% rename from tests/outputs/test__output__patient_04.json rename to tests/inputs/phenoage/test__input__patient_04.json diff --git a/tests/outputs/test__output__patient_05.json b/tests/inputs/phenoage/test__input__patient_05.json similarity index 100% rename from tests/outputs/test__output__patient_05.json rename to tests/inputs/phenoage/test__input__patient_05.json diff --git a/tests/outputs/test__output__patient_06.json b/tests/inputs/phenoage/test__input__patient_06.json similarity index 100% rename from tests/outputs/test__output__patient_06.json rename to tests/inputs/phenoage/test__input__patient_06.json diff --git a/tests/outputs/test__output__patient_07.json b/tests/inputs/phenoage/test__input__patient_07.json similarity index 100% rename from tests/outputs/test__output__patient_07.json rename to tests/inputs/phenoage/test__input__patient_07.json diff --git a/tests/outputs/test__output__patient_08.json b/tests/inputs/phenoage/test__input__patient_08.json similarity index 100% rename from tests/outputs/test__output__patient_08.json rename to tests/inputs/phenoage/test__input__patient_08.json diff --git a/tests/outputs/test__output__patient_09.json b/tests/inputs/phenoage/test__input__patient_09.json similarity index 100% rename from tests/outputs/test__output__patient_09.json rename to tests/inputs/phenoage/test__input__patient_09.json diff --git a/tests/outputs/test__output__patient_10.json b/tests/inputs/phenoage/test__input__patient_10.json similarity index 100% rename from tests/outputs/test__output__patient_10.json rename to tests/inputs/phenoage/test__input__patient_10.json diff --git a/tests/outputs/test__output__patient_11.json b/tests/inputs/phenoage/test__input__patient_11.json similarity index 100% rename from tests/outputs/test__output__patient_11.json rename to tests/inputs/phenoage/test__input__patient_11.json diff --git a/tests/outputs/test__output__patient_12.json b/tests/inputs/phenoage/test__input__patient_12.json similarity index 100% rename from tests/outputs/test__output__patient_12.json rename to tests/inputs/phenoage/test__input__patient_12.json diff --git a/tests/outputs/test__output__patient_13.json b/tests/inputs/phenoage/test__input__patient_13.json similarity index 100% rename from tests/outputs/test__output__patient_13.json rename to tests/inputs/phenoage/test__input__patient_13.json diff --git a/tests/outputs/test__output__patient_14.json b/tests/inputs/phenoage/test__input__patient_14.json similarity index 100% rename from tests/outputs/test__output__patient_14.json rename to tests/inputs/phenoage/test__input__patient_14.json diff --git a/tests/outputs/test__output__patient_15.json b/tests/inputs/phenoage/test__input__patient_15.json similarity index 100% rename from tests/outputs/test__output__patient_15.json rename to tests/inputs/phenoage/test__input__patient_15.json diff --git a/tests/outputs/test__output__patient_16.json b/tests/inputs/phenoage/test__input__patient_16.json similarity index 100% rename from tests/outputs/test__output__patient_16.json rename to tests/inputs/phenoage/test__input__patient_16.json diff --git a/tests/outputs/test__output__patient_17.json b/tests/inputs/phenoage/test__input__patient_17.json similarity index 100% rename from tests/outputs/test__output__patient_17.json rename to tests/inputs/phenoage/test__input__patient_17.json diff --git a/tests/outputs/test__output__patient_18.json b/tests/inputs/phenoage/test__input__patient_18.json similarity index 100% rename from tests/outputs/test__output__patient_18.json rename to tests/inputs/phenoage/test__input__patient_18.json diff --git a/tests/outputs/test__output__patient_19.json b/tests/inputs/phenoage/test__input__patient_19.json similarity index 100% rename from tests/outputs/test__output__patient_19.json rename to tests/inputs/phenoage/test__input__patient_19.json diff --git a/tests/outputs/test__output__patient_20.json b/tests/inputs/phenoage/test__input__patient_20.json similarity index 100% rename from tests/outputs/test__output__patient_20.json rename to tests/inputs/phenoage/test__input__patient_20.json diff --git a/tests/outputs/test__output__patient_21.json b/tests/inputs/phenoage/test__input__patient_21.json similarity index 100% rename from tests/outputs/test__output__patient_21.json rename to tests/inputs/phenoage/test__input__patient_21.json diff --git a/tests/outputs/test__output__patient_22.json b/tests/inputs/phenoage/test__input__patient_22.json similarity index 100% rename from tests/outputs/test__output__patient_22.json rename to tests/inputs/phenoage/test__input__patient_22.json diff --git a/tests/outputs/test__output__patient_23.json b/tests/inputs/phenoage/test__input__patient_23.json similarity index 100% rename from tests/outputs/test__output__patient_23.json rename to tests/inputs/phenoage/test__input__patient_23.json diff --git a/tests/outputs/test__output__patient_24.json b/tests/inputs/phenoage/test__input__patient_24.json similarity index 100% rename from tests/outputs/test__output__patient_24.json rename to tests/inputs/phenoage/test__input__patient_24.json diff --git a/tests/inputs/score2/test__input__patient_25.json b/tests/inputs/score2/test__input__patient_25.json new file mode 100755 index 0000000..c14c61d --- /dev/null +++ b/tests/inputs/score2/test__input__patient_25.json @@ -0,0 +1,35 @@ +{ + "metadata": { + "patient_id": "P024-2024-024", + "sex": "female", + "timestamp": "2024-07-04T13:45:00Z", + "test_date": "2024-07-04", + "laboratory": "NHANES Reference Labs" + }, + "raw_biomarkers": { + "age_years": { + "value": 50, + "unit": "years" + }, + "smoking_yes_no": { + "value": true, + "unit": "yes/no" + }, + "systolic_blood_pressure_mmhg": { + "value": 140, + "unit": "mmHg" + }, + "total_cholesterol_mmol_l": { + "value": 6.3, + "unit": "mmol/L" + }, + "hdl_cholesterol_mmol_l": { + "value": 1.4, + "unit": "mmol/L" + }, + "is_male_yes_no": { + "value": false, + "unit": "yes/no" + } + } +} diff --git a/tests/inputs/score2/test__input__patient_26.json b/tests/inputs/score2/test__input__patient_26.json new file mode 100755 index 0000000..47ca4c5 --- /dev/null +++ b/tests/inputs/score2/test__input__patient_26.json @@ -0,0 +1,35 @@ +{ + "metadata": { + "patient_id": "P024-2024-024", + "sex": "male", + "timestamp": "2024-07-04T13:45:00Z", + "test_date": "2024-07-04", + "laboratory": "NHANES Reference Labs" + }, + "raw_biomarkers": { + "age_years": { + "value": 50, + "unit": "years" + }, + "smoking_yes_no": { + "value": true, + "unit": "yes/no" + }, + "systolic_blood_pressure_mmhg": { + "value": 140, + "unit": "mmHg" + }, + "total_cholesterol_mmol_l": { + "value": 6.3, + "unit": "mmol/L" + }, + "hdl_cholesterol_mmol_l": { + "value": 1.4, + "unit": "mmol/L" + }, + "is_male_yes_no": { + "value": true, + "unit": "yes/no" + } + } +} diff --git a/tests/inputs/score2/test__input__patient_27.json b/tests/inputs/score2/test__input__patient_27.json new file mode 100644 index 0000000..23b76ea --- /dev/null +++ b/tests/inputs/score2/test__input__patient_27.json @@ -0,0 +1,35 @@ +{ + "metadata": { + "patient_id": "P027-2024-027", + "sex": "female", + "timestamp": "2024-07-04T14:00:00Z", + "test_date": "2024-07-04", + "laboratory": "NHANES Reference Labs" + }, + "raw_biomarkers": { + "age_years": { + "value": 55, + "unit": "years" + }, + "smoking_yes_no": { + "value": false, + "unit": "yes/no" + }, + "systolic_blood_pressure_mmhg": { + "value": 125, + "unit": "mmHg" + }, + "total_cholesterol_mmol_l": { + "value": 5.2, + "unit": "mmol/L" + }, + "hdl_cholesterol_mmol_l": { + "value": 1.6, + "unit": "mmol/L" + }, + "is_male_yes_no": { + "value": false, + "unit": "yes/no" + } + } +} diff --git a/tests/inputs/score2/test__input__patient_28.json b/tests/inputs/score2/test__input__patient_28.json new file mode 100644 index 0000000..ef74275 --- /dev/null +++ b/tests/inputs/score2/test__input__patient_28.json @@ -0,0 +1,35 @@ +{ + "metadata": { + "patient_id": "P028-2024-028", + "sex": "male", + "timestamp": "2024-07-04T14:15:00Z", + "test_date": "2024-07-04", + "laboratory": "NHANES Reference Labs" + }, + "raw_biomarkers": { + "age_years": { + "value": 45, + "unit": "years" + }, + "smoking_yes_no": { + "value": false, + "unit": "yes/no" + }, + "systolic_blood_pressure_mmhg": { + "value": 130, + "unit": "mmHg" + }, + "total_cholesterol_mmol_l": { + "value": 5.8, + "unit": "mmol/L" + }, + "hdl_cholesterol_mmol_l": { + "value": 1.3, + "unit": "mmol/L" + }, + "is_male_yes_no": { + "value": true, + "unit": "yes/no" + } + } +} diff --git a/tests/inputs/score2/test__input__patient_29.json b/tests/inputs/score2/test__input__patient_29.json new file mode 100644 index 0000000..3b072bc --- /dev/null +++ b/tests/inputs/score2/test__input__patient_29.json @@ -0,0 +1,35 @@ +{ + "metadata": { + "patient_id": "P029-2024-029", + "sex": "male", + "timestamp": "2024-07-04T14:30:00Z", + "test_date": "2024-07-04", + "laboratory": "NHANES Reference Labs" + }, + "raw_biomarkers": { + "age_years": { + "value": 40, + "unit": "years" + }, + "smoking_yes_no": { + "value": true, + "unit": "yes/no" + }, + "systolic_blood_pressure_mmhg": { + "value": 135, + "unit": "mmHg" + }, + "total_cholesterol_mmol_l": { + "value": 6.0, + "unit": "mmol/L" + }, + "hdl_cholesterol_mmol_l": { + "value": 1.2, + "unit": "mmol/L" + }, + "is_male_yes_no": { + "value": true, + "unit": "yes/no" + } + } +} diff --git a/tests/inputs/score2/test__input__patient_30.json b/tests/inputs/score2/test__input__patient_30.json new file mode 100644 index 0000000..d0d551a --- /dev/null +++ b/tests/inputs/score2/test__input__patient_30.json @@ -0,0 +1,35 @@ +{ + "metadata": { + "patient_id": "P030-2024-030", + "sex": "female", + "timestamp": "2024-07-04T14:45:00Z", + "test_date": "2024-07-04", + "laboratory": "NHANES Reference Labs" + }, + "raw_biomarkers": { + "age_years": { + "value": 60, + "unit": "years" + }, + "smoking_yes_no": { + "value": false, + "unit": "yes/no" + }, + "systolic_blood_pressure_mmhg": { + "value": 145, + "unit": "mmHg" + }, + "total_cholesterol_mmol_l": { + "value": 6.5, + "unit": "mmol/L" + }, + "hdl_cholesterol_mmol_l": { + "value": 1.5, + "unit": "mmol/L" + }, + "is_male_yes_no": { + "value": false, + "unit": "yes/no" + } + } +} diff --git a/tests/inputs/score2/test__input__patient_31.json b/tests/inputs/score2/test__input__patient_31.json new file mode 100644 index 0000000..cce3e57 --- /dev/null +++ b/tests/inputs/score2/test__input__patient_31.json @@ -0,0 +1,35 @@ +{ + "metadata": { + "patient_id": "P031-2024-031", + "sex": "male", + "timestamp": "2024-07-04T15:00:00Z", + "test_date": "2024-07-04", + "laboratory": "NHANES Reference Labs" + }, + "raw_biomarkers": { + "age_years": { + "value": 65, + "unit": "years" + }, + "smoking_yes_no": { + "value": true, + "unit": "yes/no" + }, + "systolic_blood_pressure_mmhg": { + "value": 150, + "unit": "mmHg" + }, + "total_cholesterol_mmol_l": { + "value": 7.0, + "unit": "mmol/L" + }, + "hdl_cholesterol_mmol_l": { + "value": 1.1, + "unit": "mmol/L" + }, + "is_male_yes_no": { + "value": true, + "unit": "yes/no" + } + } +} diff --git a/tests/inputs/score2/test__input__patient_32.json b/tests/inputs/score2/test__input__patient_32.json new file mode 100644 index 0000000..14ea302 --- /dev/null +++ b/tests/inputs/score2/test__input__patient_32.json @@ -0,0 +1,35 @@ +{ + "metadata": { + "patient_id": "P032-2024-032", + "sex": "female", + "timestamp": "2024-07-04T15:15:00Z", + "test_date": "2024-07-04", + "laboratory": "NHANES Reference Labs" + }, + "raw_biomarkers": { + "age_years": { + "value": 69, + "unit": "years" + }, + "smoking_yes_no": { + "value": false, + "unit": "yes/no" + }, + "systolic_blood_pressure_mmhg": { + "value": 155, + "unit": "mmHg" + }, + "total_cholesterol_mmol_l": { + "value": 7.2, + "unit": "mmol/L" + }, + "hdl_cholesterol_mmol_l": { + "value": 1.3, + "unit": "mmol/L" + }, + "is_male_yes_no": { + "value": false, + "unit": "yes/no" + } + } +} diff --git a/tests/inputs/score2/test__input__patient_33.json b/tests/inputs/score2/test__input__patient_33.json new file mode 100644 index 0000000..f71c377 --- /dev/null +++ b/tests/inputs/score2/test__input__patient_33.json @@ -0,0 +1,35 @@ +{ + "metadata": { + "patient_id": "P033-2024-033", + "sex": "male", + "timestamp": "2024-07-04T15:30:00Z", + "test_date": "2024-07-04", + "laboratory": "NHANES Reference Labs" + }, + "raw_biomarkers": { + "age_years": { + "value": 49, + "unit": "years" + }, + "smoking_yes_no": { + "value": true, + "unit": "yes/no" + }, + "systolic_blood_pressure_mmhg": { + "value": 138, + "unit": "mmHg" + }, + "total_cholesterol_mmol_l": { + "value": 6.1, + "unit": "mmol/L" + }, + "hdl_cholesterol_mmol_l": { + "value": 1.4, + "unit": "mmol/L" + }, + "is_male_yes_no": { + "value": true, + "unit": "yes/no" + } + } +} diff --git a/tests/inputs/score2/test__input__patient_34.json b/tests/inputs/score2/test__input__patient_34.json new file mode 100644 index 0000000..41ebd11 --- /dev/null +++ b/tests/inputs/score2/test__input__patient_34.json @@ -0,0 +1,35 @@ +{ + "metadata": { + "patient_id": "P034-2024-034", + "sex": "female", + "timestamp": "2024-07-04T15:45:00Z", + "test_date": "2024-07-04", + "laboratory": "NHANES Reference Labs" + }, + "raw_biomarkers": { + "age_years": { + "value": 50, + "unit": "years" + }, + "smoking_yes_no": { + "value": false, + "unit": "yes/no" + }, + "systolic_blood_pressure_mmhg": { + "value": 120, + "unit": "mmHg" + }, + "total_cholesterol_mmol_l": { + "value": 4.8, + "unit": "mmol/L" + }, + "hdl_cholesterol_mmol_l": { + "value": 1.8, + "unit": "mmol/L" + }, + "is_male_yes_no": { + "value": false, + "unit": "yes/no" + } + } +} diff --git a/tests/inputs/score2/test__input__patient_35.json b/tests/inputs/score2/test__input__patient_35.json new file mode 100644 index 0000000..6f111b0 --- /dev/null +++ b/tests/inputs/score2/test__input__patient_35.json @@ -0,0 +1,35 @@ +{ + "metadata": { + "patient_id": "P035-2024-035", + "sex": "male", + "timestamp": "2024-07-04T16:00:00Z", + "test_date": "2024-07-04", + "laboratory": "NHANES Reference Labs" + }, + "raw_biomarkers": { + "age_years": { + "value": 55, + "unit": "years" + }, + "smoking_yes_no": { + "value": true, + "unit": "yes/no" + }, + "systolic_blood_pressure_mmhg": { + "value": 142, + "unit": "mmHg" + }, + "total_cholesterol_mmol_l": { + "value": 6.4, + "unit": "mmol/L" + }, + "hdl_cholesterol_mmol_l": { + "value": 1.2, + "unit": "mmol/L" + }, + "is_male_yes_no": { + "value": true, + "unit": "yes/no" + } + } +} diff --git a/tests/inputs/score2/test__input__patient_36.json b/tests/inputs/score2/test__input__patient_36.json new file mode 100644 index 0000000..4d2e48e --- /dev/null +++ b/tests/inputs/score2/test__input__patient_36.json @@ -0,0 +1,35 @@ +{ + "metadata": { + "patient_id": "P036-2024-036", + "sex": "female", + "timestamp": "2024-07-04T16:15:00Z", + "test_date": "2024-07-04", + "laboratory": "NHANES Reference Labs" + }, + "raw_biomarkers": { + "age_years": { + "value": 45, + "unit": "years" + }, + "smoking_yes_no": { + "value": true, + "unit": "yes/no" + }, + "systolic_blood_pressure_mmhg": { + "value": 132, + "unit": "mmHg" + }, + "total_cholesterol_mmol_l": { + "value": 5.5, + "unit": "mmol/L" + }, + "hdl_cholesterol_mmol_l": { + "value": 1.5, + "unit": "mmol/L" + }, + "is_male_yes_no": { + "value": false, + "unit": "yes/no" + } + } +} diff --git a/tests/inputs/test__input__patient_01.json b/tests/raw/phenoage/test__raw__patient_01.json similarity index 100% rename from tests/inputs/test__input__patient_01.json rename to tests/raw/phenoage/test__raw__patient_01.json diff --git a/tests/inputs/test__input__patient_02.json b/tests/raw/phenoage/test__raw__patient_02.json similarity index 100% rename from tests/inputs/test__input__patient_02.json rename to tests/raw/phenoage/test__raw__patient_02.json diff --git a/tests/inputs/test__input__patient_03.json b/tests/raw/phenoage/test__raw__patient_03.json similarity index 100% rename from tests/inputs/test__input__patient_03.json rename to tests/raw/phenoage/test__raw__patient_03.json diff --git a/tests/inputs/test__input__patient_04.json b/tests/raw/phenoage/test__raw__patient_04.json similarity index 100% rename from tests/inputs/test__input__patient_04.json rename to tests/raw/phenoage/test__raw__patient_04.json diff --git a/tests/inputs/test__input__patient_05.json b/tests/raw/phenoage/test__raw__patient_05.json similarity index 100% rename from tests/inputs/test__input__patient_05.json rename to tests/raw/phenoage/test__raw__patient_05.json diff --git a/tests/inputs/test__input__patient_06.json b/tests/raw/phenoage/test__raw__patient_06.json similarity index 100% rename from tests/inputs/test__input__patient_06.json rename to tests/raw/phenoage/test__raw__patient_06.json diff --git a/tests/inputs/test__input__patient_07.json b/tests/raw/phenoage/test__raw__patient_07.json similarity index 100% rename from tests/inputs/test__input__patient_07.json rename to tests/raw/phenoage/test__raw__patient_07.json diff --git a/tests/inputs/test__input__patient_08.json b/tests/raw/phenoage/test__raw__patient_08.json similarity index 100% rename from tests/inputs/test__input__patient_08.json rename to tests/raw/phenoage/test__raw__patient_08.json diff --git a/tests/inputs/test__input__patient_09.json b/tests/raw/phenoage/test__raw__patient_09.json similarity index 100% rename from tests/inputs/test__input__patient_09.json rename to tests/raw/phenoage/test__raw__patient_09.json diff --git a/tests/inputs/test__input__patient_10.json b/tests/raw/phenoage/test__raw__patient_10.json similarity index 100% rename from tests/inputs/test__input__patient_10.json rename to tests/raw/phenoage/test__raw__patient_10.json diff --git a/tests/inputs/test__input__patient_11.json b/tests/raw/phenoage/test__raw__patient_11.json similarity index 100% rename from tests/inputs/test__input__patient_11.json rename to tests/raw/phenoage/test__raw__patient_11.json diff --git a/tests/inputs/test__input__patient_12.json b/tests/raw/phenoage/test__raw__patient_12.json similarity index 100% rename from tests/inputs/test__input__patient_12.json rename to tests/raw/phenoage/test__raw__patient_12.json diff --git a/tests/inputs/test__input__patient_13.json b/tests/raw/phenoage/test__raw__patient_13.json similarity index 100% rename from tests/inputs/test__input__patient_13.json rename to tests/raw/phenoage/test__raw__patient_13.json diff --git a/tests/inputs/test__input__patient_14.json b/tests/raw/phenoage/test__raw__patient_14.json similarity index 100% rename from tests/inputs/test__input__patient_14.json rename to tests/raw/phenoage/test__raw__patient_14.json diff --git a/tests/inputs/test__input__patient_15.json b/tests/raw/phenoage/test__raw__patient_15.json similarity index 100% rename from tests/inputs/test__input__patient_15.json rename to tests/raw/phenoage/test__raw__patient_15.json diff --git a/tests/inputs/test__input__patient_16.json b/tests/raw/phenoage/test__raw__patient_16.json similarity index 100% rename from tests/inputs/test__input__patient_16.json rename to tests/raw/phenoage/test__raw__patient_16.json diff --git a/tests/inputs/test__input__patient_17.json b/tests/raw/phenoage/test__raw__patient_17.json similarity index 100% rename from tests/inputs/test__input__patient_17.json rename to tests/raw/phenoage/test__raw__patient_17.json diff --git a/tests/inputs/test__input__patient_18.json b/tests/raw/phenoage/test__raw__patient_18.json similarity index 100% rename from tests/inputs/test__input__patient_18.json rename to tests/raw/phenoage/test__raw__patient_18.json diff --git a/tests/inputs/test__input__patient_19.json b/tests/raw/phenoage/test__raw__patient_19.json similarity index 100% rename from tests/inputs/test__input__patient_19.json rename to tests/raw/phenoage/test__raw__patient_19.json diff --git a/tests/inputs/test__input__patient_20.json b/tests/raw/phenoage/test__raw__patient_20.json similarity index 100% rename from tests/inputs/test__input__patient_20.json rename to tests/raw/phenoage/test__raw__patient_20.json diff --git a/tests/inputs/test__input__patient_21.json b/tests/raw/phenoage/test__raw__patient_21.json similarity index 100% rename from tests/inputs/test__input__patient_21.json rename to tests/raw/phenoage/test__raw__patient_21.json diff --git a/tests/inputs/test__input__patient_22.json b/tests/raw/phenoage/test__raw__patient_22.json similarity index 100% rename from tests/inputs/test__input__patient_22.json rename to tests/raw/phenoage/test__raw__patient_22.json diff --git a/tests/inputs/test__input__patient_23.json b/tests/raw/phenoage/test__raw__patient_23.json similarity index 100% rename from tests/inputs/test__input__patient_23.json rename to tests/raw/phenoage/test__raw__patient_23.json diff --git a/tests/inputs/test__input__patient_24.json b/tests/raw/phenoage/test__raw__patient_24.json similarity index 100% rename from tests/inputs/test__input__patient_24.json rename to tests/raw/phenoage/test__raw__patient_24.json diff --git a/tests/raw/score2/test__raw__patient_25.json b/tests/raw/score2/test__raw__patient_25.json new file mode 100755 index 0000000..45cdeee --- /dev/null +++ b/tests/raw/score2/test__raw__patient_25.json @@ -0,0 +1,35 @@ +{ + "metadata": { + "patient_id": "P024-2024-024", + "sex": "female", + "timestamp": "2024-07-04T13:45:00Z", + "test_date": "2024-07-04", + "laboratory": "NHANES Reference Labs" + }, + "raw_biomarkers": { + "age": { + "value": 50, + "unit": "years" + }, + "smoking": { + "value": true, + "unit": "yes/no" + }, + "systolic_blood_pressure": { + "value": 140, + "unit": "mmHg" + }, + "total_cholesterol": { + "value": 6.3, + "unit": "mmol/L" + }, + "hdl_cholesterol": { + "value": 1.4, + "unit": "mmol/L" + }, + "is_male": { + "value": false, + "unit": "yes/no" + } + } +} diff --git a/tests/raw/score2/test__raw__patient_26.json b/tests/raw/score2/test__raw__patient_26.json new file mode 100755 index 0000000..2872e8f --- /dev/null +++ b/tests/raw/score2/test__raw__patient_26.json @@ -0,0 +1,35 @@ +{ + "metadata": { + "patient_id": "P024-2024-024", + "sex": "male", + "timestamp": "2024-07-04T13:45:00Z", + "test_date": "2024-07-04", + "laboratory": "NHANES Reference Labs" + }, + "raw_biomarkers": { + "age": { + "value": 50, + "unit": "years" + }, + "smoking": { + "value": true, + "unit": "yes/no" + }, + "systolic_blood_pressure": { + "value": 140, + "unit": "mmHg" + }, + "total_cholesterol": { + "value": 6.3, + "unit": "mmol/L" + }, + "hdl_cholesterol": { + "value": 1.4, + "unit": "mmol/L" + }, + "is_male": { + "value": true, + "unit": "yes/no" + } + } +} diff --git a/tests/raw/score2/test__raw__patient_27.json b/tests/raw/score2/test__raw__patient_27.json new file mode 100644 index 0000000..c5ea1a0 --- /dev/null +++ b/tests/raw/score2/test__raw__patient_27.json @@ -0,0 +1,35 @@ +{ + "metadata": { + "patient_id": "P027-2024-027", + "sex": "female", + "timestamp": "2024-07-04T14:00:00Z", + "test_date": "2024-07-04", + "laboratory": "NHANES Reference Labs" + }, + "raw_biomarkers": { + "age": { + "value": 55, + "unit": "years" + }, + "smoking": { + "value": false, + "unit": "yes/no" + }, + "systolic_blood_pressure": { + "value": 125, + "unit": "mmHg" + }, + "total_cholesterol": { + "value": 5.2, + "unit": "mmol/L" + }, + "hdl_cholesterol": { + "value": 1.6, + "unit": "mmol/L" + }, + "is_male": { + "value": false, + "unit": "yes/no" + } + } +} diff --git a/tests/raw/score2/test__raw__patient_28.json b/tests/raw/score2/test__raw__patient_28.json new file mode 100644 index 0000000..2acaa03 --- /dev/null +++ b/tests/raw/score2/test__raw__patient_28.json @@ -0,0 +1,35 @@ +{ + "metadata": { + "patient_id": "P028-2024-028", + "sex": "male", + "timestamp": "2024-07-04T14:15:00Z", + "test_date": "2024-07-04", + "laboratory": "NHANES Reference Labs" + }, + "raw_biomarkers": { + "age": { + "value": 45, + "unit": "years" + }, + "smoking": { + "value": false, + "unit": "yes/no" + }, + "systolic_blood_pressure": { + "value": 130, + "unit": "mmHg" + }, + "total_cholesterol": { + "value": 5.8, + "unit": "mmol/L" + }, + "hdl_cholesterol": { + "value": 1.3, + "unit": "mmol/L" + }, + "is_male": { + "value": true, + "unit": "yes/no" + } + } +} diff --git a/tests/raw/score2/test__raw__patient_29.json b/tests/raw/score2/test__raw__patient_29.json new file mode 100644 index 0000000..f6fcf3f --- /dev/null +++ b/tests/raw/score2/test__raw__patient_29.json @@ -0,0 +1,35 @@ +{ + "metadata": { + "patient_id": "P029-2024-029", + "sex": "male", + "timestamp": "2024-07-04T14:30:00Z", + "test_date": "2024-07-04", + "laboratory": "NHANES Reference Labs" + }, + "raw_biomarkers": { + "age": { + "value": 40, + "unit": "years" + }, + "smoking": { + "value": true, + "unit": "yes/no" + }, + "systolic_blood_pressure": { + "value": 135, + "unit": "mmHg" + }, + "total_cholesterol": { + "value": 6.0, + "unit": "mmol/L" + }, + "hdl_cholesterol": { + "value": 1.2, + "unit": "mmol/L" + }, + "is_male": { + "value": true, + "unit": "yes/no" + } + } +} diff --git a/tests/raw/score2/test__raw__patient_30.json b/tests/raw/score2/test__raw__patient_30.json new file mode 100644 index 0000000..ca90af4 --- /dev/null +++ b/tests/raw/score2/test__raw__patient_30.json @@ -0,0 +1,35 @@ +{ + "metadata": { + "patient_id": "P030-2024-030", + "sex": "female", + "timestamp": "2024-07-04T14:45:00Z", + "test_date": "2024-07-04", + "laboratory": "NHANES Reference Labs" + }, + "raw_biomarkers": { + "age": { + "value": 60, + "unit": "years" + }, + "smoking": { + "value": false, + "unit": "yes/no" + }, + "systolic_blood_pressure": { + "value": 145, + "unit": "mmHg" + }, + "total_cholesterol": { + "value": 6.5, + "unit": "mmol/L" + }, + "hdl_cholesterol": { + "value": 1.5, + "unit": "mmol/L" + }, + "is_male": { + "value": false, + "unit": "yes/no" + } + } +} diff --git a/tests/raw/score2/test__raw__patient_31.json b/tests/raw/score2/test__raw__patient_31.json new file mode 100644 index 0000000..95fa65a --- /dev/null +++ b/tests/raw/score2/test__raw__patient_31.json @@ -0,0 +1,35 @@ +{ + "metadata": { + "patient_id": "P031-2024-031", + "sex": "male", + "timestamp": "2024-07-04T15:00:00Z", + "test_date": "2024-07-04", + "laboratory": "NHANES Reference Labs" + }, + "raw_biomarkers": { + "age": { + "value": 65, + "unit": "years" + }, + "smoking": { + "value": true, + "unit": "yes/no" + }, + "systolic_blood_pressure": { + "value": 150, + "unit": "mmHg" + }, + "total_cholesterol": { + "value": 7.0, + "unit": "mmol/L" + }, + "hdl_cholesterol": { + "value": 1.1, + "unit": "mmol/L" + }, + "is_male": { + "value": true, + "unit": "yes/no" + } + } +} diff --git a/tests/raw/score2/test__raw__patient_32.json b/tests/raw/score2/test__raw__patient_32.json new file mode 100644 index 0000000..9a90055 --- /dev/null +++ b/tests/raw/score2/test__raw__patient_32.json @@ -0,0 +1,35 @@ +{ + "metadata": { + "patient_id": "P032-2024-032", + "sex": "female", + "timestamp": "2024-07-04T15:15:00Z", + "test_date": "2024-07-04", + "laboratory": "NHANES Reference Labs" + }, + "raw_biomarkers": { + "age": { + "value": 69, + "unit": "years" + }, + "smoking": { + "value": false, + "unit": "yes/no" + }, + "systolic_blood_pressure": { + "value": 155, + "unit": "mmHg" + }, + "total_cholesterol": { + "value": 7.2, + "unit": "mmol/L" + }, + "hdl_cholesterol": { + "value": 1.3, + "unit": "mmol/L" + }, + "is_male": { + "value": false, + "unit": "yes/no" + } + } +} diff --git a/tests/raw/score2/test__raw__patient_33.json b/tests/raw/score2/test__raw__patient_33.json new file mode 100644 index 0000000..9a19590 --- /dev/null +++ b/tests/raw/score2/test__raw__patient_33.json @@ -0,0 +1,35 @@ +{ + "metadata": { + "patient_id": "P033-2024-033", + "sex": "male", + "timestamp": "2024-07-04T15:30:00Z", + "test_date": "2024-07-04", + "laboratory": "NHANES Reference Labs" + }, + "raw_biomarkers": { + "age": { + "value": 49, + "unit": "years" + }, + "smoking": { + "value": true, + "unit": "yes/no" + }, + "systolic_blood_pressure": { + "value": 138, + "unit": "mmHg" + }, + "total_cholesterol": { + "value": 6.1, + "unit": "mmol/L" + }, + "hdl_cholesterol": { + "value": 1.4, + "unit": "mmol/L" + }, + "is_male": { + "value": true, + "unit": "yes/no" + } + } +} diff --git a/tests/raw/score2/test__raw__patient_34.json b/tests/raw/score2/test__raw__patient_34.json new file mode 100644 index 0000000..3f0153c --- /dev/null +++ b/tests/raw/score2/test__raw__patient_34.json @@ -0,0 +1,35 @@ +{ + "metadata": { + "patient_id": "P034-2024-034", + "sex": "female", + "timestamp": "2024-07-04T15:45:00Z", + "test_date": "2024-07-04", + "laboratory": "NHANES Reference Labs" + }, + "raw_biomarkers": { + "age": { + "value": 50, + "unit": "years" + }, + "smoking": { + "value": false, + "unit": "yes/no" + }, + "systolic_blood_pressure": { + "value": 120, + "unit": "mmHg" + }, + "total_cholesterol": { + "value": 4.8, + "unit": "mmol/L" + }, + "hdl_cholesterol": { + "value": 1.8, + "unit": "mmol/L" + }, + "is_male": { + "value": false, + "unit": "yes/no" + } + } +} diff --git a/tests/raw/score2/test__raw__patient_35.json b/tests/raw/score2/test__raw__patient_35.json new file mode 100644 index 0000000..d176cae --- /dev/null +++ b/tests/raw/score2/test__raw__patient_35.json @@ -0,0 +1,35 @@ +{ + "metadata": { + "patient_id": "P035-2024-035", + "sex": "male", + "timestamp": "2024-07-04T16:00:00Z", + "test_date": "2024-07-04", + "laboratory": "NHANES Reference Labs" + }, + "raw_biomarkers": { + "age": { + "value": 55, + "unit": "years" + }, + "smoking": { + "value": true, + "unit": "yes/no" + }, + "systolic_blood_pressure": { + "value": 142, + "unit": "mmHg" + }, + "total_cholesterol": { + "value": 6.4, + "unit": "mmol/L" + }, + "hdl_cholesterol": { + "value": 1.2, + "unit": "mmol/L" + }, + "is_male": { + "value": true, + "unit": "yes/no" + } + } +} diff --git a/tests/raw/score2/test__raw__patient_36.json b/tests/raw/score2/test__raw__patient_36.json new file mode 100644 index 0000000..f618344 --- /dev/null +++ b/tests/raw/score2/test__raw__patient_36.json @@ -0,0 +1,35 @@ +{ + "metadata": { + "patient_id": "P036-2024-036", + "sex": "female", + "timestamp": "2024-07-04T16:15:00Z", + "test_date": "2024-07-04", + "laboratory": "NHANES Reference Labs" + }, + "raw_biomarkers": { + "age": { + "value": 45, + "unit": "years" + }, + "smoking": { + "value": true, + "unit": "yes/no" + }, + "systolic_blood_pressure": { + "value": 132, + "unit": "mmHg" + }, + "total_cholesterol": { + "value": 5.5, + "unit": "mmol/L" + }, + "hdl_cholesterol": { + "value": 1.5, + "unit": "mmol/L" + }, + "is_male": { + "value": false, + "unit": "yes/no" + } + } +} diff --git a/tests/inputs/test__input__NHANES3__bioage.csv b/tests/raw/test__input__NHANES3__bioage.csv similarity index 100% rename from tests/inputs/test__input__NHANES3__bioage.csv rename to tests/raw/test__input__NHANES3__bioage.csv diff --git a/tests/test_io.py b/tests/test_io.py index ba1b2ad..6f42c92 100755 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -5,13 +5,13 @@ from vitals.biomarkers import io -INP_FILEPATH = Path(__file__).parent / "inputs" -OUT_FILEPATH = Path(__file__).parent / "outputs" +INP_FILEPATH = Path(__file__).parent / "raw" / "phenoage" +OUT_FILEPATH = Path(__file__).parent / "inputs" / "phenoage" @pytest.mark.parametrize( "input_filename,output_filename", - [("test__input__patient_01.json", "test__output__patient_01.json")], + [("test__raw__patient_01.json", "test__input__patient_01.json")], ) def test_process_json_files(input_filename, output_filename): # Process files in the tests directory diff --git a/tests/test_phenoage.py b/tests/test_phenoage.py index cf062f3..3da32da 100755 --- a/tests/test_phenoage.py +++ b/tests/test_phenoage.py @@ -4,23 +4,23 @@ from vitals.phenoage import compute -OUT_FILEPATH = Path(__file__).parent / "outputs" +OUT_FILEPATH = Path(__file__).parent / "inputs" / "phenoage" @pytest.mark.parametrize( "filename,expected", [ - ("test__output__patient_01.json", (39.00, 39.43, 0.43)), - ("test__output__patient_02.json", (40.00, 40.57, 0.57)), - ("test__output__patient_03.json", (80.00, 74.78, -5.22)), - ("test__output__patient_04.json", (36.00, 31.05, -4.95)), - ("test__output__patient_05.json", (35.00, 39.42, 4.42)), - ("test__output__patient_06.json", (42.00, 53.71, 11.71)), - ("test__output__patient_07.json", (36.00, 31.06, -4.94)), - ("test__output__patient_08.json", (31.00, 31.64, 0.65)), - ("test__output__patient_17.json", (53.00, 52.86, -0.14)), - ("test__output__patient_19.json", (70.00, 78.85, 8.85)), - ("test__output__patient_23.json", (62.00, 61.75, -0.25)), + ("test__input__patient_01.json", (39.00, 39.43, 0.43)), + ("test__input__patient_02.json", (40.00, 40.57, 0.57)), + ("test__input__patient_03.json", (80.00, 74.78, -5.22)), + ("test__input__patient_04.json", (36.00, 31.05, -4.95)), + ("test__input__patient_05.json", (35.00, 39.42, 4.42)), + ("test__input__patient_06.json", (42.00, 53.71, 11.71)), + ("test__input__patient_07.json", (36.00, 31.06, -4.94)), + ("test__input__patient_08.json", (31.00, 31.64, 0.65)), + ("test__input__patient_17.json", (53.00, 52.86, -0.14)), + ("test__input__patient_19.json", (70.00, 78.85, 8.85)), + ("test__input__patient_23.json", (62.00, 61.75, -0.25)), ], ) def test_phenoage(filename, expected): diff --git a/tests/test_score2.py b/tests/test_score2.py new file mode 100644 index 0000000..98e76ff --- /dev/null +++ b/tests/test_score2.py @@ -0,0 +1,40 @@ +from pathlib import Path + +import pytest + +from vitals.score2 import compute + +OUT_FILEPATH = Path(__file__).parent / "inputs" / "score2" + + +@pytest.mark.parametrize( + "filename,expected", + [ + ("test__input__patient_25.json", (50.00, 4.34, "Low to moderate")), + ("test__input__patient_26.json", (50.00, 6.31, "High")), + ("test__input__patient_27.json", (55.00, 2.10, "Low to moderate")), + ("test__input__patient_28.json", (45.00, 2.40, "Low to moderate")), + ("test__input__patient_29.json", (40.00, 4.30, "High")), + ("test__input__patient_30.json", (60.00, 4.20, "Low to moderate")), + ("test__input__patient_31.json", (65.00, 14.40, "Very high")), + ("test__input__patient_32.json", (69.00, 8.40, "High")), + ("test__input__patient_33.json", (49.00, 5.70, "High")), + ("test__input__patient_34.json", (50.00, 1.20, "Low to moderate")), + ("test__input__patient_35.json", (55.00, 8.70, "High")), + ("test__input__patient_36.json", (45.00, 2.60, "High")), + ], +) +def test_score2(filename, expected): + # Get the actual fixture value using request.getfixturevalue + age, pred_risk, pred_risk_category = compute.cardiovascular_risk( + OUT_FILEPATH / filename + ) + expected_age, expected_risk, expected_category = expected + + assert age == expected_age + assert pred_risk_category == expected_category + assert pytest.approx(pred_risk, abs=0.1) == expected_risk + + +if __name__ == "__main__": + pytest.main([__file__]) diff --git a/vitals/biomarkers/helpers.py b/vitals/biomarkers/helpers.py index f834dda..a112320 100755 --- a/vitals/biomarkers/helpers.py +++ b/vitals/biomarkers/helpers.py @@ -1,9 +1,21 @@ -from typing import TypeVar +from collections.abc import Callable +from pathlib import Path +from typing import Any, TypedDict, TypeVar -import schemas from pydantic import BaseModel +from vitals.biomarkers import schemas + Biomarkers = TypeVar("Biomarkers", bound=BaseModel) +Units = schemas.PhenoageUnits | schemas.Score2Units + + +class ConversionInfo(TypedDict): + """Type definition for biomarker conversion information.""" + + target_name: str + target_unit: str + conversion: Callable[[float], float] def format_unit_suffix(unit: str) -> str: @@ -23,7 +35,7 @@ def format_unit_suffix(unit: str) -> str: return suffix -def update_biomarker_names(biomarkers: dict) -> dict: +def update_biomarker_names(biomarkers: dict[str, Any]) -> dict[str, Any]: """Update biomarker names to include unit suffixes. Args: @@ -47,7 +59,7 @@ def update_biomarker_names(biomarkers: dict) -> dict: def find_biomarker_value( - raw_biomarkers: dict, biomarker_name: str, expected_unit: str + raw_biomarkers: dict[str, Any], biomarker_name: str, expected_unit: str ) -> float | None: """ Find biomarker value by name prefix and expected unit. @@ -69,7 +81,7 @@ def find_biomarker_value( return None -def add_converted_biomarkers(biomarkers: dict) -> dict: +def add_converted_biomarkers(biomarkers: dict[str, Any]) -> dict[str, Any]: """Add converted biomarker entries for glucose, creatinine, albumin, and CRP. Args: @@ -82,7 +94,7 @@ def add_converted_biomarkers(biomarkers: dict) -> dict: result = biomarkers.copy() # Conversion mappings - conversions = { + conversions: dict[str, ConversionInfo] = { "glucose_mg_dl": { "target_name": "glucose_mmol_l", "target_unit": "mmol/L", @@ -133,7 +145,7 @@ def add_converted_biomarkers(biomarkers: dict) -> dict: # Skip if target already exists if target_name not in result: - converted_value = conversion_info["conversion"](source_value) # type: ignore + converted_value = conversion_info["conversion"](source_value) result[target_name] = { "value": round(converted_value, 4), "unit": conversion_info["target_unit"], @@ -143,9 +155,9 @@ def add_converted_biomarkers(biomarkers: dict) -> dict: def extract_biomarkers_from_json( - filepath: str, + filepath: str | Path, biomarker_class: type[Biomarkers], - biomarker_units: schemas.PhenoageUnits, + biomarker_units: Units, ) -> Biomarkers: """ Generic function to extract biomarkers from JSON file based on a Pydantic model. diff --git a/vitals/biomarkers/io.py b/vitals/biomarkers/io.py index f9ed937..8a22037 100755 --- a/vitals/biomarkers/io.py +++ b/vitals/biomarkers/io.py @@ -1,10 +1,11 @@ import json from pathlib import Path +from typing import Any -import helpers +from vitals.biomarkers import helpers -def update(input_file: Path) -> dict: +def update(input_file: Path) -> dict[str, Any]: """Process a single JSON file and create output file with converted biomarkers. Args: @@ -26,7 +27,7 @@ def update(input_file: Path) -> dict: return data -def write(data: dict, output_file: Path) -> None: +def write(data: dict[str, Any], output_file: Path) -> None: """Write biomarker data to a JSON file. Args: @@ -40,16 +41,16 @@ def write(data: dict, output_file: Path) -> None: print(f"Processed: file written at {output_file.name}") -if __name__ == "__main__": - # Process all JSON files in the input directory - input_dir = Path("tests/inputs") - output_dir = Path("tests/outputs") +# if __name__ == "__main__": +# # Process all JSON files in the input directory +# input_dir = Path("tests/raw") +# output_dir = Path("tests/inputs") - for input_file in input_dir.glob("*.json"): - output_file = output_dir / input_file.name.replace("input", "output") +# for input_file in input_dir.glob("*.json"): +# output_file = output_dir / input_file.name.replace("raw", "input") - # Update biomarker data - data = update(input_file) +# # Update biomarker data +# data = update(input_file) - # Write output file - write(data, output_file) +# # Write output file +# write(data, output_file) diff --git a/vitals/biomarkers/schemas.py b/vitals/biomarkers/schemas.py index 9a670d0..4e49f54 100755 --- a/vitals/biomarkers/schemas.py +++ b/vitals/biomarkers/schemas.py @@ -32,3 +32,28 @@ class PhenoageMarkers(BaseModel): alkaline_phosphatase: float white_blood_cell_count: float age: float + + +# ------ SCORE2 Schemas +class Score2Units(BaseModel): + """ + The expected unit to be used for Score2 computation + """ + + age: str = "years" + systolic_blood_pressure: str = "mmHg" + total_cholesterol: str = "mmol/L" + hdl_cholesterol: str = "mmol/L" + smoking: str = "yes/no" + is_male: str = "yes/no" + + +class Score2Markers(BaseModel): + """Processed Score2 biomarkers with standardized units.""" + + age: float + systolic_blood_pressure: float + total_cholesterol: float + hdl_cholesterol: float + smoking: bool + is_male: bool diff --git a/vitals/phenoage/compute.py b/vitals/phenoage/compute.py index eb404ad..faf597d 100755 --- a/vitals/phenoage/compute.py +++ b/vitals/phenoage/compute.py @@ -1,7 +1,10 @@ +from pathlib import Path + import numpy as np -from biomarkers import helpers, schemas from pydantic import BaseModel +from vitals.biomarkers import helpers, schemas + class LinearModel(BaseModel): """ @@ -41,7 +44,7 @@ def __gompertz_mortality_model(weighted_risk_score: float) -> float: ) -def biological_age(filepath: str) -> tuple[float, float, float]: +def biological_age(filepath: str | Path) -> tuple[float, float, float]: """ The Phenoage score is calculated as a weighted (coefficients available in Levine et al 2018) linear combination of these variables, which was then transformed into units of years using 2 parametric diff --git a/vitals/score2/__init__.py b/vitals/score2/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vitals/score2/compute.py b/vitals/score2/compute.py new file mode 100644 index 0000000..dc794af --- /dev/null +++ b/vitals/score2/compute.py @@ -0,0 +1,198 @@ +""" +Module for computing the SCORE2 cardiovascular risk assessment. + +This module implements the SCORE2 algorithm for 10-year cardiovascular disease risk estimation +in apparently healthy individuals aged 40-69 years in Europe. +""" + +from pathlib import Path +from typing import Literal, TypeAlias + +import numpy as np +from pydantic import BaseModel + +from vitals.biomarkers import helpers, schemas + +RiskCategory: TypeAlias = Literal["Low to moderate", "High", "Very high"] + + +class ModelCoefficients(BaseModel): + """ + Sex-specific coefficients for the SCORE2 Cox proportional hazards model. + + These coefficients are used to calculate the 10-year risk of cardiovascular disease + based on transformed risk factors and their age interactions. + """ + + # Male coefficients + male_age: float = 0.3742 + male_smoking: float = 0.6012 + male_sbp: float = 0.2777 + male_total_cholesterol: float = 0.1458 + male_hdl_cholesterol: float = -0.2698 + + # Male interaction term coefficients + male_smoking_age: float = -0.0755 + male_sbp_age: float = -0.0255 + male_tchol_age: float = -0.0281 + male_hdl_age: float = 0.0426 + + # Female coefficients + female_age: float = 0.4648 + female_smoking: float = 0.7744 + female_sbp: float = 0.3131 + female_total_cholesterol: float = 0.1002 + female_hdl_cholesterol: float = -0.2606 + + # Female interaction term coefficients + female_smoking_age: float = -0.1088 + female_sbp_age: float = -0.0277 + female_tchol_age: float = -0.0226 + female_hdl_age: float = 0.0613 + + +class BaselineSurvival(BaseModel): + """ + Sex-specific baseline survival probabilities for the SCORE2 model. + + These values represent the 10-year survival probability for individuals + with all risk factors at their reference values. + """ + + male: float = 0.9605 + female: float = 0.9776 + + +class CalibrationScales(BaseModel): + """ + Region and sex-specific calibration scales for Belgium (Low Risk region). + + These scales are used to calibrate the uncalibrated risk estimate to match + the population-specific cardiovascular disease incidence rates. + """ + + # Male calibration scales + male_scale1: float = -0.5699 + male_scale2: float = 0.7476 + + # Female calibration scales + female_scale1: float = -0.7380 + female_scale2: float = 0.7019 + + +def cardiovascular_risk(filepath: str | Path) -> tuple[float, float, RiskCategory]: + """ + Calculate the 10-year cardiovascular disease risk using the SCORE2 algorithm. + + This function implements the SCORE2 risk assessment for apparently healthy individuals + aged 40-69 years in Europe. It uses sex-specific Cox proportional hazards model + coefficients and applies regional calibration for Belgium (Low Risk region). + + Args: + filepath: Path to JSON file containing biomarker data including age, sex, + systolic blood pressure, total cholesterol, HDL cholesterol, and smoking status. + + Returns: + A tuple containing: + - age: The patient's chronological age + - risk_percentage: The calibrated 10-year CVD risk as a percentage + - risk_category: Risk stratification category ("Low to moderate", "High", or "Very high") + + Raises: + ValueError: If invalid biomarker class is used + """ + # Extract biomarkers from JSON file + biomarkers = helpers.extract_biomarkers_from_json( + filepath=filepath, + biomarker_class=schemas.Score2Markers, + biomarker_units=schemas.Score2Units(), + ) + + if not isinstance(biomarkers, schemas.Score2Markers): + raise ValueError(f"Invalid biomarker class used: {biomarkers}") + + age: float = biomarkers.age + is_male: bool = biomarkers.is_male # True for male, False for female + + # Apply transformations to biomarkers + cage: float = (age - 60) / 5 + smoking: float = float(biomarkers.smoking) # Convert bool to float (1.0 or 0.0) + csbp: float = (biomarkers.systolic_blood_pressure - 120) / 20 + ctchol: float = biomarkers.total_cholesterol - 6 + chdl: float = (biomarkers.hdl_cholesterol - 1.3) / 0.5 + + # Calculate interaction terms + smoking_age: float = smoking * cage + sbp_age: float = csbp * cage + tchol_age: float = ctchol * cage + hdl_age: float = chdl * cage + + # Get model coefficients + coef: ModelCoefficients = ModelCoefficients() + + # Calculate linear predictor (x) based on sex + + linear_pred: float + baseline_survival: float + scale1: float + scale2: float + + if is_male: + linear_pred = ( + coef.male_age * cage + + coef.male_smoking * smoking + + coef.male_sbp * csbp + + coef.male_total_cholesterol * ctchol + + coef.male_hdl_cholesterol * chdl + + coef.male_smoking_age * smoking_age + + coef.male_sbp_age * sbp_age + + coef.male_tchol_age * tchol_age + + coef.male_hdl_age * hdl_age + ) + baseline_survival = BaselineSurvival().male + scale1 = CalibrationScales().male_scale1 + scale2 = CalibrationScales().male_scale2 + else: + linear_pred = ( + coef.female_age * cage + + coef.female_smoking * smoking + + coef.female_sbp * csbp + + coef.female_total_cholesterol * ctchol + + coef.female_hdl_cholesterol * chdl + + coef.female_smoking_age * smoking_age + + coef.female_sbp_age * sbp_age + + coef.female_tchol_age * tchol_age + + coef.female_hdl_age * hdl_age + ) + baseline_survival = BaselineSurvival().female + scale1 = CalibrationScales().female_scale1 + scale2 = CalibrationScales().female_scale2 + + # Calculate uncalibrated risk + uncalibrated_risk: float = 1 - np.power(baseline_survival, np.exp(linear_pred)) + + # Apply calibration for Belgium (Low Risk region) + # Calibrated 10-year risk, % = [1 - exp(-exp(scale1 + scale2*ln(-ln(1 - 10-year risk))))] * 100 + calibrated_risk: float = float( + (1 - np.exp(-np.exp(scale1 + scale2 * np.log(-np.log(1 - uncalibrated_risk))))) + * 100 + ) + + # Determine risk category based on age + risk_category: RiskCategory + if age < 50: + if calibrated_risk < 2.5: + risk_category = "Low to moderate" + elif calibrated_risk < 7.5: + risk_category = "High" + else: + risk_category = "Very high" + else: # age 50-69 + if calibrated_risk < 5: + risk_category = "Low to moderate" + elif calibrated_risk < 10: + risk_category = "High" + else: + risk_category = "Very high" + + return (age, round(calibrated_risk, 2), risk_category)