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.
- grapp-0.5/MANIFEST.in +4 -0
- {grapp-0.4 → grapp-0.5}/PKG-INFO +11 -6
- {grapp-0.4 → grapp-0.5}/README.md +2 -2
- {grapp-0.4 → grapp-0.5}/grapp/assoc/__init__.py +111 -38
- grapp-0.5/grapp/assoc/bolt_inf_core.py +1685 -0
- grapp-0.5/grapp/assoc/bolt_lmm.py +298 -0
- {grapp-0.4 → grapp-0.5}/grapp/cli/assoc_cli.py +44 -6
- grapp-0.5/grapp/cli/bolt_lmm_cli.py +162 -0
- {grapp-0.4 → grapp-0.5}/grapp/cli/filter_cli.py +25 -6
- {grapp-0.4 → grapp-0.5}/grapp/cli/main.py +27 -0
- {grapp-0.4 → grapp-0.5}/grapp/cli/pheno_cli.py +14 -0
- grapp-0.5/grapp/cli/polar_cli.py +112 -0
- {grapp-0.4 → grapp-0.5}/grapp/cli/show_cli.py +32 -2
- grapp-0.5/grapp/cli/ts2igd_cli.py +61 -0
- grapp-0.5/grapp/grg_calculator.py +587 -0
- {grapp-0.4 → grapp-0.5}/grapp/linalg/__init__.py +50 -24
- grapp-0.5/grapp/linalg/ops_cupy.py +1370 -0
- {grapp-0.4 → grapp-0.5}/grapp/linalg/ops_scipy.py +77 -27
- grapp-0.5/grapp/popgen/__init__.py +17 -0
- grapp-0.5/grapp/popgen/polarize.py +379 -0
- grapp-0.5/grapp/popgen/sfs.py +123 -0
- grapp-0.5/grapp/popgen/simple.py +166 -0
- {grapp-0.4 → grapp-0.5}/grapp/util/__init__.py +1 -0
- {grapp-0.4 → grapp-0.5}/grapp/util/filter.py +18 -13
- {grapp-0.4 → grapp-0.5}/grapp/util/igd.py +44 -58
- grapp-0.5/grapp/util/parallel.py +118 -0
- grapp-0.5/grapp/util/simple.py +658 -0
- grapp-0.5/grapp/version.py +1 -0
- {grapp-0.4 → grapp-0.5}/grapp.egg-info/PKG-INFO +11 -6
- {grapp-0.4 → grapp-0.5}/grapp.egg-info/SOURCES.txt +14 -0
- grapp-0.5/grapp.egg-info/requires.txt +12 -0
- grapp-0.5/popgen_requirements.txt +2 -0
- grapp-0.5/requirements.txt +8 -0
- {grapp-0.4 → grapp-0.5}/setup.py +5 -0
- {grapp-0.4 → grapp-0.5}/test/testing_utils.py +99 -0
- grapp-0.4/grapp/grg_calculator.py +0 -306
- grapp-0.4/grapp/util/simple.py +0 -425
- grapp-0.4/grapp/version.py +0 -1
- grapp-0.4/grapp.egg-info/requires.txt +0 -7
- grapp-0.4/requirements.txt +0 -7
- {grapp-0.4 → grapp-0.5}/LICENSE +0 -0
- {grapp-0.4 → grapp-0.5}/grapp/__init__.py +0 -0
- {grapp-0.4 → grapp-0.5}/grapp/cli/__init__.py +0 -0
- {grapp-0.4 → grapp-0.5}/grapp/cli/export_cli.py +0 -0
- {grapp-0.4 → grapp-0.5}/grapp/cli/pca_cli.py +0 -0
- {grapp-0.4 → grapp-0.5}/grapp/cli/util.py +0 -0
- {grapp-0.4 → grapp-0.5}/grapp/linalg/proPCA.py +0 -0
- {grapp-0.4 → grapp-0.5}/grapp/nn/__init__.py +0 -0
- {grapp-0.4 → grapp-0.5}/grapp/util/exceptions.py +0 -0
- {grapp-0.4 → grapp-0.5}/grapp.egg-info/dependency_links.txt +0 -0
- {grapp-0.4 → grapp-0.5}/grapp.egg-info/entry_points.txt +0 -0
- {grapp-0.4 → grapp-0.5}/grapp.egg-info/top_level.txt +0 -0
- {grapp-0.4 → grapp-0.5}/setup.cfg +0 -0
grapp-0.5/MANIFEST.in
ADDED
{grapp-0.4 → grapp-0.5}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: grapp
|
|
3
|
-
Version: 0.
|
|
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
|
|
16
|
+
Requires-Dist: grg_pheno_sim<2.0,>=1.4
|
|
17
17
|
Requires-Dist: numpy
|
|
18
18
|
Requires-Dist: pandas
|
|
19
|
-
Requires-Dist:
|
|
20
|
-
Requires-Dist:
|
|
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
|
-
[
|
|
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/
|
|
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
|
-
[
|
|
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/
|
|
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
|
|
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
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
34
|
-
The first two columns (FID/IID) are ignored.
|
|
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
|
-
:
|
|
39
|
-
:
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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(
|
|
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.
|
|
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(
|
|
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,
|
|
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(
|
|
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]
|
|
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(
|
|
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
|
|
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
|
-
|
|
253
|
-
|
|
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
|
-
|
|
401
|
-
|
|
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 = {}
|