statgen 0.1.0__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.1.0/PKG-INFO ADDED
@@ -0,0 +1,19 @@
1
+ Metadata-Version: 2.4
2
+ Name: statgen
3
+ Version: 0.1.0
4
+ Summary: Statistical genetics data objects and loaders for Python and MATLAB/Octave.
5
+ Requires-Python: >=3.9
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: numpy
8
+ Requires-Dist: scipy
9
+ Requires-Dist: pandas
10
+
11
+ # statgen (Python package)
12
+
13
+ `statgen` provides statistical genetics data objects and loaders for aligned
14
+ reference, LD, annotation, genotype metadata, and summary-statistics workflows.
15
+
16
+ The package is developed alongside a MATLAB/Octave implementation, with shared
17
+ contracts defined in the repository `spec/` directory.
18
+
19
+ Repository: https://github.com/precimed/statgen
@@ -0,0 +1,9 @@
1
+ # statgen (Python package)
2
+
3
+ `statgen` provides statistical genetics data objects and loaders for aligned
4
+ reference, LD, annotation, genotype metadata, and summary-statistics workflows.
5
+
6
+ The package is developed alongside a MATLAB/Octave implementation, with shared
7
+ contracts defined in the repository `spec/` directory.
8
+
9
+ Repository: https://github.com/precimed/statgen
@@ -0,0 +1,19 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "statgen"
7
+ version = "0.1.0"
8
+ description = "Statistical genetics data objects and loaders for Python and MATLAB/Octave."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ dependencies = [
12
+ "numpy",
13
+ "scipy",
14
+ "pandas",
15
+ ]
16
+
17
+ [tool.setuptools.packages.find]
18
+ where = ["."]
19
+ include = ["statgen*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1 @@
1
+ # Shared internal utilities
@@ -0,0 +1 @@
1
+ # AnnotationShard, AnnotationPanel, load_annotations — Phase 3
@@ -0,0 +1 @@
1
+ # GenotypeShard, GenotypePanel, load_genotype — Phase 6
@@ -0,0 +1 @@
1
+ # LDShard, LDPanel, load_ld, fast_prune — Phase 4
@@ -0,0 +1,370 @@
1
+ import hashlib
2
+ import json
3
+ import logging
4
+ from pathlib import Path
5
+
6
+ import numpy as np
7
+ import pandas as pd
8
+ from pandas.errors import ParserError
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ _CACHE_SCHEMA = "reference_cache/0.1"
13
+ _BIM_NCOLS = 6
14
+ _CANONICAL_CHR_ORDER = [str(i) for i in range(1, 23)] + ["X"]
15
+ _CHR_RANK = {c: i for i, c in enumerate(_CANONICAL_CHR_ORDER)}
16
+
17
+
18
+ def _validate_reference_sort_order(chr_col: pd.Series, bp_col: np.ndarray, path: Path) -> None:
19
+ chr_rank = chr_col.map(_CHR_RANK)
20
+ bad_chr = chr_rank.isna()
21
+ if bad_chr.any():
22
+ idx = int(bad_chr.idxmax())
23
+ raise ValueError(
24
+ f"{path}:{idx + 1}: chr must use canonical labels 1-22 or X"
25
+ )
26
+
27
+ chr_rank_vals = chr_rank.to_numpy(dtype=np.int64)
28
+ bp_vals = np.asarray(bp_col, dtype=np.int64)
29
+ bad_order = (chr_rank_vals[1:] < chr_rank_vals[:-1]) | (
30
+ (chr_rank_vals[1:] == chr_rank_vals[:-1]) & (bp_vals[1:] < bp_vals[:-1])
31
+ )
32
+ if bad_order.any():
33
+ idx = int(np.flatnonzero(bad_order)[0]) + 1
34
+ raise ValueError(
35
+ f"{path}:{idx + 1}: rows must be sorted by canonical chromosome (1-22, X) and ascending bp"
36
+ )
37
+
38
+
39
+ def _parse_bim(path: Path) -> list:
40
+ try:
41
+ df = pd.read_csv(
42
+ path,
43
+ sep="\t",
44
+ header=None,
45
+ dtype=str,
46
+ keep_default_na=False,
47
+ na_filter=False,
48
+ )
49
+ except ParserError as exc:
50
+ raise ValueError(
51
+ f"{path}: expected 6 tab-separated columns"
52
+ ) from exc
53
+
54
+ if df.shape[1] != _BIM_NCOLS:
55
+ raise ValueError(
56
+ f"{path}: expected 6 tab-separated columns, got {df.shape[1]}"
57
+ )
58
+
59
+ chr_col = df[0]
60
+ snp_col = df[1]
61
+ cm_raw = df[2]
62
+ bp_raw = df[3]
63
+ a1_col = df[4]
64
+ a2_col = df[5]
65
+
66
+ bad_chr = chr_col.eq("")
67
+ if bad_chr.any():
68
+ idx = int(bad_chr.idxmax())
69
+ raise ValueError(f"{path}:{idx + 1}: chr must be non-empty")
70
+
71
+ bad_allele = a1_col.eq("") | a2_col.eq("")
72
+ if bad_allele.any():
73
+ idx = int(bad_allele.idxmax())
74
+ raise ValueError(f"{path}:{idx + 1}: a1 and a2 must be non-empty")
75
+
76
+ cm_num = pd.to_numeric(cm_raw, errors="coerce")
77
+ bad_cm = cm_num.isna()
78
+ if bad_cm.any():
79
+ idx = int(bad_cm.idxmax())
80
+ raise ValueError(
81
+ f"{path}:{idx + 1}: cm is not a number: {cm_raw.iat[idx]!r}"
82
+ )
83
+
84
+ bp_num = pd.to_numeric(bp_raw, errors="coerce")
85
+ bad_bp = bp_num.isna() | (np.floor(bp_num) != bp_num)
86
+ if bad_bp.any():
87
+ idx = int(bad_bp.idxmax())
88
+ raise ValueError(
89
+ f"{path}:{idx + 1}: bp is not an integer: {bp_raw.iat[idx]!r}"
90
+ )
91
+
92
+ bp_int = bp_num.astype(np.int64)
93
+ _validate_reference_sort_order(chr_col, bp_int.to_numpy(dtype=np.int64), path)
94
+
95
+ # cm is validated for BIM schema compatibility but intentionally ignored
96
+ # in-memory per spec.
97
+ return list(
98
+ zip(
99
+ chr_col.tolist(),
100
+ snp_col.tolist(),
101
+ bp_int.tolist(),
102
+ a1_col.tolist(),
103
+ a2_col.tolist(),
104
+ )
105
+ )
106
+
107
+
108
+ def _checksum(rows) -> str:
109
+ text = "".join(f"{r[0]}:{r[2]}:{r[3]}:{r[4]}\n" for r in rows)
110
+ return hashlib.md5(text.encode()).hexdigest()
111
+
112
+
113
+ def _chr_sort_key(label: str):
114
+ if label not in _CHR_RANK:
115
+ raise ValueError(f"Unsupported shard label: {label!r}; expected canonical labels 1-22 or X")
116
+ return _CHR_RANK[label]
117
+
118
+
119
+ class ReferenceShard:
120
+ def __init__(self, label: str, rows, *, _checksum_override: str | None = None):
121
+ self._label = label
122
+ self._chr = np.array([r[0] for r in rows], dtype=object)
123
+ self._snp = np.array([r[1] for r in rows], dtype=object)
124
+ self._bp = np.array([r[2] for r in rows], dtype=np.int64)
125
+ self._a1 = np.array([r[3] for r in rows], dtype=object)
126
+ self._a2 = np.array([r[4] for r in rows], dtype=object)
127
+ self._checksum = (
128
+ _checksum_override if _checksum_override is not None else _checksum(rows)
129
+ )
130
+
131
+ @property
132
+ def label(self) -> str:
133
+ return self._label
134
+
135
+ @property
136
+ def num_snp(self) -> int:
137
+ return len(self._chr)
138
+
139
+ @property
140
+ def chr(self) -> np.ndarray:
141
+ return self._chr
142
+
143
+ @property
144
+ def snp(self) -> np.ndarray:
145
+ return self._snp
146
+
147
+ @property
148
+ def bp(self) -> np.ndarray:
149
+ return self._bp
150
+
151
+ @property
152
+ def a1(self) -> np.ndarray:
153
+ return self._a1
154
+
155
+ @property
156
+ def a2(self) -> np.ndarray:
157
+ return self._a2
158
+
159
+ @property
160
+ def checksum(self) -> str:
161
+ return self._checksum
162
+
163
+ @classmethod
164
+ def _from_arrays(cls, label, chr_arr, snp_arr, bp_arr, a1_arr, a2_arr, checksum):
165
+ obj = cls.__new__(cls)
166
+ obj._label = label
167
+ obj._chr = np.asarray(chr_arr, dtype=object)
168
+ obj._snp = np.asarray(snp_arr, dtype=object)
169
+ obj._bp = np.asarray(bp_arr, dtype=np.int64)
170
+ obj._a1 = np.asarray(a1_arr, dtype=object)
171
+ obj._a2 = np.asarray(a2_arr, dtype=object)
172
+ obj._checksum = checksum
173
+ return obj
174
+
175
+
176
+ class ReferencePanel:
177
+ def __init__(self, shards: list):
178
+ self._shards = list(shards)
179
+ offsets = []
180
+ pos = 0
181
+ for s in self._shards:
182
+ offsets.append({"shard_label": s.label, "start0": pos, "stop0": pos + s.num_snp})
183
+ pos += s.num_snp
184
+ self._shard_offsets = offsets
185
+ self._num_snp = pos
186
+
187
+ @property
188
+ def num_snp(self) -> int:
189
+ return self._num_snp
190
+
191
+ @property
192
+ def chr(self) -> np.ndarray:
193
+ if not self._shards:
194
+ return np.array([], dtype=object)
195
+ return np.concatenate([s.chr for s in self._shards])
196
+
197
+ @property
198
+ def snp(self) -> np.ndarray:
199
+ if not self._shards:
200
+ return np.array([], dtype=object)
201
+ return np.concatenate([s.snp for s in self._shards])
202
+
203
+ @property
204
+ def bp(self) -> np.ndarray:
205
+ if not self._shards:
206
+ return np.array([], dtype=np.int64)
207
+ return np.concatenate([s.bp for s in self._shards])
208
+
209
+ @property
210
+ def a1(self) -> np.ndarray:
211
+ if not self._shards:
212
+ return np.array([], dtype=object)
213
+ return np.concatenate([s.a1 for s in self._shards])
214
+
215
+ @property
216
+ def a2(self) -> np.ndarray:
217
+ if not self._shards:
218
+ return np.array([], dtype=object)
219
+ return np.concatenate([s.a2 for s in self._shards])
220
+
221
+ @property
222
+ def shard_offsets(self) -> list:
223
+ return list(self._shard_offsets)
224
+
225
+ @property
226
+ def shards(self) -> list:
227
+ return list(self._shards)
228
+
229
+ def is_object_compatible(self, obj) -> bool:
230
+ log_fn = logger.warning
231
+
232
+ obj_shards = getattr(obj, "shards", None)
233
+ if obj_shards is None:
234
+ log_fn("statgen: is_object_compatible: object has no shards attribute")
235
+ return False
236
+
237
+ obj_shards = list(obj_shards)
238
+ if len(obj_shards) != len(self._shards):
239
+ log_fn(
240
+ "statgen: is_object_compatible: shard count mismatch: "
241
+ f"reference has {len(self._shards)}, object has {len(obj_shards)}"
242
+ )
243
+ return False
244
+
245
+ ok = True
246
+ for ref_s, obj_s in zip(self._shards, obj_shards):
247
+ obj_num = getattr(obj_s, "num_snp", None)
248
+ if obj_num is None:
249
+ log_fn(
250
+ f"statgen: is_object_compatible: shard {ref_s.label}: "
251
+ "object shard has no num_snp"
252
+ )
253
+ ok = False
254
+ continue
255
+ if obj_num != ref_s.num_snp:
256
+ log_fn(
257
+ f"statgen: is_object_compatible: shard {ref_s.label}: "
258
+ f"row count mismatch: reference {ref_s.num_snp}, object {obj_num}"
259
+ )
260
+ ok = False
261
+ continue
262
+ obj_chk = getattr(obj_s, "checksum", None)
263
+ if obj_chk is not None and obj_chk != ref_s.checksum:
264
+ log_fn(
265
+ f"statgen: is_object_compatible: shard {ref_s.label}: checksum mismatch"
266
+ )
267
+ ok = False
268
+ return ok
269
+
270
+
271
+ def load_reference(path, no_shard: bool = False) -> ReferencePanel:
272
+ path_str = str(path)
273
+ path_obj = Path(path_str)
274
+
275
+ if "@" in path_str:
276
+ parent = path_obj.parent
277
+ name = path_obj.name
278
+ at_idx = name.index("@")
279
+ name_prefix = name[:at_idx]
280
+ name_suffix = name[at_idx + 1:]
281
+
282
+ candidates = []
283
+ for f in sorted(parent.iterdir()):
284
+ if not f.is_file():
285
+ continue
286
+ n = f.name
287
+ if not n.startswith(name_prefix):
288
+ continue
289
+ if name_suffix and not n.endswith(name_suffix):
290
+ continue
291
+ inner = n[len(name_prefix): -len(name_suffix) if name_suffix else None]
292
+ if inner:
293
+ candidates.append((inner, f))
294
+
295
+ try:
296
+ candidates.sort(key=lambda x: _chr_sort_key(x[0]))
297
+ except ValueError as exc:
298
+ raise ValueError(f"{path_str}: {exc}") from None
299
+
300
+ if not candidates:
301
+ raise FileNotFoundError(
302
+ f"No BIM shards found matching template: {path_str}"
303
+ )
304
+
305
+ shards = [
306
+ ReferenceShard(label, _parse_bim(bim_path))
307
+ for label, bim_path in candidates
308
+ ]
309
+ return ReferencePanel(shards)
310
+
311
+ rows = _parse_bim(path_obj)
312
+
313
+ if no_shard:
314
+ return ReferencePanel([ReferenceShard("all", rows)])
315
+
316
+ by_chr: dict = {}
317
+ for r in rows:
318
+ c = r[0]
319
+ if c not in by_chr:
320
+ by_chr[c] = []
321
+ by_chr[c].append(r)
322
+
323
+ return ReferencePanel([ReferenceShard(c, by_chr[c]) for c in by_chr])
324
+
325
+
326
+ def save_reference_cache(panel: ReferencePanel, path) -> None:
327
+ # Metadata (schema, labels, checksums) as a compact JSON blob stored in the npz.
328
+ # SNP-axis numeric vectors (bp) and string arrays are stored as native
329
+ # binary numpy arrays — not JSON — per the performance contract.
330
+ meta = {
331
+ "schema": _CACHE_SCHEMA,
332
+ "shard_labels": [s.label for s in panel.shards],
333
+ "shard_checksums": [s.checksum for s in panel.shards],
334
+ }
335
+ arrays: dict = {
336
+ "_meta": np.frombuffer(json.dumps(meta).encode(), dtype=np.uint8)
337
+ }
338
+ for i, s in enumerate(panel.shards):
339
+ p = f"s{i}_"
340
+ arrays[p + "chr"] = np.asarray([str(v) for v in s.chr], dtype=str)
341
+ arrays[p + "snp"] = np.asarray([str(v) for v in s.snp], dtype=str)
342
+ arrays[p + "bp"] = s.bp
343
+ arrays[p + "a1"] = np.asarray([str(v) for v in s.a1], dtype=str)
344
+ arrays[p + "a2"] = np.asarray([str(v) for v in s.a2], dtype=str)
345
+ np.savez_compressed(path, **arrays)
346
+
347
+
348
+ def load_reference_cache(path) -> ReferencePanel:
349
+ data = np.load(path, allow_pickle=False)
350
+ meta = json.loads(bytes(data["_meta"]).decode())
351
+ schema = meta.get("schema")
352
+ if schema != _CACHE_SCHEMA:
353
+ raise ValueError(f"Unsupported reference cache schema: {schema!r}")
354
+ shards = []
355
+ for i, (label, checksum) in enumerate(
356
+ zip(meta["shard_labels"], meta["shard_checksums"])
357
+ ):
358
+ p = f"s{i}_"
359
+ shards.append(
360
+ ReferenceShard._from_arrays(
361
+ label,
362
+ data[p + "chr"],
363
+ data[p + "snp"],
364
+ data[p + "bp"],
365
+ data[p + "a1"],
366
+ data[p + "a2"],
367
+ checksum,
368
+ )
369
+ )
370
+ return ReferencePanel(shards)
@@ -0,0 +1 @@
1
+ # SumstatsShard, Sumstats, load_sumstats — Phase 2
@@ -0,0 +1,19 @@
1
+ Metadata-Version: 2.4
2
+ Name: statgen
3
+ Version: 0.1.0
4
+ Summary: Statistical genetics data objects and loaders for Python and MATLAB/Octave.
5
+ Requires-Python: >=3.9
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: numpy
8
+ Requires-Dist: scipy
9
+ Requires-Dist: pandas
10
+
11
+ # statgen (Python package)
12
+
13
+ `statgen` provides statistical genetics data objects and loaders for aligned
14
+ reference, LD, annotation, genotype metadata, and summary-statistics workflows.
15
+
16
+ The package is developed alongside a MATLAB/Octave implementation, with shared
17
+ contracts defined in the repository `spec/` directory.
18
+
19
+ Repository: https://github.com/precimed/statgen
@@ -0,0 +1,14 @@
1
+ README.md
2
+ pyproject.toml
3
+ statgen/__init__.py
4
+ statgen/_utils.py
5
+ statgen/annotations.py
6
+ statgen/genotype.py
7
+ statgen/ld.py
8
+ statgen/reference.py
9
+ statgen/sumstats.py
10
+ statgen.egg-info/PKG-INFO
11
+ statgen.egg-info/SOURCES.txt
12
+ statgen.egg-info/dependency_links.txt
13
+ statgen.egg-info/requires.txt
14
+ statgen.egg-info/top_level.txt
@@ -0,0 +1,3 @@
1
+ numpy
2
+ scipy
3
+ pandas
@@ -0,0 +1 @@
1
+ statgen