Spaces:
Runtime error
Runtime error
| """Tests for the Claus Breast Cancer Risk Model. | |
| Ground truth values will be validated using web calculator. | |
| References: | |
| - https://github.com/ColorGenomics/risk-models | |
| - https://www.princetonradiology.com/service/mammography/breast-cancer-risk-assessment/ | |
| """ | |
| import pytest | |
| from sentinel.risk_models.claus import ClausRiskModel | |
| from sentinel.user_input import ( | |
| Anthropometrics, | |
| CancerType, | |
| Demographics, | |
| Ethnicity, | |
| FamilyMemberCancer, | |
| FamilyRelation, | |
| FamilySide, | |
| Lifestyle, | |
| PersonalMedicalHistory, | |
| RelationshipDegree, | |
| Sex, | |
| SmokingHistory, | |
| SmokingStatus, | |
| UserInput, | |
| ) | |
| GROUND_TRUTH_CASES = [ | |
| { | |
| "name": "no_family_history", | |
| "input": UserInput( | |
| demographics=Demographics( | |
| age_years=49, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[], | |
| ), | |
| "expected": None, | |
| }, | |
| { | |
| "name": "mother_only", | |
| "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(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=55, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ) | |
| ], | |
| ), | |
| "expected": 8.7, | |
| }, | |
| { | |
| "name": "multiple_first_degree", | |
| "input": UserInput( | |
| demographics=Demographics( | |
| age_years=40, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=45, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.SISTER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=52, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| ], | |
| ), | |
| "expected": 26.7, | |
| }, | |
| { | |
| "name": "mother_maternal_aunt", | |
| "input": UserInput( | |
| demographics=Demographics( | |
| age_years=35, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=50, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=60, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| ], | |
| ), | |
| "expected": 17.6, | |
| }, | |
| { | |
| "name": "complex_family_history", | |
| "input": UserInput( | |
| demographics=Demographics( | |
| age_years=45, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=40, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=55, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=65, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| ], | |
| ), | |
| "expected": 23.5, | |
| }, | |
| ] | |
| class TestClausModel: | |
| """Test suite for ClausRiskModel.""" | |
| def setup_method(self): | |
| """Initialize ClausRiskModel instance for testing.""" | |
| self.model = ClausRiskModel() | |
| def test_ground_truth_validation(self, case): | |
| """Test against reference implementation ground truth results. | |
| These values are validated against the Color Genomics reference | |
| implementation of the Claus model for the exact ages specified. | |
| Args: | |
| case: Parameterized ground truth case dict. | |
| """ | |
| calculated_risk = self.model.calculate_risk(case["input"]) | |
| if calculated_risk is None: | |
| assert case["expected"] is None | |
| else: | |
| calculated_pct = calculated_risk * 100 | |
| assert calculated_pct == pytest.approx(case["expected"], abs=0.1) | |
| def test_user_input_integration(self): | |
| """Test integration with UserInput model.""" | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=45, | |
| sex=Sex.FEMALE, | |
| ethnicity=Ethnicity.WHITE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=55, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=60, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| ], | |
| ) | |
| score = self.model.compute_score(user) | |
| assert "N/A" not in score | |
| assert "%" in score | |
| risk_value = float(score.replace("%", "")) | |
| assert risk_value > 0 | |
| def test_male_patient_handling(self): | |
| """Test that male patients receive N/A response.""" | |
| male_user = UserInput( | |
| demographics=Demographics( | |
| age_years=45, | |
| sex=Sex.MALE, | |
| anthropometrics=Anthropometrics(height_cm=175.0, weight_kg=75.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| ) | |
| score = self.model.compute_score(male_user) | |
| assert score == "N/A: Score available only for female patients." | |
| def test_age_validation_lower_bound(self): | |
| """Test age validation at lower boundary.""" | |
| young_user = UserInput( | |
| demographics=Demographics( | |
| age_years=19, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=45, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ) | |
| ], | |
| ) | |
| with pytest.raises( | |
| ValueError, | |
| match=r"Invalid inputs for Claus.*age_years.*greater than or equal to 20", | |
| ): | |
| self.model.compute_score(young_user) | |
| valid_age_user = UserInput( | |
| demographics=Demographics( | |
| age_years=20, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=45, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ) | |
| ], | |
| ) | |
| score = self.model.compute_score(valid_age_user) | |
| assert "Age is outside" not in score | |
| def test_age_validation_upper_bound(self): | |
| """Test age validation at upper boundary.""" | |
| old_user = UserInput( | |
| demographics=Demographics( | |
| age_years=80, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=55, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ) | |
| ], | |
| ) | |
| with pytest.raises( | |
| ValueError, | |
| match=r"Invalid inputs for Claus.*age_years.*less than or equal to 79", | |
| ): | |
| self.model.compute_score(old_user) | |
| valid_age_user = UserInput( | |
| demographics=Demographics( | |
| age_years=79, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=55, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ) | |
| ], | |
| ) | |
| score = self.model.compute_score(valid_age_user) | |
| assert "Age is outside" not in score | |
| def test_no_family_history(self): | |
| """Test handling of no family history.""" | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=45, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[], | |
| ) | |
| assert ( | |
| self.model.compute_score(user) | |
| == "N/A: No breast cancer family history available." | |
| ) | |
| def test_non_breast_cancer_family_history(self): | |
| """Test that non-breast cancer family history is ignored.""" | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=45, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MOTHER, | |
| cancer_type=CancerType.LUNG, | |
| age_at_diagnosis=60, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.SISTER, | |
| cancer_type=CancerType.OVARIAN, | |
| age_at_diagnosis=50, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| ], | |
| ) | |
| assert ( | |
| self.model.compute_score(user) | |
| == "N/A: No breast cancer family history available." | |
| ) | |
| def test_relationship_mapping(self): | |
| """Test proper mapping of different relationship types.""" | |
| # Mapping from string names to enum values | |
| relationship_map = { | |
| "mother": FamilyRelation.MOTHER, | |
| "daughter": FamilyRelation.DAUGHTER, | |
| "sister": FamilyRelation.SISTER, | |
| "maternal_aunt": FamilyRelation.MATERNAL_AUNT, | |
| "paternal_aunt": FamilyRelation.PATERNAL_AUNT, | |
| "maternal_grandmother": FamilyRelation.MATERNAL_GRANDMOTHER, | |
| "paternal_grandmother": FamilyRelation.PATERNAL_GRANDMOTHER, | |
| } | |
| relationships = [ | |
| ("mother", "mother_onset_age"), | |
| ("daughter", "daughter_onset_ages"), | |
| ("sister", "full_sister_onset_ages"), | |
| ("maternal_aunt", "maternal_aunt_onset_ages"), | |
| ("paternal_aunt", "paternal_aunt_onset_ages"), | |
| ("maternal_grandmother", "maternal_grandmother_onset_ages"), | |
| ("paternal_grandmother", "paternal_grandmother_onset_ages"), | |
| ] | |
| for relative_name, _expected_field in relationships: | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=45, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=relationship_map[relative_name], | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=50, | |
| degree=RelationshipDegree.FIRST | |
| if relative_name in ["mother", "daughter", "sister"] | |
| else RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL | |
| if "maternal" in relative_name | |
| else FamilySide.PATERNAL | |
| if "paternal" in relative_name | |
| else FamilySide.MATERNAL, | |
| ) | |
| ], | |
| ) | |
| score = self.model.compute_score(user) | |
| assert "N/A" not in score, f"Failed for {relative_name}" | |
| def test_family_member_age_filtering(self): | |
| """Test that family members outside age range are filtered.""" | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=45, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=15, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.SISTER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=85, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| ], | |
| ) | |
| assert ( | |
| self.model.compute_score(user) | |
| == "N/A: No breast cancer family history available." | |
| ) | |
| user_with_valid = UserInput( | |
| demographics=Demographics( | |
| age_years=45, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=15, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.SISTER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=50, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| ], | |
| ) | |
| score = self.model.compute_score(user_with_valid) | |
| assert "N/A" not in score | |
| def test_model_metadata(self): | |
| """Test model metadata methods.""" | |
| assert self.model.name == "claus" | |
| assert self.model.cancer_type() == "breast" | |
| assert "Claus" in self.model.description() | |
| assert "lifetime risk" in self.model.interpretation().lower() | |
| assert isinstance(self.model.references(), list) | |
| assert len(self.model.references()) > 0 | |
| assert any("Claus" in ref for ref in self.model.references()) | |
| def test_calculate_risk_mother_only(self): | |
| """Test risk calculation with only mother's history.""" | |
| user = 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(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=55, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ) | |
| ], | |
| ) | |
| risk = self.model.calculate_risk(user) | |
| assert risk is not None | |
| assert 0 < risk < 1 | |
| def test_calculate_risk_multiple_relatives(self): | |
| """Test risk calculation with multiple relatives.""" | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=40, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=45, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=50, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=60, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| ], | |
| ) | |
| risk = self.model.calculate_risk(user) | |
| assert risk is not None | |
| assert 0 < risk < 1 | |
| def test_calculate_risk_no_history_returns_none(self): | |
| """Test that no family history returns None.""" | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=45, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[], | |
| ) | |
| risk = self.model.calculate_risk(user) | |
| assert risk is None | |
| def test_output_format(self): | |
| """Test that output is properly formatted as percentage.""" | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=45, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=55, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ) | |
| ], | |
| ) | |
| score = self.model.compute_score(user) | |
| assert "%" in score | |
| assert score.endswith("%") | |
| risk_str = score[:-1] | |
| risk_value = float(risk_str) | |
| assert 0 <= risk_value <= 100 | |
| def test_run_method_returns_risk_score(self): | |
| """Test that run() method returns proper RiskScore object.""" | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=45, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=55, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ) | |
| ], | |
| ) | |
| risk_score = self.model.run(user) | |
| assert risk_score.name == "claus" | |
| assert risk_score.cancer_type == "breast" | |
| assert risk_score.description is not None | |
| assert risk_score.interpretation is not None | |
| assert risk_score.references is not None | |
| def test_sister_variations(self): | |
| """Test different ways of specifying sister relationship.""" | |
| variations = ["sister", "full sister", "full_sister"] | |
| for sister_variant in variations: | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=45, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.SISTER, # All sister variants map to SISTER | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=50, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ) | |
| ], | |
| ) | |
| score = self.model.compute_score(user) | |
| assert "N/A" not in score, f"Failed for variant: {sister_variant}" | |
| def test_half_sister_relationships(self): | |
| """Test maternal and paternal half-sister relationships.""" | |
| for half_sister_type in [ | |
| "maternal_half_sister", | |
| "maternal half-sister", | |
| "paternal_half_sister", | |
| "paternal half-sister", | |
| ]: | |
| # Determine side based on half-sister type | |
| side = ( | |
| FamilySide.MATERNAL | |
| if "maternal" in half_sister_type | |
| else FamilySide.PATERNAL | |
| ) | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=45, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.SISTER, # Half-sister not in enum | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=50, | |
| degree=RelationshipDegree.SECOND, | |
| side=side, | |
| ) | |
| ], | |
| ) | |
| score = self.model.compute_score(user) | |
| assert "N/A" not in score, f"Failed for: {half_sister_type}" | |
| def test_two_first_degree_relatives(self): | |
| """Test scenario with two first-degree relatives.""" | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=40, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=45, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.SISTER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=50, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| ], | |
| ) | |
| risk = self.model.calculate_risk(user) | |
| assert risk is not None | |
| user_mother_daughter = 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(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=45, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.DAUGHTER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=30, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| ], | |
| ) | |
| risk_mother_daughter = self.model.calculate_risk(user_mother_daughter) | |
| assert risk_mother_daughter is not None | |
| def test_maximum_risk_selection(self): | |
| """Test that model selects maximum risk among applicable tables.""" | |
| user_complex = UserInput( | |
| demographics=Demographics( | |
| age_years=35, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=40, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.SISTER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=45, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=50, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| ], | |
| ) | |
| risk_complex = self.model.calculate_risk(user_complex) | |
| user_mother_only = UserInput( | |
| demographics=Demographics( | |
| age_years=35, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=40, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| ], | |
| ) | |
| risk_mother = self.model.calculate_risk(user_mother_only) | |
| assert risk_complex is not None | |
| assert risk_mother is not None | |
| assert risk_complex >= risk_mother | |
| def test_web_calculator_validation(self): | |
| """Verify against web calculator using upper bounds of age ranges. | |
| Note: Web calculators often use the upper bound of age ranges | |
| (e.g., age 59 for "50-59" range), so these tests verify that | |
| our implementation matches at those boundary points. | |
| These values can be validated using a web calculator: | |
| - Test case 1: Patient 59, Mother 55 → Expected: 6.4% | |
| - Test case 2: Patient 49, Mother 45, Sister 52 → Expected: 22.6% | |
| - Test case 3: Patient 39, Mother 50, Aunt 60 → Expected: 17.1% | |
| - Test case 4: Patient 49, Mother 40, Aunts 55,65 → Expected: 21.7% | |
| """ | |
| # Case 1: Mother only (matches ground truth case 2 at upper bound) | |
| user1 = UserInput( | |
| demographics=Demographics( | |
| age_years=59, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=55, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ) | |
| ], | |
| ) | |
| risk1 = self.model.calculate_risk(user1) | |
| assert risk1 is not None | |
| assert risk1 * 100 == pytest.approx(6.4, abs=0.1) | |
| # Case 2: Multiple first degree (matches ground truth case 3 at upper bound) | |
| user2 = UserInput( | |
| demographics=Demographics( | |
| age_years=49, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=45, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.SISTER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=52, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| ], | |
| ) | |
| risk2 = self.model.calculate_risk(user2) | |
| assert risk2 is not None | |
| assert risk2 * 100 == pytest.approx(22.6, abs=0.1) | |
| # Case 3: Mother + maternal aunt (matches ground truth case 4 at upper bound) | |
| user3 = UserInput( | |
| demographics=Demographics( | |
| age_years=39, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=50, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=60, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| ], | |
| ) | |
| risk3 = self.model.calculate_risk(user3) | |
| assert risk3 is not None | |
| assert risk3 * 100 == pytest.approx(17.1, abs=0.1) | |
| # Case 4: Complex family history (matches ground truth case 5 at upper bound) | |
| user4 = UserInput( | |
| demographics=Demographics( | |
| age_years=49, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=40, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=55, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=65, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| ], | |
| ) | |
| risk4 = self.model.calculate_risk(user4) | |
| assert risk4 is not None | |
| assert risk4 * 100 == pytest.approx(21.7, abs=0.1) | |
| class TestClausAlgorithm: | |
| """Test suite for core Claus algorithm logic from reference implementation.""" | |
| def setup_method(self): | |
| """Initialize ClausRiskModel instance for testing.""" | |
| self.model = ClausRiskModel() | |
| def test_direct_table_values(self): | |
| """Test against hardcoded values from the Claus paper tables. | |
| This verifies our tables match the published paper and that | |
| calculations produce mathematically correct conditional risks. | |
| """ | |
| from sentinel.risk_models.claus import ONE_FIRST_DEG_TABLE | |
| # Verify table values match the published Claus paper | |
| # ONE_FIRST_DEG_TABLE[patient_age_index][relative_age_index] | |
| assert ONE_FIRST_DEG_TABLE[5][2] == 0.132 # Lifetime (79), mother age 40-49 | |
| assert ONE_FIRST_DEG_TABLE[0][2] == 0.003 # Age 29, mother age 40-49 | |
| assert ONE_FIRST_DEG_TABLE[2][3] == 0.023 # Age 49, mother age 50-59 | |
| # Test: Patient at exact table boundary (age 29) | |
| # Patient age 29, mother age 44 (index 2 for 40-49) | |
| # Conditional risk = (lifetime - current) / (1 - current) | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=29, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=44, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ) | |
| ], | |
| ) | |
| risk = self.model.calculate_risk(user) | |
| # Manual calculation from hardcoded table values | |
| lifetime = 0.132 # ONE_FIRST_DEG_TABLE[5][2] | |
| current = 0.003 # ONE_FIRST_DEG_TABLE[0][2] | |
| expected = round((lifetime - current) / (1 - current), 3) | |
| assert risk == expected # Should be 0.129 | |
| def test_one_first_degree_relative(self): | |
| """Test scenarios with one first-degree relative.""" | |
| from sentinel.risk_models.claus import ( | |
| ONE_FIRST_DEG_TABLE, | |
| _get_lifetime_risk, | |
| ) | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=20, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=44, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ) | |
| ], | |
| ) | |
| score = self.model.calculate_risk(user) | |
| expected = _get_lifetime_risk(ONE_FIRST_DEG_TABLE, 20, 2) | |
| assert score == expected | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=20, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.DAUGHTER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=23, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ) | |
| ], | |
| ) | |
| score = self.model.calculate_risk(user) | |
| expected = _get_lifetime_risk(ONE_FIRST_DEG_TABLE, 20, 0) | |
| assert score == expected | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=20, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.SISTER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=32, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MATERNAL_GRANDMOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=55, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| ], | |
| ) | |
| score = self.model.calculate_risk(user) | |
| expected = _get_lifetime_risk(ONE_FIRST_DEG_TABLE, 20, 1) | |
| assert score == expected | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=20, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.SISTER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=11, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.SISTER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=22, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| ], | |
| ) | |
| score = self.model.calculate_risk(user) | |
| expected = _get_lifetime_risk(ONE_FIRST_DEG_TABLE, 20, 0) | |
| assert score == expected | |
| def test_one_second_degree_relative(self): | |
| """Test scenarios with one second-degree relative.""" | |
| from sentinel.risk_models.claus import ( | |
| ONE_SECOND_DEG_TABLE, | |
| _get_lifetime_risk, | |
| ) | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=20, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=44, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ) | |
| ], | |
| ) | |
| score = self.model.calculate_risk(user) | |
| expected = _get_lifetime_risk(ONE_SECOND_DEG_TABLE, 20, 2) | |
| assert score == expected | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=20, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.PATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=54, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.PATERNAL, | |
| ) | |
| ], | |
| ) | |
| score = self.model.calculate_risk(user) | |
| expected = _get_lifetime_risk(ONE_SECOND_DEG_TABLE, 20, 3) | |
| assert score == expected | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=20, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MATERNAL_GRANDMOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=77, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ) | |
| ], | |
| ) | |
| score = self.model.calculate_risk(user) | |
| expected = _get_lifetime_risk(ONE_SECOND_DEG_TABLE, 20, 5) | |
| assert score == expected | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=20, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MATERNAL_GRANDMOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=67, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.SISTER, # Half-sister not in enum | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=12, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.PATERNAL, | |
| ), | |
| ], | |
| ) | |
| score = self.model.calculate_risk(user) | |
| expected = _get_lifetime_risk(ONE_SECOND_DEG_TABLE, 20, 4) | |
| assert score == expected | |
| def test_two_first_degree_relatives(self): | |
| """Test scenarios with two first-degree relatives.""" | |
| from sentinel.risk_models.claus import ( | |
| TWO_FIRST_DEG_TABLE, | |
| _get_lifetime_risk, | |
| ) | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=20, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=44, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.DAUGHTER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=12, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.DAUGHTER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=22, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=33, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| ], | |
| ) | |
| score = self.model.calculate_risk(user) | |
| expected = _get_lifetime_risk(TWO_FIRST_DEG_TABLE, 20, 0, 2) | |
| assert score == expected | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=20, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=44, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.DAUGHTER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=12, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.DAUGHTER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=22, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=33, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.SISTER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=11, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.SISTER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=23, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| ], | |
| ) | |
| score = self.model.calculate_risk(user) | |
| expected = _get_lifetime_risk(TWO_FIRST_DEG_TABLE, 20, 0, 0) | |
| assert score == expected | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=20, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.DAUGHTER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=12, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.DAUGHTER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=22, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=33, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=44, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.SISTER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=11, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.SISTER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=34, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.SISTER, # Half-sister not in enum | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=55, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.PATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.PATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=22, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.PATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.PATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=23, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.PATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.PATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=24, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.PATERNAL, | |
| ), | |
| ], | |
| ) | |
| score = self.model.calculate_risk(user) | |
| expected = _get_lifetime_risk(TWO_FIRST_DEG_TABLE, 20, 0, 1) | |
| assert score == expected | |
| def test_mother_and_maternal_aunt(self): | |
| """Test scenarios with mother and maternal aunt.""" | |
| from sentinel.risk_models.claus import ( | |
| MOTHER_MATERNAL_AUNT, | |
| _get_lifetime_risk, | |
| ) | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=20, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=55, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=66, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| ], | |
| ) | |
| score = self.model.calculate_risk(user) | |
| expected = _get_lifetime_risk(MOTHER_MATERNAL_AUNT, 20, 3, 4) | |
| assert score == expected | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=20, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=55, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=66, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.PATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=52, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.PATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.PATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=43, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.PATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.PATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=54, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.PATERNAL, | |
| ), | |
| ], | |
| ) | |
| score = self.model.calculate_risk(user) | |
| expected = _get_lifetime_risk(MOTHER_MATERNAL_AUNT, 20, 3, 4) | |
| assert score == expected | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=20, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=45, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=19, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=33, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=44, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.PATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=22, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.PATERNAL, | |
| ), | |
| ], | |
| ) | |
| score = self.model.calculate_risk(user) | |
| expected = _get_lifetime_risk(MOTHER_MATERNAL_AUNT, 20, 2, 1) | |
| assert score == expected | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=20, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=55, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=66, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=44, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.PATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=22, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.PATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.PATERNAL_GRANDMOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=88, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.PATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.PATERNAL_GRANDMOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=34, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.PATERNAL, | |
| ), | |
| ], | |
| ) | |
| score = self.model.calculate_risk(user) | |
| expected = _get_lifetime_risk(MOTHER_MATERNAL_AUNT, 20, 3, 2) | |
| assert score == expected | |
| def test_mother_and_paternal_aunt(self): | |
| """Test scenarios with mother and paternal aunt.""" | |
| from sentinel.risk_models.claus import ( | |
| MOTHER_PATERNAL_AUNT, | |
| _get_lifetime_risk, | |
| ) | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=20, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=55, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.PATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=22, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.PATERNAL, | |
| ), | |
| ], | |
| ) | |
| score = self.model.calculate_risk(user) | |
| expected = _get_lifetime_risk(MOTHER_PATERNAL_AUNT, 20, 3, 0) | |
| assert score == expected | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=20, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=45, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=99, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.PATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=63, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.PATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.PATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=22, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.PATERNAL, | |
| ), | |
| ], | |
| ) | |
| score = self.model.calculate_risk(user) | |
| expected = _get_lifetime_risk(MOTHER_PATERNAL_AUNT, 20, 2, 0) | |
| assert score == expected | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=20, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=25, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=99, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.PATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=33, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.PATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.PATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=52, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.PATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.SISTER, # Half-sister not in enum | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=64, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.PATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.SISTER, # Half-sister not in enum | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=53, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.PATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.SISTER, # Half-sister not in enum | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=62, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.PATERNAL, | |
| ), | |
| ], | |
| ) | |
| score = self.model.calculate_risk(user) | |
| expected = _get_lifetime_risk(MOTHER_PATERNAL_AUNT, 20, 0, 1) | |
| assert score == expected | |
| def test_two_second_degree_different_sides(self): | |
| """Test scenarios with two second-degree relatives on different sides.""" | |
| from sentinel.risk_models.claus import ( | |
| TWO_SEC_DEG_DIFF_SIDE_TABLE, | |
| _get_lifetime_risk, | |
| ) | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=20, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.DAUGHTER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=12, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MATERNAL_GRANDMOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=78, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.SISTER, # Half-sister not in enum | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=44, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.PATERNAL, | |
| ), | |
| ], | |
| ) | |
| score = self.model.calculate_risk(user) | |
| expected = _get_lifetime_risk(TWO_SEC_DEG_DIFF_SIDE_TABLE, 20, 2, 5) | |
| assert score == expected | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=20, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.DAUGHTER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=12, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=55, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=90, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.SISTER, # Half-sister not in enum | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=44, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.PATERNAL, | |
| ), | |
| ], | |
| ) | |
| score = self.model.calculate_risk(user) | |
| expected = _get_lifetime_risk(TWO_SEC_DEG_DIFF_SIDE_TABLE, 20, 2, 3) | |
| assert score == expected | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=20, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=12, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.SISTER, # Half-sister not in enum | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=55, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.SISTER, # Half-sister not in enum | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=66, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.PATERNAL, | |
| ), | |
| ], | |
| ) | |
| score = self.model.calculate_risk(user) | |
| expected = _get_lifetime_risk(TWO_SEC_DEG_DIFF_SIDE_TABLE, 20, 3, 4) | |
| assert score == expected | |
| def test_two_second_degree_same_side(self): | |
| """Test scenarios with two second-degree relatives on same side.""" | |
| from sentinel.risk_models.claus import ( | |
| TWO_SEC_DEG_SAME_SIDE_TABLE, | |
| _get_lifetime_risk, | |
| ) | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=20, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.SISTER, # Half-sister not in enum | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=55, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MATERNAL_GRANDMOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=77, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| ], | |
| ) | |
| score = self.model.calculate_risk(user) | |
| expected = _get_lifetime_risk(TWO_SEC_DEG_SAME_SIDE_TABLE, 20, 3, 5) | |
| assert score == expected | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=20, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.SISTER, # Half-sister not in enum | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=55, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.SISTER, # Half-sister not in enum | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=12, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.SISTER, # Half-sister not in enum | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=44, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MATERNAL_GRANDMOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=77, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.PATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=55, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.PATERNAL, | |
| ), | |
| ], | |
| ) | |
| score = self.model.calculate_risk(user) | |
| expected = _get_lifetime_risk(TWO_SEC_DEG_SAME_SIDE_TABLE, 20, 2, 3) | |
| assert score == expected | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=20, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MATERNAL_GRANDMOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=33, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.PATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=55, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.PATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.PATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=22, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.PATERNAL, | |
| ), | |
| ], | |
| ) | |
| score = self.model.calculate_risk(user) | |
| expected = _get_lifetime_risk(TWO_SEC_DEG_SAME_SIDE_TABLE, 20, 0, 3) | |
| assert score == expected | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=20, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MATERNAL_GRANDMOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=33, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.PATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=77, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.PATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.PATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=22, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.PATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.PATERNAL_GRANDMOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=55, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.PATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.SISTER, # Half-sister not in enum | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=44, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.PATERNAL, | |
| ), | |
| ], | |
| ) | |
| score = self.model.calculate_risk(user) | |
| expected = _get_lifetime_risk(TWO_SEC_DEG_SAME_SIDE_TABLE, 20, 0, 2) | |
| assert score == expected | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=20, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MATERNAL_GRANDMOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=55, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MATERNAL_GRANDMOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=66, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.PATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=33, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.PATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.PATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=44, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.PATERNAL, | |
| ), | |
| ], | |
| ) | |
| score = self.model.calculate_risk(user) | |
| expected = _get_lifetime_risk(TWO_SEC_DEG_SAME_SIDE_TABLE, 20, 1, 2) | |
| assert score == expected | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=20, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MATERNAL_GRANDMOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=22, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MATERNAL_GRANDMOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=77, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.PATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=44, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.PATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.PATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=55, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.PATERNAL, | |
| ), | |
| ], | |
| ) | |
| score = self.model.calculate_risk(user) | |
| expected = _get_lifetime_risk(TWO_SEC_DEG_SAME_SIDE_TABLE, 20, 0, 5) | |
| assert score == expected | |
| def test_linear_interpolation_one_relative(self): | |
| """Test linear interpolation for patient's current age with one relative.""" | |
| from sentinel.risk_models.claus import ( | |
| ONE_FIRST_DEG_TABLE, | |
| _get_lifetime_risk, | |
| ) | |
| # Test linear interpolation with a UserInput | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=32, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=50, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ) | |
| ], | |
| ) | |
| computed_score = self.model.calculate_risk(user) | |
| # Manual calculation for verification | |
| expected_score = _get_lifetime_risk(ONE_FIRST_DEG_TABLE, 32, 3) | |
| current_age_risk = ( | |
| ONE_FIRST_DEG_TABLE[0][3] | |
| + (ONE_FIRST_DEG_TABLE[1][3] - ONE_FIRST_DEG_TABLE[0][3]) * 3 / 10 | |
| ) | |
| manual_expected = (ONE_FIRST_DEG_TABLE[5][3] - current_age_risk) / ( | |
| 1 - current_age_risk | |
| ) | |
| assert computed_score == round(manual_expected, 3) | |
| assert computed_score == expected_score | |
| def test_linear_interpolation_two_relatives(self): | |
| """Test linear interpolation for patient's current age with two relatives.""" | |
| from sentinel.risk_models.claus import ( | |
| TWO_SEC_DEG_DIFF_SIDE_TABLE, | |
| _get_lifetime_risk, | |
| ) | |
| # Test linear interpolation with two relatives using UserInput | |
| user = UserInput( | |
| demographics=Demographics( | |
| age_years=47, | |
| sex=Sex.FEMALE, | |
| anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MATERNAL_GRANDMOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=50, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.PATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=30, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.PATERNAL, | |
| ), | |
| ], | |
| ) | |
| computed_score = self.model.calculate_risk(user) | |
| # Manual calculation for verification - this should use TWO_SEC_DEG_DIFF_SIDE_TABLE | |
| # because we have one maternal second-degree (grandmother) and one paternal second-degree (aunt) | |
| expected_score = _get_lifetime_risk(TWO_SEC_DEG_DIFF_SIDE_TABLE, 47, 4, 1) | |
| current_age_risk = ( | |
| TWO_SEC_DEG_DIFF_SIDE_TABLE[1][4][1] | |
| + ( | |
| TWO_SEC_DEG_DIFF_SIDE_TABLE[2][4][1] | |
| - TWO_SEC_DEG_DIFF_SIDE_TABLE[1][4][1] | |
| ) | |
| * 8 | |
| / 10 | |
| ) | |
| manual_expected = (TWO_SEC_DEG_DIFF_SIDE_TABLE[5][4][1] - current_age_risk) / ( | |
| 1 - current_age_risk | |
| ) | |
| # The computed score should be reasonable (between 0 and 1) | |
| assert 0 <= computed_score <= 1 | |
| # The computed score should be close to the expected score from the function | |
| # (allowing for the model to select a different table if it gives higher risk) | |
| assert abs(computed_score - expected_score) < 0.01 | |
| # Allow for small rounding differences in manual calculation | |
| assert abs(computed_score - round(manual_expected, 3)) < 0.01 | |