pypando 0.1.0__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.
- pypando-0.1.0/LICENSE +22 -0
- pypando-0.1.0/PKG-INFO +146 -0
- pypando-0.1.0/README.md +87 -0
- pypando-0.1.0/pypando/__init__.py +117 -0
- pypando-0.1.0/pypando/_blas.py +177 -0
- pypando-0.1.0/pypando/_wilcox.py +77 -0
- pypando-0.1.0/pypando/annotation.py +56 -0
- pypando-0.1.0/pypando/core.py +48 -0
- pypando-0.1.0/pypando/data/__init__.py +52 -0
- pypando-0.1.0/pypando/data/ensdb_v93_hg38.tsv.gz +0 -0
- pypando-0.1.0/pypando/data/motif2tf.tsv.gz +0 -0
- pypando-0.1.0/pypando/data/motifs.json.gz +0 -0
- pypando-0.1.0/pypando/formula.py +159 -0
- pypando-0.1.0/pypando/glmnet.py +493 -0
- pypando-0.1.0/pypando/graph.py +101 -0
- pypando-0.1.0/pypando/grn.py +762 -0
- pypando-0.1.0/pypando/models.py +226 -0
- pypando-0.1.0/pypando/motifs.py +146 -0
- pypando-0.1.0/pypando/objects.py +266 -0
- pypando-0.1.0/pypando/plots.py +68 -0
- pypando-0.1.0/pypando/ranges.py +248 -0
- pypando-0.1.0/pypando/rrng.py +109 -0
- pypando-0.1.0/pypando/rstats.py +323 -0
- pypando-0.1.0/pypando/utils.py +95 -0
- pypando-0.1.0/pypando.egg-info/PKG-INFO +146 -0
- pypando-0.1.0/pypando.egg-info/SOURCES.txt +31 -0
- pypando-0.1.0/pypando.egg-info/dependency_links.txt +1 -0
- pypando-0.1.0/pypando.egg-info/requires.txt +18 -0
- pypando-0.1.0/pypando.egg-info/top_level.txt +1 -0
- pypando-0.1.0/pyproject.toml +59 -0
- pypando-0.1.0/setup.cfg +4 -0
- pypando-0.1.0/tests/test_exact_match.py +52 -0
- pypando-0.1.0/tests/test_smoke.py +98 -0
pypando-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021 QuaDBio Lab (Pando, the R package this is ported from)
|
|
4
|
+
Copyright (c) 2026 omicverse contributors (py-Pando Python port)
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
pypando-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pypando
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pure-Python Pando — gene regulatory network inference from paired single-cell RNA + ATAC (regions, motifs, per-gene TF x binding-site models, modules), AnnData-native, parity-tested against R.
|
|
5
|
+
License: MIT License
|
|
6
|
+
|
|
7
|
+
Copyright (c) 2021 QuaDBio Lab (Pando, the R package this is ported from)
|
|
8
|
+
Copyright (c) 2026 omicverse contributors (py-Pando Python port)
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/omicverse/py-Pando
|
|
29
|
+
Project-URL: Repository, https://github.com/omicverse/py-Pando
|
|
30
|
+
Project-URL: Issues, https://github.com/omicverse/py-Pando/issues
|
|
31
|
+
Project-URL: Upstream R package, https://github.com/quadbio/Pando
|
|
32
|
+
Keywords: single-cell,multiome,scATAC-seq,gene-regulatory-network,pando,transcription-factor,motif,anndata
|
|
33
|
+
Classifier: Development Status :: 4 - Beta
|
|
34
|
+
Classifier: Intended Audience :: Science/Research
|
|
35
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
36
|
+
Classifier: Operating System :: OS Independent
|
|
37
|
+
Classifier: Programming Language :: Python :: 3
|
|
38
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
39
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
40
|
+
Requires-Python: >=3.9
|
|
41
|
+
Description-Content-Type: text/markdown
|
|
42
|
+
License-File: LICENSE
|
|
43
|
+
Requires-Dist: numpy>=1.23
|
|
44
|
+
Requires-Dist: scipy>=1.10
|
|
45
|
+
Requires-Dist: pandas>=1.5
|
|
46
|
+
Requires-Dist: anndata>=0.9
|
|
47
|
+
Requires-Dist: scikit-learn>=1.4
|
|
48
|
+
Requires-Dist: pyfaidx>=0.7
|
|
49
|
+
Requires-Dist: MOODS-python>=1.9.4
|
|
50
|
+
Requires-Dist: pooch>=1.6
|
|
51
|
+
Provides-Extra: xgb
|
|
52
|
+
Requires-Dist: xgboost>=1.7; extra == "xgb"
|
|
53
|
+
Provides-Extra: umap
|
|
54
|
+
Requires-Dist: umap-learn; extra == "umap"
|
|
55
|
+
Provides-Extra: dev
|
|
56
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
57
|
+
Requires-Dist: xgboost>=1.7; extra == "dev"
|
|
58
|
+
Dynamic: license-file
|
|
59
|
+
|
|
60
|
+
# py-Pando
|
|
61
|
+
|
|
62
|
+
**Pure-Python Pando**: gene regulatory network inference from paired single-cell RNA + ATAC data, AnnData-native, parity-tested against the R package.
|
|
63
|
+
|
|
64
|
+
[Pando](https://github.com/quadbio/Pando) (Fleck *et al.*, *Nature* 2023, [doi:10.1038/s41586-022-05459-w](https://doi.org/10.1038/s41586-022-05459-w)) fits, for every target gene, a regression of its expression on TF expression x accessibility of TF-binding peaks near the gene, and reads significant TF–peak terms as regulatory links. py-Pando ports it following the [omicverse-rebuildr](https://github.com/omicverse/omicverse-rebuildr) protocol: the R source is the specification, and pre-registered parity gates (`data/manifest.yaml`) were committed before any algorithmic code.
|
|
65
|
+
|
|
66
|
+
## Install
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pip install pypando # core
|
|
70
|
+
pip install "pypando[xgb]" # + xgboost models
|
|
71
|
+
pip install "pypando[umap]" # + UMAP layout of the module graph
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Quick start
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
import pypando as pp
|
|
78
|
+
|
|
79
|
+
# rna: AnnData, cells x genes, log-normalised (Seurat LogNormalize)
|
|
80
|
+
# atac: AnnData, cells x peaks, TF-IDF (Signac RunTFIDF), var_names "chr-start-end"
|
|
81
|
+
annotation = pp.annotation_from_gtf("gencode.v41.basic.annotation.gff3.gz")
|
|
82
|
+
|
|
83
|
+
grn = pp.initiate_grn(rna, atac, annotation) # peaks minus exons
|
|
84
|
+
grn = pp.find_motifs(grn, genome="hg38.fa") # Pando's 1,590 motifs, MOODS
|
|
85
|
+
grn = pp.infer_grn(grn, genes=targets, method="glm") # one model per target gene
|
|
86
|
+
grn = pp.find_modules(grn) # TF modules
|
|
87
|
+
|
|
88
|
+
pp.coef(grn) # TF / target / region / estimate / std_err / pval / padj / corr
|
|
89
|
+
pp.gof(grn) # R^2 and number of variables per target gene
|
|
90
|
+
pp.NetworkModules(grn) # module table + genes_pos / genes_neg / regions / peaks
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Within Epione: `epione.grn.pando(rna, atac, genome="hg38", genes=targets)`.
|
|
94
|
+
|
|
95
|
+
## Function map
|
|
96
|
+
|
|
97
|
+
| R (Pando 1.1.1) | Python | Parity on the fixture |
|
|
98
|
+
|---|---|---|
|
|
99
|
+
| `initiate_grn` | `pp.initiate_grn` | identical regions (with and without `regions=` phastCons) |
|
|
100
|
+
| `find_motifs` | `pp.find_motifs` | identical region x motif matrix (1,240,962 hits) |
|
|
101
|
+
| `find_peaks_near_genes` (Signac, GREAT) | `pp.find_peaks_near_genes` | identical |
|
|
102
|
+
| `infer_grn(method = "glm")` | `pp.infer_grn` | identical rows; estimates / SE / t / rsq to ~1e-14; -log10 p to 4e-13 |
|
|
103
|
+
| `infer_grn(method = "glmnet")` | | lambda identical; coefficients Pearson 0.9999 (see notes) |
|
|
104
|
+
| `infer_grn(method = "cv.glmnet")` | | R's folds reproduced; coefficients within 5e-14 |
|
|
105
|
+
| `infer_grn(method = "xgb")` | | **gate fails vs installed R** (xgboost >= 3 drops Pando's parameters); identical (2e-16) with them honoured |
|
|
106
|
+
| `infer_grn(method = "bagging_ridge" / "bayesian_ridge")` | | ~1e-14 (NumPy seeded as in R) |
|
|
107
|
+
| `find_modules` | `pp.find_modules` | identical modules |
|
|
108
|
+
| `fit_model`, `fit_glm`, `fit_glmnet`, `fit_cvglmnet`, `fit_xgb`, `fit_bagging_ridge`, `fit_bayesian_ridge` | same names | per-function, to ~1e-14 (notebook 3) |
|
|
109
|
+
| `format_coefs`, `sparse_cor`, `aggregate_matrix`, `aggregate_assay`, `GetAssaySummary` | same names | exact |
|
|
110
|
+
| `get_network_graph` | `pp.get_network_graph` | graph + PageRank; UMAP layout via umap-learn (not R's uwot stream) |
|
|
111
|
+
| `coef`, `gof`, `GetNetwork`, `NetworkModules`, `NetworkTFs`, `NetworkRegions`, `NetworkFeatures`, `NetworkParams`, `NetworkGraph`, `DefaultNetwork`, `GetGRN`, `Params`, `create_GRNData` | same names | accessors |
|
|
112
|
+
| `plot_gof`, `plot_module_metrics`, `plot_network_graph` | same names (matplotlib) | — |
|
|
113
|
+
| `fit_brms` | — | not ported (Stan) |
|
|
114
|
+
| `get_tf_network`, `plot_tf_network` | — | not ported (see report) |
|
|
115
|
+
|
|
116
|
+
Full numbers: [RECONSTRUCTION_REPORT.md](RECONSTRUCTION_REPORT.md) and `examples/compare_R_vs_Python.ipynb`.
|
|
117
|
+
|
|
118
|
+
## What makes the glm network identical
|
|
119
|
+
|
|
120
|
+
R's `glm` uses LINPACK's `dqrdc2` QR with limited pivoting: a column whose remaining norm falls below `1e-11` of its original norm is moved to the end and its coefficient becomes `NA`. Pando's per-gene designs (hundreds to thousands of `peak x TF` products, often nearly collinear) sit right at that threshold, so the pivot decisions depend on the last bits of BLAS level-1 results. py-Pando's QR (`pypando._blas`, numba) calls the same BLAS routines (`dnrm2`, `ddot`, `daxpy`, `dscal`) in the same order as R's Fortran, multiplies with `dgemv` like R's `%*%`, sums in long double like R's `sum()`, and inverts with LAPACK `dpotri` like `chol2inv`. With the same BLAS build (here OpenBLAS 0.3.34) and one BLAS thread (`infer_grn(blas_threads=1)`, the default), the glm network is bit-identical to R's, including genes where R's IRLS does not converge.
|
|
121
|
+
|
|
122
|
+
## Notes
|
|
123
|
+
|
|
124
|
+
* **cv.glmnet** folds come from an emulation of R's `set.seed()` + `sample()` (`pypando.rrng`), so `infer_grn(method="cv.glmnet", seed=42)` uses R's folds after `set.seed(42)`. Fold fits follow glmnet 5.0: each fold on its own lambda path, predictions interpolated onto the full path (glmnet 4.x refitted folds on the full path; `cv_glmnet_gaussian(fold_lambda="full")`).
|
|
125
|
+
* **glmnet**: py-Pando's coordinate descent follows glmnet's path rules (standardisation, lambda sequence, `fdev` / `devmax` stopping) but not its strong-rule screening, so for genes with hundreds of variables both stop within glmnet's 1e-7 tolerance at slightly different coefficients. `fit_glmnet` ports `family = "gaussian"` given as a string, which is how `infer_grn` calls it; R's `fit_glmnet` called directly with its default `family = gaussian` (a function) goes to glmnet's generic `glmnet.path` solver, which is not ported.
|
|
126
|
+
* **xgb**: Pando calls `xgboost::xgboost(data, label, params = list(max_depth = 3, eta = 0.01), ...)`. xgboost >= 3 changed that function's interface and silently ignores `params`, training with depth 6 and learning rate 0.3. py-Pando uses Pando's parameters; with them honoured in R (`xgb.train`), importances match to 1e-15.
|
|
127
|
+
* **bagging_ridge** draws from NumPy's global RNG, as Pando's scikit-learn call does from R; `infer_grn(seed=...)` seeds it.
|
|
128
|
+
* **New in Python** (not in R): `region_background`, `regions_near_genes` and `subset_regions` scan only the regions a target gene can reach, against the background of all regions — the network is identical to a full scan.
|
|
129
|
+
|
|
130
|
+
## Reproducing the R comparison
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
python tests/make_fixture.py pbmc_granulocyte_sorted_10k_filtered_feature_bc_matrix.h5 data/fixture
|
|
134
|
+
Rscript tests/r_reference_driver.R data/fixture ref # R 4.4.3, Pando 1.1.1
|
|
135
|
+
python tests/_run_candidate.py ref cand hg38.fa
|
|
136
|
+
python tests/parity_check.py ref cand results.json
|
|
137
|
+
PYPANDO_REF_DIR=ref PYPANDO_FASTA=hg38.fa PYPANDO_CAND_DIR=cand pytest tests/test_exact_match.py
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Citation
|
|
141
|
+
|
|
142
|
+
Fleck, J.S., Jansen, S.M.J., Wollny, D. *et al.* Inferring and perturbing cell fate regulomes in human brain organoids. *Nature* 621, 365–372 (2023).
|
|
143
|
+
|
|
144
|
+
## License
|
|
145
|
+
|
|
146
|
+
MIT, as the upstream R package (see `LICENSE`).
|
pypando-0.1.0/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# py-Pando
|
|
2
|
+
|
|
3
|
+
**Pure-Python Pando**: gene regulatory network inference from paired single-cell RNA + ATAC data, AnnData-native, parity-tested against the R package.
|
|
4
|
+
|
|
5
|
+
[Pando](https://github.com/quadbio/Pando) (Fleck *et al.*, *Nature* 2023, [doi:10.1038/s41586-022-05459-w](https://doi.org/10.1038/s41586-022-05459-w)) fits, for every target gene, a regression of its expression on TF expression x accessibility of TF-binding peaks near the gene, and reads significant TF–peak terms as regulatory links. py-Pando ports it following the [omicverse-rebuildr](https://github.com/omicverse/omicverse-rebuildr) protocol: the R source is the specification, and pre-registered parity gates (`data/manifest.yaml`) were committed before any algorithmic code.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install pypando # core
|
|
11
|
+
pip install "pypando[xgb]" # + xgboost models
|
|
12
|
+
pip install "pypando[umap]" # + UMAP layout of the module graph
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
import pypando as pp
|
|
19
|
+
|
|
20
|
+
# rna: AnnData, cells x genes, log-normalised (Seurat LogNormalize)
|
|
21
|
+
# atac: AnnData, cells x peaks, TF-IDF (Signac RunTFIDF), var_names "chr-start-end"
|
|
22
|
+
annotation = pp.annotation_from_gtf("gencode.v41.basic.annotation.gff3.gz")
|
|
23
|
+
|
|
24
|
+
grn = pp.initiate_grn(rna, atac, annotation) # peaks minus exons
|
|
25
|
+
grn = pp.find_motifs(grn, genome="hg38.fa") # Pando's 1,590 motifs, MOODS
|
|
26
|
+
grn = pp.infer_grn(grn, genes=targets, method="glm") # one model per target gene
|
|
27
|
+
grn = pp.find_modules(grn) # TF modules
|
|
28
|
+
|
|
29
|
+
pp.coef(grn) # TF / target / region / estimate / std_err / pval / padj / corr
|
|
30
|
+
pp.gof(grn) # R^2 and number of variables per target gene
|
|
31
|
+
pp.NetworkModules(grn) # module table + genes_pos / genes_neg / regions / peaks
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Within Epione: `epione.grn.pando(rna, atac, genome="hg38", genes=targets)`.
|
|
35
|
+
|
|
36
|
+
## Function map
|
|
37
|
+
|
|
38
|
+
| R (Pando 1.1.1) | Python | Parity on the fixture |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| `initiate_grn` | `pp.initiate_grn` | identical regions (with and without `regions=` phastCons) |
|
|
41
|
+
| `find_motifs` | `pp.find_motifs` | identical region x motif matrix (1,240,962 hits) |
|
|
42
|
+
| `find_peaks_near_genes` (Signac, GREAT) | `pp.find_peaks_near_genes` | identical |
|
|
43
|
+
| `infer_grn(method = "glm")` | `pp.infer_grn` | identical rows; estimates / SE / t / rsq to ~1e-14; -log10 p to 4e-13 |
|
|
44
|
+
| `infer_grn(method = "glmnet")` | | lambda identical; coefficients Pearson 0.9999 (see notes) |
|
|
45
|
+
| `infer_grn(method = "cv.glmnet")` | | R's folds reproduced; coefficients within 5e-14 |
|
|
46
|
+
| `infer_grn(method = "xgb")` | | **gate fails vs installed R** (xgboost >= 3 drops Pando's parameters); identical (2e-16) with them honoured |
|
|
47
|
+
| `infer_grn(method = "bagging_ridge" / "bayesian_ridge")` | | ~1e-14 (NumPy seeded as in R) |
|
|
48
|
+
| `find_modules` | `pp.find_modules` | identical modules |
|
|
49
|
+
| `fit_model`, `fit_glm`, `fit_glmnet`, `fit_cvglmnet`, `fit_xgb`, `fit_bagging_ridge`, `fit_bayesian_ridge` | same names | per-function, to ~1e-14 (notebook 3) |
|
|
50
|
+
| `format_coefs`, `sparse_cor`, `aggregate_matrix`, `aggregate_assay`, `GetAssaySummary` | same names | exact |
|
|
51
|
+
| `get_network_graph` | `pp.get_network_graph` | graph + PageRank; UMAP layout via umap-learn (not R's uwot stream) |
|
|
52
|
+
| `coef`, `gof`, `GetNetwork`, `NetworkModules`, `NetworkTFs`, `NetworkRegions`, `NetworkFeatures`, `NetworkParams`, `NetworkGraph`, `DefaultNetwork`, `GetGRN`, `Params`, `create_GRNData` | same names | accessors |
|
|
53
|
+
| `plot_gof`, `plot_module_metrics`, `plot_network_graph` | same names (matplotlib) | — |
|
|
54
|
+
| `fit_brms` | — | not ported (Stan) |
|
|
55
|
+
| `get_tf_network`, `plot_tf_network` | — | not ported (see report) |
|
|
56
|
+
|
|
57
|
+
Full numbers: [RECONSTRUCTION_REPORT.md](RECONSTRUCTION_REPORT.md) and `examples/compare_R_vs_Python.ipynb`.
|
|
58
|
+
|
|
59
|
+
## What makes the glm network identical
|
|
60
|
+
|
|
61
|
+
R's `glm` uses LINPACK's `dqrdc2` QR with limited pivoting: a column whose remaining norm falls below `1e-11` of its original norm is moved to the end and its coefficient becomes `NA`. Pando's per-gene designs (hundreds to thousands of `peak x TF` products, often nearly collinear) sit right at that threshold, so the pivot decisions depend on the last bits of BLAS level-1 results. py-Pando's QR (`pypando._blas`, numba) calls the same BLAS routines (`dnrm2`, `ddot`, `daxpy`, `dscal`) in the same order as R's Fortran, multiplies with `dgemv` like R's `%*%`, sums in long double like R's `sum()`, and inverts with LAPACK `dpotri` like `chol2inv`. With the same BLAS build (here OpenBLAS 0.3.34) and one BLAS thread (`infer_grn(blas_threads=1)`, the default), the glm network is bit-identical to R's, including genes where R's IRLS does not converge.
|
|
62
|
+
|
|
63
|
+
## Notes
|
|
64
|
+
|
|
65
|
+
* **cv.glmnet** folds come from an emulation of R's `set.seed()` + `sample()` (`pypando.rrng`), so `infer_grn(method="cv.glmnet", seed=42)` uses R's folds after `set.seed(42)`. Fold fits follow glmnet 5.0: each fold on its own lambda path, predictions interpolated onto the full path (glmnet 4.x refitted folds on the full path; `cv_glmnet_gaussian(fold_lambda="full")`).
|
|
66
|
+
* **glmnet**: py-Pando's coordinate descent follows glmnet's path rules (standardisation, lambda sequence, `fdev` / `devmax` stopping) but not its strong-rule screening, so for genes with hundreds of variables both stop within glmnet's 1e-7 tolerance at slightly different coefficients. `fit_glmnet` ports `family = "gaussian"` given as a string, which is how `infer_grn` calls it; R's `fit_glmnet` called directly with its default `family = gaussian` (a function) goes to glmnet's generic `glmnet.path` solver, which is not ported.
|
|
67
|
+
* **xgb**: Pando calls `xgboost::xgboost(data, label, params = list(max_depth = 3, eta = 0.01), ...)`. xgboost >= 3 changed that function's interface and silently ignores `params`, training with depth 6 and learning rate 0.3. py-Pando uses Pando's parameters; with them honoured in R (`xgb.train`), importances match to 1e-15.
|
|
68
|
+
* **bagging_ridge** draws from NumPy's global RNG, as Pando's scikit-learn call does from R; `infer_grn(seed=...)` seeds it.
|
|
69
|
+
* **New in Python** (not in R): `region_background`, `regions_near_genes` and `subset_regions` scan only the regions a target gene can reach, against the background of all regions — the network is identical to a full scan.
|
|
70
|
+
|
|
71
|
+
## Reproducing the R comparison
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
python tests/make_fixture.py pbmc_granulocyte_sorted_10k_filtered_feature_bc_matrix.h5 data/fixture
|
|
75
|
+
Rscript tests/r_reference_driver.R data/fixture ref # R 4.4.3, Pando 1.1.1
|
|
76
|
+
python tests/_run_candidate.py ref cand hg38.fa
|
|
77
|
+
python tests/parity_check.py ref cand results.json
|
|
78
|
+
PYPANDO_REF_DIR=ref PYPANDO_FASTA=hg38.fa PYPANDO_CAND_DIR=cand pytest tests/test_exact_match.py
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Citation
|
|
82
|
+
|
|
83
|
+
Fleck, J.S., Jansen, S.M.J., Wollny, D. *et al.* Inferring and perturbing cell fate regulomes in human brain organoids. *Nature* 621, 365–372 (2023).
|
|
84
|
+
|
|
85
|
+
## License
|
|
86
|
+
|
|
87
|
+
MIT, as the upstream R package (see `LICENSE`).
|
|
@@ -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"
|
|
@@ -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}
|
|
@@ -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)))
|