pypando 0.1.0__py3-none-any.whl
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.
- pypando/__init__.py +117 -0
- pypando/_blas.py +177 -0
- pypando/_wilcox.py +77 -0
- pypando/annotation.py +56 -0
- pypando/core.py +48 -0
- pypando/data/__init__.py +52 -0
- pypando/data/ensdb_v93_hg38.tsv.gz +0 -0
- pypando/data/motif2tf.tsv.gz +0 -0
- pypando/data/motifs.json.gz +0 -0
- pypando/formula.py +159 -0
- pypando/glmnet.py +493 -0
- pypando/graph.py +101 -0
- pypando/grn.py +762 -0
- pypando/models.py +226 -0
- pypando/motifs.py +146 -0
- pypando/objects.py +266 -0
- pypando/plots.py +68 -0
- pypando/ranges.py +248 -0
- pypando/rrng.py +109 -0
- pypando/rstats.py +323 -0
- pypando/utils.py +95 -0
- pypando-0.1.0.dist-info/METADATA +146 -0
- pypando-0.1.0.dist-info/RECORD +26 -0
- pypando-0.1.0.dist-info/WHEEL +5 -0
- pypando-0.1.0.dist-info/licenses/LICENSE +22 -0
- pypando-0.1.0.dist-info/top_level.txt +1 -0
pypando/__init__.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""py-Pando — pure-Python port of Pando (Fleck et al., Nature 2023).
|
|
2
|
+
|
|
3
|
+
Pando infers gene regulatory networks from paired single-cell RNA and ATAC
|
|
4
|
+
data: for every target gene it regresses expression on TF expression x
|
|
5
|
+
accessibility of TF-binding peaks near the gene.
|
|
6
|
+
|
|
7
|
+
Functional API (mirrors the R package)::
|
|
8
|
+
|
|
9
|
+
import pypando as pp
|
|
10
|
+
grn = pp.initiate_grn(rna, atac, annotation)
|
|
11
|
+
grn = pp.find_motifs(grn, genome="hg38.fa")
|
|
12
|
+
grn = pp.infer_grn(grn, genes=targets)
|
|
13
|
+
grn = pp.find_modules(grn)
|
|
14
|
+
pp.coef(grn); pp.gof(grn); pp.NetworkModules(grn)
|
|
15
|
+
"""
|
|
16
|
+
from . import data
|
|
17
|
+
from .annotation import annotation_from_gtf
|
|
18
|
+
from .core import Pando
|
|
19
|
+
from .grn import (
|
|
20
|
+
find_modules,
|
|
21
|
+
find_motifs,
|
|
22
|
+
find_peaks_near_genes,
|
|
23
|
+
fit_grn_models,
|
|
24
|
+
format_coefs,
|
|
25
|
+
infer_grn,
|
|
26
|
+
initiate_grn,
|
|
27
|
+
region_background,
|
|
28
|
+
regions_near_genes,
|
|
29
|
+
subset_regions,
|
|
30
|
+
)
|
|
31
|
+
from .graph import get_network_graph
|
|
32
|
+
from .models import (
|
|
33
|
+
fit_bagging_ridge,
|
|
34
|
+
fit_bayesian_ridge,
|
|
35
|
+
fit_brms,
|
|
36
|
+
fit_cvglmnet,
|
|
37
|
+
fit_glm,
|
|
38
|
+
fit_glmnet,
|
|
39
|
+
fit_model,
|
|
40
|
+
fit_xgb,
|
|
41
|
+
)
|
|
42
|
+
from .motifs import PFM, load_motif2tf, load_pando_motifs
|
|
43
|
+
from .objects import (
|
|
44
|
+
GRNData,
|
|
45
|
+
Modules,
|
|
46
|
+
Network,
|
|
47
|
+
Regions,
|
|
48
|
+
RegulatoryNetwork,
|
|
49
|
+
DefaultNetwork,
|
|
50
|
+
GetGRN,
|
|
51
|
+
GetNetwork,
|
|
52
|
+
NetworkFeatures,
|
|
53
|
+
NetworkGraph,
|
|
54
|
+
NetworkModules,
|
|
55
|
+
NetworkParams,
|
|
56
|
+
NetworkRegions,
|
|
57
|
+
NetworkTFs,
|
|
58
|
+
Params,
|
|
59
|
+
coef,
|
|
60
|
+
gof,
|
|
61
|
+
)
|
|
62
|
+
from .objects import GetAssaySummary, aggregate_assay, create_GRNData
|
|
63
|
+
from .plots import plot_gof, plot_module_metrics, plot_network_graph
|
|
64
|
+
from .utils import aggregate_matrix, sparse_cor
|
|
65
|
+
|
|
66
|
+
__all__ = [
|
|
67
|
+
"Pando",
|
|
68
|
+
"annotation_from_gtf",
|
|
69
|
+
"initiate_grn",
|
|
70
|
+
"find_motifs",
|
|
71
|
+
"find_peaks_near_genes",
|
|
72
|
+
"infer_grn",
|
|
73
|
+
"fit_grn_models",
|
|
74
|
+
"format_coefs",
|
|
75
|
+
"find_modules",
|
|
76
|
+
"region_background",
|
|
77
|
+
"regions_near_genes",
|
|
78
|
+
"subset_regions",
|
|
79
|
+
"get_network_graph",
|
|
80
|
+
"fit_model",
|
|
81
|
+
"fit_glm",
|
|
82
|
+
"fit_glmnet",
|
|
83
|
+
"fit_cvglmnet",
|
|
84
|
+
"fit_xgb",
|
|
85
|
+
"fit_bagging_ridge",
|
|
86
|
+
"fit_bayesian_ridge",
|
|
87
|
+
"fit_brms",
|
|
88
|
+
"PFM",
|
|
89
|
+
"load_pando_motifs",
|
|
90
|
+
"load_motif2tf",
|
|
91
|
+
"GRNData",
|
|
92
|
+
"RegulatoryNetwork",
|
|
93
|
+
"Regions",
|
|
94
|
+
"Network",
|
|
95
|
+
"Modules",
|
|
96
|
+
"GetGRN",
|
|
97
|
+
"GetNetwork",
|
|
98
|
+
"DefaultNetwork",
|
|
99
|
+
"NetworkFeatures",
|
|
100
|
+
"NetworkTFs",
|
|
101
|
+
"NetworkRegions",
|
|
102
|
+
"NetworkModules",
|
|
103
|
+
"NetworkParams",
|
|
104
|
+
"NetworkGraph",
|
|
105
|
+
"Params",
|
|
106
|
+
"coef",
|
|
107
|
+
"gof",
|
|
108
|
+
"aggregate_matrix",
|
|
109
|
+
"aggregate_assay",
|
|
110
|
+
"GetAssaySummary",
|
|
111
|
+
"create_GRNData",
|
|
112
|
+
"plot_gof",
|
|
113
|
+
"plot_module_metrics",
|
|
114
|
+
"plot_network_graph",
|
|
115
|
+
"sparse_cor",
|
|
116
|
+
]
|
|
117
|
+
__version__ = "0.1.0"
|
pypando/_blas.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""R's QR (``dqrdc2`` / ``dqrls``) compiled with numba, calling the BLAS.
|
|
2
|
+
|
|
3
|
+
R's Fortran routines call the BLAS level-1 kernels ``dnrm2``, ``ddot``,
|
|
4
|
+
``daxpy`` and ``dscal``. Floating-point results of those kernels depend on
|
|
5
|
+
their summation order, so for decisions that sit on a knife edge (a column
|
|
6
|
+
is "negligible" if its residual norm falls below ``1e-11`` times its
|
|
7
|
+
original norm) the only way to reproduce R is to make the same calls in the
|
|
8
|
+
same order. This module does that through SciPy's BLAS (the library NumPy
|
|
9
|
+
and SciPy are linked to); with the same BLAS build as R's, the QR is
|
|
10
|
+
bit-identical to R's.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import ctypes
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
__all__ = ["dqrdc2_blas", "dqrls_blas", "available"]
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
import numba
|
|
22
|
+
from numba import njit
|
|
23
|
+
import scipy.linalg.cython_blas as _cb
|
|
24
|
+
|
|
25
|
+
def _fn(name, restype, *argtypes):
|
|
26
|
+
cap = _cb.__pyx_capi__[name]
|
|
27
|
+
ctypes.pythonapi.PyCapsule_GetName.restype = ctypes.c_char_p
|
|
28
|
+
ctypes.pythonapi.PyCapsule_GetName.argtypes = [ctypes.py_object]
|
|
29
|
+
ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p
|
|
30
|
+
ctypes.pythonapi.PyCapsule_GetPointer.argtypes = [ctypes.py_object, ctypes.c_char_p]
|
|
31
|
+
ptr = ctypes.pythonapi.PyCapsule_GetPointer(cap, ctypes.pythonapi.PyCapsule_GetName(cap))
|
|
32
|
+
return ctypes.CFUNCTYPE(restype, *argtypes)(ptr)
|
|
33
|
+
|
|
34
|
+
_P = ctypes.c_void_p
|
|
35
|
+
_dnrm2 = _fn("dnrm2", ctypes.c_double, _P, _P, _P)
|
|
36
|
+
_ddot = _fn("ddot", ctypes.c_double, _P, _P, _P, _P, _P)
|
|
37
|
+
_daxpy = _fn("daxpy", None, _P, _P, _P, _P, _P, _P)
|
|
38
|
+
_dscal = _fn("dscal", None, _P, _P, _P, _P)
|
|
39
|
+
available = True
|
|
40
|
+
except Exception: # pragma: no cover - numba or the capsules missing
|
|
41
|
+
available = False
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
if available:
|
|
45
|
+
|
|
46
|
+
@njit(cache=False)
|
|
47
|
+
def _dqrdc2(x, tol, jpvt, qraux, work, ib, db):
|
|
48
|
+
# x: Fortran-ordered (n, p). ib: int32 scratch[4], db: float64 scratch[2]
|
|
49
|
+
n, p = x.shape
|
|
50
|
+
xp = x.ctypes.data
|
|
51
|
+
ld = n
|
|
52
|
+
step = 8 # sizeof(double)
|
|
53
|
+
one = ib.ctypes.data # ib[0] = 1 (incx)
|
|
54
|
+
nn = ib.ctypes.data + 4 # ib[1] = length
|
|
55
|
+
ib[0] = 1
|
|
56
|
+
if n > 0:
|
|
57
|
+
for j in range(p):
|
|
58
|
+
ib[1] = n
|
|
59
|
+
v = _dnrm2(nn, xp + (j * ld) * step, one)
|
|
60
|
+
qraux[j] = v
|
|
61
|
+
work[j, 0] = v
|
|
62
|
+
work[j, 1] = v if v != 0.0 else 1.0
|
|
63
|
+
lup = min(n, p)
|
|
64
|
+
k = p + 1
|
|
65
|
+
for l in range(lup):
|
|
66
|
+
while not ((l + 1) >= k or qraux[l] >= work[l, 1] * tol):
|
|
67
|
+
for i in range(n):
|
|
68
|
+
t = x[i, l]
|
|
69
|
+
for j in range(l + 1, p):
|
|
70
|
+
x[i, j - 1] = x[i, j]
|
|
71
|
+
x[i, p - 1] = t
|
|
72
|
+
i0 = jpvt[l]
|
|
73
|
+
t = qraux[l]
|
|
74
|
+
tt = work[l, 0]
|
|
75
|
+
ttt = work[l, 1]
|
|
76
|
+
for j in range(l + 1, p):
|
|
77
|
+
jpvt[j - 1] = jpvt[j]
|
|
78
|
+
qraux[j - 1] = qraux[j]
|
|
79
|
+
work[j - 1, 0] = work[j, 0]
|
|
80
|
+
work[j - 1, 1] = work[j, 1]
|
|
81
|
+
jpvt[p - 1] = i0
|
|
82
|
+
qraux[p - 1] = t
|
|
83
|
+
work[p - 1, 0] = tt
|
|
84
|
+
work[p - 1, 1] = ttt
|
|
85
|
+
k -= 1
|
|
86
|
+
if (l + 1) != n:
|
|
87
|
+
m = n - l
|
|
88
|
+
ib[1] = m
|
|
89
|
+
col_l = xp + (l * ld + l) * step
|
|
90
|
+
nrmxl = _dnrm2(nn, col_l, one)
|
|
91
|
+
if nrmxl != 0.0:
|
|
92
|
+
if x[l, l] != 0.0:
|
|
93
|
+
nrmxl = abs(nrmxl) if x[l, l] >= 0 else -abs(nrmxl)
|
|
94
|
+
db[0] = 1.0 / nrmxl
|
|
95
|
+
_dscal(nn, db.ctypes.data, col_l, one)
|
|
96
|
+
x[l, l] = 1.0 + x[l, l]
|
|
97
|
+
for j in range(l + 1, p):
|
|
98
|
+
col_j = xp + (j * ld + l) * step
|
|
99
|
+
t = -_ddot(nn, col_l, one, col_j, one) / x[l, l]
|
|
100
|
+
db[1] = t
|
|
101
|
+
_daxpy(nn, db.ctypes.data + 8, col_l, one, col_j, one)
|
|
102
|
+
if qraux[j] != 0.0:
|
|
103
|
+
r = abs(x[l, j]) / qraux[j]
|
|
104
|
+
tt = 1.0 - r * r
|
|
105
|
+
if tt < 0.0:
|
|
106
|
+
tt = 0.0
|
|
107
|
+
t = tt
|
|
108
|
+
if abs(t) >= 1e-6:
|
|
109
|
+
qraux[j] = qraux[j] * np.sqrt(t)
|
|
110
|
+
else:
|
|
111
|
+
ib[2] = n - l - 1
|
|
112
|
+
qraux[j] = _dnrm2(ib.ctypes.data + 8, xp + (j * ld + l + 1) * step, one)
|
|
113
|
+
work[j, 0] = qraux[j]
|
|
114
|
+
qraux[l] = x[l, l]
|
|
115
|
+
x[l, l] = -nrmxl
|
|
116
|
+
return min(k - 1, n)
|
|
117
|
+
|
|
118
|
+
@njit(cache=False)
|
|
119
|
+
def _dqrsl_b(x, k, qraux, y, b, ib, db):
|
|
120
|
+
# qty (job digit b) then b (job digit c), as dqrsl with job 1110
|
|
121
|
+
n = x.shape[0]
|
|
122
|
+
xp = x.ctypes.data
|
|
123
|
+
step = 8
|
|
124
|
+
ib[0] = 1
|
|
125
|
+
one = ib.ctypes.data
|
|
126
|
+
nn = ib.ctypes.data + 4
|
|
127
|
+
qty = y.copy()
|
|
128
|
+
qp = qty.ctypes.data
|
|
129
|
+
ju = min(k, n - 1)
|
|
130
|
+
for j in range(ju):
|
|
131
|
+
if qraux[j] != 0.0:
|
|
132
|
+
temp = x[j, j]
|
|
133
|
+
x[j, j] = qraux[j]
|
|
134
|
+
ib[1] = n - j
|
|
135
|
+
col = xp + (j * n + j) * step
|
|
136
|
+
t = -_ddot(nn, col, one, qp + j * step, one) / x[j, j]
|
|
137
|
+
db[1] = t
|
|
138
|
+
_daxpy(nn, db.ctypes.data + 8, col, one, qp + j * step, one)
|
|
139
|
+
x[j, j] = temp
|
|
140
|
+
for i in range(k):
|
|
141
|
+
b[i] = qty[i]
|
|
142
|
+
bp = b.ctypes.data
|
|
143
|
+
for jj in range(k):
|
|
144
|
+
j = k - jj - 1
|
|
145
|
+
if x[j, j] == 0.0:
|
|
146
|
+
return j + 1 # info
|
|
147
|
+
b[j] = b[j] / x[j, j]
|
|
148
|
+
if j > 0:
|
|
149
|
+
db[1] = -b[j]
|
|
150
|
+
ib[1] = j
|
|
151
|
+
_daxpy(nn, db.ctypes.data + 8, xp + (j * n) * step, one, bp, one)
|
|
152
|
+
return 0
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def dqrdc2_blas(x: np.ndarray, tol: float):
|
|
156
|
+
"""Same contract as :func:`pypando.rstats.dqrdc2`, BLAS-exact."""
|
|
157
|
+
X = np.array(x, dtype=np.float64, order="F", copy=True)
|
|
158
|
+
p = X.shape[1]
|
|
159
|
+
jpvt = np.arange(p, dtype=np.int64)
|
|
160
|
+
qraux = np.zeros(p)
|
|
161
|
+
work = np.zeros((p, 2))
|
|
162
|
+
ib = np.zeros(4, dtype=np.int32)
|
|
163
|
+
db = np.zeros(2)
|
|
164
|
+
k = _dqrdc2(X, float(tol), jpvt, qraux, work, ib, db)
|
|
165
|
+
return X, int(k), qraux, jpvt
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def dqrls_blas(x: np.ndarray, y: np.ndarray, tol: float):
|
|
169
|
+
"""Same contract as :func:`pypando.rstats.dqrls`, BLAS-exact."""
|
|
170
|
+
qr, k, qraux, pivot = dqrdc2_blas(x, tol)
|
|
171
|
+
p = qr.shape[1]
|
|
172
|
+
b = np.zeros(p)
|
|
173
|
+
if k > 0:
|
|
174
|
+
ib = np.zeros(4, dtype=np.int32)
|
|
175
|
+
db = np.zeros(2)
|
|
176
|
+
_dqrsl_b(qr, k, qraux, np.ascontiguousarray(y, dtype=np.float64), b, ib, db)
|
|
177
|
+
return {"coefficients": b, "rank": k, "pivot": pivot, "qr": qr, "qraux": qraux}
|
pypando/_wilcox.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""One-sample Wilcoxon signed rank test as in R 4.4 (``stats::wilcox.test``).
|
|
2
|
+
|
|
3
|
+
R 4.4.x (the reference environment, R 4.4.3) drops zeros, ranks
|
|
4
|
+
``abs(x)`` without rounding (``digits.rank = Inf``) and uses the exact
|
|
5
|
+
null distribution when ``n < 50`` with no ties and no zeros; otherwise the
|
|
6
|
+
normal approximation with tie correction and continuity correction.
|
|
7
|
+
R >= 4.6 changed the zero handling; this follows 4.4.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import math
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
from scipy import stats
|
|
15
|
+
|
|
16
|
+
__all__ = ["wilcox_signed_rank_pvalue", "psignrank"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _csignrank_counts(n: int) -> np.ndarray:
|
|
20
|
+
"""Number of subsets of {1..n} with each sum 0..n(n+1)/2."""
|
|
21
|
+
u = n * (n + 1) // 2
|
|
22
|
+
c = np.zeros(u + 1, dtype=object)
|
|
23
|
+
c[0] = 1
|
|
24
|
+
for k in range(1, n + 1):
|
|
25
|
+
c[k:] = c[k:] + c[:-k] if k <= u else c[k:]
|
|
26
|
+
return c
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def psignrank(q: float, n: int, lower_tail: bool = True) -> float:
|
|
30
|
+
"""``stats::psignrank`` (exact)."""
|
|
31
|
+
q = math.floor(q + 1e-7)
|
|
32
|
+
u = n * (n + 1) // 2
|
|
33
|
+
if q < 0:
|
|
34
|
+
return 0.0 if lower_tail else 1.0
|
|
35
|
+
if q >= u:
|
|
36
|
+
return 1.0 if lower_tail else 0.0
|
|
37
|
+
c = _csignrank_counts(n)
|
|
38
|
+
total = 2**n
|
|
39
|
+
low = sum(c[: q + 1])
|
|
40
|
+
p = low / total
|
|
41
|
+
return float(p) if lower_tail else float((total - low) / total)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def wilcox_signed_rank_pvalue(x, mu: float = 0.0, correct: bool = True, exact=None) -> float:
|
|
45
|
+
"""``wilcox.test(x, mu = mu)$p.value``, two-sided, R 4.4 semantics."""
|
|
46
|
+
x = np.asarray(x, dtype=np.float64)
|
|
47
|
+
x = x[~np.isnan(x)]
|
|
48
|
+
if len(x) < 1:
|
|
49
|
+
raise ValueError("not enough (non-missing) 'x' observations")
|
|
50
|
+
x = x - mu
|
|
51
|
+
zeroes = bool(np.any(x == 0))
|
|
52
|
+
if zeroes:
|
|
53
|
+
x = x[x != 0]
|
|
54
|
+
n = float(len(x))
|
|
55
|
+
if exact is None:
|
|
56
|
+
exact = n < 50
|
|
57
|
+
r = stats.rankdata(np.abs(x), method="average")
|
|
58
|
+
statistic = float(r[x > 0].sum())
|
|
59
|
+
ties = len(r) != len(np.unique(r))
|
|
60
|
+
if exact and not ties and not zeroes:
|
|
61
|
+
ni = int(n)
|
|
62
|
+
if statistic > n * (n + 1) / 4:
|
|
63
|
+
p = psignrank(statistic - 1, ni, lower_tail=False)
|
|
64
|
+
else:
|
|
65
|
+
p = psignrank(statistic, ni)
|
|
66
|
+
return min(2 * p, 1.0)
|
|
67
|
+
_, nties = np.unique(r, return_counts=True)
|
|
68
|
+
z = statistic - n * (n + 1) / 4
|
|
69
|
+
sigma = math.sqrt(max(0.0, n * (n + 1) * (2 * n + 1) / 24 - float(np.sum(nties.astype(float) ** 3 - nties)) / 48))
|
|
70
|
+
correction = math.copysign(0.5, z) if (correct and z != 0) else 0.0
|
|
71
|
+
# IEEE arithmetic as in R: sigma = 0 (e.g. all values were zeros and were
|
|
72
|
+
# dropped) gives z = NaN (0/0) or +-Inf, and the p-value NaN or 0.
|
|
73
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
74
|
+
z = np.float64(z - correction) / np.float64(sigma)
|
|
75
|
+
if np.isnan(z):
|
|
76
|
+
return float("nan")
|
|
77
|
+
return float(2 * min(stats.norm.cdf(z), stats.norm.sf(z)))
|
pypando/annotation.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Gene annotation in the shape Pando reads (Signac's ``Annotation(object)``).
|
|
2
|
+
|
|
3
|
+
Pando uses a gene annotation for two things: a gene's extent (the union of
|
|
4
|
+
its exon / CDS / UTR rows; Signac's intronic ``gap`` rows are dropped) and
|
|
5
|
+
the exons it removes from the candidate regions. Signac builds this table
|
|
6
|
+
with ``GetGRangesFromEnsDb``; :func:`annotation_from_gtf` builds the same
|
|
7
|
+
table from a GENCODE or Ensembl GTF/GFF3 file.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import gzip
|
|
12
|
+
import re
|
|
13
|
+
from typing import Sequence
|
|
14
|
+
|
|
15
|
+
import pandas as pd
|
|
16
|
+
|
|
17
|
+
__all__ = ["annotation_from_gtf"]
|
|
18
|
+
|
|
19
|
+
# Signac::GetGRangesFromEnsDb default biotypes (GENCODE >= v32 calls lincRNA "lncRNA")
|
|
20
|
+
DEFAULT_BIOTYPES = ("protein_coding", "lncRNA", "lincRNA", "rRNA", "processed_transcript")
|
|
21
|
+
_TYPES = {"exon": "exon", "CDS": "cds", "five_prime_UTR": "utr", "three_prime_UTR": "utr", "UTR": "utr"}
|
|
22
|
+
_ATTR = re.compile(r'(\w+)[ =]"?([^";]+)"?')
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def annotation_from_gtf(path, biotypes: Sequence[str] | None = DEFAULT_BIOTYPES, chr_prefix: str | None = None) -> pd.DataFrame:
|
|
26
|
+
"""Signac-style annotation from a GTF / GFF3 (optionally gzipped).
|
|
27
|
+
|
|
28
|
+
Returns columns ``seqnames, start, end, strand, tx_id, gene_name, gene_id,
|
|
29
|
+
gene_biotype, type`` with ``type`` in ``exon``, ``cds``, ``utr``, for genes
|
|
30
|
+
whose biotype is in ``biotypes`` (``None`` keeps all). ``chr_prefix``
|
|
31
|
+
(e.g. ``"chr"``) is prepended to sequence names that lack it, for
|
|
32
|
+
Ensembl files used with UCSC-style peaks.
|
|
33
|
+
"""
|
|
34
|
+
opener = gzip.open if str(path).endswith(".gz") else open
|
|
35
|
+
keep = None if biotypes is None else set(biotypes)
|
|
36
|
+
rows = []
|
|
37
|
+
with opener(path, "rt") as fh:
|
|
38
|
+
for line in fh:
|
|
39
|
+
if line.startswith("#"):
|
|
40
|
+
continue
|
|
41
|
+
f = line.rstrip("\n").split("\t")
|
|
42
|
+
if len(f) < 9 or f[2] not in _TYPES:
|
|
43
|
+
continue
|
|
44
|
+
attrs = dict(_ATTR.findall(f[8]))
|
|
45
|
+
biotype = attrs.get("gene_type") or attrs.get("gene_biotype") or ""
|
|
46
|
+
if keep is not None and biotype not in keep:
|
|
47
|
+
continue
|
|
48
|
+
chrom = f[0]
|
|
49
|
+
if chr_prefix and not chrom.startswith(chr_prefix):
|
|
50
|
+
chrom = chr_prefix + chrom
|
|
51
|
+
rows.append((chrom, int(f[3]), int(f[4]), f[6],
|
|
52
|
+
(attrs.get("transcript_id") or "").split(".")[0],
|
|
53
|
+
attrs.get("gene_name", ""), (attrs.get("gene_id") or "").split(".")[0],
|
|
54
|
+
biotype, _TYPES[f[2]]))
|
|
55
|
+
return pd.DataFrame(rows, columns=["seqnames", "start", "end", "strand", "tx_id", "gene_name",
|
|
56
|
+
"gene_id", "gene_biotype", "type"])
|
pypando/core.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Class API: the same workflow as method chaining on one object."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from . import grn as _grn
|
|
5
|
+
from .graph import get_network_graph
|
|
6
|
+
from .objects import GRNData, coef, gof, NetworkModules
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Pando:
|
|
10
|
+
"""Method-chaining wrapper around :class:`~pypando.objects.GRNData`.
|
|
11
|
+
|
|
12
|
+
>>> p = Pando(rna, atac, annotation).find_motifs(genome="hg38.fa").infer_grn(genes=targets).find_modules()
|
|
13
|
+
>>> p.coefs.head()
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, rna, atac, annotation, **initiate_kwargs):
|
|
17
|
+
self.data: GRNData = _grn.initiate_grn(rna, atac, annotation, **initiate_kwargs)
|
|
18
|
+
|
|
19
|
+
def find_motifs(self, **kwargs) -> "Pando":
|
|
20
|
+
_grn.find_motifs(self.data, **kwargs)
|
|
21
|
+
return self
|
|
22
|
+
|
|
23
|
+
def infer_grn(self, **kwargs) -> "Pando":
|
|
24
|
+
_grn.infer_grn(self.data, **kwargs)
|
|
25
|
+
return self
|
|
26
|
+
|
|
27
|
+
def find_modules(self, **kwargs) -> "Pando":
|
|
28
|
+
_grn.find_modules(self.data, **kwargs)
|
|
29
|
+
return self
|
|
30
|
+
|
|
31
|
+
def network_graph(self, **kwargs) -> "Pando":
|
|
32
|
+
get_network_graph(self.data, **kwargs)
|
|
33
|
+
return self
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def coefs(self):
|
|
37
|
+
return coef(self.data)
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def gof(self):
|
|
41
|
+
return gof(self.data)
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def modules(self):
|
|
45
|
+
return NetworkModules(self.data)
|
|
46
|
+
|
|
47
|
+
def __repr__(self):
|
|
48
|
+
return repr(self.data)
|
pypando/data/__init__.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Pando's bundled data sets.
|
|
2
|
+
|
|
3
|
+
Shipped with the wheel: the motif collection (:func:`pypando.motifs.load_pando_motifs`),
|
|
4
|
+
the motif-to-TF table (:func:`pypando.motifs.load_motif2tf`) and the Ensembl v93
|
|
5
|
+
GRCh38 annotation used by ``peak_to_gene_method="GREAT"`` (:func:`ensdb_v93_hg38`).
|
|
6
|
+
|
|
7
|
+
Downloaded on first use (pooch, checksum-pinned), because they are too large for a
|
|
8
|
+
wheel: the phastCons 20-mammal conserved elements and the SCREEN cCREs that Pando
|
|
9
|
+
offers as ``regions`` for :func:`pypando.initiate_grn`.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from importlib import resources
|
|
14
|
+
|
|
15
|
+
import pandas as pd
|
|
16
|
+
|
|
17
|
+
__all__ = ["ensdb_v93_hg38", "phastcons_hg38", "screen_ccre_hg38"]
|
|
18
|
+
|
|
19
|
+
_ASSET_BASE = "https://github.com/omicverse/py-Pando/releases/download/data-v1/"
|
|
20
|
+
_ASSETS = {
|
|
21
|
+
"phastcons20_hg38.tsv.gz": "sha256:4e7e30bb0d2727a3ff5409fa8e8e1948d4543a3a635b0925ce785c3d8ceb5950",
|
|
22
|
+
"screen_ccre_hg38.tsv.gz": "sha256:13815dd04916f6a31702b943cc49adf047647cef5451c9268191adff059f466d",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _read(name: str) -> pd.DataFrame:
|
|
27
|
+
with resources.files(__name__).joinpath(name).open("rb") as fh:
|
|
28
|
+
return pd.read_csv(fh, sep="\t", compression="gzip", keep_default_na=False, low_memory=False)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def ensdb_v93_hg38() -> pd.DataFrame:
|
|
32
|
+
"""``EnsDb.Hsapiens.v93.annot.UCSC.hg38`` (one row per transcript, gene ranges)."""
|
|
33
|
+
df = _read("ensdb_v93_hg38.tsv.gz")
|
|
34
|
+
for c in ("start", "end"):
|
|
35
|
+
df[c] = df[c].astype("int64")
|
|
36
|
+
return df
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _fetch(name: str) -> str:
|
|
40
|
+
import pooch
|
|
41
|
+
|
|
42
|
+
return pooch.retrieve(_ASSET_BASE + name, known_hash=_ASSETS[name], progressbar=False)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def phastcons_hg38() -> pd.DataFrame:
|
|
46
|
+
"""``phastConsElements20Mammals.UCSC.hg38`` (2,244,053 conserved elements)."""
|
|
47
|
+
return pd.read_csv(_fetch("phastcons20_hg38.tsv.gz"), sep="\t", keep_default_na=False)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def screen_ccre_hg38() -> pd.DataFrame:
|
|
51
|
+
"""``SCREEN.ccRE.UCSC.hg38`` (926,535 candidate cis-regulatory elements)."""
|
|
52
|
+
return pd.read_csv(_fetch("screen_ccre_hg38.tsv.gz"), sep="\t", keep_default_na=False)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|