Spaces:
Runtime error
Runtime error
| # pylint: disable=missing-docstring | |
| """Basic tests for the PCPT prostate cancer risk model. | |
| Web calculator available at: https://riskcalc.org/PCPTRC/ | |
| """ | |
| import pytest | |
| from sentinel.risk_models import PCPTRiskModel | |
| from sentinel.user_input import ( | |
| Anthropometrics, | |
| CancerType, | |
| ClinicalTests, | |
| Demographics, | |
| DREResult, | |
| DRETest, | |
| Ethnicity, | |
| FamilyMemberCancer, | |
| FamilyRelation, | |
| FamilySide, | |
| Lifestyle, | |
| PCA3Test, | |
| PercentFreePSATest, | |
| PersonalMedicalHistory, | |
| PSATest, | |
| RelationshipDegree, | |
| Sex, | |
| SmokingHistory, | |
| SmokingStatus, | |
| T2ERGTest, | |
| UserInput, | |
| ) | |
| # Ground-truth regression fixtures collected from the official PCPT web calculator. | |
| GROUND_TRUTH_CASES = [ | |
| { | |
| "name": "low_risk", | |
| "input": UserInput( | |
| demographics=Demographics( | |
| age_years=70, | |
| sex=Sex.MALE, | |
| ethnicity=Ethnicity.WHITE, | |
| anthropometrics=Anthropometrics(height_cm=175.0, weight_kg=80.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER, pack_years=0.0), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory( | |
| prior_negative_prostate_biopsy=False, | |
| ), | |
| family_history=[], | |
| clinical_tests=ClinicalTests( | |
| psa=PSATest(value_ng_ml=10.0), | |
| dre=DRETest(result=DREResult.NORMAL), | |
| ), | |
| ), | |
| "expected_high_grade": 15.0, | |
| "expected_low_grade": 23.0, | |
| "expected_no_cancer": 62.0, | |
| }, | |
| { | |
| "name": "medium_high_risk", | |
| "input": UserInput( | |
| demographics=Demographics( | |
| age_years=80, | |
| sex=Sex.MALE, | |
| ethnicity=Ethnicity.WHITE, | |
| anthropometrics=Anthropometrics(height_cm=175.0, weight_kg=80.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER, pack_years=0.0), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory( | |
| prior_negative_prostate_biopsy=False, | |
| ), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.FATHER, | |
| side=FamilySide.PATERNAL, | |
| degree=RelationshipDegree.FIRST, | |
| cancer_type=CancerType.PROSTATE, | |
| age_at_diagnosis=70, | |
| ) | |
| ], | |
| clinical_tests=ClinicalTests( | |
| psa=PSATest(value_ng_ml=25.0), | |
| ), | |
| ), | |
| "expected_high_grade": 42.0, | |
| "expected_low_grade": 26.0, | |
| "expected_no_cancer": 32.0, | |
| }, | |
| { | |
| "name": "high_risk", | |
| "input": UserInput( | |
| demographics=Demographics( | |
| age_years=65, | |
| sex=Sex.MALE, | |
| ethnicity=Ethnicity.BLACK, | |
| anthropometrics=Anthropometrics(height_cm=175.0, weight_kg=80.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER, pack_years=0.0), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory( | |
| prior_negative_prostate_biopsy=False, | |
| ), | |
| family_history=[], | |
| clinical_tests=ClinicalTests( | |
| psa=PSATest(value_ng_ml=36.0), | |
| dre=DRETest(result=DREResult.ABNORMAL), | |
| ), | |
| ), | |
| "expected_high_grade": 66.0, | |
| "expected_low_grade": 13.0, | |
| "expected_no_cancer": 21.0, | |
| }, | |
| ] | |
| class TestPCPTRiskModel: | |
| def setup_method(self) -> None: | |
| self.model = PCPTRiskModel() | |
| def test_metadata(self) -> None: | |
| assert self.model.name == "pcpt" | |
| assert self.model.cancer_type() == "prostate" | |
| assert "PCPT" in self.model.description() | |
| assert "percent" in self.model.interpretation().lower() | |
| assert len(self.model.references()) > 0 | |
| def test_absolute_risk_basic(self) -> None: | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=60, | |
| sex=Sex.MALE, | |
| ethnicity=Ethnicity.WHITE, | |
| anthropometrics=Anthropometrics(height_cm=175.0, weight_kg=80.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER, pack_years=0.0), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory( | |
| prior_negative_prostate_biopsy=False, | |
| ), | |
| family_history=[], | |
| clinical_tests=ClinicalTests( | |
| psa=PSATest(value_ng_ml=2.5), | |
| dre=DRETest(result=DREResult.NORMAL), | |
| ), | |
| ) | |
| risks = self.model.absolute_risk(user) | |
| assert risks["no_cancer"] > 0 | |
| assert risks["low_grade"] > 0 | |
| assert risks["high_grade"] > 0 | |
| total = risks["no_cancer"] + risks["low_grade"] + risks["high_grade"] | |
| assert 99.5 <= total <= 100.5 | |
| def test_ground_truth_cases(self, case) -> None: | |
| risks = self.model.absolute_risk(case["input"]) | |
| assert risks["high_grade"] == pytest.approx( | |
| case["expected_high_grade"], abs=2.0 | |
| ) | |
| assert risks["low_grade"] == pytest.approx(case["expected_low_grade"], abs=2.0) | |
| assert risks["no_cancer"] == pytest.approx(case["expected_no_cancer"], abs=2.0) | |
| def test_compute_score_with_male_user_input(self) -> None: | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=60, | |
| sex=Sex.MALE, | |
| ethnicity=Ethnicity.WHITE, | |
| anthropometrics=Anthropometrics(height_cm=175.0, weight_kg=80.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER, pack_years=0.0), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory( | |
| prior_negative_prostate_biopsy=False, | |
| ), | |
| family_history=[], | |
| clinical_tests=ClinicalTests( | |
| psa=PSATest(value_ng_ml=2.5), | |
| percent_free_psa=PercentFreePSATest(value_percent=18.0), | |
| dre=DRETest(result=DREResult.NORMAL), | |
| pca3=PCA3Test(score=25.0), | |
| t2erg=T2ERGTest(score=10.0), | |
| ), | |
| ) | |
| score = self.model.compute_score(user) | |
| assert "No Cancer" in score | |
| assert "Low Grade" in score | |
| assert "High Grade" in score | |
| def test_compute_score_rejects_female_user(self) -> None: | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=60, | |
| sex=Sex.FEMALE, | |
| ethnicity=Ethnicity.WHITE, | |
| anthropometrics=Anthropometrics(height_cm=175.0, weight_kg=80.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER, pack_years=0.0), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[], | |
| clinical_tests=ClinicalTests( | |
| psa=PSATest(value_ng_ml=2.5), | |
| ), | |
| ) | |
| score = self.model.compute_score(user) | |
| assert score == "N/A: PCPT applies to male patients only." | |
| def test_validation_errors(self) -> None: | |
| """Test validation errors for missing required fields.""" | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=60, | |
| sex=Sex.MALE, | |
| ethnicity=Ethnicity.WHITE, | |
| anthropometrics=Anthropometrics(height_cm=175.0, weight_kg=80.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER, pack_years=0.0), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[], | |
| clinical_tests=ClinicalTests(), # Missing PSA | |
| ) | |
| with pytest.raises(ValueError, match=r"Invalid inputs for PCPT:"): | |
| self.model.compute_score(user) | |
| def test_age_out_of_range(self) -> None: | |
| """Test age outside validated range raises ValueError.""" | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=50, # Below minimum | |
| sex=Sex.MALE, | |
| ethnicity=Ethnicity.WHITE, | |
| anthropometrics=Anthropometrics(height_cm=175.0, weight_kg=80.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER, pack_years=0.0), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[], | |
| clinical_tests=ClinicalTests( | |
| psa=PSATest(value_ng_ml=2.5), | |
| ), | |
| ) | |
| with pytest.raises(ValueError, match=r"Invalid inputs for PCPT:"): | |
| self.model.compute_score(user) | |