allelign 0.1.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.
- allelign/__init__.py +69 -0
- allelign/__main__.py +5 -0
- allelign/_common.py +95 -0
- allelign/_numba.py +22 -0
- allelign/align.py +1252 -0
- allelign/alleles.py +90 -0
- allelign/build.py +57 -0
- allelign/catalog.py +88 -0
- allelign/cli.py +323 -0
- allelign/download.py +525 -0
- allelign/genotypes.py +241 -0
- allelign/ldcheck.py +771 -0
- allelign/qc.py +638 -0
- allelign/status.py +130 -0
- allelign/sumstats.py +1207 -0
- allelign/variants.py +155 -0
- allelign/warning_markers.py +15 -0
- allelign/weights.py +614 -0
- allelign-0.1.0.dist-info/METADATA +175 -0
- allelign-0.1.0.dist-info/RECORD +24 -0
- allelign-0.1.0.dist-info/WHEEL +5 -0
- allelign-0.1.0.dist-info/entry_points.txt +2 -0
- allelign-0.1.0.dist-info/licenses/LICENSE +21 -0
- allelign-0.1.0.dist-info/top_level.txt +1 -0
allelign/status.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Per-variant terminal QC status: one code per input row.
|
|
2
|
+
|
|
3
|
+
Every filter and the harmoniser can stamp a caller-allocated ``int8`` array with
|
|
4
|
+
the reason a row was dropped (0 = kept). The codes are the vocabulary of
|
|
5
|
+
ldpred3's ``--qc-out`` table, kept numerically identical so tables written by
|
|
6
|
+
either package can be read by the other's consumers.
|
|
7
|
+
|
|
8
|
+
* file-level filters (:func:`allelign.qc.qc_sumstats`): codes 1-7;
|
|
9
|
+
* harmonisation (:func:`allelign.align.harmonize`): codes 8-13;
|
|
10
|
+
* reference-dependent checks: 14-20 (16-19 are
|
|
11
|
+
:func:`~allelign.qc.af_concordance_mask` and
|
|
12
|
+
:func:`~allelign.qc.sd_consistency_mask`, 20 the LD-consistency screen);
|
|
13
|
+
* 21: identifier re-anchoring could not anchor the row.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
|
|
20
|
+
from ._common import atomic_text
|
|
21
|
+
|
|
22
|
+
__all__ = ["QC_REASON_NAMES", "new_status", "status_counts",
|
|
23
|
+
"write_status_table", "STATUS_HEADER"]
|
|
24
|
+
|
|
25
|
+
NONFINITE = 1 # beta/se non-finite, se <= 0, or N non-finite
|
|
26
|
+
NONPOSITIVE_N = 2 # N finite and <= 0 (beta/se valid)
|
|
27
|
+
DUPLICATE = 3 # id (or chrom:pos:alleles) appears more than once
|
|
28
|
+
LOW_N = 4 # n_eff < min_n_ratio * anchor
|
|
29
|
+
LOW_MAF = 5 # minor allele frequency below the floor
|
|
30
|
+
LOW_INFO = 6 # imputation INFO below the floor
|
|
31
|
+
CHISQ_OUTLIER = 7 # (beta/se)^2 above max_chisq
|
|
32
|
+
UNMATCHED = 8 # no reference row at this id or coordinate
|
|
33
|
+
AMBIGUOUS_ID = 9 # multi-locus id, coordinates do not resolve it
|
|
34
|
+
AMBIGUOUS_ALLELES = 10 # palindromic allele pair
|
|
35
|
+
DUPLICATE_MATCH = 11 # every candidate reference row already matched
|
|
36
|
+
ALLELE_MISMATCH = 12 # no orientation maps (ea, oa) onto (a1, a2)
|
|
37
|
+
NONFINITE_BETA_SE = 13 # matched, but beta/se non-finite
|
|
38
|
+
ABSENT_LD_CACHE = 14 # harmonised, but absent from the LD cache
|
|
39
|
+
LD_REFERENCE_INTERSECT = 15 # outside the target and LD-reference intersection
|
|
40
|
+
AF_DIFF = 16 # GWAS/reference allele-frequency gap too large
|
|
41
|
+
SD_LOW = 17 # sumstats-implied dosage SD too small
|
|
42
|
+
SD_HIGH = 18 # sumstats-implied dosage SD too large
|
|
43
|
+
SD_UNTESTABLE = 19 # non-finite SD or near-monomorphic reference
|
|
44
|
+
LD_INCONSISTENT = 20 # LD-consistency (DENTIST-style) outlier
|
|
45
|
+
REANCHOR_DROPPED = 21 # identifier not held once by the reference
|
|
46
|
+
|
|
47
|
+
#: Code -> reason name. 0 is "no drop". Names match ldpred3's table.
|
|
48
|
+
QC_REASON_NAMES = {
|
|
49
|
+
0: "",
|
|
50
|
+
NONFINITE: "nonfinite",
|
|
51
|
+
NONPOSITIVE_N: "nonpositive_n",
|
|
52
|
+
DUPLICATE: "duplicate",
|
|
53
|
+
LOW_N: "low_n",
|
|
54
|
+
LOW_MAF: "low_maf",
|
|
55
|
+
LOW_INFO: "low_info",
|
|
56
|
+
CHISQ_OUTLIER: "chisq_outlier",
|
|
57
|
+
UNMATCHED: "unmatched",
|
|
58
|
+
AMBIGUOUS_ID: "ambiguous_id",
|
|
59
|
+
AMBIGUOUS_ALLELES: "ambiguous_alleles",
|
|
60
|
+
DUPLICATE_MATCH: "duplicate_match",
|
|
61
|
+
ALLELE_MISMATCH: "allele_mismatch",
|
|
62
|
+
NONFINITE_BETA_SE: "nonfinite_beta_se",
|
|
63
|
+
ABSENT_LD_CACHE: "absent_ld_cache",
|
|
64
|
+
LD_REFERENCE_INTERSECT: "ld_reference_intersect",
|
|
65
|
+
AF_DIFF: "af_diff",
|
|
66
|
+
SD_LOW: "sd_low",
|
|
67
|
+
SD_HIGH: "sd_high",
|
|
68
|
+
SD_UNTESTABLE: "sd_untestable",
|
|
69
|
+
LD_INCONSISTENT: "dentist_outlier",
|
|
70
|
+
REANCHOR_DROPPED: "reanchor_dropped",
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
#: Columns of the status table. ``EA``/``OA`` are the alleles as read.
|
|
74
|
+
STATUS_HEADER = ("ID", "CHR", "POS", "EA", "OA", "STATUS", "REASON")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def new_status(n):
|
|
78
|
+
"""A zeroed ``int8`` status array for ``n`` input rows."""
|
|
79
|
+
return np.zeros(int(n), dtype=np.int8)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def check_status(status, n):
|
|
83
|
+
"""Validate a caller-allocated status array; returns it or ``None``."""
|
|
84
|
+
if status is None:
|
|
85
|
+
return None
|
|
86
|
+
status = np.asarray(status)
|
|
87
|
+
if status.shape != (n,) or status.dtype != np.int8:
|
|
88
|
+
raise ValueError(f"status must be an int8 array of length {n}")
|
|
89
|
+
return status
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def mark(status, code, bad):
|
|
93
|
+
"""Stamp rows that are still unmarked and fail ``bad`` with ``code``."""
|
|
94
|
+
if status is not None:
|
|
95
|
+
status[(status == 0) & np.asarray(bad, dtype=bool)] = code
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def status_counts(codes):
|
|
99
|
+
"""``{reason name or 'kept': count}`` over a status array."""
|
|
100
|
+
codes = np.asarray(codes)
|
|
101
|
+
out = {}
|
|
102
|
+
values, counts = np.unique(codes, return_counts=True)
|
|
103
|
+
for value, count in zip(values, counts):
|
|
104
|
+
name = QC_REASON_NAMES.get(int(value))
|
|
105
|
+
if name is None:
|
|
106
|
+
raise ValueError(f"unknown QC status code {int(value)}")
|
|
107
|
+
out[name or "kept"] = int(count)
|
|
108
|
+
return out
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def write_status_table(path, *, id, chrom, pos, ea, oa, codes):
|
|
112
|
+
"""Write one TSV row per input variant with its terminal status.
|
|
113
|
+
|
|
114
|
+
Columns ``ID CHR POS EA OA STATUS REASON`` in input order; ``STATUS`` is
|
|
115
|
+
``kept`` or ``dropped`` and ``REASON`` names the stage that dropped the
|
|
116
|
+
row. Published atomically; a ``.gz`` path is compressed. Returns ``path``.
|
|
117
|
+
"""
|
|
118
|
+
n = len(np.asarray(codes))
|
|
119
|
+
for name, values in {"id": id, "chrom": chrom, "pos": pos, "ea": ea,
|
|
120
|
+
"oa": oa}.items():
|
|
121
|
+
if len(np.asarray(values)) != n:
|
|
122
|
+
raise ValueError(f"{name} must have one value per input row ({n})")
|
|
123
|
+
with atomic_text(path) as fh:
|
|
124
|
+
fh.write("\t".join(STATUS_HEADER) + "\n")
|
|
125
|
+
for vid, c, p, a1, a2, code in zip(id, chrom, pos, ea, oa, codes):
|
|
126
|
+
code = int(code)
|
|
127
|
+
status = "kept" if code == 0 else "dropped"
|
|
128
|
+
reason = QC_REASON_NAMES[code] if code else "."
|
|
129
|
+
fh.write(f"{vid}\t{c}\t{int(p)}\t{a1}\t{a2}\t{status}\t{reason}\n")
|
|
130
|
+
return path
|