statgen 0.3.3__tar.gz → 0.3.4__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.3.3 → statgen-0.3.4}/PKG-INFO +1 -1
- {statgen-0.3.3 → statgen-0.3.4}/pyproject.toml +1 -1
- {statgen-0.3.3 → statgen-0.3.4}/statgen/__init__.py +1 -1
- {statgen-0.3.3 → statgen-0.3.4}/statgen/annotations.py +144 -55
- {statgen-0.3.3 → statgen-0.3.4}/statgen.egg-info/PKG-INFO +1 -1
- {statgen-0.3.3 → statgen-0.3.4}/README.md +0 -0
- {statgen-0.3.3 → statgen-0.3.4}/setup.cfg +0 -0
- {statgen-0.3.3 → statgen-0.3.4}/statgen/_annotation_painting.py +0 -0
- {statgen-0.3.3 → statgen-0.3.4}/statgen/_bfile_utils.py +0 -0
- {statgen-0.3.3 → statgen-0.3.4}/statgen/_ld_npz.py +0 -0
- {statgen-0.3.3 → statgen-0.3.4}/statgen/_ld_reference.py +0 -0
- {statgen-0.3.3 → statgen-0.3.4}/statgen/_ld_schema.py +0 -0
- {statgen-0.3.3 → statgen-0.3.4}/statgen/_ld_writer.py +0 -0
- {statgen-0.3.3 → statgen-0.3.4}/statgen/_utils.py +0 -0
- {statgen-0.3.3 → statgen-0.3.4}/statgen/_variant_match.py +0 -0
- {statgen-0.3.3 → statgen-0.3.4}/statgen/_verbosity.py +0 -0
- {statgen-0.3.3 → statgen-0.3.4}/statgen/genotype.py +0 -0
- {statgen-0.3.3 → statgen-0.3.4}/statgen/ld.py +0 -0
- {statgen-0.3.3 → statgen-0.3.4}/statgen/reference.py +0 -0
- {statgen-0.3.3 → statgen-0.3.4}/statgen/sumstats.py +0 -0
- {statgen-0.3.3 → statgen-0.3.4}/statgen.egg-info/SOURCES.txt +0 -0
- {statgen-0.3.3 → statgen-0.3.4}/statgen.egg-info/dependency_links.txt +0 -0
- {statgen-0.3.3 → statgen-0.3.4}/statgen.egg-info/requires.txt +0 -0
- {statgen-0.3.3 → statgen-0.3.4}/statgen.egg-info/top_level.txt +0 -0
|
@@ -137,16 +137,27 @@ def _annotation_name_from_path(path: Path) -> str:
|
|
|
137
137
|
return name
|
|
138
138
|
|
|
139
139
|
|
|
140
|
-
def _generated_metadata(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
140
|
+
def _generated_metadata(
|
|
141
|
+
path: Path,
|
|
142
|
+
has_header: bool,
|
|
143
|
+
num_source_intervals: int,
|
|
144
|
+
*,
|
|
145
|
+
value_column=None,
|
|
146
|
+
group_column=None,
|
|
147
|
+
group_value=None,
|
|
148
|
+
) -> str:
|
|
149
|
+
payload = {
|
|
150
|
+
"source_file": str(path),
|
|
151
|
+
"source_file_has_header": 1 if has_header else 0,
|
|
152
|
+
"num_source_intervals": int(num_source_intervals),
|
|
153
|
+
}
|
|
154
|
+
if value_column is not None:
|
|
155
|
+
payload["value_column"] = value_column
|
|
156
|
+
if group_column is not None:
|
|
157
|
+
payload["group_column"] = group_column
|
|
158
|
+
if group_value is not None:
|
|
159
|
+
payload["group_value"] = group_value
|
|
160
|
+
return json.dumps(payload, sort_keys=True, separators=(",", ":"))
|
|
150
161
|
|
|
151
162
|
|
|
152
163
|
def _read_sidecar_exact(path) -> str:
|
|
@@ -167,7 +178,7 @@ def _read_column_metadata_sidecar(path, num_columns: int) -> list[str]:
|
|
|
167
178
|
return lines
|
|
168
179
|
|
|
169
180
|
|
|
170
|
-
def _read_annotation_table(path: Path,
|
|
181
|
+
def _read_annotation_table(path: Path, has_header: bool) -> tuple[pd.DataFrame, list[str] | None, int]:
|
|
171
182
|
if not path.is_file():
|
|
172
183
|
raise FileNotFoundError(f"annotation file not found: {path}")
|
|
173
184
|
|
|
@@ -175,7 +186,7 @@ def _read_annotation_table(path: Path, header: bool) -> tuple[pd.DataFrame, list
|
|
|
175
186
|
df = pd.read_csv(
|
|
176
187
|
path,
|
|
177
188
|
sep="\t",
|
|
178
|
-
header=0 if
|
|
189
|
+
header=0 if has_header else None,
|
|
179
190
|
dtype=str,
|
|
180
191
|
keep_default_na=False,
|
|
181
192
|
na_filter=False,
|
|
@@ -192,14 +203,14 @@ def _read_annotation_table(path: Path, header: bool) -> tuple[pd.DataFrame, list
|
|
|
192
203
|
|
|
193
204
|
if df.shape[1] < 3:
|
|
194
205
|
raise ValueError(f"{path}: BED must have at least 3 tab-separated columns")
|
|
195
|
-
header_fields = [str(x) for x in df.columns] if
|
|
206
|
+
header_fields = [str(x) for x in df.columns] if has_header else None
|
|
196
207
|
if header_fields is not None:
|
|
197
208
|
if any(x == "" for x in header_fields):
|
|
198
209
|
raise ValueError(f"{path}: header names must be non-empty")
|
|
199
210
|
if len(set(header_fields)) != len(header_fields):
|
|
200
211
|
raise ValueError(f"{path}: header names must be unique")
|
|
201
212
|
|
|
202
|
-
return df, header_fields, 1 if
|
|
213
|
+
return df, header_fields, 1 if has_header else 0
|
|
203
214
|
|
|
204
215
|
|
|
205
216
|
def _validate_interval_columns(
|
|
@@ -251,15 +262,17 @@ def _validate_interval_columns(
|
|
|
251
262
|
return chr_values, start_int, end_int
|
|
252
263
|
|
|
253
264
|
|
|
254
|
-
def _binary_intervals_by_chr(path: Path) -> dict[str, np.ndarray]:
|
|
255
|
-
df, _, row_base0 = _read_annotation_table(path,
|
|
265
|
+
def _binary_intervals_by_chr(path: Path) -> tuple[dict[str, np.ndarray], int]:
|
|
266
|
+
df, _, row_base0 = _read_annotation_table(path, has_header=False)
|
|
256
267
|
if df.shape[1] < 3:
|
|
257
268
|
raise ValueError(f"{path}: BED must have at least 3 tab-separated columns")
|
|
258
269
|
chr_values, starts, ends = _validate_interval_columns(df, path, row_base0=row_base0)
|
|
259
|
-
return binary_intervals_by_chr_from_arrays(chr_values, starts, ends)
|
|
270
|
+
return binary_intervals_by_chr_from_arrays(chr_values, starts, ends), int(df.shape[0])
|
|
260
271
|
|
|
261
272
|
|
|
262
|
-
def _paint_binary_annotations(
|
|
273
|
+
def _paint_binary_annotations(
|
|
274
|
+
bed_paths: list[Path], reference
|
|
275
|
+
) -> tuple[sparse.csr_matrix, list[str], list[int]]:
|
|
263
276
|
annonames = [_annotation_name_from_path(p) for p in bed_paths]
|
|
264
277
|
if len(set(annonames)) != len(annonames):
|
|
265
278
|
raise ValueError("duplicate annotation names derived from BED basenames")
|
|
@@ -267,47 +280,79 @@ def _paint_binary_annotations(bed_paths: list[Path], reference) -> tuple[sparse.
|
|
|
267
280
|
n = int(reference.num_snp)
|
|
268
281
|
k = len(bed_paths)
|
|
269
282
|
if n == 0:
|
|
270
|
-
|
|
283
|
+
counts = []
|
|
284
|
+
for bed_path in bed_paths:
|
|
285
|
+
_, count = _binary_intervals_by_chr(bed_path)
|
|
286
|
+
counts.append(count)
|
|
287
|
+
return sparse.csr_matrix((0, k), dtype=np.float64), annonames, counts
|
|
271
288
|
|
|
272
289
|
columns = []
|
|
290
|
+
counts = []
|
|
273
291
|
for bed_path in bed_paths:
|
|
274
|
-
intervals_by_chr = _binary_intervals_by_chr(bed_path)
|
|
292
|
+
intervals_by_chr, count = _binary_intervals_by_chr(bed_path)
|
|
275
293
|
columns.append(paint_binary_column(intervals_by_chr, reference))
|
|
294
|
+
counts.append(count)
|
|
276
295
|
|
|
277
|
-
return sparse.hstack(columns, format="csr"), annonames
|
|
296
|
+
return sparse.hstack(columns, format="csr"), annonames, counts
|
|
278
297
|
|
|
279
298
|
|
|
280
|
-
def
|
|
281
|
-
if
|
|
299
|
+
def _selector_list(selectors, name: str) -> list | None:
|
|
300
|
+
if selectors is None:
|
|
282
301
|
return None
|
|
283
|
-
if isinstance(
|
|
284
|
-
|
|
302
|
+
if isinstance(selectors, (str, int, np.integer)):
|
|
303
|
+
values = [selectors]
|
|
285
304
|
else:
|
|
286
|
-
|
|
287
|
-
if not
|
|
288
|
-
raise ValueError("
|
|
305
|
+
values = list(selectors)
|
|
306
|
+
if not values:
|
|
307
|
+
raise ValueError(f"{name} must be a non-empty list")
|
|
308
|
+
return values
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _normalize_column_selectors(selectors, header_fields, num_columns: int, path: Path, name: str) -> tuple[list[int], list] | None:
|
|
312
|
+
selector_values = _selector_list(selectors, name)
|
|
313
|
+
if selector_values is None:
|
|
314
|
+
return None
|
|
289
315
|
|
|
290
316
|
out = []
|
|
291
|
-
|
|
317
|
+
metadata_values = []
|
|
318
|
+
singular = name[:-1] if name.endswith("s") else name
|
|
319
|
+
for selector in selector_values:
|
|
292
320
|
if isinstance(selector, str):
|
|
293
321
|
if header_fields is None:
|
|
294
|
-
raise ValueError("named
|
|
322
|
+
raise ValueError(f"named {name} are invalid when has_header=False")
|
|
295
323
|
if selector not in header_fields:
|
|
296
|
-
raise ValueError(f"{path}: unknown
|
|
324
|
+
raise ValueError(f"{path}: unknown {singular} name {selector!r}")
|
|
297
325
|
col0 = header_fields.index(selector)
|
|
326
|
+
metadata_value = selector
|
|
298
327
|
elif isinstance(selector, (int, np.integer)) and not isinstance(selector, bool):
|
|
299
328
|
col0 = int(selector)
|
|
329
|
+
metadata_value = int(selector)
|
|
300
330
|
else:
|
|
301
|
-
raise ValueError("
|
|
331
|
+
raise ValueError(f"{name} must contain column names or integer indices")
|
|
302
332
|
if col0 < 3 or col0 >= num_columns:
|
|
303
333
|
raise ValueError(
|
|
304
|
-
f"{path}:
|
|
334
|
+
f"{path}: {singular} {selector!r} is out of range; selected columns must be physical columns 4 or later"
|
|
305
335
|
)
|
|
306
336
|
out.append(col0)
|
|
337
|
+
metadata_values.append(metadata_value)
|
|
307
338
|
|
|
308
339
|
if len(set(out)) != len(out):
|
|
309
|
-
raise ValueError("
|
|
310
|
-
return out
|
|
340
|
+
raise ValueError(f"{name} must not contain duplicates")
|
|
341
|
+
return out, metadata_values
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _normalize_value_columns(value_columns, header_fields, num_columns: int, path: Path) -> tuple[list[int], list] | None:
|
|
345
|
+
return _normalize_column_selectors(value_columns, header_fields, num_columns, path, "value_columns")
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _normalize_group_column(group_column, header_fields, num_columns: int, path: Path) -> tuple[int, object] | None:
|
|
349
|
+
normalized = _normalize_column_selectors(group_column, header_fields, num_columns, path, "group_column")
|
|
350
|
+
if normalized is None:
|
|
351
|
+
return None
|
|
352
|
+
columns, metadata_values = normalized
|
|
353
|
+
if len(columns) != 1:
|
|
354
|
+
raise ValueError("group_column must identify exactly one source column")
|
|
355
|
+
return columns[0], metadata_values[0]
|
|
311
356
|
|
|
312
357
|
|
|
313
358
|
def _default_annotation_names(
|
|
@@ -323,7 +368,7 @@ def _default_annotation_names(
|
|
|
323
368
|
if header_fields is None:
|
|
324
369
|
return [_annotation_name_from_path(path)]
|
|
325
370
|
return [str(header_fields[3])]
|
|
326
|
-
raise ValueError(f"{path}: input with five or more columns requires explicit value_columns")
|
|
371
|
+
raise ValueError(f"{path}: input with five or more columns requires explicit value_columns or group_column")
|
|
327
372
|
|
|
328
373
|
if header_fields is None and num_columns >= 5:
|
|
329
374
|
raise ValueError(f"{path}: headerless input with five or more columns requires annotation_names")
|
|
@@ -613,7 +658,7 @@ def load_annotations(
|
|
|
613
658
|
if not p.is_file():
|
|
614
659
|
raise FileNotFoundError(f"BED file not found: {p}")
|
|
615
660
|
|
|
616
|
-
annomat, annonames = _paint_binary_annotations(paths, reference)
|
|
661
|
+
annomat, annonames, source_counts = _paint_binary_annotations(paths, reference)
|
|
617
662
|
if annotation_metadata is not None:
|
|
618
663
|
metadata = _coerce_string_vector(annotation_metadata, len(paths), "annotation_metadata")
|
|
619
664
|
elif annotation_metadata_paths is not None:
|
|
@@ -623,14 +668,17 @@ def load_annotations(
|
|
|
623
668
|
f"annotation_metadata_paths length mismatch: expected {len(paths)}, got {len(meta_paths)}"
|
|
624
669
|
)
|
|
625
670
|
values = []
|
|
626
|
-
for bed_path, meta_path in zip(paths, meta_paths):
|
|
671
|
+
for bed_path, meta_path, count in zip(paths, meta_paths, source_counts):
|
|
627
672
|
if meta_path is None or str(meta_path) == "":
|
|
628
|
-
values.append(_generated_metadata(bed_path,
|
|
673
|
+
values.append(_generated_metadata(bed_path, False, count))
|
|
629
674
|
else:
|
|
630
675
|
values.append(_read_sidecar_exact(meta_path))
|
|
631
676
|
metadata = np.asarray(values, dtype=object)
|
|
632
677
|
else:
|
|
633
|
-
metadata = np.asarray(
|
|
678
|
+
metadata = np.asarray(
|
|
679
|
+
[_generated_metadata(p, False, count) for p, count in zip(paths, source_counts)],
|
|
680
|
+
dtype=object,
|
|
681
|
+
)
|
|
634
682
|
|
|
635
683
|
return create_annotations(
|
|
636
684
|
reference,
|
|
@@ -644,26 +692,67 @@ def load_annotations(
|
|
|
644
692
|
def load_annotation(
|
|
645
693
|
path,
|
|
646
694
|
reference,
|
|
647
|
-
|
|
695
|
+
has_header: bool = False,
|
|
648
696
|
value_columns=None,
|
|
697
|
+
group_column=None,
|
|
649
698
|
annotation_names=None,
|
|
650
699
|
annotation_metadata=None,
|
|
651
700
|
annotation_metadata_path=None,
|
|
652
701
|
) -> AnnotationPanel:
|
|
653
702
|
if annotation_metadata is not None and annotation_metadata_path is not None:
|
|
654
703
|
raise ValueError("load_annotation accepts at most one of annotation_metadata and annotation_metadata_path")
|
|
704
|
+
if value_columns is not None and group_column is not None:
|
|
705
|
+
raise ValueError("group_column and value_columns are mutually exclusive")
|
|
706
|
+
if group_column is not None and annotation_names is not None:
|
|
707
|
+
raise ValueError("annotation_names is invalid with group_column")
|
|
708
|
+
if group_column is not None and annotation_metadata is not None:
|
|
709
|
+
raise ValueError("annotation_metadata is invalid with group_column")
|
|
710
|
+
if group_column is not None and annotation_metadata_path is not None:
|
|
711
|
+
raise ValueError("annotation_metadata_path is invalid with group_column")
|
|
655
712
|
|
|
656
713
|
source_path = Path(path)
|
|
657
|
-
df, header_fields, row_base0 = _read_annotation_table(source_path, bool(
|
|
714
|
+
df, header_fields, row_base0 = _read_annotation_table(source_path, bool(has_header))
|
|
658
715
|
num_columns = int(df.shape[1])
|
|
659
716
|
if num_columns < 3:
|
|
660
717
|
raise ValueError(f"{source_path}: annotation input must have at least 3 tab-separated columns")
|
|
661
718
|
|
|
662
|
-
|
|
719
|
+
value_normalized = _normalize_value_columns(value_columns, header_fields, num_columns, source_path)
|
|
720
|
+
value_columns0 = None if value_normalized is None else value_normalized[0]
|
|
721
|
+
value_column_metadata = None if value_normalized is None else value_normalized[1]
|
|
722
|
+
group_normalized = _normalize_group_column(group_column, header_fields, num_columns, source_path)
|
|
723
|
+
group_column0 = None if group_normalized is None else group_normalized[0]
|
|
724
|
+
group_column_metadata = None if group_normalized is None else group_normalized[1]
|
|
663
725
|
chr_values, starts, ends = _validate_interval_columns(df, source_path, row_base0=row_base0)
|
|
664
726
|
n = int(reference.num_snp)
|
|
665
|
-
|
|
666
|
-
|
|
727
|
+
num_source_intervals = int(df.shape[0])
|
|
728
|
+
|
|
729
|
+
if group_column0 is not None:
|
|
730
|
+
group_values = df.iloc[:, group_column0].astype(str).to_numpy(dtype=object)
|
|
731
|
+
empty = group_values == ""
|
|
732
|
+
if empty.any():
|
|
733
|
+
idx = int(np.flatnonzero(empty)[0])
|
|
734
|
+
raise ValueError(f"{source_path}: row {row_base0 + idx + 1}: group_column values must be non-empty")
|
|
735
|
+
names = [str(x) for x in pd.unique(group_values)]
|
|
736
|
+
columns = []
|
|
737
|
+
metadata_values = []
|
|
738
|
+
# PERF: loop retained because grouped binary interval union is defined independently per group.
|
|
739
|
+
for group_value in names:
|
|
740
|
+
rows = group_values == group_value
|
|
741
|
+
intervals_by_chr = binary_intervals_by_chr_from_arrays(chr_values[rows], starts[rows], ends[rows])
|
|
742
|
+
columns.append(paint_binary_column(intervals_by_chr, reference))
|
|
743
|
+
metadata_values.append(
|
|
744
|
+
_generated_metadata(
|
|
745
|
+
source_path,
|
|
746
|
+
bool(has_header),
|
|
747
|
+
int(np.count_nonzero(rows)),
|
|
748
|
+
group_column=group_column_metadata,
|
|
749
|
+
group_value=group_value,
|
|
750
|
+
)
|
|
751
|
+
)
|
|
752
|
+
annomat = sparse.hstack(columns, format="csr") if columns else sparse.csr_matrix((n, 0), dtype=np.float64)
|
|
753
|
+
is_binary = np.ones(len(names), dtype=bool)
|
|
754
|
+
metadata = np.asarray(metadata_values, dtype=object)
|
|
755
|
+
elif value_columns0 is None and num_columns == 3:
|
|
667
756
|
names = (
|
|
668
757
|
_default_annotation_names(source_path, num_columns, header_fields, value_columns0)
|
|
669
758
|
if annotation_names is None
|
|
@@ -674,23 +763,22 @@ def load_annotation(
|
|
|
674
763
|
intervals_by_chr = binary_intervals_by_chr_from_arrays(chr_values, starts, ends)
|
|
675
764
|
annomat = paint_binary_column(intervals_by_chr, reference)
|
|
676
765
|
is_binary = np.array([True], dtype=bool)
|
|
677
|
-
source_columns = [None]
|
|
678
|
-
source_column_names = [None]
|
|
679
766
|
if annotation_metadata_path is not None:
|
|
680
767
|
metadata = np.asarray([_read_sidecar_exact(annotation_metadata_path)], dtype=object)
|
|
681
768
|
elif annotation_metadata is not None:
|
|
682
769
|
metadata = _coerce_string_vector(annotation_metadata, 1, "annotation_metadata")
|
|
683
770
|
else:
|
|
684
771
|
metadata = np.asarray(
|
|
685
|
-
[_generated_metadata(source_path,
|
|
772
|
+
[_generated_metadata(source_path, bool(has_header), num_source_intervals)],
|
|
686
773
|
dtype=object,
|
|
687
774
|
)
|
|
688
775
|
else:
|
|
689
776
|
if value_columns0 is None:
|
|
690
777
|
if num_columns == 4:
|
|
691
778
|
value_columns0 = [3]
|
|
779
|
+
value_column_metadata = [str(header_fields[3])] if header_fields is not None else [3]
|
|
692
780
|
else:
|
|
693
|
-
raise ValueError(f"{source_path}: input with five or more columns requires explicit value_columns")
|
|
781
|
+
raise ValueError(f"{source_path}: input with five or more columns requires explicit value_columns or group_column")
|
|
694
782
|
names = (
|
|
695
783
|
_default_annotation_names(source_path, num_columns, header_fields, value_columns0)
|
|
696
784
|
if annotation_names is None
|
|
@@ -732,10 +820,6 @@ def load_annotation(
|
|
|
732
820
|
blocks[shard_idx] = paint_numeric_sparse(ref_shard.bp, intervals, value_block)
|
|
733
821
|
annomat = sparse.vstack(blocks, format="csr")
|
|
734
822
|
is_binary = np.zeros(len(value_columns0), dtype=bool)
|
|
735
|
-
source_columns = value_columns0
|
|
736
|
-
source_column_names = [
|
|
737
|
-
None if header_fields is None else str(header_fields[i]) for i in value_columns0
|
|
738
|
-
]
|
|
739
823
|
|
|
740
824
|
if annotation_metadata_path is not None:
|
|
741
825
|
sidecar_lines = _read_column_metadata_sidecar(annotation_metadata_path, num_columns)
|
|
@@ -747,8 +831,13 @@ def load_annotation(
|
|
|
747
831
|
else:
|
|
748
832
|
metadata = np.asarray(
|
|
749
833
|
[
|
|
750
|
-
_generated_metadata(
|
|
751
|
-
|
|
834
|
+
_generated_metadata(
|
|
835
|
+
source_path,
|
|
836
|
+
bool(has_header),
|
|
837
|
+
num_source_intervals,
|
|
838
|
+
value_column=value_column,
|
|
839
|
+
)
|
|
840
|
+
for value_column in value_column_metadata
|
|
752
841
|
],
|
|
753
842
|
dtype=object,
|
|
754
843
|
)
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|