gpsea 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. gpsea/__init__.py +5 -0
  2. gpsea/analysis/__init__.py +15 -0
  3. gpsea/analysis/_api.py +472 -0
  4. gpsea/analysis/_config.py +373 -0
  5. gpsea/analysis/_gp_analysis.py +202 -0
  6. gpsea/analysis/_gp_impl.py +195 -0
  7. gpsea/analysis/_stats.py +154 -0
  8. gpsea/analysis/_test_fisherExact.py +28 -0
  9. gpsea/analysis/_util.py +61 -0
  10. gpsea/analysis/mtc_filter/__init__.py +14 -0
  11. gpsea/analysis/mtc_filter/_impl.py +697 -0
  12. gpsea/analysis/pcats/__init__.py +29 -0
  13. gpsea/analysis/pcats/_impl.py +561 -0
  14. gpsea/analysis/pcats/stats/__init__.py +5 -0
  15. gpsea/analysis/pcats/stats/_stats.py +177 -0
  16. gpsea/analysis/pcats/stats/_test__stats.py +33 -0
  17. gpsea/analysis/predicate/__init__.py +7 -0
  18. gpsea/analysis/predicate/_api.py +212 -0
  19. gpsea/analysis/predicate/genotype/__init__.py +12 -0
  20. gpsea/analysis/predicate/genotype/_api.py +188 -0
  21. gpsea/analysis/predicate/genotype/_counter.py +65 -0
  22. gpsea/analysis/predicate/genotype/_gt_predicates.py +223 -0
  23. gpsea/analysis/predicate/genotype/_predicates.py +596 -0
  24. gpsea/analysis/predicate/genotype/_variant.py +373 -0
  25. gpsea/analysis/predicate/phenotype/__init__.py +19 -0
  26. gpsea/analysis/predicate/phenotype/_pheno.py +240 -0
  27. gpsea/analysis/predicate/phenotype/_util.py +88 -0
  28. gpsea/analysis/pscore/__init__.py +7 -0
  29. gpsea/analysis/pscore/_api.py +162 -0
  30. gpsea/analysis/pscore/_impl.py +135 -0
  31. gpsea/analysis/pscore/stats/__init__.py +7 -0
  32. gpsea/analysis/pscore/stats/_stats.py +44 -0
  33. gpsea/config.py +51 -0
  34. gpsea/data/__init__.py +3 -0
  35. gpsea/data/_toy.py +210 -0
  36. gpsea/io.py +317 -0
  37. gpsea/model/__init__.py +23 -0
  38. gpsea/model/_base.py +60 -0
  39. gpsea/model/_cohort.py +302 -0
  40. gpsea/model/_gt.py +176 -0
  41. gpsea/model/_phenotype.py +129 -0
  42. gpsea/model/_protein.py +284 -0
  43. gpsea/model/_test_gt.py +30 -0
  44. gpsea/model/_test_tx.py +64 -0
  45. gpsea/model/_tx.py +223 -0
  46. gpsea/model/_variant.py +909 -0
  47. gpsea/model/_variant_effects.py +77 -0
  48. gpsea/model/genome/GCF_000001405.25_GRCh37.p13_assembly_report.tsv +333 -0
  49. gpsea/model/genome/GCF_000001405.39_GRCh38.p13_assembly_report.tsv +703 -0
  50. gpsea/model/genome/__init__.py +22 -0
  51. gpsea/model/genome/_builds.py +49 -0
  52. gpsea/model/genome/_genome.py +553 -0
  53. gpsea/model/genome/_test_builds.py +42 -0
  54. gpsea/model/genome/_test_genome.py +248 -0
  55. gpsea/preprocessing/__init__.py +33 -0
  56. gpsea/preprocessing/_api.py +227 -0
  57. gpsea/preprocessing/_audit.py +372 -0
  58. gpsea/preprocessing/_config.py +410 -0
  59. gpsea/preprocessing/_generic.py +54 -0
  60. gpsea/preprocessing/_patient.py +54 -0
  61. gpsea/preprocessing/_phenopacket.py +450 -0
  62. gpsea/preprocessing/_phenotype.py +114 -0
  63. gpsea/preprocessing/_protein.py +97 -0
  64. gpsea/preprocessing/_uniprot.py +99 -0
  65. gpsea/preprocessing/_variant.py +118 -0
  66. gpsea/preprocessing/_vep.py +211 -0
  67. gpsea/preprocessing/_vv.py +385 -0
  68. gpsea/py.typed +0 -0
  69. gpsea/view/__init__.py +17 -0
  70. gpsea/view/_cohort.py +190 -0
  71. gpsea/view/_disease.py +50 -0
  72. gpsea/view/_draw_variants.py +430 -0
  73. gpsea/view/_formatter.py +47 -0
  74. gpsea/view/_protein_viewer.py +79 -0
  75. gpsea/view/_protein_visualizable.py +153 -0
  76. gpsea/view/_protein_visualizer.py +691 -0
  77. gpsea/view/_stats.py +65 -0
  78. gpsea/view/_txp.py +108 -0
  79. gpsea/view/templates/cohort.html +162 -0
  80. gpsea/view/templates/disease.html +106 -0
  81. gpsea/view/templates/protein.html +86 -0
  82. gpsea/view/templates/stats.html +71 -0
  83. gpsea-0.2.0.dist-info/LICENSE +21 -0
  84. gpsea-0.2.0.dist-info/METADATA +112 -0
  85. gpsea-0.2.0.dist-info/RECORD +87 -0
  86. gpsea-0.2.0.dist-info/WHEEL +5 -0
  87. gpsea-0.2.0.dist-info/top_level.txt +1 -0
gpsea/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """
2
+ GPSEA is a library for analyzing genotype-phenotype correlations in cohorts of rare disease patients.
3
+ """
4
+
5
+ __version__ = "0.2.0"
@@ -0,0 +1,15 @@
1
+ from ._api import CohortAnalysis, GenotypePhenotypeAnalysisResult, HpoMtcReport
2
+ # TODO This should go away
3
+ from ._config import CohortAnalysisConfiguration, configure_cohort_analysis, configure_default_protein_metadata_service, MtcStrategy
4
+ from ._gp_analysis import apply_predicates_on_patients
5
+ from ._util import prepare_hpo_terms_of_interest
6
+
7
+ __all__ = [
8
+ 'configure_cohort_analysis',
9
+ 'CohortAnalysis', 'GenotypePhenotypeAnalysisResult',
10
+ 'CohortAnalysisConfiguration', 'MtcStrategy',
11
+ 'HpoMtcReport',
12
+ 'apply_predicates_on_patients',
13
+ 'configure_default_protein_metadata_service',
14
+ 'prepare_hpo_terms_of_interest',
15
+ ]
gpsea/analysis/_api.py ADDED
@@ -0,0 +1,472 @@
1
+ import abc
2
+ import typing
3
+ from collections import namedtuple, defaultdict
4
+
5
+ import hpotk
6
+ import pandas as pd
7
+
8
+ from gpsea.model import Patient
9
+ from gpsea.preprocessing import ProteinMetadataService
10
+ from .predicate import PolyPredicate, PatientCategory
11
+ from .predicate.genotype import GenotypePolyPredicate, VariantPredicate, ProteinPredicates
12
+ from .predicate.phenotype import P, PhenotypePolyPredicate
13
+ from .pscore import PhenotypeScorer, CountingPhenotypeScorer
14
+
15
+ PatientsByHPO = namedtuple('PatientsByHPO', field_names=['all_with_hpo', 'all_without_hpo'])
16
+
17
+
18
+ class HpoMtcReport:
19
+ """
20
+ Class to simplify reporting results of multiple testing filtering by HpoMtcFilter subclasses.
21
+ """
22
+ # TODO: delete with no replacement.
23
+
24
+ def __init__(
25
+ self,
26
+ filter_name: str,
27
+ mtc_name: str,
28
+ filter_results_map: typing.Mapping[str, int],
29
+ n_terms_before_filtering: int,
30
+ ):
31
+ """
32
+ Args:
33
+ filter_name: name of the MTC filter strategy (e.g. `heuristic sampler`)
34
+ mtc_name: name of the MTC function (e.g. `bonferroni`)
35
+ filter_results_map: mapping with reasons for filtering out a term as keys, and counts of filtered terms as values
36
+ n_terms_before_filtering: the number of HPO terms before filtering
37
+ """
38
+ self._filter_name = filter_name
39
+ self._mtc_name = mtc_name
40
+ self._results_map = filter_results_map
41
+ self._n_terms_before_filtering = n_terms_before_filtering
42
+
43
+ @property
44
+ def filter_method(self) -> str:
45
+ """
46
+ Returns:
47
+ the name of the HpoMtcFilter method used.
48
+ """
49
+ return self._filter_name
50
+
51
+ @property
52
+ def skipped_terms_dict(self) -> typing.Mapping[str, int]:
53
+ """
54
+ Returns:
55
+ a mapping with reasons why an HPO term was skipped as keys and counts of the skipped terms as values.
56
+ """
57
+ return self._results_map
58
+
59
+ @property
60
+ def mtc_method(self) -> str:
61
+ """
62
+ Returns:
63
+ the name of the multiple testing correction method used (e.g. `bonferroni`).
64
+ """
65
+ return self._mtc_name
66
+
67
+ @property
68
+ def n_terms_before_filtering(self) -> int:
69
+ """
70
+ Get the number of terms before filtering.
71
+ """
72
+ return self._n_terms_before_filtering
73
+
74
+
75
+ class GenotypePhenotypeAnalysisResult:
76
+ """
77
+ `GenotypePhenotypeAnalysisResult` summarizes results of genotype-phenotype correlation analysis of a cohort.
78
+ """
79
+ # TODO: delete and use `gpsea.analysis.pcats.MultiPhenotypeAnalysisResult`.
80
+
81
+ def __init__(
82
+ self,
83
+ n_usable: typing.Mapping[P, int],
84
+ all_counts: typing.Mapping[P, pd.DataFrame],
85
+ pvals: pd.Series,
86
+ corrected_pvals: typing.Optional[pd.Series],
87
+ phenotype_categories: typing.Iterable[PatientCategory],
88
+ geno_predicate: PolyPredicate,
89
+ mtc_filter_report: typing.Optional[HpoMtcReport] = None
90
+ ):
91
+ self._n_usable = n_usable
92
+ self._all_counts = all_counts
93
+ self._pvals = pvals
94
+ self._corrected_pvals = corrected_pvals
95
+ self._phenotype_categories = tuple(phenotype_categories)
96
+ self._geno_predicate = geno_predicate
97
+ self._mtc_filter_report = mtc_filter_report
98
+
99
+ @property
100
+ def n_usable(self) -> typing.Mapping[P, int]:
101
+ """
102
+ Get a mapping from a phenotype `P` (either an HPO term or a disease ID)
103
+ to an `int` with the number of patients where the phenotype was assessable,
104
+ and are, thus, usable for genotype-phenotype correlation analysis.
105
+ """
106
+ return self._n_usable
107
+
108
+ @property
109
+ def all_counts(self) -> typing.Mapping[P, pd.DataFrame]:
110
+ """
111
+ Get a mapping from the phenotype item to :class:`pandas.DataFrame` with counts of patients
112
+ in genotype and phenotype groups.
113
+
114
+ An example for a genotype predicate that bins into two categories (`Yes` and `No`) based on presence
115
+ of a missense variant in transcript `NM_123456.7`, and phenotype predicate that checks
116
+ presence/absence of `HP:0001166` (a phenotype term)::
117
+
118
+ Has MISSENSE_VARIANT in NM_123456.7
119
+ No Yes
120
+ Present
121
+ Yes 1 13
122
+ No 7 5
123
+
124
+ The rows correspond to the phenotype categories, and the columns represent the genotype categories.
125
+ """
126
+ return self._all_counts
127
+
128
+ @property
129
+ def pvals(self) -> pd.Series:
130
+ """
131
+ Get a :class:`pandas.Series` with p values for each tested HPO term.
132
+ """
133
+ return self._pvals
134
+
135
+ @property
136
+ def corrected_pvals(self) -> typing.Optional[pd.Series]:
137
+ """
138
+ Get an optional :class:`pandas.Series` with p values for each tested HPO term after multiple testing correction.
139
+ """
140
+ return self._corrected_pvals
141
+
142
+ @property
143
+ def phenotype_categories(self) -> typing.Sequence[PatientCategory]:
144
+ """
145
+ Get a sequence of phenotype patient categories that can be investigated.
146
+ """
147
+ return self._phenotype_categories
148
+
149
+ @property
150
+ def total_tests(self) -> int:
151
+ """
152
+ Get total count of tests that were run for this analysis.
153
+ """
154
+ return len(self._all_counts)
155
+
156
+ @property
157
+ def mtc_filter_report(self) -> typing.Optional[HpoMtcReport]:
158
+ return self._mtc_filter_report
159
+
160
+ def summarize(
161
+ self, hpo: hpotk.MinimalOntology,
162
+ category: PatientCategory,
163
+ ) -> pd.DataFrame:
164
+ """
165
+ Create a data frame with summary of the genotype phenotype analysis.
166
+
167
+ The *rows* of the frame correspond to the analyzed HPO terms.
168
+
169
+ The columns of the data frame have `Count` and `Percentage` per used genotype predicate.
170
+
171
+ **Example**
172
+
173
+ If we use :class:`~gpsea.analysis.predicate.genotype.VariantEffectPredicate`
174
+ which can compare phenotype with and without a missense variant, we will have a data frame
175
+ that looks like this::
176
+
177
+ MISSENSE_VARIANT on `NM_1234.5` No Yes
178
+ Count Percent Count Percent p value Corrected p value
179
+ Arachnodactyly [HP:0001166] 1/10 10% 13/16 81% 0.000781 0.020299
180
+ Abnormality of the musculature [HP:0003011] 6/6 100% 11/11 100% 1.000000 1.000000
181
+ Abnormal nervous system physiology [HP:0012638] 9/9 100% 15/15 100% 1.000000 1.000000
182
+ ... ... ... ... ... ... ...
183
+ """
184
+ if category not in self._phenotype_categories:
185
+ raise ValueError(f'Unknown phenotype category: {category}. Use one of {self._phenotype_categories}')
186
+
187
+ # Row index: a list of tested HPO terms
188
+ pheno_idx = pd.Index(self._n_usable.keys())
189
+ # Column index: multiindex of counts and percentages for all genotype predicate groups
190
+ geno_idx = pd.MultiIndex.from_product(
191
+ iterables=(self._geno_predicate.get_categories(), ('Count', 'Percent')),
192
+ names=(self._geno_predicate.get_question(), None),
193
+ )
194
+
195
+ # We'll fill this frame with data
196
+ df = pd.DataFrame(index=pheno_idx, columns=geno_idx)
197
+
198
+ for pf, count in self._all_counts.items():
199
+ gt_totals = count.sum() # Sum across the phenotype categories (collapse the rows).
200
+ for gt_cat in count.columns:
201
+ cnt = count.loc[category, gt_cat]
202
+ total = gt_totals[gt_cat]
203
+ df.loc[pf, (gt_cat, 'Count')] = f'{cnt}/{total}'
204
+ pct = 0 if total == 0 else round(cnt * 100 / total)
205
+ df.loc[pf, (gt_cat, 'Percent')] = f'{pct}%'
206
+
207
+ # Add columns with p values and corrected p values (if present)
208
+ df.insert(df.shape[1], ('', self._pvals.name), self._pvals)
209
+ if self._corrected_pvals is not None:
210
+ df.insert(df.shape[1], ('', self._corrected_pvals.name), self._corrected_pvals)
211
+
212
+ # Format the index values: `HP:0001250` -> `Seizure [HP:0001250]` if the index members are HPO terms
213
+ # or just use the term ID CURIE otherwise (e.g. `OMIM:123000`).
214
+ labeled_idx = df.index.map(lambda term_id: GenotypePhenotypeAnalysisResult._format_term_id(hpo, term_id))
215
+
216
+ # Last, sort by corrected p value or just p value
217
+ df = df.set_index(labeled_idx)
218
+ if self._corrected_pvals is not None:
219
+ return df.sort_values(by=[('', self._corrected_pvals.name), ('', self._pvals.name)])
220
+ else:
221
+ return df.sort_values(by=('', self._pvals.name))
222
+
223
+ @staticmethod
224
+ def _format_term_id(
225
+ hpo: hpotk.MinimalOntology,
226
+ term_id: hpotk.TermId,
227
+ ) -> str:
228
+ """
229
+ Format a `term_id` as a `str`. HPO term ID is formatted as `<name> [<term_id>]` whereas other term IDs
230
+ are formatted as CURIEs (e.g. `OMIM:123000`).
231
+ """
232
+ if term_id.prefix == 'HP':
233
+ min_onto = hpo.get_term(term_id)
234
+ return f'{min_onto.name} [{term_id.value}]'
235
+ else:
236
+ return term_id.value
237
+
238
+
239
+ class PhenotypeScoreAnalysisResult:
240
+ """
241
+ `PhenotypeScoreAnalysisResult` includes results of testing genotypes vs. phenotype scores.
242
+
243
+ See :ref:`Mann Whitney U Test for phenotype score <phenotype-score-stats>` for more background.
244
+ """
245
+ # TODO: delete and use `gpsea.analysis.pscore.PhenotypeScoreAnalysisResult`
246
+
247
+ def __init__(
248
+ self,
249
+ genotype_phenotype_scores: pd.DataFrame,
250
+ p_value: float,
251
+ ):
252
+ self._genotype_phenotype_scores = genotype_phenotype_scores
253
+ self._p_value = p_value
254
+
255
+ @property
256
+ def genotype_phenotype_scores(
257
+ self,
258
+ ) -> pd.DataFrame:
259
+ """
260
+ Get the DataFrame with the genotype group and the phenotype score for each patient.
261
+
262
+ The DataFrame has the following structure:
263
+
264
+ ========== ======== =========
265
+ patient_id genotype phenotype
266
+ ========== ======== =========
267
+ patient_1 0 1
268
+ patient_2 0 3
269
+ patient_3 1 2
270
+ ... ... ...
271
+ ========== ======== =========
272
+
273
+ The DataFrame index includes the patient IDs, and then there are 2 columns
274
+ with the `genotype` group id (:attr:`~gpsea.analysis.predicate.PatientCategory.cat_id`)
275
+ and the `phenotype` score.
276
+ """
277
+ return self._genotype_phenotype_scores
278
+
279
+ @property
280
+ def p_value(self) -> float:
281
+ return self._p_value
282
+
283
+ def __str__(self) -> str:
284
+ return 'PhenotypeGroupAnalysisResult(' \
285
+ f'genotype_phenotype_scores={self._genotype_phenotype_scores}, ' \
286
+ f'p_value={self._p_value})'
287
+
288
+ def __repr__(self) -> str:
289
+ return str(self)
290
+
291
+
292
+ class CohortAnalysis(metaclass=abc.ABCMeta):
293
+ """
294
+ `CohortAnalysis` is a driver class for running genotype-phenotype correlation analyses.
295
+
296
+ The class provides various methods to test genotype-phenotype correlations. All methods wrap results
297
+ into :class:`GenotypePhenotypeAnalysisResult`.
298
+ """
299
+ # TODO: remove and use the analyses described in `User Guide > Statistical tests`.
300
+
301
+ def __init__(
302
+ self,
303
+ hpo: hpotk.MinimalOntology,
304
+ protein_service: ProteinMetadataService,
305
+ ):
306
+ self._hpo = hpotk.util.validate_instance(hpo, hpotk.MinimalOntology, 'hpo')
307
+ self._protein_service = protein_service
308
+ self._protein_predicates = ProteinPredicates(self._protein_service)
309
+
310
+ @abc.abstractmethod
311
+ def compare_hpo_vs_genotype(
312
+ self,
313
+ predicate: VariantPredicate,
314
+ ) -> GenotypePhenotypeAnalysisResult:
315
+ """
316
+ Bin patients according to a presence of at least one allele that matches `predicate`
317
+ and test for genotype-phenotype correlations.
318
+ """
319
+ pass
320
+
321
+ @abc.abstractmethod
322
+ def compare_hpo_vs_recessive_genotype(
323
+ self,
324
+ predicate: VariantPredicate,
325
+ ) -> GenotypePhenotypeAnalysisResult:
326
+ """
327
+ Bin patients according to a presence of zero, one, or two alleles that matche the `predicate`
328
+ and test for genotype-phenotype correlations.
329
+ """
330
+ pass
331
+
332
+ @abc.abstractmethod
333
+ def compare_hpo_vs_genotype_groups(
334
+ self,
335
+ predicates: typing.Iterable[VariantPredicate],
336
+ group_names: typing.Iterable[str],
337
+ ) -> GenotypePhenotypeAnalysisResult:
338
+ """
339
+ Bin patients according to a presence of at least one allele that matches
340
+ any of the provided `predicates` and test for genotype-phenotype correlations
341
+ between the groups.
342
+
343
+ Note, the patients that pass testing by >1 genotype predicate are *OMITTED* from the analysis!
344
+ """
345
+ pass
346
+
347
+ @abc.abstractmethod
348
+ def compare_disease_vs_genotype(
349
+ self,
350
+ predicate: VariantPredicate,
351
+ disease_ids: typing.Optional[typing.Sequence[typing.Union[str, hpotk.TermId]]] = None,
352
+ ) -> GenotypePhenotypeAnalysisResult:
353
+ pass
354
+
355
+ def compare_genotype_vs_phenotype_group_count(
356
+ self,
357
+ gt_predicate: GenotypePolyPredicate,
358
+ phenotype_group_terms: typing.Iterable[typing.Union[str, hpotk.TermId]],
359
+ ) -> PhenotypeScoreAnalysisResult:
360
+ # TODO: separate into pscore module
361
+ assert isinstance(gt_predicate, GenotypePolyPredicate)
362
+ assert gt_predicate.n_categorizations() == 2
363
+
364
+ counting_scorer = CountingPhenotypeScorer.from_query_curies(
365
+ hpo=self._hpo,
366
+ query=phenotype_group_terms,
367
+ )
368
+
369
+ return self.compare_genotype_vs_phenotype_score(
370
+ gt_predicate=gt_predicate,
371
+ phenotype_scorer=counting_scorer,
372
+ )
373
+
374
+ @abc.abstractmethod
375
+ def compare_genotype_vs_phenotype_score(
376
+ self,
377
+ gt_predicate: GenotypePolyPredicate,
378
+ phenotype_scorer: PhenotypeScorer,
379
+ ) -> PhenotypeScoreAnalysisResult:
380
+ """
381
+ Score the patients with a phenotype scoring method and test for correlation between the genotype group
382
+ and the phenotype score.
383
+
384
+ Args:
385
+ gt_predicate: a genotype predicate for binning the patients along the genotype axis.
386
+ phenotype_scorer: a callable that computes a phenotype score for a given `Patient`.
387
+ """
388
+ pass
389
+
390
+ @abc.abstractmethod
391
+ def compare_genotype_vs_cohort_phenotypes(
392
+ self,
393
+ gt_predicate: GenotypePolyPredicate,
394
+ ) -> GenotypePhenotypeAnalysisResult:
395
+ pass
396
+
397
+ @abc.abstractmethod
398
+ def compare_genotype_vs_phenotypes(
399
+ self,
400
+ gt_predicate: GenotypePolyPredicate,
401
+ pheno_predicates: typing.Iterable[PhenotypePolyPredicate[P]],
402
+ ):
403
+ """
404
+ All analysis functions go through this function.
405
+
406
+ The genotype predicate will partition the individuals into non-overlapping groups
407
+ along the genotype axis.
408
+ The phenotype predicates represent the phenotypes we want to test.
409
+ Less phenotypes may actually be tested thanks to :class:`~gpsea.analysis.PhenotypeMtcFilter`.
410
+
411
+ Args:
412
+ gt_predicate: a predicate for binning the individuals along the genotype axis
413
+ pheno_predicates: phenotype predicates for test the individuals along the phenotype axis
414
+ """
415
+ pass
416
+
417
+ @staticmethod
418
+ def _check_min_perc_patients_w_hpo(min_perc_patients_w_hpo: typing.Union[int, float],
419
+ cohort_size: int) -> float:
420
+ """
421
+ Check if the input meets the requirements.
422
+ """
423
+ if isinstance(min_perc_patients_w_hpo, int):
424
+ if min_perc_patients_w_hpo > 0:
425
+ return min_perc_patients_w_hpo / cohort_size
426
+ else:
427
+ raise ValueError(f'`min_perc_patients_w_hpo` must be a positive `int` '
428
+ f'but got {min_perc_patients_w_hpo}')
429
+ elif isinstance(min_perc_patients_w_hpo, float):
430
+ if 0 < min_perc_patients_w_hpo <= 1:
431
+ return min_perc_patients_w_hpo
432
+ else:
433
+ raise ValueError(f'`min_perc_patients_w_hpo` must be a `float` in range (0, 1] '
434
+ f'but got {min_perc_patients_w_hpo}')
435
+ else:
436
+ raise ValueError(f'`min_perc_patients_w_hpo` must be a positive `int` or a `float` in range (0, 1] '
437
+ f'but got {type(min_perc_patients_w_hpo)}')
438
+
439
+ @staticmethod
440
+ def _group_patients_by_hpo(phenotypic_features: typing.Iterable[hpotk.TermId],
441
+ patients: typing.Iterable[Patient],
442
+ hpo: hpotk.GraphAware,
443
+ missing_implies_excluded: bool) -> PatientsByHPO:
444
+ all_with_hpo = defaultdict(list)
445
+ all_without_hpo = defaultdict(list)
446
+ for hpo_term in phenotypic_features:
447
+ for patient in patients:
448
+ found = False
449
+ for pf in patient.present_phenotypes():
450
+ if hpo_term == pf.identifier or hpo.graph.is_ancestor_of(hpo_term, pf):
451
+ # Patient is annotated with `hpo_term` because `pf` is equal to `hpo_term`
452
+ # or it is a descendant of `hpo_term`.
453
+ all_with_hpo[hpo_term].append(patient)
454
+
455
+ # If one `pf` of the patient is found to be a descendant of `hpo`, we must break to prevent
456
+ # adding the patient to `present_hpo` more than once due to another descendant!
457
+ found = True
458
+ break
459
+ if not found:
460
+ # The patient is not annotated by the `hpo_term`.
461
+
462
+ if missing_implies_excluded:
463
+ # The `hpo_term` annotation is missing, hence implicitly excluded.
464
+ all_without_hpo[hpo_term].append(patient)
465
+ else:
466
+ # The `hpo_term` must be explicitly excluded patient to be accounted for.
467
+ for ef in patient.excluded_phenotypes():
468
+ if hpo_term == ef.identifier or hpo.graph.is_descendant_of(hpo_term, ef):
469
+ all_with_hpo[hpo_term].append(patient)
470
+ break
471
+
472
+ return PatientsByHPO(all_with_hpo, all_without_hpo)