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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: statgen
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Statistical genetics data objects and loaders for Python and MATLAB/Octave.
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "statgen"
7
- version = "0.2.1"
7
+ version = "0.2.2"
8
8
  description = "Statistical genetics data objects and loaders for Python and MATLAB/Octave."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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
- "chr_rank": chr_rank.to_numpy(dtype=np.int64),
40
- "bp": np.asarray(bp_col, dtype=np.int64),
41
- "a1": a1_col.to_numpy(dtype=object),
42
- "a2": a2_col.to_numpy(dtype=object),
43
- "line": np.asarray(line_numbers, dtype=np.int64),
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", "a1", "a2"], keep="first"
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, a1, a2) tuple is not allowed"
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, a1, a2) in canonical contig order"
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(cls, label, chr_arr, snp_arr, bp_arr, a1_arr, a2_arr, checksum):
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 _key_from_arrays(chr_arr, bp_arr, a1_arr, a2_arr) -> pd.Series:
16
- return (
17
- pd.Series(chr_arr, dtype="string")
18
- .str.cat(pd.Series(bp_arr, dtype=np.int64).astype("string"), sep=":")
19
- .str.cat(pd.Series(a1_arr, dtype="string"), sep=":")
20
- .str.cat(pd.Series(a2_arr, dtype="string"), sep=":")
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
- ref_chr = np.asarray(reference.chr, dtype=object)
293
- ref_bp = np.asarray(reference.bp, dtype=np.int64)
294
- ref_a1 = np.asarray(reference.a1, dtype=object)
295
- ref_a2 = np.asarray(reference.a2, dtype=object)
296
- ref_keys = _key_from_arrays(ref_chr, ref_bp, ref_a1, ref_a2)
297
-
298
- src_keys = _key_from_arrays(
299
- df["chr"].to_numpy(dtype=object),
300
- df["bp"].to_numpy(dtype=np.int64),
301
- df["a1"].to_numpy(dtype=object),
302
- df["a2"].to_numpy(dtype=object),
303
- )
304
- src = df.copy()
305
- src["_key"] = src_keys
306
-
307
- key_to = {}
308
- for col in ("z", "n") + _OPTIONAL_COLS:
309
- if col in src.columns:
310
- key_to[col] = src.set_index("_key")[col]
311
-
312
- aligned_z = key_to["z"].reindex(ref_keys).to_numpy(dtype=float)
313
- aligned_n = key_to["n"].reindex(ref_keys).to_numpy(dtype=float)
314
- if "p" in key_to:
315
- aligned_p = key_to["p"].reindex(ref_keys).to_numpy(dtype=float)
316
- aligned_logp = _derive_logp(aligned_p)
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 = np.full(ref_keys.size, np.nan, dtype=float)
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: statgen
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Statistical genetics data objects and loaders for Python and MATLAB/Octave.
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -1 +0,0 @@
1
- __version__ = "0.2.1"
@@ -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