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 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)
@@ -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