statgen 0.2.1__tar.gz → 0.2.2__tar.gz
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.
- {statgen-0.2.1 → statgen-0.2.2}/PKG-INFO +1 -1
- {statgen-0.2.1 → statgen-0.2.2}/pyproject.toml +1 -1
- statgen-0.2.2/statgen/__init__.py +1 -0
- statgen-0.2.2/statgen/_utils.py +77 -0
- {statgen-0.2.1 → statgen-0.2.2}/statgen/reference.py +103 -30
- {statgen-0.2.1 → statgen-0.2.2}/statgen/sumstats.py +105 -41
- {statgen-0.2.1 → statgen-0.2.2}/statgen.egg-info/PKG-INFO +1 -1
- statgen-0.2.1/statgen/__init__.py +0 -1
- statgen-0.2.1/statgen/_utils.py +0 -30
- {statgen-0.2.1 → statgen-0.2.2}/README.md +0 -0
- {statgen-0.2.1 → statgen-0.2.2}/setup.cfg +0 -0
- {statgen-0.2.1 → statgen-0.2.2}/statgen/_ld_npz.py +0 -0
- {statgen-0.2.1 → statgen-0.2.2}/statgen/_ld_reference.py +0 -0
- {statgen-0.2.1 → statgen-0.2.2}/statgen/_ld_schema.py +0 -0
- {statgen-0.2.1 → statgen-0.2.2}/statgen/_ld_writer.py +0 -0
- {statgen-0.2.1 → statgen-0.2.2}/statgen/annotations.py +0 -0
- {statgen-0.2.1 → statgen-0.2.2}/statgen/genotype.py +0 -0
- {statgen-0.2.1 → statgen-0.2.2}/statgen/ld.py +0 -0
- {statgen-0.2.1 → statgen-0.2.2}/statgen.egg-info/SOURCES.txt +0 -0
- {statgen-0.2.1 → statgen-0.2.2}/statgen.egg-info/dependency_links.txt +0 -0
- {statgen-0.2.1 → statgen-0.2.2}/statgen.egg-info/requires.txt +0 -0
- {statgen-0.2.1 → statgen-0.2.2}/statgen.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.2"
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
CANONICAL_CHR_ORDER = [str(i) for i in range(1, 23)] + ["X"]
|
|
6
|
+
CHR_RANK = {c: i for i, c in enumerate(CANONICAL_CHR_ORDER)}
|
|
7
|
+
_ALLELE_HASH_P = 2_147_483_647
|
|
8
|
+
_ALLELE_HASH_BASE1 = 257
|
|
9
|
+
_ALLELE_HASH_BASE2 = 263
|
|
10
|
+
_ALLELE_HASH_MAX_CHARS = 150
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def validate_requested_shards(shards, available_labels, where: str) -> list[str]:
|
|
14
|
+
labels = list(available_labels)
|
|
15
|
+
if shards is None:
|
|
16
|
+
return labels
|
|
17
|
+
if isinstance(shards, str):
|
|
18
|
+
raise ValueError(f"{where}: shards must be a non-empty list of unique canonical contig labels")
|
|
19
|
+
|
|
20
|
+
requested = list(shards)
|
|
21
|
+
if not requested:
|
|
22
|
+
raise ValueError(f"{where}: shards must be a non-empty list of unique canonical contig labels")
|
|
23
|
+
|
|
24
|
+
seen = set()
|
|
25
|
+
prev_rank = None
|
|
26
|
+
for label in requested:
|
|
27
|
+
if label not in CHR_RANK:
|
|
28
|
+
raise ValueError(f"{where}: unsupported shard label {label!r}; expected canonical labels 1-22 or X")
|
|
29
|
+
if label in seen:
|
|
30
|
+
raise ValueError(f"{where}: duplicate shard label {label!r} in shards")
|
|
31
|
+
rank = CHR_RANK[label]
|
|
32
|
+
if prev_rank is not None and rank <= prev_rank:
|
|
33
|
+
raise ValueError(f"{where}: shards must be in canonical subsequence order")
|
|
34
|
+
seen.add(label)
|
|
35
|
+
prev_rank = rank
|
|
36
|
+
if label not in labels:
|
|
37
|
+
raise ValueError(f"{where}: requested shard {label!r} is not present")
|
|
38
|
+
return requested
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def allele_hash64(alleles) -> np.ndarray:
|
|
42
|
+
arr = np.asarray(alleles, dtype=object).reshape(-1)
|
|
43
|
+
n = arr.size
|
|
44
|
+
if n == 0:
|
|
45
|
+
return np.array([], dtype=np.uint64)
|
|
46
|
+
|
|
47
|
+
strings = [str(allele) for allele in arr]
|
|
48
|
+
lengths = np.fromiter((len(s) for s in strings), dtype=np.int64, count=n)
|
|
49
|
+
if np.any(lengths > _ALLELE_HASH_MAX_CHARS):
|
|
50
|
+
warnings.warn(
|
|
51
|
+
"Allele length exceeds 150 characters; hashing uses first 150 characters",
|
|
52
|
+
RuntimeWarning,
|
|
53
|
+
stacklevel=2,
|
|
54
|
+
)
|
|
55
|
+
strings = [s[:_ALLELE_HASH_MAX_CHARS] for s in strings]
|
|
56
|
+
lengths = np.minimum(lengths, _ALLELE_HASH_MAX_CHARS)
|
|
57
|
+
|
|
58
|
+
encoded = [s.encode("utf-8") for s in strings]
|
|
59
|
+
byte_lengths = np.fromiter((len(b) for b in encoded), dtype=np.int64, count=n)
|
|
60
|
+
max_len = int(byte_lengths.max(initial=0))
|
|
61
|
+
mat = np.zeros((n, max_len), dtype=np.uint8)
|
|
62
|
+
if max_len:
|
|
63
|
+
flat = np.frombuffer(b"".join(encoded), dtype=np.uint8)
|
|
64
|
+
row_idx = np.repeat(np.arange(n, dtype=np.int64), byte_lengths)
|
|
65
|
+
starts = np.cumsum(byte_lengths, dtype=np.int64) - byte_lengths
|
|
66
|
+
col_idx = np.arange(flat.size, dtype=np.int64) - np.repeat(starts, byte_lengths)
|
|
67
|
+
mat[row_idx, col_idx] = flat
|
|
68
|
+
|
|
69
|
+
h1 = np.ones(n, dtype=np.int64)
|
|
70
|
+
h2 = np.ones(n, dtype=np.int64)
|
|
71
|
+
for j in range(max_len):
|
|
72
|
+
active = j < byte_lengths
|
|
73
|
+
x = mat[active, j].astype(np.int64) + 1
|
|
74
|
+
h1[active] = (h1[active] * _ALLELE_HASH_BASE1 + x) % _ALLELE_HASH_P
|
|
75
|
+
h2[active] = (h2[active] * _ALLELE_HASH_BASE2 + x) % _ALLELE_HASH_P
|
|
76
|
+
|
|
77
|
+
return (h1.astype(np.uint64) << np.uint64(32)) + h2.astype(np.uint64)
|
|
@@ -8,7 +8,7 @@ import numpy as np
|
|
|
8
8
|
import pandas as pd
|
|
9
9
|
from pandas.errors import ParserError
|
|
10
10
|
|
|
11
|
-
from ._utils import CANONICAL_CHR_ORDER, CHR_RANK, validate_requested_shards
|
|
11
|
+
from ._utils import CANONICAL_CHR_ORDER, CHR_RANK, allele_hash64, validate_requested_shards
|
|
12
12
|
|
|
13
13
|
logger = logging.getLogger(__name__)
|
|
14
14
|
|
|
@@ -16,8 +16,6 @@ _CACHE_SCHEMA = "reference_cache/0.1"
|
|
|
16
16
|
_BIM_NCOLS = 6
|
|
17
17
|
_IGNORED_CHR = {"Y", "MT"}
|
|
18
18
|
_DNA_ALLELE_RE = re.compile(r"^[ACGT]+$")
|
|
19
|
-
|
|
20
|
-
|
|
21
19
|
def _validate_reference_sort_order(
|
|
22
20
|
chr_col: pd.Series,
|
|
23
21
|
bp_col: np.ndarray,
|
|
@@ -34,23 +32,21 @@ def _validate_reference_sort_order(
|
|
|
34
32
|
f"{path}:{idx + 1}: chr must use canonical labels 1-22 or X"
|
|
35
33
|
)
|
|
36
34
|
|
|
37
|
-
order_df = pd.DataFrame(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
)
|
|
35
|
+
order_df = pd.DataFrame({
|
|
36
|
+
"chr_rank": chr_rank.to_numpy(dtype=np.int64),
|
|
37
|
+
"bp": np.asarray(bp_col, dtype=np.int64),
|
|
38
|
+
"a1_hash64": allele_hash64(a1_col.to_numpy(dtype=object)),
|
|
39
|
+
"a2_hash64": allele_hash64(a2_col.to_numpy(dtype=object)),
|
|
40
|
+
"line": np.asarray(line_numbers, dtype=np.int64),
|
|
41
|
+
})
|
|
46
42
|
|
|
47
43
|
dup_mask = order_df.duplicated(
|
|
48
|
-
subset=["chr_rank", "bp", "
|
|
44
|
+
subset=["chr_rank", "bp", "a1_hash64", "a2_hash64"], keep="first"
|
|
49
45
|
)
|
|
50
46
|
if dup_mask.any():
|
|
51
47
|
line = int(order_df.loc[dup_mask.idxmax(), "line"])
|
|
52
48
|
raise ValueError(
|
|
53
|
-
f"{path}:{line}: duplicate (chr, bp,
|
|
49
|
+
f"{path}:{line}: duplicate (chr, bp, a1_hash64, a2_hash64) matching key is not allowed"
|
|
54
50
|
)
|
|
55
51
|
|
|
56
52
|
prev = order_df.shift(1)
|
|
@@ -58,26 +54,14 @@ def _validate_reference_sort_order(
|
|
|
58
54
|
(order_df["chr_rank"] < prev["chr_rank"])
|
|
59
55
|
| (
|
|
60
56
|
(order_df["chr_rank"] == prev["chr_rank"])
|
|
61
|
-
& (
|
|
62
|
-
(order_df["bp"] < prev["bp"])
|
|
63
|
-
| (
|
|
64
|
-
(order_df["bp"] == prev["bp"])
|
|
65
|
-
& (
|
|
66
|
-
(order_df["a1"] < prev["a1"])
|
|
67
|
-
| (
|
|
68
|
-
(order_df["a1"] == prev["a1"])
|
|
69
|
-
& (order_df["a2"] < prev["a2"])
|
|
70
|
-
)
|
|
71
|
-
)
|
|
72
|
-
)
|
|
73
|
-
)
|
|
57
|
+
& (order_df["bp"] < prev["bp"])
|
|
74
58
|
)
|
|
75
59
|
)
|
|
76
60
|
bad_order = bad_order.fillna(False)
|
|
77
61
|
if bad_order.any():
|
|
78
62
|
line = int(order_df.loc[bad_order.idxmax(), "line"])
|
|
79
63
|
raise ValueError(
|
|
80
|
-
f"{path}:{line}: rows must be sorted by (chr_rank, bp
|
|
64
|
+
f"{path}:{line}: rows must be sorted by (chr_rank, bp) in canonical contig order"
|
|
81
65
|
)
|
|
82
66
|
|
|
83
67
|
|
|
@@ -221,6 +205,18 @@ class ReferenceShard:
|
|
|
221
205
|
self._bp = np.asarray(bp_arr, dtype=np.int64)
|
|
222
206
|
self._a1 = np.asarray(a1_arr, dtype=object)
|
|
223
207
|
self._a2 = np.asarray(a2_arr, dtype=object)
|
|
208
|
+
if not (
|
|
209
|
+
self._chr.size
|
|
210
|
+
== self._snp.size
|
|
211
|
+
== self._bp.size
|
|
212
|
+
== self._a1.size
|
|
213
|
+
== self._a2.size
|
|
214
|
+
):
|
|
215
|
+
raise ValueError("ReferenceShard vector lengths must match")
|
|
216
|
+
if self._chr.size and not np.all(self._chr == self._label):
|
|
217
|
+
raise ValueError(f"Reference shard {self._label} contains multiple chr labels")
|
|
218
|
+
self._a1_hash64 = allele_hash64(self._a1)
|
|
219
|
+
self._a2_hash64 = allele_hash64(self._a2)
|
|
224
220
|
self._checksum = _checksum_from_arrays(self._chr, self._bp, self._a1, self._a2)
|
|
225
221
|
|
|
226
222
|
@property
|
|
@@ -251,12 +247,31 @@ class ReferenceShard:
|
|
|
251
247
|
def a2(self) -> np.ndarray:
|
|
252
248
|
return self._a2
|
|
253
249
|
|
|
250
|
+
@property
|
|
251
|
+
def a1_hash64(self) -> np.ndarray:
|
|
252
|
+
return self._a1_hash64
|
|
253
|
+
|
|
254
|
+
@property
|
|
255
|
+
def a2_hash64(self) -> np.ndarray:
|
|
256
|
+
return self._a2_hash64
|
|
257
|
+
|
|
254
258
|
@property
|
|
255
259
|
def checksum(self) -> str:
|
|
256
260
|
return self._checksum
|
|
257
261
|
|
|
258
262
|
@classmethod
|
|
259
|
-
def _from_arrays(
|
|
263
|
+
def _from_arrays(
|
|
264
|
+
cls,
|
|
265
|
+
label,
|
|
266
|
+
chr_arr,
|
|
267
|
+
snp_arr,
|
|
268
|
+
bp_arr,
|
|
269
|
+
a1_arr,
|
|
270
|
+
a2_arr,
|
|
271
|
+
checksum,
|
|
272
|
+
a1_hash64=None,
|
|
273
|
+
a2_hash64=None,
|
|
274
|
+
):
|
|
260
275
|
obj = cls.__new__(cls)
|
|
261
276
|
obj._label = label
|
|
262
277
|
obj._chr = np.asarray(chr_arr, dtype=object)
|
|
@@ -264,6 +279,28 @@ class ReferenceShard:
|
|
|
264
279
|
obj._bp = np.asarray(bp_arr, dtype=np.int64)
|
|
265
280
|
obj._a1 = np.asarray(a1_arr, dtype=object)
|
|
266
281
|
obj._a2 = np.asarray(a2_arr, dtype=object)
|
|
282
|
+
if not (
|
|
283
|
+
obj._chr.size
|
|
284
|
+
== obj._snp.size
|
|
285
|
+
== obj._bp.size
|
|
286
|
+
== obj._a1.size
|
|
287
|
+
== obj._a2.size
|
|
288
|
+
):
|
|
289
|
+
raise ValueError("Invalid reference cache: panel-wide vector lengths mismatch")
|
|
290
|
+
if obj._chr.size and not np.all(obj._chr == obj._label):
|
|
291
|
+
raise ValueError(f"Invalid reference cache: shard {obj._label} contains multiple chr labels")
|
|
292
|
+
obj._a1_hash64 = (
|
|
293
|
+
allele_hash64(obj._a1)
|
|
294
|
+
if a1_hash64 is None
|
|
295
|
+
else np.asarray(a1_hash64, dtype=np.uint64).reshape(-1)
|
|
296
|
+
)
|
|
297
|
+
obj._a2_hash64 = (
|
|
298
|
+
allele_hash64(obj._a2)
|
|
299
|
+
if a2_hash64 is None
|
|
300
|
+
else np.asarray(a2_hash64, dtype=np.uint64).reshape(-1)
|
|
301
|
+
)
|
|
302
|
+
if obj._a1_hash64.size != obj._bp.size or obj._a2_hash64.size != obj._bp.size:
|
|
303
|
+
raise ValueError("Invalid reference cache: allele hash vector lengths mismatch")
|
|
267
304
|
obj._checksum = checksum
|
|
268
305
|
return obj
|
|
269
306
|
|
|
@@ -313,6 +350,18 @@ class ReferencePanel:
|
|
|
313
350
|
return np.array([], dtype=object)
|
|
314
351
|
return np.concatenate([s.a2 for s in self._shards])
|
|
315
352
|
|
|
353
|
+
@property
|
|
354
|
+
def a1_hash64(self) -> np.ndarray:
|
|
355
|
+
if not self._shards:
|
|
356
|
+
return np.array([], dtype=np.uint64)
|
|
357
|
+
return np.concatenate([s.a1_hash64 for s in self._shards])
|
|
358
|
+
|
|
359
|
+
@property
|
|
360
|
+
def a2_hash64(self) -> np.ndarray:
|
|
361
|
+
if not self._shards:
|
|
362
|
+
return np.array([], dtype=np.uint64)
|
|
363
|
+
return np.concatenate([s.a2_hash64 for s in self._shards])
|
|
364
|
+
|
|
316
365
|
@property
|
|
317
366
|
def shard_offsets(self) -> list:
|
|
318
367
|
return list(self._shard_offsets)
|
|
@@ -327,6 +376,13 @@ class ReferencePanel:
|
|
|
327
376
|
by_label = {s.label: s for s in self._shards}
|
|
328
377
|
return ReferencePanel([by_label[label] for label in selected])
|
|
329
378
|
|
|
379
|
+
def validate_checksums(self) -> bool:
|
|
380
|
+
for shard in self._shards:
|
|
381
|
+
computed = _checksum_from_arrays(shard.chr, shard.bp, shard.a1, shard.a2)
|
|
382
|
+
if computed != shard.checksum:
|
|
383
|
+
raise ValueError(f"Reference checksum mismatch for shard {shard.label}")
|
|
384
|
+
return True
|
|
385
|
+
|
|
330
386
|
def is_object_compatible(self, obj) -> bool:
|
|
331
387
|
log_fn = logger.warning
|
|
332
388
|
|
|
@@ -441,12 +497,18 @@ def load_reference(path, shards=None) -> ReferencePanel:
|
|
|
441
497
|
return ReferencePanel(out_shards)
|
|
442
498
|
|
|
443
499
|
|
|
444
|
-
def save_reference_cache(panel: ReferencePanel, path) -> None:
|
|
500
|
+
def save_reference_cache(panel: ReferencePanel, path, mode: str = "full") -> None:
|
|
501
|
+
mode = str(mode).lower()
|
|
502
|
+
if mode not in {"full", "thin"}:
|
|
503
|
+
raise ValueError("save_reference_cache mode must be 'full' or 'thin'")
|
|
504
|
+
|
|
445
505
|
# Metadata (schema, labels, checksums) as a compact JSON blob stored in the npz.
|
|
446
506
|
# SNP-axis numeric vectors (bp) and string arrays are stored as native
|
|
447
507
|
# binary numpy arrays — not JSON — per the performance contract.
|
|
508
|
+
# Python keeps reference caches full even when mode='thin' is requested.
|
|
448
509
|
meta = {
|
|
449
510
|
"schema": _CACHE_SCHEMA,
|
|
511
|
+
"mode": "full",
|
|
450
512
|
"shard_labels": [s.label for s in panel.shards],
|
|
451
513
|
"shard_checksums": [s.checksum for s in panel.shards],
|
|
452
514
|
}
|
|
@@ -460,6 +522,8 @@ def save_reference_cache(panel: ReferencePanel, path) -> None:
|
|
|
460
522
|
arrays[p + "bp"] = s.bp
|
|
461
523
|
arrays[p + "a1"] = np.asarray(s.a1, dtype=str)
|
|
462
524
|
arrays[p + "a2"] = np.asarray(s.a2, dtype=str)
|
|
525
|
+
arrays[p + "a1_hash64"] = s.a1_hash64
|
|
526
|
+
arrays[p + "a2_hash64"] = s.a2_hash64
|
|
463
527
|
np.savez_compressed(path, **arrays)
|
|
464
528
|
|
|
465
529
|
|
|
@@ -469,6 +533,11 @@ def load_reference_cache(path, shards=None) -> ReferencePanel:
|
|
|
469
533
|
schema = meta.get("schema")
|
|
470
534
|
if schema != _CACHE_SCHEMA:
|
|
471
535
|
raise ValueError(f"Unsupported reference cache schema: {schema!r}")
|
|
536
|
+
mode = meta.get("mode")
|
|
537
|
+
if mode is None:
|
|
538
|
+
raise ValueError("reference cache missing mode; delete and rebuild old cache")
|
|
539
|
+
if mode != "full":
|
|
540
|
+
raise ValueError(f"Unsupported Python reference cache mode: {mode!r}")
|
|
472
541
|
|
|
473
542
|
labels = list(meta.get("shard_labels", []))
|
|
474
543
|
checksums = list(meta.get("shard_checksums", []))
|
|
@@ -481,6 +550,8 @@ def load_reference_cache(path, shards=None) -> ReferencePanel:
|
|
|
481
550
|
for label in selected:
|
|
482
551
|
i = label_to_index[label]
|
|
483
552
|
p = f"s{i}_"
|
|
553
|
+
if p + "a1_hash64" not in data or p + "a2_hash64" not in data:
|
|
554
|
+
raise ValueError("reference cache missing a1_hash64/a2_hash64; rebuild cache")
|
|
484
555
|
shard_objs.append(
|
|
485
556
|
ReferenceShard._from_arrays(
|
|
486
557
|
label,
|
|
@@ -490,6 +561,8 @@ def load_reference_cache(path, shards=None) -> ReferencePanel:
|
|
|
490
561
|
data[p + "a1"],
|
|
491
562
|
data[p + "a2"],
|
|
492
563
|
checksums[i],
|
|
564
|
+
data[p + "a1_hash64"],
|
|
565
|
+
data[p + "a2_hash64"],
|
|
493
566
|
)
|
|
494
567
|
)
|
|
495
568
|
return ReferencePanel(shard_objs)
|
|
@@ -5,20 +5,77 @@ import numpy as np
|
|
|
5
5
|
import pandas as pd
|
|
6
6
|
from pandas.errors import ParserError
|
|
7
7
|
|
|
8
|
-
from ._utils import validate_requested_shards
|
|
8
|
+
from ._utils import allele_hash64, validate_requested_shards
|
|
9
9
|
|
|
10
10
|
_CACHE_SCHEMA = "sumstats_cache/0.1"
|
|
11
11
|
_REQUIRED_COLS = ("chr", "bp", "a1", "a2", "z", "n")
|
|
12
12
|
_OPTIONAL_COLS = ("p", "beta", "se", "eaf", "info")
|
|
13
|
+
_SUMSTATS_COL_MAP = {
|
|
14
|
+
"pos": "bp",
|
|
15
|
+
"effectallele": "a1",
|
|
16
|
+
"otherallele": "a2",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
_KEY_DTYPE = np.dtype([("bp", "<i8"), ("a1_hash64", "<u8"), ("a2_hash64", "<u8")])
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _numeric_keys(bp_arr, a1_hash64, a2_hash64) -> np.ndarray:
|
|
24
|
+
bp = np.asarray(bp_arr, dtype=np.int64).reshape(-1)
|
|
25
|
+
a1h = np.asarray(a1_hash64, dtype=np.uint64).reshape(-1)
|
|
26
|
+
a2h = np.asarray(a2_hash64, dtype=np.uint64).reshape(-1)
|
|
27
|
+
if not (bp.size == a1h.size == a2h.size):
|
|
28
|
+
raise ValueError("numeric key vector lengths mismatch")
|
|
29
|
+
keys = np.empty(bp.size, dtype=_KEY_DTYPE)
|
|
30
|
+
keys["bp"] = bp
|
|
31
|
+
keys["a1_hash64"] = a1h
|
|
32
|
+
keys["a2_hash64"] = a2h
|
|
33
|
+
return keys
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _check_unique_sorted_keys(sorted_keys: np.ndarray, where: str) -> None:
|
|
37
|
+
if sorted_keys.size < 2:
|
|
38
|
+
return
|
|
39
|
+
dup = sorted_keys[1:] == sorted_keys[:-1]
|
|
40
|
+
if np.any(dup):
|
|
41
|
+
pos = int(np.flatnonzero(dup)[0] + 1)
|
|
42
|
+
key = sorted_keys[pos]
|
|
43
|
+
raise ValueError(
|
|
44
|
+
f"Ambiguous duplicate sumstats/reference matching key in {where}: "
|
|
45
|
+
f"bp={int(key['bp'])}, a1_hash64={int(key['a1_hash64'])}, "
|
|
46
|
+
f"a2_hash64={int(key['a2_hash64'])}"
|
|
47
|
+
)
|
|
13
48
|
|
|
14
49
|
|
|
15
|
-
def
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
50
|
+
def _match_shard_numeric(ref_keys: np.ndarray, src_keys: np.ndarray, shard_label: str) -> np.ndarray:
|
|
51
|
+
if src_keys.size == 0:
|
|
52
|
+
return np.full(ref_keys.size, -1, dtype=np.int64)
|
|
53
|
+
|
|
54
|
+
ref_order = np.argsort(ref_keys, kind="mergesort", order=("bp", "a1_hash64", "a2_hash64"))
|
|
55
|
+
src_order = np.argsort(src_keys, kind="mergesort", order=("bp", "a1_hash64", "a2_hash64"))
|
|
56
|
+
ref_sorted = ref_keys[ref_order]
|
|
57
|
+
src_sorted = src_keys[src_order]
|
|
58
|
+
_check_unique_sorted_keys(ref_sorted, f"reference shard {shard_label}")
|
|
59
|
+
_check_unique_sorted_keys(src_sorted, f"sumstats shard {shard_label}")
|
|
60
|
+
|
|
61
|
+
pos = np.searchsorted(src_sorted, ref_keys)
|
|
62
|
+
in_range = pos < src_sorted.size
|
|
63
|
+
matched = np.zeros(ref_keys.size, dtype=bool)
|
|
64
|
+
matched[in_range] = src_sorted[pos[in_range]] == ref_keys[in_range]
|
|
65
|
+
|
|
66
|
+
out = np.full(ref_keys.size, -1, dtype=np.int64)
|
|
67
|
+
out[matched] = src_order[pos[matched]]
|
|
68
|
+
return out
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _canonicalize_columns(df: pd.DataFrame, path: Path) -> pd.DataFrame:
|
|
72
|
+
columns = [_SUMSTATS_COL_MAP.get(str(c).lower(), str(c).lower()) for c in df.columns]
|
|
73
|
+
duplicates = sorted({c for c in columns if columns.count(c) > 1})
|
|
74
|
+
if duplicates:
|
|
75
|
+
raise ValueError(f"{path}: duplicate columns after column normalization: {', '.join(duplicates)}")
|
|
76
|
+
out = df.copy()
|
|
77
|
+
out.columns = columns
|
|
78
|
+
return out
|
|
22
79
|
|
|
23
80
|
|
|
24
81
|
def _parse_sumstats(path: Path) -> pd.DataFrame:
|
|
@@ -34,6 +91,8 @@ def _parse_sumstats(path: Path) -> pd.DataFrame:
|
|
|
34
91
|
except ParserError as exc:
|
|
35
92
|
raise ValueError(f"{path}: malformed TSV") from exc
|
|
36
93
|
|
|
94
|
+
df = _canonicalize_columns(df, path)
|
|
95
|
+
|
|
37
96
|
missing = [c for c in _REQUIRED_COLS if c not in df.columns]
|
|
38
97
|
if missing:
|
|
39
98
|
raise ValueError(f"{path}: missing required columns: {', '.join(missing)}")
|
|
@@ -289,40 +348,45 @@ def _build_sumstats_from_aligned(
|
|
|
289
348
|
|
|
290
349
|
|
|
291
350
|
def _build_sumstats_panel(df: pd.DataFrame, reference) -> Sumstats:
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
)
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
351
|
+
n = int(reference.num_snp)
|
|
352
|
+
aligned_z = np.full(n, np.nan, dtype=float)
|
|
353
|
+
aligned_n = np.full(n, np.nan, dtype=float)
|
|
354
|
+
aligned_p = np.full(n, np.nan, dtype=float) if "p" in df.columns else None
|
|
355
|
+
|
|
356
|
+
aligned_optional = {
|
|
357
|
+
col: (np.full(n, np.nan, dtype=float) if col in df.columns else None)
|
|
358
|
+
for col in ("beta", "se", "eaf", "info")
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
src_chr = df["chr"].to_numpy(dtype=object)
|
|
362
|
+
src_bp = df["bp"].to_numpy(dtype=np.int64)
|
|
363
|
+
src_a1_hash64 = allele_hash64(df["a1"].to_numpy(dtype=object))
|
|
364
|
+
src_a2_hash64 = allele_hash64(df["a2"].to_numpy(dtype=object))
|
|
365
|
+
|
|
366
|
+
for ref_shard, off in zip(reference.shards, reference.shard_offsets):
|
|
367
|
+
start = int(off["start0"])
|
|
368
|
+
stop = int(off["stop0"])
|
|
369
|
+
src_mask = src_chr == ref_shard.label
|
|
370
|
+
src_idx = np.flatnonzero(src_mask)
|
|
371
|
+
ref_keys = _numeric_keys(ref_shard.bp, ref_shard.a1_hash64, ref_shard.a2_hash64)
|
|
372
|
+
src_keys = _numeric_keys(src_bp[src_idx], src_a1_hash64[src_idx], src_a2_hash64[src_idx])
|
|
373
|
+
local_match = _match_shard_numeric(ref_keys, src_keys, ref_shard.label)
|
|
374
|
+
has_match = local_match >= 0
|
|
375
|
+
matched_src = src_idx[local_match[has_match]]
|
|
376
|
+
aligned_ix = np.arange(start, stop, dtype=np.int64)[has_match]
|
|
377
|
+
|
|
378
|
+
aligned_z[aligned_ix] = df["z"].to_numpy(dtype=float)[matched_src]
|
|
379
|
+
aligned_n[aligned_ix] = df["n"].to_numpy(dtype=float)[matched_src]
|
|
380
|
+
if aligned_p is not None:
|
|
381
|
+
aligned_p[aligned_ix] = df["p"].to_numpy(dtype=float)[matched_src]
|
|
382
|
+
for col, out in aligned_optional.items():
|
|
383
|
+
if out is not None:
|
|
384
|
+
out[aligned_ix] = df[col].to_numpy(dtype=float)[matched_src]
|
|
385
|
+
|
|
386
|
+
if aligned_p is None:
|
|
387
|
+
aligned_logp = np.full(n, np.nan, dtype=float)
|
|
317
388
|
else:
|
|
318
|
-
aligned_logp =
|
|
319
|
-
|
|
320
|
-
aligned_optional = {}
|
|
321
|
-
for col in ("beta", "se", "eaf", "info"):
|
|
322
|
-
if col in key_to:
|
|
323
|
-
aligned_optional[col] = key_to[col].reindex(ref_keys).to_numpy(dtype=float)
|
|
324
|
-
else:
|
|
325
|
-
aligned_optional[col] = None
|
|
389
|
+
aligned_logp = _derive_logp(aligned_p)
|
|
326
390
|
|
|
327
391
|
return _build_sumstats_from_aligned(
|
|
328
392
|
reference,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.2.1"
|
statgen-0.2.1/statgen/_utils.py
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
CANONICAL_CHR_ORDER = [str(i) for i in range(1, 23)] + ["X"]
|
|
2
|
-
CHR_RANK = {c: i for i, c in enumerate(CANONICAL_CHR_ORDER)}
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
def validate_requested_shards(shards, available_labels, where: str) -> list[str]:
|
|
6
|
-
labels = list(available_labels)
|
|
7
|
-
if shards is None:
|
|
8
|
-
return labels
|
|
9
|
-
if isinstance(shards, str):
|
|
10
|
-
raise ValueError(f"{where}: shards must be a non-empty list of unique canonical contig labels")
|
|
11
|
-
|
|
12
|
-
requested = list(shards)
|
|
13
|
-
if not requested:
|
|
14
|
-
raise ValueError(f"{where}: shards must be a non-empty list of unique canonical contig labels")
|
|
15
|
-
|
|
16
|
-
seen = set()
|
|
17
|
-
prev_rank = None
|
|
18
|
-
for label in requested:
|
|
19
|
-
if label not in CHR_RANK:
|
|
20
|
-
raise ValueError(f"{where}: unsupported shard label {label!r}; expected canonical labels 1-22 or X")
|
|
21
|
-
if label in seen:
|
|
22
|
-
raise ValueError(f"{where}: duplicate shard label {label!r} in shards")
|
|
23
|
-
rank = CHR_RANK[label]
|
|
24
|
-
if prev_rank is not None and rank <= prev_rank:
|
|
25
|
-
raise ValueError(f"{where}: shards must be in canonical subsequence order")
|
|
26
|
-
seen.add(label)
|
|
27
|
-
prev_rank = rank
|
|
28
|
-
if label not in labels:
|
|
29
|
-
raise ValueError(f"{where}: requested shard {label!r} is not present")
|
|
30
|
-
return requested
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|