diff --git a/pyproject.toml b/pyproject.toml index 36450f14..3ed849c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,6 +70,3 @@ bugtracker = "https://github.com/P2GX/gpsea/issues" [tool.setuptools] package-dir = { "" = "src" } - -[tool.setuptools.dynamic] -version = { attr = "gpsea.__version__" } diff --git a/src/gpsea/analysis/clf/_pheno.py b/src/gpsea/analysis/clf/_pheno.py index 6da89e43..e184adf2 100644 --- a/src/gpsea/analysis/clf/_pheno.py +++ b/src/gpsea/analysis/clf/_pheno.py @@ -76,9 +76,6 @@ def test( ) -> typing.Optional[PhenotypeCategorization[hpotk.TermId]]: self._check_patient(patient) - if len(patient.phenotypes) == 0: - return None - for phenotype in patient.phenotypes: if phenotype.is_present: if self._query == phenotype.identifier or any( @@ -94,7 +91,10 @@ def test( ): return self._phenotype_excluded - return None + if self._missing_implies_phenotype_excluded: + return self._phenotype_excluded + else: + return None def __eq__(self, value: object) -> bool: return ( diff --git a/src/gpsea/analysis/clf/_util.py b/src/gpsea/analysis/clf/_util.py index 7235252a..38720985 100644 --- a/src/gpsea/analysis/clf/_util.py +++ b/src/gpsea/analysis/clf/_util.py @@ -4,7 +4,8 @@ import hpotk -from ._pheno import PhenotypeClassifier, HpoClassifier +from ._api import PhenotypeClassifier +from ._pheno import HpoClassifier from gpsea.model import Patient diff --git a/tests/analysis/clf/test_disease.py b/tests/analysis/clf/test_disease.py new file mode 100644 index 00000000..58725a73 --- /dev/null +++ b/tests/analysis/clf/test_disease.py @@ -0,0 +1,36 @@ +import hpotk +import pytest + +from gpsea.analysis.clf import DiseasePresenceClassifier +from gpsea.model import Cohort, Patient + + +class TestDiseasePresencePredicate: + @pytest.mark.parametrize( + "patient_id, patient_category", + [ + ("HetSingleVar", "Yes"), + ("HomoVar", "No"), + ], + ) + def test_disease_predicate( + self, + patient_id: str, + patient_category: str, + toy_cohort: Cohort, + ): + patient = find_patient(patient_id, toy_cohort) + disease_id = hpotk.TermId.from_curie("OMIM:148050") + predicate = DiseasePresenceClassifier(disease_id) + actual = predicate.test(patient) + + assert actual is not None + assert actual.phenotype == disease_id + assert actual.category.name == patient_category + + +def find_patient(pat_id: str, cohort: Cohort) -> Patient: + for pat in cohort.all_patients: + if pat.patient_id == pat_id: + return pat + raise ValueError(f"Could not find patient {pat_id}") diff --git a/tests/analysis/clf/test_phenotype.py b/tests/analysis/clf/test_phenotype.py index 35f44f67..6dfdd54d 100644 --- a/tests/analysis/clf/test_phenotype.py +++ b/tests/analysis/clf/test_phenotype.py @@ -1,14 +1,18 @@ import hpotk +import pytest -from gpsea.model import Cohort - -from gpsea.analysis.clf import PhenotypeClassifier -from gpsea.analysis.clf import prepare_hpo_terms_of_interest, prepare_classifiers_for_terms_of_interest +from gpsea.analysis.clf import ( + HpoClassifier, + PhenotypeClassifier, + prepare_classifiers_for_terms_of_interest, + prepare_hpo_terms_of_interest, +) +from gpsea.model import Cohort, Patient def test_prepare_hpo_terms_of_interest( suox_cohort: Cohort, - hpo: hpotk.MinimalOntology, + hpo: hpotk.MinimalOntology[hpotk.TermId, hpotk.MinimalTerm], ): terms = prepare_hpo_terms_of_interest( cohort=suox_cohort, @@ -20,7 +24,7 @@ def test_prepare_hpo_terms_of_interest( def test_prepare_predicates_for_terms_of_interest( suox_cohort: Cohort, - hpo: hpotk.MinimalOntology, + hpo: hpotk.MinimalOntology[hpotk.TermId, hpotk.MinimalTerm], ): predicates = prepare_classifiers_for_terms_of_interest( cohort=suox_cohort, @@ -29,3 +33,123 @@ def test_prepare_predicates_for_terms_of_interest( assert len(predicates) == 71 assert all(isinstance(p, PhenotypeClassifier) for p in predicates) + + +class TestHpoPredicate: + @pytest.mark.parametrize( + "curie, patient_id, expected", + # Patient "HetSingleVar" has Phenotypes: + # Measured and present - 'HP:0001166;HP:0002266', # Arachnodactyly;Focal clonic seizure + # Measured but excluded - 'HP:0001257', # Spasticity + [ + # Test exact match + ( + "HP:0001166", # Arachnodactyly + "HetSingleVar", + "Yes", + ), + # Test inferred annotations + ( + "HP:0001250", # Seizure + "HetSingleVar", + "Yes", + ), + # Test excluded feature + ( + "HP:0001257", # Spasticity + "HetSingleVar", + "No", + ), + ], + ) + def test_phenotype_predicate__present_or_excluded( + self, + toy_cohort: Cohort, + hpo: hpotk.MinimalOntology[hpotk.TermId, hpotk.MinimalTerm], + curie: str, + patient_id: str, + expected: str, + ): + patient = find_patient(patient_id, toy_cohort) + term_id = hpotk.TermId.from_curie(curie) + predicate = HpoClassifier(hpo=hpo, query=term_id) + actual = predicate.test(patient) + + assert actual is not None + assert actual.phenotype == term_id + assert actual.category.name == expected + + def test_phenotype_predicate__unknown( + self, + toy_cohort: Cohort, + hpo: hpotk.MinimalOntology[hpotk.TermId, hpotk.MinimalTerm], + ): + # Not Measured and not Observed - 'HP:0006280', # Chronic pancreatitis + patient = find_patient("HetSingleVar", toy_cohort) + term_id = hpotk.TermId.from_curie("HP:0006280") + predicate = HpoClassifier(hpo=hpo, query=term_id) + actual = predicate.test(patient) + + assert actual is None + + @pytest.mark.parametrize( + "curie,patient_id,expected", + [ + # Test exact match + ( + "HP:0001166", # Arachnodactyly + "HetDoubleVar1", + "Yes", + ), + # An explicitly excluded feature is categorized + # even when missing implies excluded + ( + "HP:0001257", # Spasticity + "HetSingleVar", + "No", + ), + # An unannotated feature is implied to be excluded. + # This works in an individual with an excluded feature. + # Not Measured and not Observed - 'HP:0006280' + ( + "HP:0006280", # Chronic pancreatitis + "HetSingleVar", + "No", + ), + # An unannotated feature is implied to be excluded. + # This works even in an individual with no excluded features. + # Not Measured and not Observed - 'HP:0006280' + ( + "HP:0006280", # Chronic pancreatitis + "HetDoubleVar1", + "No", + ), + ], + ) + def test_phenotype_predicate__missing_implies_excluded( + self, + toy_cohort: Cohort, + hpo: hpotk.MinimalOntology[hpotk.TermId, hpotk.MinimalTerm], + curie: str, + patient_id: str, + expected: str, + ): + patient = find_patient(patient_id, toy_cohort) + term_id = hpotk.TermId.from_curie(curie) + predicate = HpoClassifier( + hpo=hpo, + query=term_id, + missing_implies_phenotype_excluded=True, + ) + actual = predicate.test(patient) + + assert actual is not None + assert actual.phenotype == term_id + assert actual.category.name == expected + + +def find_patient(pat_id: str, cohort: Cohort) -> Patient: + for pat in cohort.all_patients: + if pat.patient_id == pat_id: + return pat + raise ValueError(f"Could not find patient {pat_id}") diff --git a/tests/test_predicates.py b/tests/test_predicates.py deleted file mode 100644 index 1ab9858f..00000000 --- a/tests/test_predicates.py +++ /dev/null @@ -1,94 +0,0 @@ -import hpotk -import pytest - -from gpsea.analysis.clf import HpoClassifier, DiseasePresenceClassifier -from gpsea.model import Cohort, Patient - - -def find_patient(pat_id: str, cohort: Cohort) -> Patient: - for pat in cohort.all_patients: - if pat.patient_id == pat_id: - return pat - raise ValueError(f"Could not find patient {pat_id}") - - -class TestHpoPredicate: - @pytest.mark.parametrize( - "curie, patient_id, expected", - # Patient "HetSingleVar" has Phenotypes: - # Measured and present - 'HP:0001166;HP:0002266', # Arachnodactyly;Focal clonic seizure - # Measured but excluded - 'HP:0001257', # Spasticity - [ - # Test exact match - ( - "HP:0001166", # Arachnodactyly - "HetSingleVar", - "Yes", - ), - # Test inferred annotations - ( - "HP:0001250", # Seizure - "HetSingleVar", - "Yes", - ), - # Test excluded feature - ( - "HP:0001257", # Spasticity - "HetSingleVar", - "No", - ), - ], - ) - def test_phenotype_predicate__present_or_excluded( - self, - toy_cohort: Cohort, - hpo: hpotk.MinimalOntology, - curie: str, - patient_id: str, - expected: str, - ): - patient = find_patient(patient_id, toy_cohort) - term_id = hpotk.TermId.from_curie(curie) - predicate = HpoClassifier(hpo=hpo, query=term_id) - actual = predicate.test(patient) - - assert actual is not None - assert actual.phenotype == term_id - assert actual.category.name == expected - - def test_phenotype_predicate__unknown( - self, - toy_cohort: Cohort, - hpo: hpotk.MinimalOntology, - ): - # Not Measured and not Observed - 'HP:0006280', # Chronic pancreatitis - patient = find_patient("HetSingleVar", toy_cohort) - term_id = hpotk.TermId.from_curie("HP:0006280") - predicate = HpoClassifier(hpo=hpo, query=term_id) - actual = predicate.test(patient) - - assert actual is None - - -class TestDiseasePresencePredicate: - @pytest.mark.parametrize( - "patient_id, patient_category", - [ - ("HetSingleVar", "Yes"), - ("HomoVar", "No"), - ], - ) - def test_disease_predicate( - self, - patient_id: str, - patient_category: str, - toy_cohort: Cohort, - ): - patient = find_patient(patient_id, toy_cohort) - disease_id = hpotk.TermId.from_curie("OMIM:148050") - predicate = DiseasePresenceClassifier(disease_id) - actual = predicate.test(patient) - - assert actual is not None - assert actual.phenotype == disease_id - assert actual.category.name == patient_category