grapp 0.4__tar.gz → 0.5__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 (53) hide show
  1. grapp-0.5/MANIFEST.in +4 -0
  2. {grapp-0.4 → grapp-0.5}/PKG-INFO +11 -6
  3. {grapp-0.4 → grapp-0.5}/README.md +2 -2
  4. {grapp-0.4 → grapp-0.5}/grapp/assoc/__init__.py +111 -38
  5. grapp-0.5/grapp/assoc/bolt_inf_core.py +1685 -0
  6. grapp-0.5/grapp/assoc/bolt_lmm.py +298 -0
  7. {grapp-0.4 → grapp-0.5}/grapp/cli/assoc_cli.py +44 -6
  8. grapp-0.5/grapp/cli/bolt_lmm_cli.py +162 -0
  9. {grapp-0.4 → grapp-0.5}/grapp/cli/filter_cli.py +25 -6
  10. {grapp-0.4 → grapp-0.5}/grapp/cli/main.py +27 -0
  11. {grapp-0.4 → grapp-0.5}/grapp/cli/pheno_cli.py +14 -0
  12. grapp-0.5/grapp/cli/polar_cli.py +112 -0
  13. {grapp-0.4 → grapp-0.5}/grapp/cli/show_cli.py +32 -2
  14. grapp-0.5/grapp/cli/ts2igd_cli.py +61 -0
  15. grapp-0.5/grapp/grg_calculator.py +587 -0
  16. {grapp-0.4 → grapp-0.5}/grapp/linalg/__init__.py +50 -24
  17. grapp-0.5/grapp/linalg/ops_cupy.py +1370 -0
  18. {grapp-0.4 → grapp-0.5}/grapp/linalg/ops_scipy.py +77 -27
  19. grapp-0.5/grapp/popgen/__init__.py +17 -0
  20. grapp-0.5/grapp/popgen/polarize.py +379 -0
  21. grapp-0.5/grapp/popgen/sfs.py +123 -0
  22. grapp-0.5/grapp/popgen/simple.py +166 -0
  23. {grapp-0.4 → grapp-0.5}/grapp/util/__init__.py +1 -0
  24. {grapp-0.4 → grapp-0.5}/grapp/util/filter.py +18 -13
  25. {grapp-0.4 → grapp-0.5}/grapp/util/igd.py +44 -58
  26. grapp-0.5/grapp/util/parallel.py +118 -0
  27. grapp-0.5/grapp/util/simple.py +658 -0
  28. grapp-0.5/grapp/version.py +1 -0
  29. {grapp-0.4 → grapp-0.5}/grapp.egg-info/PKG-INFO +11 -6
  30. {grapp-0.4 → grapp-0.5}/grapp.egg-info/SOURCES.txt +14 -0
  31. grapp-0.5/grapp.egg-info/requires.txt +12 -0
  32. grapp-0.5/popgen_requirements.txt +2 -0
  33. grapp-0.5/requirements.txt +8 -0
  34. {grapp-0.4 → grapp-0.5}/setup.py +5 -0
  35. {grapp-0.4 → grapp-0.5}/test/testing_utils.py +99 -0
  36. grapp-0.4/grapp/grg_calculator.py +0 -306
  37. grapp-0.4/grapp/util/simple.py +0 -425
  38. grapp-0.4/grapp/version.py +0 -1
  39. grapp-0.4/grapp.egg-info/requires.txt +0 -7
  40. grapp-0.4/requirements.txt +0 -7
  41. {grapp-0.4 → grapp-0.5}/LICENSE +0 -0
  42. {grapp-0.4 → grapp-0.5}/grapp/__init__.py +0 -0
  43. {grapp-0.4 → grapp-0.5}/grapp/cli/__init__.py +0 -0
  44. {grapp-0.4 → grapp-0.5}/grapp/cli/export_cli.py +0 -0
  45. {grapp-0.4 → grapp-0.5}/grapp/cli/pca_cli.py +0 -0
  46. {grapp-0.4 → grapp-0.5}/grapp/cli/util.py +0 -0
  47. {grapp-0.4 → grapp-0.5}/grapp/linalg/proPCA.py +0 -0
  48. {grapp-0.4 → grapp-0.5}/grapp/nn/__init__.py +0 -0
  49. {grapp-0.4 → grapp-0.5}/grapp/util/exceptions.py +0 -0
  50. {grapp-0.4 → grapp-0.5}/grapp.egg-info/dependency_links.txt +0 -0
  51. {grapp-0.4 → grapp-0.5}/grapp.egg-info/entry_points.txt +0 -0
  52. {grapp-0.4 → grapp-0.5}/grapp.egg-info/top_level.txt +0 -0
  53. {grapp-0.4 → grapp-0.5}/setup.cfg +0 -0
grapp-0.5/MANIFEST.in ADDED
@@ -0,0 +1,4 @@
1
+ include LICENSE
2
+ include README.md
3
+ include requirements.txt
4
+ include popgen_requirements.txt
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: grapp
3
- Version: 0.4
3
+ Version: 0.5
4
4
  Summary: Statistical and population genetics methods on GRG
5
5
  Home-page: https://aprilweilab.github.io/
6
6
  Author: Drew DeHaas
@@ -13,19 +13,24 @@ Classifier: Programming Language :: Python :: 3.12
13
13
  Classifier: Programming Language :: Python :: 3.13
14
14
  Description-Content-Type: text/markdown
15
15
  License-File: LICENSE
16
- Requires-Dist: grg_pheno_sim>=1.4
16
+ Requires-Dist: grg_pheno_sim<2.0,>=1.4
17
17
  Requires-Dist: numpy
18
18
  Requires-Dist: pandas
19
- Requires-Dist: pygrgl>=2.7
20
- Requires-Dist: pyigd>=1.3
19
+ Requires-Dist: pyfaidx
20
+ Requires-Dist: pygrgl<3.0,>=2.10
21
+ Requires-Dist: pyigd<2.0,>=1.3
21
22
  Requires-Dist: scikit-learn
22
23
  Requires-Dist: scipy
24
+ Provides-Extra: popgen
25
+ Requires-Dist: pyfaidx; extra == "popgen"
26
+ Requires-Dist: scikit-allel; extra == "popgen"
23
27
  Dynamic: author
24
28
  Dynamic: classifier
25
29
  Dynamic: description
26
30
  Dynamic: description-content-type
27
31
  Dynamic: home-page
28
32
  Dynamic: license-file
33
+ Dynamic: provides-extra
29
34
  Dynamic: requires-dist
30
35
  Dynamic: summary
31
36
 
@@ -36,7 +41,7 @@ on top of the Genotype Representation Graph (GRG) format. [GRG](https://github.c
36
41
  a file format and data structure that losslessly represents a genetic dataset. It has the advantage of
37
42
  compressing large datasets significantly, while also making calculations over that dataset extremely fast
38
43
  (see [the paper](https://www.nature.com/articles/s43588-024-00739-9) and the
39
- [core library](https://github.com/aprilweilab/grgl)).
44
+ [main GRG documentation](https://grgl.readthedocs.io/en/latest/index.html)).
40
45
 
41
46
  Some notes on usage are below, and you can also browse [the Python documentation for grapp](https://grapp.readthedocs.io/en/latest/).
42
47
 
@@ -177,7 +182,7 @@ mutation filters:
177
182
 
178
183
  #### Library
179
184
 
180
- Library API reference document is [here](https://grapp.readthedocs.io/en/latest/). There are also some tutorials in the [GRG documentation](https://grgl.readthedocs.io/en/stable/tutorials/GWAS.html) (and correspond [Jupyter notebooks](https://github.com/aprilweilab/grgl/tree/main/doc/tutorials/notebooks)).
185
+ Library API reference document is [here](https://grapp.readthedocs.io/en/latest/). There are also some tutorials in the [GRG documentation](https://grgl.readthedocs.io/en/latest/tutorials/GWAS.html) (and correspond [Jupyter notebooks](https://github.com/aprilweilab/grgl/tree/main/doc/tutorials/notebooks)).
181
186
 
182
187
  ### nn
183
188
 
@@ -5,7 +5,7 @@ on top of the Genotype Representation Graph (GRG) format. [GRG](https://github.c
5
5
  a file format and data structure that losslessly represents a genetic dataset. It has the advantage of
6
6
  compressing large datasets significantly, while also making calculations over that dataset extremely fast
7
7
  (see [the paper](https://www.nature.com/articles/s43588-024-00739-9) and the
8
- [core library](https://github.com/aprilweilab/grgl)).
8
+ [main GRG documentation](https://grgl.readthedocs.io/en/latest/index.html)).
9
9
 
10
10
  Some notes on usage are below, and you can also browse [the Python documentation for grapp](https://grapp.readthedocs.io/en/latest/).
11
11
 
@@ -146,7 +146,7 @@ mutation filters:
146
146
 
147
147
  #### Library
148
148
 
149
- Library API reference document is [here](https://grapp.readthedocs.io/en/latest/). There are also some tutorials in the [GRG documentation](https://grgl.readthedocs.io/en/stable/tutorials/GWAS.html) (and correspond [Jupyter notebooks](https://github.com/aprilweilab/grgl/tree/main/doc/tutorials/notebooks)).
149
+ Library API reference document is [here](https://grapp.readthedocs.io/en/latest/). There are also some tutorials in the [GRG documentation](https://grgl.readthedocs.io/en/latest/tutorials/GWAS.html) (and correspond [Jupyter notebooks](https://github.com/aprilweilab/grgl/tree/main/doc/tutorials/notebooks)).
150
150
 
151
151
  ### nn
152
152
 
@@ -1,11 +1,17 @@
1
1
  from grapp.linalg.ops_scipy import SciPyStdXOperator, SciPyXOperator
2
- from grapp.util.simple import allele_counts, _GenotypeDist, common_mut_dataframe
2
+ from grapp.util.simple import (
3
+ allele_counts,
4
+ _GenotypeDist,
5
+ common_mut_dataframe,
6
+ _div_or_default,
7
+ )
8
+ from grapp.util.exceptions import UserInputError
3
9
  from grapp.grg_calculator import (
4
10
  GRGCalcInterface as _GRGCalcInterface,
5
11
  _wrap_grg,
6
12
  )
7
13
  from scipy.stats import t as t_distribution
8
- from typing import List, Optional, Union
14
+ from typing import List, Optional, Union, Tuple
9
15
  import itertools
10
16
  import math
11
17
  import numpy
@@ -16,54 +22,89 @@ import sklearn.linear_model
16
22
  import sys
17
23
 
18
24
 
19
- def _div_or_default(a, b, d):
20
- """
21
- y = a / b, unless b_i is 0, then y_i will be set to 0.
22
-
23
- :param a: Numerator
24
- :param b: Denominator
25
- :param d: Default value for when denominator is 0.
26
- """
27
- result = numpy.full(a.shape, d)
28
- return numpy.divide(a, b, out=result, where=(b != 0))
29
-
30
-
31
- def read_plink_covariates(covar_path: str) -> numpy.typing.NDArray:
25
+ def read_plink_covariates(
26
+ covar_path: str, return_indivs: bool = False, verbose: bool = False
27
+ ) -> Union[numpy.typing.NDArray, Tuple[numpy.typing.NDArray, List[str]]]:
32
28
  """
33
- Reads a PLINK-style covariate file (no headers) and returns a NumPy matrix of covariate values.
34
- The first two columns (FID/IID) are ignored. Optionally adds an intercept column of 1s.
29
+ Reads a PLINK-style covariate file: Optional header line, FID and IID in first two columns,
30
+ covariates in remaining columns. The first two columns (FID/IID) are ignored. Does not allow
35
31
 
36
32
  :param path: Path to the covariate file.
37
33
  :type path: str
38
- :return: A NumPy array of shape (n_samples, n_covariates [+1 if intercept]).
39
- :rtype: numpy.ndarray
34
+ :param return_indivs: If True, return a list of individual identifiers (IIDs from the plink file).
35
+ :type return_indivs: bool
36
+ :param verbose: Emit warnings/information about the file if True. Default: True.
37
+ :type verbose: bool
38
+ :return: The covariates as a numpy array of shape (n_samples, n_covariates). If return_indivs is True, then also
39
+ return a list of individual IDs (strings).
40
+ :rtype: Union[numpy.typing.NDArray, Tuple[numpy.typing.NDArray, List[str]]]
40
41
  """
41
42
  rows = []
43
+ num_covar = 0
44
+ covar_names = []
45
+ iids = []
42
46
  with open(covar_path, "r") as f:
43
- for line in f:
47
+ for i, line in enumerate(f):
44
48
  parts = line.strip().split()
45
49
  if not parts:
46
50
  continue
47
- # skip FID/IID, keep covariates
48
- cov_vals = [float(v) for v in parts[2:]]
49
- rows.append(cov_vals)
51
+ if i == 0 and parts[0] == "FID":
52
+ assert (
53
+ len(parts) > 1 and parts[1] == "IID"
54
+ ), "Header line must include FID followed by IID."
55
+ covar_names = parts[2:]
56
+ num_covar = len(covar_names)
57
+ else:
58
+ cov_vals = [float(v) for v in parts[2:]]
59
+ if num_covar == 0:
60
+ num_covar = len(cov_vals)
61
+ assert num_covar == len(
62
+ cov_vals
63
+ ), f"Inconsistent number of columns in covariates file (line: {i}, cols: {len(cov_vals)})"
64
+ rows.append(cov_vals)
65
+ iids.append(parts[1])
66
+ assert num_covar > 0, "No covariates are present in the file (only two columns)."
50
67
 
51
68
  # stack into (n_samples × K)
52
69
  X = numpy.vstack(rows) if rows else numpy.empty((0, 0))
53
70
 
71
+ if verbose:
72
+ print(f"Loaded {num_covar} covariates for {len(rows)} individuals")
73
+ if covar_names:
74
+ print(f"Covariate names: {covar_names}")
75
+ if return_indivs:
76
+ return (X, iids)
54
77
  return X
55
78
 
56
79
 
57
- def read_pheno(filename: str, verbose: bool = True) -> numpy.typing.NDArray:
80
+ def read_pheno(
81
+ filename: str, return_indivs: bool = False, verbose: bool = True
82
+ ) -> Union[numpy.typing.NDArray, Tuple[numpy.typing.NDArray, List[str]]]:
58
83
  """
59
- Reads a PLINK/GCTA/GRG-style phenotype file and returns the phenotype vector.
84
+ Reads a PLINK/GCTA/GRG-style phenotype file and returns the phenotype vector. In all cases, the
85
+ row order of the file must match the individual indexing order of the GRG.
86
+
87
+ PLINK-style: Optional header "FID IID PHEN". The first column is the family ID (ignored by grapp),
88
+ the second column is the IID (used for validation by grapp), and the third column in the numerical
89
+ phenotype value. The IID order is checked against the GRG individual ID order, when IDs are present
90
+ in the GRG (and also not "NA" in the plink input file).
91
+
92
+ GRG-style: Optional header "person_id phenotypes". The first column is checked against the GRG individual
93
+ ID order, when IDs are present in GRG (and also not "NA" in the phenotype file). The second column is
94
+ the numerical phenotype value.
95
+
96
+ If there is no header, the number of columns determines the type (3 = plink, 2 = GRG).
97
+
98
+ In all cases, columns can be tab-separated or space separated.
60
99
 
61
100
  :param path: Path to the phenotype file.
62
101
  :type path: str
102
+ :param return_indivs: If True, return a list of individual identifiers (IIDs from the plink file).
103
+ :type return_indivs: bool
63
104
  :param verbose: Emit warnings/information about the file if True. Default: True.
64
105
  :type verbose: bool
65
106
  :return: A one-dimensional NumPy array of phenotype values.
66
- :rtype: numpy.ndarray
107
+ :rtype: Union[numpy.typing.NDArray, Tuple[numpy.typing.NDArray, List[str]]]
67
108
  """
68
109
  header_line = None
69
110
  with open(filename, "r") as f:
@@ -76,33 +117,56 @@ def read_pheno(filename: str, verbose: bool = True) -> numpy.typing.NDArray:
76
117
  header_line = i
77
118
  break
78
119
  # grg_pheno_sim header
79
- if re.match(r"^(person_id\tphenotypes)", line.strip(), re.IGNORECASE):
120
+ if re.match(
121
+ r"^((person_id|individual_id)\s+(phenotypes|phenotype))",
122
+ line.strip(),
123
+ re.IGNORECASE,
124
+ ):
80
125
  header_line = i
81
126
  break
82
127
 
83
128
  # Read data starting from the header (if present)
84
129
  if header_line is not None:
85
130
  df = pandas.read_csv(
86
- filename, sep=r"\s+", skiprows=header_line, engine="python"
131
+ filename,
132
+ sep=r"\s+",
133
+ skiprows=header_line,
134
+ engine="python",
135
+ skipinitialspace=True,
87
136
  )
88
137
  else:
89
- df = pandas.read_csv(filename, sep=r"\s+", header=None, engine="python")
138
+ df = pandas.read_csv(
139
+ filename,
140
+ sep=r"\s+",
141
+ header=None,
142
+ engine="python",
143
+ skipinitialspace=True,
144
+ )
90
145
 
91
146
  # Check column count
92
- if df.shape[1] not in (2, 3):
93
- raise ValueError(f"Expected 2 or 3 columns, but found {df.shape[1]}.")
147
+ if df.shape[1] > 3:
148
+ raise ValueError(f"Expected 1, 2, or 3 columns, but found {df.shape[1]}.")
94
149
 
95
150
  # Extract last column and make sure it's a number
96
151
  try:
97
152
  last_col = df.iloc[:, -1].astype(float).to_numpy()
98
153
  except ValueError:
99
- raise ValueError("Last column contains non-numeric values.")
154
+ raise ValueError(
155
+ "Last column should be phenotype, but it contains non-numeric values."
156
+ )
100
157
  if verbose:
101
158
  print(
102
- f"Using the column {df.shape[1]} (the last one) for phenotype.",
159
+ f"Using the {df.shape[1]}th column (the last one) for phenotype ({df.columns[-1]}).",
103
160
  file=sys.stderr,
104
161
  )
105
162
 
163
+ if return_indivs:
164
+ if len(df.columns) >= 2:
165
+ iids = df.iloc[:, -2].to_list()
166
+ else:
167
+ iids = ["NA"] * len(df)
168
+ return (last_col, iids)
169
+
106
170
  return last_col
107
171
 
108
172
 
@@ -122,6 +186,11 @@ def _computeDiagXTX(
122
186
  # diag(X^T @ X): for the non-standardized genotype matrix, GRG has a special initialization
123
187
  # method "xtx" which uses coalescence information to compute the diagonal in a single pass
124
188
  if dist == _GenotypeDist.SAMPLE.value:
189
+ if not grg.has_individual_coals:
190
+ raise UserInputError(
191
+ "GRG does not have individual coalescences; try using the binomial approximation for variance"
192
+ )
193
+
125
194
  XX = grg.matmul(
126
195
  numpy.ones((1, grg.num_samples), dtype=numpy.int32),
127
196
  pygrgl.TraversalDirection.UP,
@@ -249,8 +318,9 @@ def linear_assoc_no_covar(
249
318
  s_tot = yy - (total_pheno**2) / n
250
319
  r2 = 1 - sse / s_tot
251
320
 
252
- cdf_vals = t_distribution.cdf(t_stat, df=n - 2)
253
- p_val = 2 * numpy.where(t_stat > 0, 1 - cdf_vals, cdf_vals)
321
+ # The CDF is symmetric around 0, so for numerical reasons we always choose the smaller
322
+ # probability (tail) and do cdf(-|t|) instead of (1 - cdf(|t|)).
323
+ p_val = 2 * t_distribution.cdf(-numpy.abs(t_stat), df=n - 2)
254
324
 
255
325
  return common_mut_dataframe(
256
326
  grg, COUNT=acount, BETA=beta, B0=b0, SE=se, R2=r2, T=t_stat, P=p_val
@@ -296,6 +366,7 @@ def linear_assoc_covar(
296
366
  GAMMA columns.
297
367
  :rtype: pandas.DataFrame
298
368
  """
369
+ # TODO: generalize this. We just require the use of binomial variance when doing non-diploid
299
370
  PLOIDY = 2
300
371
  assert grg.ploidy == PLOIDY, "GWAS is only supported on diploid individuals"
301
372
  assert method in ("QR", "regress"), 'Invalid "method" parameter'
@@ -383,10 +454,11 @@ def linear_assoc_covar(
383
454
  xadjTxadj = XX - diagonal
384
455
 
385
456
  # Compute (Xadj^TYadj)
386
- xadjTyadj = Yadj @ X_op
457
+ xadjTyadj = numpy.where(acount < (PLOIDY * n_j), Yadj @ X_op, math.nan)
387
458
 
388
459
  beta = numpy.zeros(XX.size)
389
460
  beta = _div_or_default(xadjTyadj, xadjTxadj, math.nan)
461
+
390
462
  if only_beta:
391
463
  return pandas.DataFrame({"BETA": beta})
392
464
 
@@ -397,8 +469,9 @@ def linear_assoc_covar(
397
469
  se = numpy.sqrt(numpy.abs(_div_or_default(SSE, (df * xadjTxadj), math.nan)))
398
470
  t_vals = beta / se
399
471
 
400
- cdf_vals = t_distribution.cdf(t_vals, df)
401
- p = 2 * numpy.where(t_vals > 0, 1 - cdf_vals, cdf_vals)
472
+ # The CDF is symmetric around 0, so for numerical reasons we always choose the smaller
473
+ # probability (tail) and do cdf(-|t|) instead of (1 - cdf(|t|)).
474
+ p = 2 * t_distribution.cdf(-numpy.abs(t_vals), df=df)
402
475
 
403
476
  # Optional GAMMA calculation
404
477
  gamma_cols = {}