From deead6bd8b1a674a2f7f5ee4c6103dc689fbb4f5 Mon Sep 17 00:00:00 2001 From: Daniel Danis Date: Tue, 1 Sep 2026 16:38:44 +0200 Subject: [PATCH 1/5] Missing implies excluded flag did not work in case of a phenopacket with 0 excluded HPO terms. --- src/gpsea/analysis/clf/_pheno.py | 8 ++++---- tests/test_predicates.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) 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/tests/test_predicates.py b/tests/test_predicates.py index 1ab9858f..16a6a017 100644 --- a/tests/test_predicates.py +++ b/tests/test_predicates.py @@ -69,6 +69,25 @@ def test_phenotype_predicate__unknown( assert actual is None + def test_phenotype_predicate__missing_implies_excluded( + 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, + missing_implies_phenotype_excluded=True, + ) + actual = predicate.test(patient) + + assert actual is not None + assert actual.phenotype == term_id + assert actual.category.name == "No" + class TestDiseasePresencePredicate: @pytest.mark.parametrize( From e20cc661c7ad73238adabc9249e2c9fe8ab438ea Mon Sep 17 00:00:00 2001 From: Daniel Danis Date: Tue, 1 Sep 2026 16:38:57 +0200 Subject: [PATCH 2/5] Delete redundant version specifier. --- pyproject.toml | 3 --- 1 file changed, 3 deletions(-) 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__" } From 770a21a878088fa7bf027213cbcd8db57a2d630f Mon Sep 17 00:00:00 2001 From: Daniel Danis Date: Tue, 1 Sep 2026 16:44:45 +0200 Subject: [PATCH 3/5] Move tests. --- tests/analysis/clf/test_disease.py | 36 +++++++++ tests/analysis/clf/test_phenotype.py | 96 ++++++++++++++++++++++- tests/test_predicates.py | 113 --------------------------- 3 files changed, 128 insertions(+), 117 deletions(-) create mode 100644 tests/analysis/clf/test_disease.py delete mode 100644 tests/test_predicates.py 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..3067098c 100644 --- a/tests/analysis/clf/test_phenotype.py +++ b/tests/analysis/clf/test_phenotype.py @@ -1,9 +1,13 @@ 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( @@ -29,3 +33,87 @@ 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, + 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 + + def test_phenotype_predicate__missing_implies_excluded( + 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, + missing_implies_phenotype_excluded=True, + ) + actual = predicate.test(patient) + + assert actual is not None + assert actual.phenotype == term_id + assert actual.category.name == "No" + + +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 16a6a017..00000000 --- a/tests/test_predicates.py +++ /dev/null @@ -1,113 +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 - - def test_phenotype_predicate__missing_implies_excluded( - 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, - missing_implies_phenotype_excluded=True, - ) - actual = predicate.test(patient) - - assert actual is not None - assert actual.phenotype == term_id - assert actual.category.name == "No" - - -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 From fd4f70c7da99b8d5b3fdda56a88acdefb504b8b7 Mon Sep 17 00:00:00 2001 From: Daniel Danis Date: Wed, 2 Sep 2026 10:57:58 +0200 Subject: [PATCH 4/5] Improve tests. --- src/gpsea/analysis/clf/_util.py | 3 +- tests/analysis/clf/test_phenotype.py | 51 +++++++++++++++++++++++----- 2 files changed, 44 insertions(+), 10 deletions(-) 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_phenotype.py b/tests/analysis/clf/test_phenotype.py index 3067098c..a5b142d9 100644 --- a/tests/analysis/clf/test_phenotype.py +++ b/tests/analysis/clf/test_phenotype.py @@ -12,7 +12,7 @@ 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, @@ -24,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, @@ -65,7 +65,7 @@ class TestHpoPredicate: def test_phenotype_predicate__present_or_excluded( self, toy_cohort: Cohort, - hpo: hpotk.MinimalOntology, + hpo: hpotk.MinimalOntology[hpotk.TermId, hpotk.MinimalTerm], curie: str, patient_id: str, expected: str, @@ -82,7 +82,7 @@ def test_phenotype_predicate__present_or_excluded( def test_phenotype_predicate__unknown( self, toy_cohort: Cohort, - hpo: hpotk.MinimalOntology, + hpo: hpotk.MinimalOntology[hpotk.TermId, hpotk.MinimalTerm], ): # Not Measured and not Observed - 'HP:0006280', # Chronic pancreatitis patient = find_patient("HetSingleVar", toy_cohort) @@ -92,14 +92,47 @@ def test_phenotype_predicate__unknown( 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, + hpo: hpotk.MinimalOntology[hpotk.TermId, hpotk.MinimalTerm], + curie: str, + patient_id: str, + expected: str, ): - # Not Measured and not Observed - 'HP:0006280', # Chronic pancreatitis - patient = find_patient("HetSingleVar", toy_cohort) - term_id = hpotk.TermId.from_curie("HP:0006280") + patient = find_patient(patient_id, toy_cohort) + term_id = hpotk.TermId.from_curie(curie) predicate = HpoClassifier( hpo=hpo, query=term_id, @@ -109,7 +142,7 @@ def test_phenotype_predicate__missing_implies_excluded( assert actual is not None assert actual.phenotype == term_id - assert actual.category.name == "No" + assert actual.category.name == expected def find_patient(pat_id: str, cohort: Cohort) -> Patient: From fc3c0d4eb4c186780f250bc1040c9827bae785c2 Mon Sep 17 00:00:00 2001 From: Daniel Danis Date: Wed, 2 Sep 2026 11:00:05 +0200 Subject: [PATCH 5/5] Format. --- tests/analysis/clf/test_phenotype.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/analysis/clf/test_phenotype.py b/tests/analysis/clf/test_phenotype.py index a5b142d9..6dfdd54d 100644 --- a/tests/analysis/clf/test_phenotype.py +++ b/tests/analysis/clf/test_phenotype.py @@ -92,7 +92,9 @@ def test_phenotype_predicate__unknown( assert actual is None - @pytest.mark.parametrize("curie,patient_id,expected", [ + @pytest.mark.parametrize( + "curie,patient_id,expected", + [ # Test exact match ( "HP:0001166", # Arachnodactyly @@ -122,7 +124,8 @@ def test_phenotype_predicate__unknown( "HetDoubleVar1", "No", ), - ]) + ], + ) def test_phenotype_predicate__missing_implies_excluded( self, toy_cohort: Cohort,