sentinel / tests /test_risk_models /test_claus_model.py
jeuko's picture
Sync from GitHub (main)
8018595 verified
"""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()
@pytest.mark.parametrize("case", GROUND_TRUTH_CASES, ids=lambda x: x["name"])
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