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.
Files changed (24) hide show
  1. {statgen-0.3.3 → statgen-0.3.4}/PKG-INFO +1 -1
  2. {statgen-0.3.3 → statgen-0.3.4}/pyproject.toml +1 -1
  3. {statgen-0.3.3 → statgen-0.3.4}/statgen/__init__.py +1 -1
  4. {statgen-0.3.3 → statgen-0.3.4}/statgen/annotations.py +144 -55
  5. {statgen-0.3.3 → statgen-0.3.4}/statgen.egg-info/PKG-INFO +1 -1
  6. {statgen-0.3.3 → statgen-0.3.4}/README.md +0 -0
  7. {statgen-0.3.3 → statgen-0.3.4}/setup.cfg +0 -0
  8. {statgen-0.3.3 → statgen-0.3.4}/statgen/_annotation_painting.py +0 -0
  9. {statgen-0.3.3 → statgen-0.3.4}/statgen/_bfile_utils.py +0 -0
  10. {statgen-0.3.3 → statgen-0.3.4}/statgen/_ld_npz.py +0 -0
  11. {statgen-0.3.3 → statgen-0.3.4}/statgen/_ld_reference.py +0 -0
  12. {statgen-0.3.3 → statgen-0.3.4}/statgen/_ld_schema.py +0 -0
  13. {statgen-0.3.3 → statgen-0.3.4}/statgen/_ld_writer.py +0 -0
  14. {statgen-0.3.3 → statgen-0.3.4}/statgen/_utils.py +0 -0
  15. {statgen-0.3.3 → statgen-0.3.4}/statgen/_variant_match.py +0 -0
  16. {statgen-0.3.3 → statgen-0.3.4}/statgen/_verbosity.py +0 -0
  17. {statgen-0.3.3 → statgen-0.3.4}/statgen/genotype.py +0 -0
  18. {statgen-0.3.3 → statgen-0.3.4}/statgen/ld.py +0 -0
  19. {statgen-0.3.3 → statgen-0.3.4}/statgen/reference.py +0 -0
  20. {statgen-0.3.3 → statgen-0.3.4}/statgen/sumstats.py +0 -0
  21. {statgen-0.3.3 → statgen-0.3.4}/statgen.egg-info/SOURCES.txt +0 -0
  22. {statgen-0.3.3 → statgen-0.3.4}/statgen.egg-info/dependency_links.txt +0 -0
  23. {statgen-0.3.3 → statgen-0.3.4}/statgen.egg-info/requires.txt +0 -0
  24. {statgen-0.3.3 → statgen-0.3.4}/statgen.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: statgen
3
- Version: 0.3.3
3
+ Version: 0.3.4
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.3.3"
7
+ version = "0.3.4"
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"
@@ -1,5 +1,5 @@
1
1
  from ._verbosity import get_verbosity, set_verbosity
2
2
 
3
- __version__ = "0.3.3"
3
+ __version__ = "0.3.4"
4
4
 
5
5
  __all__ = ["__version__", "get_verbosity", "set_verbosity"]
@@ -137,16 +137,27 @@ def _annotation_name_from_path(path: Path) -> str:
137
137
  return name
138
138
 
139
139
 
140
- def _generated_metadata(path: Path, source_column0, source_column_name) -> str:
141
- return json.dumps(
142
- {
143
- "source_file": str(path),
144
- "source_column0": source_column0,
145
- "source_column_name": source_column_name,
146
- },
147
- sort_keys=True,
148
- separators=(",", ":"),
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, header: bool) -> tuple[pd.DataFrame, list[str] | None, int]:
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 header else None,
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 header else None
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 header else 0
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, header=False)
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(bed_paths: list[Path], reference) -> tuple[sparse.csr_matrix, list[str]]:
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
- return sparse.csr_matrix((0, k), dtype=np.float64), annonames
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 _normalize_value_columns(value_columns, header_fields, num_columns: int, path: Path) -> list[int] | None:
281
- if value_columns is None:
299
+ def _selector_list(selectors, name: str) -> list | None:
300
+ if selectors is None:
282
301
  return None
283
- if isinstance(value_columns, (str, int, np.integer)):
284
- selectors = [value_columns]
302
+ if isinstance(selectors, (str, int, np.integer)):
303
+ values = [selectors]
285
304
  else:
286
- selectors = list(value_columns)
287
- if not selectors:
288
- raise ValueError("value_columns must be a non-empty list")
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
- for selector in selectors:
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 value_columns are invalid when header=False")
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 value column name {selector!r}")
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("value_columns must contain column names or integer indices")
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}: value column {selector!r} is out of range; selected columns must be physical columns 4 or later"
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("value_columns must not contain duplicates")
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, None, None))
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([_generated_metadata(p, None, None) for p in paths], dtype=object)
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
- header: bool = False,
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(header))
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
- value_columns0 = _normalize_value_columns(value_columns, header_fields, num_columns, source_path)
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
- if value_columns0 is None and num_columns == 3:
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, source_columns[0], source_column_names[0])],
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(source_path, col0, col_name)
751
- for col0, col_name in zip(source_columns, source_column_names)
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
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: statgen
3
- Version: 0.3.3
3
+ Version: 0.3.4
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
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