PedNet 2.0.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.
- pednet/__init__.py +24 -0
- pednet/__main__.py +5 -0
- pednet/_trained.py +29 -0
- pednet/cli.py +378 -0
- pednet/compound.py +180 -0
- pednet/engine.py +203 -0
- pednet/features.py +522 -0
- pednet/genotypes.py +253 -0
- pednet/graph.py +473 -0
- pednet/inheritance.py +262 -0
- pednet/models.py +209 -0
- pednet/pedigree.py +387 -0
- pednet/segregation.py +751 -0
- pednet/simulate.py +277 -0
- pednet-2.0.0.dist-info/METADATA +352 -0
- pednet-2.0.0.dist-info/RECORD +20 -0
- pednet-2.0.0.dist-info/WHEEL +5 -0
- pednet-2.0.0.dist-info/entry_points.txt +2 -0
- pednet-2.0.0.dist-info/licenses/LICENSE +21 -0
- pednet-2.0.0.dist-info/top_level.txt +1 -0
pednet/segregation.py
ADDED
|
@@ -0,0 +1,751 @@
|
|
|
1
|
+
"""How strongly each candidate variant co-segregates with the condition.
|
|
2
|
+
|
|
3
|
+
For each variant, PedNet compares two explanations of the family's phenotypes:
|
|
4
|
+
|
|
5
|
+
- the variant causes the condition: the genotypes at the disease locus are the variant's genotypes, so the people
|
|
6
|
+
who were sequenced fix part of the picture, and the genotypes of the rest are summed over
|
|
7
|
+
- it doesn't: the condition comes from some other, unseen locus, which the proband carries (the family having come
|
|
8
|
+
to notice through the proband), and whose genotypes in everyone else are summed over
|
|
9
|
+
|
|
10
|
+
The ratio of the two probabilities is the variant's co-segregation likelihood ratio (a Bayes factor). It is 2 to
|
|
11
|
+
the power of the number of informative meioses for a variant that follows the condition exactly with full
|
|
12
|
+
penetrance, the measure used for the co-segregation criteria of variant classification. Above 1, the variant
|
|
13
|
+
follows the condition through the family better than an unseen locus would be expected to; below 1, worse. Both
|
|
14
|
+
explanations average over the same penetrances, and over the modes of inheritance that suit the variant's
|
|
15
|
+
chromosome (autosomal dominant and recessive for an autosomal variant, X-linked recessive and dominant for one on
|
|
16
|
+
the X), each weighted by how well it explains the phenotypes.
|
|
17
|
+
|
|
18
|
+
Candidates are ranked by their odds against an unseen locus: the Bayes factor times the probability that the mode
|
|
19
|
+
is one that suits the candidate's chromosome, so that an X-linked variant ranks low in a family whose condition is
|
|
20
|
+
clearly autosomal, and the other way round.
|
|
21
|
+
|
|
22
|
+
Pairs of variants in the same gene are scored in the same way as compound heterozygotes, under the recessive modes
|
|
23
|
+
(see compound), and rank alongside the single variants.
|
|
24
|
+
|
|
25
|
+
The Bayes factor is the full-likelihood Bayes factor for co-segregation of Thompson et al. (2003), as segregatr
|
|
26
|
+
computes it, except that PedNet conditions on the proband having the disease genotype rather than on the proband's
|
|
27
|
+
genotype call: the same condition for every variant, so that their Bayes factors can be compared. Its strength as
|
|
28
|
+
evidence for the PP1 criterion of the ACMG/AMP guidelines (or against, BS4) is given by the Bayes factor thresholds
|
|
29
|
+
of Tavtigian et al. (2018), as Belman et al. (2020) apply them to co-segregation: 2.08 for supporting evidence, 4.33
|
|
30
|
+
moderate and 18.7 strong, and their reciprocals against.
|
|
31
|
+
"""
|
|
32
|
+
import warnings
|
|
33
|
+
from collections import Counter
|
|
34
|
+
from dataclasses import dataclass, field
|
|
35
|
+
from typing import Dict, Iterable, List, Optional, Sequence, Tuple
|
|
36
|
+
|
|
37
|
+
import numpy as np
|
|
38
|
+
|
|
39
|
+
from .engine import Network
|
|
40
|
+
from .inheritance import (DEFAULT_PENETRANCE, DEFAULT_PHENOCOPY, LN10, _logsumexp, _modes, _prior, _weights,
|
|
41
|
+
mode_log_likelihoods)
|
|
42
|
+
from .models import MODES, X_LINKED, Model, call_codes, founder_prior, genotype_table, phenotype_factor
|
|
43
|
+
from .pedigree import MALE, Pedigree
|
|
44
|
+
from .compound import PAIR_MODES, PairLocus, has_disease_genotype, pair_log_likelihoods
|
|
45
|
+
|
|
46
|
+
DEFAULT_VARIANT_FREQUENCY = 0.001
|
|
47
|
+
# Rarer variants are treated as having this frequency. Below it, the frequency hardly changes how the variant can
|
|
48
|
+
# pass through the family, and a variant seen in the family is there, however rare it is in the population.
|
|
49
|
+
MIN_VARIANT_FREQUENCY = 0.001
|
|
50
|
+
DEFAULT_ERROR_RATE = 1e-4
|
|
51
|
+
_X_NAMES = {'x', 'chrx', '23', 'chr23'}
|
|
52
|
+
# PLINK numbers the Y 24, the pseudoautosomal regions 25 (inherited like an autosome) and the mitochondria 26
|
|
53
|
+
_Y_NAMES = {'y', 'chry', '24', 'chr24'}
|
|
54
|
+
_MT_NAMES = {'mt', 'm', 'chrm', 'chrmt', '26', 'chr26'}
|
|
55
|
+
|
|
56
|
+
# The strengths of evidence for co-segregation (PP1) or against it (BS4), by the log10 Bayes factor: the thresholds of
|
|
57
|
+
# Tavtigian et al. (2018) for the ACMG/AMP criteria, 18.7 strong, 4.33 moderate and 2.08 supporting
|
|
58
|
+
EVIDENCE = ((np.log10(18.7), 'Strong'), (np.log10(4.33), 'Moderate'), (np.log10(2.08), 'Supporting'))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def evidence_strength(log10_bf: Optional[float]) -> str:
|
|
62
|
+
"""PP1_Strong, PP1_Moderate or PP1_Supporting for co-segregation, BS4_ likewise against it, or '' for neither"""
|
|
63
|
+
if log10_bf is None:
|
|
64
|
+
return ''
|
|
65
|
+
for threshold, strength in EVIDENCE:
|
|
66
|
+
if log10_bf >= threshold - 1e-12:
|
|
67
|
+
return f'PP1_{strength}'
|
|
68
|
+
if log10_bf <= -threshold + 1e-12:
|
|
69
|
+
return f'BS4_{strength}'
|
|
70
|
+
return ''
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class Variant:
|
|
75
|
+
"""
|
|
76
|
+
A candidate variant: its genotypes, as the number of copies of the alternate allele each person carries (0, 1 or 2;
|
|
77
|
+
None, or no entry, if not genotyped), with where it is if known, its allele frequency in the population, and
|
|
78
|
+
the gene it is in (which pairs variants up as compound heterozygotes).
|
|
79
|
+
"""
|
|
80
|
+
id: str
|
|
81
|
+
genotypes: Dict[str, Optional[int]]
|
|
82
|
+
chrom: Optional[str] = None
|
|
83
|
+
pos: Optional[int] = None
|
|
84
|
+
ref: Optional[str] = None
|
|
85
|
+
alt: Optional[str] = None
|
|
86
|
+
allele_frequency: Optional[float] = None
|
|
87
|
+
gene: Optional[str] = None
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def x_linked(self) -> Optional[bool]:
|
|
91
|
+
"""True on the X chromosome, False on another, None if the chromosome isn't known"""
|
|
92
|
+
if self.chrom is None:
|
|
93
|
+
return None
|
|
94
|
+
return str(self.chrom).strip().lower() in _X_NAMES
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def unmodelled(self) -> bool:
|
|
98
|
+
"""Whether the variant is on the Y chromosome or in the mitochondria, whose inheritance PedNet doesn't model"""
|
|
99
|
+
return self.chrom is not None and str(self.chrom).strip().lower() in _Y_NAMES | _MT_NAMES
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass
|
|
103
|
+
class VariantScore:
|
|
104
|
+
"""
|
|
105
|
+
One candidate's evidence: a variant, or with a partner, a pair of variants in one gene as a compound
|
|
106
|
+
heterozygote.
|
|
107
|
+
|
|
108
|
+
log10_lr is the log10 likelihood ratio for co-segregation under each mode that suits the candidate's chromosome
|
|
109
|
+
on its own (only AR, or XLR, for a pair), and log_likelihood the natural log of the probability of the
|
|
110
|
+
phenotypes, given the genotypes, with the candidate as the cause, under each of those modes. log10_bf, the log10
|
|
111
|
+
co-segregation Bayes factor, combines the modes, each weighted by how probable the family's phenotypes make it:
|
|
112
|
+
it is near 0 for a variant whose genotypes say nothing, and evidence is its strength as evidence for
|
|
113
|
+
co-segregation (PP1) or against it (BS4). evidence is left blank, and evidence_withheld says why, for a variant
|
|
114
|
+
that may be new in an affected person and is otherwise consistent with the family (for PS2 or PM6 to judge,
|
|
115
|
+
not PP1 or BS4), and for one that is part of a pair which is evidence for co-segregation (PP1, a log10 BF of
|
|
116
|
+
0.318 or more) and better evidence than the variant alone. best_mode is the most probable mode of inheritance if
|
|
117
|
+
the candidate is the cause. log10_bf is None, and note says why, for a candidate that can't be scored.
|
|
118
|
+
impossible_modes lists the modes under which its genotypes can't have been passed down at all, which happens
|
|
119
|
+
only with an error rate of 0: they are left out, and if no mode is left, the candidate isn't scored.
|
|
120
|
+
|
|
121
|
+
mode_probability is the probability, from the phenotypes, that the mode is one that suits the candidate
|
|
122
|
+
(autosomal or X-linked, or recessive for a pair), and log10_odds = log10_bf + log10(mode_probability): the
|
|
123
|
+
log10 odds that this candidate, rather than some other locus, causes the condition, with even odds beforehand.
|
|
124
|
+
The candidates are ranked by it, so that an X-linked variant in a family whose condition is clearly autosomal
|
|
125
|
+
doesn't rank highly. rank is 1 for the candidate with the most evidence, and tied candidates share the average
|
|
126
|
+
of their ranks.
|
|
127
|
+
|
|
128
|
+
The rest describes the genotypes. A carrier has the variant (for a pair, the genotype of a compound heterozygote:
|
|
129
|
+
both variants, or two copies of either). concordance is the share of the genotyped people with a known phenotype
|
|
130
|
+
whose genotype fits it under the best mode, as a plain check. mendelian_errors counts the genotyped children whose
|
|
131
|
+
genotype their genotyped parents can't have given them, which suggests calling errors: the Bayes factor allows
|
|
132
|
+
for errors, but such a variant is worth checking. de_novo lists the genotyped children who carry the variant
|
|
133
|
+
though both their parents were genotyped without it (on the X, a son whose mother was): the Bayes factor counts
|
|
134
|
+
such a variant against itself, since PedNet doesn't model new mutations, and it needs judging on its own.
|
|
135
|
+
"""
|
|
136
|
+
variant: Variant
|
|
137
|
+
log10_bf: Optional[float]
|
|
138
|
+
log10_lr: Dict[str, float] = field(default_factory=dict)
|
|
139
|
+
log_likelihood: Dict[str, float] = field(default_factory=dict)
|
|
140
|
+
rank: Optional[float] = None
|
|
141
|
+
log10_odds: Optional[float] = None
|
|
142
|
+
mode_probability: Optional[float] = None
|
|
143
|
+
genotyped: int = 0
|
|
144
|
+
affected_carriers: int = 0
|
|
145
|
+
affected_noncarriers: int = 0
|
|
146
|
+
unaffected_carriers: int = 0
|
|
147
|
+
unaffected_noncarriers: int = 0
|
|
148
|
+
best_mode: Optional[str] = None
|
|
149
|
+
concordance: Optional[float] = None
|
|
150
|
+
mendelian_errors: int = 0
|
|
151
|
+
note: str = ''
|
|
152
|
+
partner: Optional[Variant] = None
|
|
153
|
+
de_novo: List[str] = field(default_factory=list)
|
|
154
|
+
evidence_withheld: str = ''
|
|
155
|
+
impossible_modes: List[str] = field(default_factory=list)
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def id(self) -> str:
|
|
159
|
+
return self.variant.id if self.partner is None else f'{self.variant.id} + {self.partner.id}'
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def compound(self) -> bool:
|
|
163
|
+
return self.partner is not None
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def evidence(self) -> str:
|
|
167
|
+
"""The strength of the evidence, or '' when there is none or evidence_withheld says why it isn't given"""
|
|
168
|
+
return '' if self.evidence_withheld else evidence_strength(self.log10_bf)
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def notes(self) -> str:
|
|
172
|
+
"""The note, with the Mendelian errors, possible new mutations and why any evidence is withheld"""
|
|
173
|
+
parts = [self.note] if self.note else []
|
|
174
|
+
if self.mendelian_errors:
|
|
175
|
+
parts.append(f'{self.mendelian_errors} Mendelian error{"s" if self.mendelian_errors > 1 else ""}')
|
|
176
|
+
if self.de_novo:
|
|
177
|
+
parts.append(f'possibly de novo in {", ".join(self.de_novo)}')
|
|
178
|
+
if self.evidence_withheld:
|
|
179
|
+
parts.append(self.evidence_withheld)
|
|
180
|
+
return '; '.join(parts)
|
|
181
|
+
|
|
182
|
+
def row(self) -> dict:
|
|
183
|
+
v, w = self.variant, self.partner
|
|
184
|
+
|
|
185
|
+
def both(name):
|
|
186
|
+
if w is None:
|
|
187
|
+
return getattr(v, name)
|
|
188
|
+
return ','.join('' if x is None else str(x) for x in (getattr(v, name), getattr(w, name)))
|
|
189
|
+
|
|
190
|
+
row = {'rank': self.rank, 'candidate': self.id, 'kind': 'compound heterozygote' if w else 'variant',
|
|
191
|
+
'gene': v.gene, 'chrom': v.chrom, 'pos': both('pos'), 'ref': both('ref'), 'alt': both('alt'),
|
|
192
|
+
'allele_frequency': both('allele_frequency'), 'log10_bf': self.log10_bf, 'evidence': self.evidence,
|
|
193
|
+
'log10_odds': self.log10_odds, 'mode_probability': self.mode_probability, 'best_mode': self.best_mode}
|
|
194
|
+
for mode in MODES:
|
|
195
|
+
row[f'log10_lr_{mode}'] = self.log10_lr.get(mode)
|
|
196
|
+
row.update(genotyped=self.genotyped, affected_carriers=self.affected_carriers,
|
|
197
|
+
affected_noncarriers=self.affected_noncarriers, unaffected_carriers=self.unaffected_carriers,
|
|
198
|
+
unaffected_noncarriers=self.unaffected_noncarriers, concordance=self.concordance,
|
|
199
|
+
mendelian_errors=self.mendelian_errors, de_novo=','.join(self.de_novo), note=self.note,
|
|
200
|
+
evidence_withheld=self.evidence_withheld)
|
|
201
|
+
return row
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _fits(mode: str, copies: int, male: Optional[bool], affected: bool) -> bool:
|
|
205
|
+
"""Whether a genotype fits a phenotype under a mode with full penetrance and no phenocopies"""
|
|
206
|
+
if mode == 'AD':
|
|
207
|
+
disease = copies >= 1
|
|
208
|
+
elif mode == 'AR':
|
|
209
|
+
disease = copies == 2
|
|
210
|
+
elif mode == 'XLD':
|
|
211
|
+
disease = copies >= 1
|
|
212
|
+
else:
|
|
213
|
+
disease = copies >= 1 if male else copies == 2
|
|
214
|
+
return disease == affected
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def fair_ranks(values: Sequence[Optional[float]], decimals: int = 9) -> List[Optional[float]]:
|
|
218
|
+
"""
|
|
219
|
+
Ranks, highest first, with tied values sharing the average of their ranks, and None for a missing value. Values
|
|
220
|
+
equal to the given number of decimal places are tied, so that likelihood ratios that are equal in theory but differ
|
|
221
|
+
in the last digits of the arithmetic share their rank.
|
|
222
|
+
"""
|
|
223
|
+
rounded = [None if v is None or v != v else round(v, decimals) for v in values]
|
|
224
|
+
count = Counter(v for v in rounded if v is not None)
|
|
225
|
+
higher, above = {}, 0
|
|
226
|
+
for v in sorted(count, reverse=True):
|
|
227
|
+
higher[v] = above
|
|
228
|
+
above += count[v]
|
|
229
|
+
return [None if v is None else 1 + higher[v] + (count[v] - 1) / 2 for v in rounded]
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _frequency(v: Variant, default: float, lowest: float) -> float:
|
|
233
|
+
q = v.allele_frequency if v.allele_frequency is not None else default
|
|
234
|
+
return float(np.clip(q, max(lowest, 1e-8), 0.5))
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def rank_variants(pedigree: Pedigree, variants: Iterable[Variant], modes: Sequence[str] = MODES,
|
|
238
|
+
penetrance=DEFAULT_PENETRANCE, penetrance_weights=None, phenocopy: float = DEFAULT_PHENOCOPY,
|
|
239
|
+
allele_frequency: Optional[Dict[str, float]] = None, carrier_penetrance: Optional[float] = None,
|
|
240
|
+
variant_frequency: float = DEFAULT_VARIANT_FREQUENCY, min_frequency: float = MIN_VARIANT_FREQUENCY,
|
|
241
|
+
error_rate: float = DEFAULT_ERROR_RATE, prior: Optional[Dict[str, float]] = None,
|
|
242
|
+
proband: Optional[str] = None, compound: bool = True, chunk: int = 2000) -> List[VariantScore]:
|
|
243
|
+
"""
|
|
244
|
+
Scores candidate variants for co-segregation with the condition in one family, and ranks them, the strongest
|
|
245
|
+
evidence first.
|
|
246
|
+
|
|
247
|
+
variants: Variant objects (see read_vcf and read_genotype_table); people not in the pedigree are ignored
|
|
248
|
+
modes: the modes of inheritance to allow (by default all four); give one if the mode is known
|
|
249
|
+
penetrance, penetrance_weights, phenocopy, allele_frequency, carrier_penetrance, prior, proband: as for
|
|
250
|
+
infer_mode; allele_frequency is that of the unseen locus in the explanation without the variant, and proband
|
|
251
|
+
the person that locus is taken to be carried by (by default, each affected person in turn)
|
|
252
|
+
variant_frequency: the allele frequency to use for a variant that doesn't give its own
|
|
253
|
+
min_frequency: the lowest frequency used; rarer variants are treated as having this one. If it were far below
|
|
254
|
+
error_rate, a genotype call in one person would be put down to an error rather than to the variant
|
|
255
|
+
error_rate: the chance that a genotype call is one copy out (see models.genotype_factor)
|
|
256
|
+
compound: whether to score pairs of variants in one gene (see Variant.gene) as compound heterozygotes too, when a
|
|
257
|
+
recessive mode is allowed: the pairs that an affected person (the proband, if given) carries both of
|
|
258
|
+
"""
|
|
259
|
+
modes = _modes(modes)
|
|
260
|
+
priors = _prior(modes, prior)
|
|
261
|
+
pens = (float(penetrance),) if np.isscalar(penetrance) else tuple(float(f) for f in penetrance)
|
|
262
|
+
with np.errstate(divide='ignore'):
|
|
263
|
+
log_w = np.log(_weights(pens, penetrance_weights))
|
|
264
|
+
allele_frequency = allele_frequency or {}
|
|
265
|
+
variants = list(variants)
|
|
266
|
+
networks: Dict[bool, Network] = {}
|
|
267
|
+
if proband is not None and proband not in pedigree:
|
|
268
|
+
raise ValueError(f'{proband} is not in family {pedigree.family}')
|
|
269
|
+
if proband is not None and not pedigree[proband].affected:
|
|
270
|
+
raise ValueError(f'The proband, {proband}, is not recorded as affected: the Bayes factors take the family to '
|
|
271
|
+
'have come to notice through an affected person')
|
|
272
|
+
models = {m: Model(m, allele_frequency.get(m), pens[0], phenocopy, carrier_penetrance) for m in modes}
|
|
273
|
+
|
|
274
|
+
# the explanation without the variant: P(phenotypes | the proband carries the disease genotype, mode), averaged
|
|
275
|
+
# over the penetrances and the probands, as for the mode of inheritance
|
|
276
|
+
log_without = mode_log_likelihoods(pedigree, modes, pens, penetrance_weights, phenocopy, allele_frequency,
|
|
277
|
+
carrier_penetrance, [proband] if proband else None, networks)
|
|
278
|
+
log_priors = {m: float(np.log(priors[m])) for m in modes if priors[m] > 0}
|
|
279
|
+
|
|
280
|
+
scores = [VariantScore(v, None) for v in variants]
|
|
281
|
+
errors = [mendelian_errors(pedigree, v) for v in variants]
|
|
282
|
+
for k, v in enumerate(variants):
|
|
283
|
+
_describe(pedigree, v, scores[k])
|
|
284
|
+
scores[k].mendelian_errors = errors[k]
|
|
285
|
+
scores[k].de_novo = de_novo(pedigree, v)
|
|
286
|
+
|
|
287
|
+
# the explanation with the variant as the cause, for each mode that suits its chromosome
|
|
288
|
+
for x in (False, True):
|
|
289
|
+
chosen = [k for k, v in enumerate(variants)
|
|
290
|
+
if v.x_linked in (x, None) and not v.unmodelled and scores[k].genotyped]
|
|
291
|
+
suited = [m for m in modes if X_LINKED[m] == x and priors[m] > 0]
|
|
292
|
+
if not chosen or not suited:
|
|
293
|
+
continue
|
|
294
|
+
if x not in networks:
|
|
295
|
+
networks[x] = Network(pedigree, x)
|
|
296
|
+
for start in range(0, len(chosen), chunk):
|
|
297
|
+
batch = chosen[start:start + chunk]
|
|
298
|
+
_score_batch(pedigree, networks[x], [variants[k] for k in batch], [scores[k] for k in batch], suited,
|
|
299
|
+
models, pens, log_w, variant_frequency, min_frequency, error_rate, log_without)
|
|
300
|
+
|
|
301
|
+
# pairs of variants in one gene, as compound heterozygotes
|
|
302
|
+
pairs = []
|
|
303
|
+
if compound:
|
|
304
|
+
allowed = {x: [m for m in PAIR_MODES if m in modes and priors[m] > 0 and x in (X_LINKED[m], None)]
|
|
305
|
+
for x in (False, True, None)}
|
|
306
|
+
pairs = [(i, j) for i, j in candidate_pairs(pedigree, variants, proband) if allowed[variants[i].x_linked]]
|
|
307
|
+
pair_scores = []
|
|
308
|
+
for i, j in pairs:
|
|
309
|
+
score = VariantScore(variants[i], None, partner=variants[j], mendelian_errors=errors[i] + errors[j],
|
|
310
|
+
de_novo=sorted(set(scores[i].de_novo) | set(scores[j].de_novo)))
|
|
311
|
+
_describe_pair(pedigree, variants[i], variants[j], score)
|
|
312
|
+
pair_scores.append(score)
|
|
313
|
+
left_out = set()
|
|
314
|
+
for x in (False, True):
|
|
315
|
+
chosen = [k for k, (i, j) in enumerate(pairs) if variants[i].x_linked in (x, None)]
|
|
316
|
+
suited = [m for m in PAIR_MODES if m in modes and X_LINKED[m] == x and priors[m] > 0]
|
|
317
|
+
if not chosen or not suited:
|
|
318
|
+
continue
|
|
319
|
+
try:
|
|
320
|
+
net = Network(pedigree, x, PairLocus())
|
|
321
|
+
except ValueError as error:
|
|
322
|
+
warnings.warn(f'Pairs of variants in one gene are left out: {error} for a pair (single variants are '
|
|
323
|
+
'still scored)')
|
|
324
|
+
left_out.update(chosen)
|
|
325
|
+
continue
|
|
326
|
+
for start in range(0, len(chosen), chunk):
|
|
327
|
+
batch = chosen[start:start + chunk]
|
|
328
|
+
both = [(variants[pairs[k][0]], variants[pairs[k][1]]) for k in batch]
|
|
329
|
+
frequencies = [(_frequency(a, variant_frequency, min_frequency), _frequency(b, variant_frequency, min_frequency))
|
|
330
|
+
for a, b in both]
|
|
331
|
+
logs = pair_log_likelihoods(pedigree, net, both, frequencies, suited, models, pens, log_w, error_rate)
|
|
332
|
+
for mode, values in logs.items():
|
|
333
|
+
for k, value in zip(batch, values):
|
|
334
|
+
_set_mode(pair_scores[k], mode, float(value), log_without[mode])
|
|
335
|
+
pair_scores = [p for k, p in enumerate(pair_scores) if k not in left_out]
|
|
336
|
+
|
|
337
|
+
groups: Dict[Tuple[str, ...], List[VariantScore]] = {}
|
|
338
|
+
for s in scores + pair_scores:
|
|
339
|
+
if s.variant.unmodelled:
|
|
340
|
+
s.note = 'Y-linked and mitochondrial inheritance aren\'t modelled'
|
|
341
|
+
elif not s.genotyped:
|
|
342
|
+
s.note = 'no one in the family was genotyped for it'
|
|
343
|
+
elif s.impossible_modes and not s.log10_lr:
|
|
344
|
+
s.note = _IMPOSSIBLE
|
|
345
|
+
elif not s.log10_lr:
|
|
346
|
+
s.note = 'none of the modes allowed suits its chromosome'
|
|
347
|
+
else:
|
|
348
|
+
if s.impossible_modes:
|
|
349
|
+
# a variant of unknown chromosome whose genotypes rule out the X, say: the other modes remain
|
|
350
|
+
s.note = (f'{" and ".join(s.impossible_modes)} {"is" if len(s.impossible_modes) == 1 else "are"} '
|
|
351
|
+
'left out, since its genotypes can\'t have been passed down that way without a genotyping '
|
|
352
|
+
'error, and the error rate is 0')
|
|
353
|
+
groups.setdefault(tuple(s.log10_lr), []).append(s)
|
|
354
|
+
for suited, group in groups.items():
|
|
355
|
+
_combine_modes(group, suited, log_priors, log_without)
|
|
356
|
+
for s in group:
|
|
357
|
+
if s.best_mode is None:
|
|
358
|
+
continue
|
|
359
|
+
if s.partner is None:
|
|
360
|
+
s.concordance = _concordance(pedigree, s.variant, s.best_mode)
|
|
361
|
+
else:
|
|
362
|
+
s.concordance = _pair_concordance(pedigree, s.variant, s.partner)
|
|
363
|
+
_withhold_evidence(pedigree, scores, pair_scores)
|
|
364
|
+
scores = scores + pair_scores
|
|
365
|
+
for s, r in zip(scores, fair_ranks([s.log10_odds for s in scores])):
|
|
366
|
+
s.rank = r
|
|
367
|
+
scores.sort(key=lambda s: (s.rank is None, s.rank if s.rank is not None else 0))
|
|
368
|
+
return scores
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _set_mode(score: 'VariantScore', mode: str, log_with: float, log_without: float):
|
|
372
|
+
"""
|
|
373
|
+
Records the log likelihood of the phenotypes under a mode with the candidate as the cause, and the ratio. A
|
|
374
|
+
likelihood of NaN comes from genotypes that have no probability at all under the mode (0 / 0), which only an error
|
|
375
|
+
rate of 0 and a Mendelian error can give: the mode is then left out for the candidate (see impossible_modes).
|
|
376
|
+
"""
|
|
377
|
+
if log_with != log_with:
|
|
378
|
+
if mode not in score.impossible_modes:
|
|
379
|
+
score.impossible_modes.append(mode)
|
|
380
|
+
return
|
|
381
|
+
score.log_likelihood[mode] = log_with
|
|
382
|
+
with np.errstate(invalid='ignore'):
|
|
383
|
+
ratio = (log_with - log_without) / LN10
|
|
384
|
+
score.log10_lr[mode] = float(ratio) if ratio == ratio else float('-inf')
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _combine_modes(group: List['VariantScore'], suited: Sequence[str], log_priors: Dict[str, float],
|
|
388
|
+
log_without: Dict[str, float]):
|
|
389
|
+
"""
|
|
390
|
+
The Bayes factor and odds of candidates whose likelihoods are for the modes suited: the probability of the
|
|
391
|
+
phenotypes with the candidate as the cause, averaged over those modes by their priors, over the same without it.
|
|
392
|
+
A mode that can't explain the phenotypes without the candidate adds nothing to the second, rather than making the
|
|
393
|
+
likelihood ratio infinite.
|
|
394
|
+
"""
|
|
395
|
+
suited = [m for m in suited if m in log_priors]
|
|
396
|
+
log_all = _logsumexp([log_priors[m] + log_without[m] for m in log_priors])
|
|
397
|
+
log_suited = _logsumexp([log_priors[m] + log_without[m] for m in suited]) if suited else -np.inf
|
|
398
|
+
if not np.isfinite(log_suited) or not np.isfinite(log_all):
|
|
399
|
+
for s in group:
|
|
400
|
+
s.note = s.note or 'the phenotypes rule out every mode that suits its chromosome'
|
|
401
|
+
return
|
|
402
|
+
priors = np.array([log_priors[m] for m in suited])
|
|
403
|
+
for s in group:
|
|
404
|
+
with_modes = priors + np.array([s.log_likelihood[m] for m in suited])
|
|
405
|
+
value = _logsumexp(with_modes)
|
|
406
|
+
if not np.isfinite(value):
|
|
407
|
+
s.note = s.note or 'its genotypes can\'t give these phenotypes under any mode that suits it'
|
|
408
|
+
s.log10_bf = s.log10_odds = float('-inf')
|
|
409
|
+
s.mode_probability = float(np.exp(log_suited - log_all))
|
|
410
|
+
s.best_mode = suited[int(np.argmax(with_modes))]
|
|
411
|
+
continue
|
|
412
|
+
s.log10_bf = float((value - log_suited) / LN10)
|
|
413
|
+
s.mode_probability = float(np.exp(log_suited - log_all))
|
|
414
|
+
s.log10_odds = float((value - log_all) / LN10)
|
|
415
|
+
s.best_mode = suited[int(np.argmax(with_modes))]
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _withhold_evidence(pedigree: Pedigree, scores: List['VariantScore'], pair_scores: List['VariantScore']):
|
|
419
|
+
"""Leaves out the strength of evidence where it would mislead (see VariantScore)"""
|
|
420
|
+
best_pair: Dict[int, 'VariantScore'] = {}
|
|
421
|
+
for p in pair_scores:
|
|
422
|
+
if p.log10_bf is None:
|
|
423
|
+
continue
|
|
424
|
+
for member in (p.variant, p.partner):
|
|
425
|
+
known = best_pair.get(id(member))
|
|
426
|
+
if known is None or p.log10_bf > known.log10_bf:
|
|
427
|
+
best_pair[id(member)] = p
|
|
428
|
+
for s in scores:
|
|
429
|
+
if s.log10_bf is None:
|
|
430
|
+
continue
|
|
431
|
+
pair = best_pair.get(id(s.variant))
|
|
432
|
+
if _new_in_affected(pedigree, s):
|
|
433
|
+
s.evidence_withheld = _NEW
|
|
434
|
+
elif pair is not None and evidence_strength(pair.log10_bf).startswith('PP1') and pair.log10_bf > s.log10_bf:
|
|
435
|
+
# the pair is evidence for co-segregation, and better evidence than the variant alone
|
|
436
|
+
s.evidence_withheld = f'judge it with its pair, {pair.id} (log10 BF {pair.log10_bf:.2f})'
|
|
437
|
+
for p in pair_scores:
|
|
438
|
+
if p.log10_bf is not None and _new_in_affected(pedigree, p):
|
|
439
|
+
p.evidence_withheld = _NEW
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
_NEW = 'co-segregation can\'t judge a variant new in an affected person: see PS2 and PM6 instead'
|
|
443
|
+
_IMPOSSIBLE = ('its genotypes can\'t have been passed down without a genotyping error, and the error rate is 0, so it '
|
|
444
|
+
'isn\'t scored')
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _new_in_affected(pedigree: Pedigree, s: 'VariantScore') -> bool:
|
|
448
|
+
"""
|
|
449
|
+
Whether the candidate may be new in an affected person, and its genotypes are otherwise consistent: no other
|
|
450
|
+
Mendelian errors, and no affected person genotyped without it. Then the Bayes factor, which counts the new
|
|
451
|
+
mutation against it, says nothing about whether it causes the condition. An affected relative without it is
|
|
452
|
+
evidence against it whatever its origin.
|
|
453
|
+
"""
|
|
454
|
+
return (any(pedigree[pid].affected for pid in s.de_novo) and s.mendelian_errors <= len(s.de_novo)
|
|
455
|
+
and s.affected_noncarriers == 0)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def candidate_pairs(pedigree: Pedigree, variants: Sequence[Variant],
|
|
459
|
+
proband: Optional[str] = None) -> List[Tuple[int, int]]:
|
|
460
|
+
"""
|
|
461
|
+
The pairs of variants in one gene, as pairs of positions in variants, that an affected person (the proband, if
|
|
462
|
+
given) carries both of; on the X, an affected person who isn't a man, since a man has only one copy
|
|
463
|
+
"""
|
|
464
|
+
who = [proband] if proband else pedigree.affected()
|
|
465
|
+
by_gene: Dict[str, List[int]] = {}
|
|
466
|
+
for k, v in enumerate(variants):
|
|
467
|
+
if v.gene:
|
|
468
|
+
by_gene.setdefault(v.gene, []).append(k)
|
|
469
|
+
pairs = []
|
|
470
|
+
for ks in by_gene.values():
|
|
471
|
+
for a in range(len(ks)):
|
|
472
|
+
for b in range(a + 1, len(ks)):
|
|
473
|
+
v, w = variants[ks[a]], variants[ks[b]]
|
|
474
|
+
if v.x_linked != w.x_linked or v.unmodelled or w.unmodelled:
|
|
475
|
+
continue
|
|
476
|
+
for pid in who:
|
|
477
|
+
if pid not in pedigree or (v.x_linked and pedigree[pid].sex == MALE):
|
|
478
|
+
continue
|
|
479
|
+
if (v.genotypes.get(pid) or 0) >= 1 and (w.genotypes.get(pid) or 0) >= 1:
|
|
480
|
+
pairs.append((ks[a], ks[b]))
|
|
481
|
+
break
|
|
482
|
+
return pairs
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def _describe(pedigree: Pedigree, v: Variant, s: VariantScore):
|
|
486
|
+
for pid, copies in v.genotypes.items():
|
|
487
|
+
if copies is None or pid not in pedigree:
|
|
488
|
+
continue
|
|
489
|
+
person = pedigree[pid]
|
|
490
|
+
s.genotyped += 1
|
|
491
|
+
carrier = copies >= 1
|
|
492
|
+
if person.affected:
|
|
493
|
+
if carrier:
|
|
494
|
+
s.affected_carriers += 1
|
|
495
|
+
else:
|
|
496
|
+
s.affected_noncarriers += 1
|
|
497
|
+
elif person.unaffected:
|
|
498
|
+
if carrier:
|
|
499
|
+
s.unaffected_carriers += 1
|
|
500
|
+
else:
|
|
501
|
+
s.unaffected_noncarriers += 1
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _describe_pair(pedigree: Pedigree, v: Variant, w: Variant, s: VariantScore):
|
|
505
|
+
for pid in pedigree.ids:
|
|
506
|
+
a, b = v.genotypes.get(pid), w.genotypes.get(pid)
|
|
507
|
+
if a is None and b is None:
|
|
508
|
+
continue
|
|
509
|
+
s.genotyped += 1
|
|
510
|
+
if a is None or b is None:
|
|
511
|
+
continue
|
|
512
|
+
person = pedigree[pid]
|
|
513
|
+
carrier = has_disease_genotype(a, b, v.x_linked and person.sex == MALE)
|
|
514
|
+
if person.affected:
|
|
515
|
+
if carrier:
|
|
516
|
+
s.affected_carriers += 1
|
|
517
|
+
else:
|
|
518
|
+
s.affected_noncarriers += 1
|
|
519
|
+
elif person.unaffected:
|
|
520
|
+
if carrier:
|
|
521
|
+
s.unaffected_carriers += 1
|
|
522
|
+
else:
|
|
523
|
+
s.unaffected_noncarriers += 1
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def _pair_concordance(pedigree: Pedigree, v: Variant, w: Variant) -> Optional[float]:
|
|
527
|
+
fits = total = 0
|
|
528
|
+
for pid in pedigree.ids:
|
|
529
|
+
a, b = v.genotypes.get(pid), w.genotypes.get(pid)
|
|
530
|
+
person = pedigree[pid]
|
|
531
|
+
if a is None or b is None or person.phenotype not in (1, 2):
|
|
532
|
+
continue
|
|
533
|
+
total += 1
|
|
534
|
+
fits += has_disease_genotype(a, b, v.x_linked and person.sex == MALE) == person.affected
|
|
535
|
+
return fits / total if total else None
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def de_novo(pedigree: Pedigree, v: Variant) -> List[str]:
|
|
539
|
+
"""
|
|
540
|
+
The genotyped children who carry the variant though both their parents were genotyped without it; on the X, a
|
|
541
|
+
son who carries it though his mother was genotyped without it
|
|
542
|
+
"""
|
|
543
|
+
found = []
|
|
544
|
+
x = v.x_linked
|
|
545
|
+
if v.unmodelled:
|
|
546
|
+
return found
|
|
547
|
+
for p in pedigree.people:
|
|
548
|
+
child = v.genotypes.get(p.id)
|
|
549
|
+
if not child or p.father is None:
|
|
550
|
+
continue
|
|
551
|
+
father, mother = v.genotypes.get(p.father), v.genotypes.get(p.mother)
|
|
552
|
+
if x and p.sex == MALE:
|
|
553
|
+
if mother == 0:
|
|
554
|
+
found.append(p.id)
|
|
555
|
+
elif father == 0 and mother == 0:
|
|
556
|
+
found.append(p.id)
|
|
557
|
+
return found
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def mendelian_errors(pedigree: Pedigree, v: Variant) -> int:
|
|
561
|
+
"""The genotyped children whose genotype can't have come from their genotyped parents"""
|
|
562
|
+
if v.unmodelled:
|
|
563
|
+
return 0
|
|
564
|
+
x = v.x_linked
|
|
565
|
+
errors = 0
|
|
566
|
+
for p in pedigree.people:
|
|
567
|
+
child = v.genotypes.get(p.id)
|
|
568
|
+
if child is None or p.father is None:
|
|
569
|
+
continue
|
|
570
|
+
father, mother = v.genotypes.get(p.father), v.genotypes.get(p.mother)
|
|
571
|
+
if x:
|
|
572
|
+
# a son has only his mother's X; a daughter has her father's (all of it) and one of her mother's
|
|
573
|
+
if p.sex == 1:
|
|
574
|
+
if mother is not None and ((mother == 0 and child >= 1) or (mother == 2 and child == 0)):
|
|
575
|
+
errors += 1
|
|
576
|
+
continue
|
|
577
|
+
if p.sex != 2:
|
|
578
|
+
continue
|
|
579
|
+
low = (1 if father is not None and father >= 1 else 0) + (1 if mother == 2 else 0)
|
|
580
|
+
high = (0 if father == 0 else 1) + (0 if mother == 0 else 1)
|
|
581
|
+
else:
|
|
582
|
+
low = (1 if father == 2 else 0) + (1 if mother == 2 else 0)
|
|
583
|
+
high = (0 if father == 0 else 1) + (0 if mother == 0 else 1)
|
|
584
|
+
if not low <= child <= high:
|
|
585
|
+
errors += 1
|
|
586
|
+
return errors
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def _concordance(pedigree: Pedigree, v: Variant, mode: str) -> Optional[float]:
|
|
590
|
+
fits = total = 0
|
|
591
|
+
for pid, copies in v.genotypes.items():
|
|
592
|
+
if copies is None or pid not in pedigree:
|
|
593
|
+
continue
|
|
594
|
+
person = pedigree[pid]
|
|
595
|
+
if person.phenotype not in (1, 2):
|
|
596
|
+
continue
|
|
597
|
+
male = person.sex == 1 if person.sex in (1, 2) else None
|
|
598
|
+
total += 1
|
|
599
|
+
fits += _fits(mode, copies, male, person.affected)
|
|
600
|
+
return fits / total if total else None
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def _score_batch(pedigree, net, variants, scores, modes, models, pens, log_w, variant_frequency, min_frequency,
|
|
604
|
+
error_rate, log_without):
|
|
605
|
+
V, F = len(variants), len(pens)
|
|
606
|
+
q = np.array([_frequency(v, variant_frequency, min_frequency) for v in variants])
|
|
607
|
+
evidence = []
|
|
608
|
+
for i, (kind, person) in enumerate(zip(net.kinds, pedigree.people)):
|
|
609
|
+
e = genotype_table(kind, error_rate)[call_codes(variants, person.id)]
|
|
610
|
+
if net.parents[i] is None:
|
|
611
|
+
e = e * founder_prior(kind, q)
|
|
612
|
+
evidence.append(e)
|
|
613
|
+
# P(genotypes), which doesn't depend on the mode except through the chromosome
|
|
614
|
+
log_genotypes = net.log_likelihood(evidence)
|
|
615
|
+
for mode in modes:
|
|
616
|
+
model = models[mode]
|
|
617
|
+
unary = []
|
|
618
|
+
for i, (kind, person) in enumerate(zip(net.kinds, pedigree.people)):
|
|
619
|
+
phen = np.stack([phenotype_factor(model.with_(penetrance=f), kind, person.phenotype) for f in pens])
|
|
620
|
+
unary.append((phen[:, None, :] * evidence[i][None, :, :]).reshape(F * V, -1))
|
|
621
|
+
joint = net.log_likelihood(unary).reshape(F, V)
|
|
622
|
+
with np.errstate(invalid='ignore'):
|
|
623
|
+
# NaN where the genotypes have no probability at all (see _set_mode)
|
|
624
|
+
with_variant = _logsumexp(joint + log_w[:, None], axis=0) - log_genotypes
|
|
625
|
+
for s, value in zip(scores, with_variant):
|
|
626
|
+
_set_mode(s, mode, float(value), log_without[mode])
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
@dataclass
|
|
630
|
+
class CombinedScore:
|
|
631
|
+
"""
|
|
632
|
+
A candidate's evidence from several families with the same condition, over the families in which an affected
|
|
633
|
+
person (the family's proband, if given) carries it, whose scores are in families. A family in which no affected
|
|
634
|
+
person carries it counts only towards the mode of inheritance, since the condition may have another cause there.
|
|
635
|
+
|
|
636
|
+
log10_bf is the log10 Bayes factor over those families together, with the mode of inheritance shared between
|
|
637
|
+
them. With one mode allowed it is the sum of the families' log10 Bayes factors. With several, each mode is
|
|
638
|
+
weighted by how well it explains all the families together, so the evidence can be more than the sum when the
|
|
639
|
+
families agree on the mode and less when they point to different modes, and each family's own log10_bf (in
|
|
640
|
+
families) is for the modes weighted by that family alone. A mode under which the candidate's genotypes are
|
|
641
|
+
impossible in one family (with an error rate of 0) is left out for all of them. mode_probability is the
|
|
642
|
+
probability, from all the families' phenotypes, that the mode is one that suits the candidate, and log10_odds is
|
|
643
|
+
log10_bf + log10(mode_probability), as for VariantScore. log10_bf is None, and note says why, when the candidate
|
|
644
|
+
can't be scored.
|
|
645
|
+
"""
|
|
646
|
+
id: str
|
|
647
|
+
log10_bf: Optional[float]
|
|
648
|
+
families: Dict[str, VariantScore]
|
|
649
|
+
rank: Optional[float] = None
|
|
650
|
+
log10_odds: Optional[float] = None
|
|
651
|
+
mode_probability: Optional[float] = None
|
|
652
|
+
evidence_withheld: str = ''
|
|
653
|
+
note: str = ''
|
|
654
|
+
|
|
655
|
+
@property
|
|
656
|
+
def evidence(self) -> str:
|
|
657
|
+
if self.evidence_withheld or self.log10_bf != self.log10_bf:
|
|
658
|
+
return ''
|
|
659
|
+
return evidence_strength(self.log10_bf)
|
|
660
|
+
|
|
661
|
+
@property
|
|
662
|
+
def compound(self) -> bool:
|
|
663
|
+
return next(iter(self.families.values())).compound
|
|
664
|
+
|
|
665
|
+
def row(self) -> dict:
|
|
666
|
+
first = next(iter(self.families.values()))
|
|
667
|
+
row = {'rank': self.rank, 'candidate': self.id, 'kind': 'compound heterozygote' if self.compound else 'variant',
|
|
668
|
+
'gene': first.variant.gene, 'log10_bf': self.log10_bf, 'evidence': self.evidence,
|
|
669
|
+
'log10_odds': self.log10_odds, 'mode_probability': self.mode_probability,
|
|
670
|
+
'families': ','.join(self.families)}
|
|
671
|
+
for name, s in self.families.items():
|
|
672
|
+
row[f'log10_bf_{name}'] = s.log10_bf
|
|
673
|
+
row.update(evidence_withheld=self.evidence_withheld, note=self.note)
|
|
674
|
+
return row
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def rank_variants_across(pedigrees, variants: Iterable[Variant], probands: Optional[Dict[str, str]] = None,
|
|
678
|
+
**options) -> List[CombinedScore]:
|
|
679
|
+
"""
|
|
680
|
+
Ranks candidates over several families with the same condition (see CombinedScore). The families are taken to
|
|
681
|
+
share one mode of inheritance: for each mode, the families' likelihoods are multiplied, and the modes are then
|
|
682
|
+
weighed as for a single family, by how well they explain all the families' phenotypes.
|
|
683
|
+
|
|
684
|
+
pedigrees: a dict of family to Pedigree, as read_ped gives, or a list of pedigrees
|
|
685
|
+
variants: the candidates, whose genotypes can cover the people of every family
|
|
686
|
+
probands: the proband of each family, if known (a dict of family to person)
|
|
687
|
+
options: passed on to rank_variants
|
|
688
|
+
"""
|
|
689
|
+
if not isinstance(pedigrees, dict):
|
|
690
|
+
pedigrees = {p.family: p for p in pedigrees}
|
|
691
|
+
variants = list(variants)
|
|
692
|
+
probands = probands or {}
|
|
693
|
+
modes = _modes(options.get('modes', MODES))
|
|
694
|
+
priors = _prior(modes, options.get('prior'))
|
|
695
|
+
log_priors = {m: float(np.log(priors[m])) for m in modes if priors[m] > 0}
|
|
696
|
+
# each family's likelihood of its phenotypes under each mode without the candidate, and its candidates' scores
|
|
697
|
+
without: Dict[str, Dict[str, float]] = {}
|
|
698
|
+
found: Dict[Tuple[int, int], Dict[str, VariantScore]] = {}
|
|
699
|
+
for name, pedigree in pedigrees.items():
|
|
700
|
+
proband = probands.get(name)
|
|
701
|
+
without[name] = mode_log_likelihoods(pedigree, modes, options.get('penetrance', DEFAULT_PENETRANCE),
|
|
702
|
+
options.get('penetrance_weights'), options.get('phenocopy', DEFAULT_PHENOCOPY),
|
|
703
|
+
options.get('allele_frequency'), options.get('carrier_penetrance'),
|
|
704
|
+
[proband] if proband else None)
|
|
705
|
+
who = [proband] if proband else pedigree.affected()
|
|
706
|
+
for s in rank_variants(pedigree, variants, proband=proband, **options):
|
|
707
|
+
if s.partner is None and not any((s.variant.genotypes.get(p) or 0) >= 1 for p in who):
|
|
708
|
+
continue
|
|
709
|
+
if s.note != _IMPOSSIBLE and (s.log10_bf is None or not s.log_likelihood):
|
|
710
|
+
continue
|
|
711
|
+
found.setdefault((id(s.variant), id(s.partner)), {})[name] = s
|
|
712
|
+
log_all = _logsumexp([log_priors[m] + sum(without[f][m] for f in pedigrees) for m in log_priors])
|
|
713
|
+
combined = []
|
|
714
|
+
for by_family in found.values():
|
|
715
|
+
first = next(iter(by_family.values()))
|
|
716
|
+
c = CombinedScore(first.id, None, by_family)
|
|
717
|
+
combined.append(c)
|
|
718
|
+
unscored = [f for f, s in by_family.items() if s.note == _IMPOSSIBLE]
|
|
719
|
+
if unscored:
|
|
720
|
+
c.note = (f'its genotypes in {", ".join(unscored)} can\'t have been passed down without a genotyping '
|
|
721
|
+
'error, and the error rate is 0, so it isn\'t scored')
|
|
722
|
+
continue
|
|
723
|
+
# the modes that suit the candidate in every family where it is scored: a mode under which its genotypes
|
|
724
|
+
# are impossible in one family (with an error rate of 0) is ruled out for them all
|
|
725
|
+
suited = [m for m in log_priors if all(m in s.log_likelihood for s in by_family.values())]
|
|
726
|
+
if not suited:
|
|
727
|
+
c.note = (f'its genotypes in {", ".join(by_family)} can\'t have been passed down under any one mode '
|
|
728
|
+
'without a genotyping error, and the error rate is 0, so it isn\'t scored')
|
|
729
|
+
continue
|
|
730
|
+
ruled_out = [m for m in log_priors if any(m in s.impossible_modes for s in by_family.values())]
|
|
731
|
+
if ruled_out:
|
|
732
|
+
where = [f for f, s in by_family.items() if s.impossible_modes]
|
|
733
|
+
c.note = (f'{" and ".join(ruled_out)} {"is" if len(ruled_out) == 1 else "are"} left out for every '
|
|
734
|
+
f'family, since its genotypes in {", ".join(where)} can\'t have been passed down that way '
|
|
735
|
+
'without a genotyping error, and the error rate is 0')
|
|
736
|
+
with_modes = [log_priors[m] + sum(by_family[f].log_likelihood[m] if f in by_family else without[f][m]
|
|
737
|
+
for f in pedigrees) for m in suited]
|
|
738
|
+
suited_without = [log_priors[m] + sum(without[f][m] for f in pedigrees) for m in suited]
|
|
739
|
+
log_with, log_suited = _logsumexp(with_modes), _logsumexp(suited_without)
|
|
740
|
+
if not (np.isfinite(log_suited) and np.isfinite(log_all)):
|
|
741
|
+
c.note = 'the families\' phenotypes rule out every mode that suits its chromosome'
|
|
742
|
+
else:
|
|
743
|
+
c.log10_bf = float((log_with - log_suited) / LN10)
|
|
744
|
+
c.mode_probability = float(np.exp(log_suited - log_all))
|
|
745
|
+
c.log10_odds = float((log_with - log_all) / LN10)
|
|
746
|
+
if any(s.evidence_withheld for s in by_family.values()):
|
|
747
|
+
c.evidence_withheld = next(s.evidence_withheld for s in by_family.values() if s.evidence_withheld)
|
|
748
|
+
for c, r in zip(combined, fair_ranks([c.log10_odds for c in combined])):
|
|
749
|
+
c.rank = r
|
|
750
|
+
combined.sort(key=lambda c: (c.rank is None, c.rank if c.rank is not None else 0))
|
|
751
|
+
return combined
|