Spaces:
Runtime error
Runtime error
| """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() | |
| 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) | |