sentinel / tests /test_risk_models /test_mrat_model.py
jeuko's picture
Sync from GitHub (main)
8018595 verified
"""Tests for the MRAT Melanoma Risk Model.
Ground truth values collected from: https://mrisktool.cancer.gov/calculator.html.
All scenarios assume the patient is Non-Hispanic white, matching the published MRAT scope.
"""
import pytest
from sentinel.risk_models import MRATRiskModel
from sentinel.user_input import (
Anthropometrics,
ComplexionLevel,
Demographics,
DermatologicProfile,
FemaleSmallMolesCategory,
FemaleTanResponse,
FrecklingIntensity,
Lifestyle,
MaleSmallMolesCategory,
PersonalMedicalHistory,
Sex,
SmokingHistory,
SmokingStatus,
UserInput,
USGeographicRegion,
)
GROUND_TRUTH_CASES = [
{
"name": "male_light_complexion_high_damage",
"input": UserInput(
demographics=Demographics(
age_years=30,
sex=Sex.MALE,
anthropometrics=Anthropometrics(height_cm=175.0, weight_kg=70.0),
),
lifestyle=Lifestyle(
smoking=SmokingHistory(status=SmokingStatus.NEVER),
),
personal_medical_history=PersonalMedicalHistory(),
dermatologic=DermatologicProfile(
region=USGeographicRegion.NORTHERN,
complexion=ComplexionLevel.LIGHT,
freckling=FrecklingIntensity.MILD,
male_sunburn=True, # True=YES, had sunburn
male_has_two_or_more_big_moles=False, # False=<2 moles
male_small_moles=MaleSmallMolesCategory.LESS_THAN_SEVEN,
solar_damage=False, # False=NO damage
),
),
"expected": 0.02,
},
{
"name": "male_medium_complexion_average_moles",
"input": UserInput(
demographics=Demographics(
age_years=45,
sex=Sex.MALE,
anthropometrics=Anthropometrics(height_cm=175.0, weight_kg=70.0),
),
lifestyle=Lifestyle(
smoking=SmokingHistory(status=SmokingStatus.NEVER),
),
personal_medical_history=PersonalMedicalHistory(),
dermatologic=DermatologicProfile(
region=USGeographicRegion.CENTRAL,
complexion=ComplexionLevel.MEDIUM,
freckling=FrecklingIntensity.MODERATE,
male_sunburn=False, # False=NO sunburn
male_has_two_or_more_big_moles=True, # True=≥2 moles
male_small_moles=MaleSmallMolesCategory.SEVEN_TO_SIXTEEN,
solar_damage=True, # True=YES damage
),
),
"expected": 0.54,
},
{
"name": "female_central_region_moderate_features",
"input": UserInput(
demographics=Demographics(
age_years=50,
sex=Sex.FEMALE,
anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0),
),
lifestyle=Lifestyle(
smoking=SmokingHistory(status=SmokingStatus.NEVER),
),
personal_medical_history=PersonalMedicalHistory(),
dermatologic=DermatologicProfile(
region=USGeographicRegion.SOUTHERN,
complexion=ComplexionLevel.MEDIUM,
freckling=FrecklingIntensity.MODERATE,
female_tan=FemaleTanResponse.MODERATE,
female_small_moles=FemaleSmallMolesCategory.FIVE_TO_ELEVEN,
),
),
"expected": 0.16,
},
{
"name": "female_northern_region_severe_freckling",
"input": UserInput(
demographics=Demographics(
age_years=65,
sex=Sex.FEMALE,
anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0),
),
lifestyle=Lifestyle(
smoking=SmokingHistory(status=SmokingStatus.NEVER),
),
personal_medical_history=PersonalMedicalHistory(),
dermatologic=DermatologicProfile(
region=USGeographicRegion.NORTHERN,
complexion=ComplexionLevel.LIGHT,
freckling=FrecklingIntensity.SEVERE,
female_tan=FemaleTanResponse.NONE,
female_small_moles=FemaleSmallMolesCategory.TWELVE_OR_MORE,
),
),
"expected": 1.19,
},
{
"name": "male_dark_complexion_extensive_moles",
"input": UserInput(
demographics=Demographics(
age_years=55,
sex=Sex.MALE,
anthropometrics=Anthropometrics(height_cm=175.0, weight_kg=70.0),
),
lifestyle=Lifestyle(
smoking=SmokingHistory(status=SmokingStatus.NEVER),
),
personal_medical_history=PersonalMedicalHistory(),
dermatologic=DermatologicProfile(
region=USGeographicRegion.SOUTHERN,
complexion=ComplexionLevel.DARK,
freckling=FrecklingIntensity.ABSENT,
male_sunburn=False, # False=NO sunburn
male_has_two_or_more_big_moles=False, # False=<2 moles
male_small_moles=MaleSmallMolesCategory.SEVENTEEN_OR_MORE,
solar_damage=False, # False=NO damage
),
),
"expected": 0.52,
},
]
class TestMRATModel:
"""Test suite for MRATRiskModel."""
def setup_method(self) -> None:
"""Initialise the MRAT model instance for each test."""
self.model = MRATRiskModel()
@pytest.mark.parametrize("case", GROUND_TRUTH_CASES, ids=lambda x: x["name"])
def test_ground_truth_placeholders(self, case):
"""Check that absolute risk calculation returns a float for each scenario.
Args:
case (dict[str, MRATInput | float | str]): Test scenario definition.
"""
result = self.model.absolute_risk(case["input"])
assert isinstance(result, float)
def test_compute_score_male_user(self):
"""Ensure male user profiles yield a percentage string from compute_score."""
user = UserInput(
demographics=Demographics(
age_years=42,
sex=Sex.MALE,
anthropometrics=Anthropometrics(height_cm=175.0, weight_kg=70.0),
),
lifestyle=Lifestyle(
smoking=SmokingHistory(status=SmokingStatus.NEVER),
),
personal_medical_history=PersonalMedicalHistory(),
dermatologic=DermatologicProfile(
region=USGeographicRegion.NORTHERN,
complexion=ComplexionLevel.LIGHT,
freckling=FrecklingIntensity.MILD,
male_sunburn=True,
male_has_two_or_more_big_moles=True,
male_small_moles=MaleSmallMolesCategory.SEVEN_TO_SIXTEEN,
solar_damage=False,
),
)
score = self.model.compute_score(user)
assert score.endswith("%")
def test_compute_score_female_user(self):
"""Ensure female user profiles yield a percentage string from compute_score."""
user = UserInput(
demographics=Demographics(
age_years=37,
sex=Sex.FEMALE,
anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0),
),
lifestyle=Lifestyle(
smoking=SmokingHistory(status=SmokingStatus.NEVER),
),
personal_medical_history=PersonalMedicalHistory(),
dermatologic=DermatologicProfile(
region=USGeographicRegion.SOUTHERN,
complexion=ComplexionLevel.MEDIUM,
freckling=FrecklingIntensity.MODERATE,
female_tan=FemaleTanResponse.MODERATE,
female_small_moles=FemaleSmallMolesCategory.FIVE_TO_ELEVEN,
),
)
score = self.model.compute_score(user)
assert score.endswith("%")
def test_missing_dermatologic(self):
"""Verify missing dermatologic information raises ValueError."""
user = UserInput(
demographics=Demographics(
age_years=30,
sex=Sex.MALE,
anthropometrics=Anthropometrics(height_cm=175.0, weight_kg=70.0),
),
lifestyle=Lifestyle(
smoking=SmokingHistory(status=SmokingStatus.NEVER),
),
personal_medical_history=PersonalMedicalHistory(),
dermatologic=None,
)
with pytest.raises(ValueError, match=r"Invalid inputs for MRAT:"):
self.model.compute_score(user)
def test_validation_errors(self):
"""Test that model raises ValueError for invalid inputs."""
user_input = UserInput(
demographics=Demographics(
age_years=15, # Below minimum
sex=Sex.MALE,
anthropometrics=Anthropometrics(height_cm=175.0, weight_kg=70.0),
),
lifestyle=Lifestyle(
smoking=SmokingHistory(status=SmokingStatus.NEVER),
),
personal_medical_history=PersonalMedicalHistory(),
dermatologic=DermatologicProfile(
region=USGeographicRegion.NORTHERN,
complexion=ComplexionLevel.LIGHT,
freckling=FrecklingIntensity.MILD,
),
)
with pytest.raises(ValueError, match=r"Invalid inputs for MRAT:"):
self.model.compute_score(user_input)