pynichede 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.
- pynichede-0.1.0/LICENSE +21 -0
- pynichede-0.1.0/PKG-INFO +353 -0
- pynichede-0.1.0/README.md +294 -0
- pynichede-0.1.0/pynichede/__init__.py +94 -0
- pynichede-0.1.0/pynichede/core.py +179 -0
- pynichede-0.1.0/pynichede/downstream.py +407 -0
- pynichede-0.1.0/pynichede/niche_de.py +381 -0
- pynichede-0.1.0/pynichede/object_creation.py +479 -0
- pynichede-0.1.0/pynichede/poolr.py +253 -0
- pynichede-0.1.0/pynichede/pvalues.py +358 -0
- pynichede-0.1.0/pynichede/rstats.py +510 -0
- pynichede-0.1.0/pynichede.egg-info/PKG-INFO +353 -0
- pynichede-0.1.0/pynichede.egg-info/SOURCES.txt +22 -0
- pynichede-0.1.0/pynichede.egg-info/dependency_links.txt +1 -0
- pynichede-0.1.0/pynichede.egg-info/requires.txt +15 -0
- pynichede-0.1.0/pynichede.egg-info/top_level.txt +1 -0
- pynichede-0.1.0/pyproject.toml +59 -0
- pynichede-0.1.0/setup.cfg +4 -0
- pynichede-0.1.0/tests/test_exact_match.py +251 -0
- pynichede-0.1.0/tests/test_glm_differential.py +83 -0
- pynichede-0.1.0/tests/test_multibatch_parity.py +104 -0
- pynichede-0.1.0/tests/test_poolr_table.py +103 -0
- pynichede-0.1.0/tests/test_rstats.py +195 -0
- pynichede-0.1.0/tests/test_smoke.py +94 -0
pynichede-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Kmason23
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pynichede-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pynichede
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pure-Python Niche-DE — niche-differential gene expression and context-dependent cell-cell interactions in spatial transcriptomics, AnnData-native.
|
|
5
|
+
Author-email: Zehua Zeng <starlitnightly@163.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2023 Kmason23
|
|
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-nichede
|
|
29
|
+
Project-URL: Repository, https://github.com/omicverse/py-nichede
|
|
30
|
+
Project-URL: Issues, https://github.com/omicverse/py-nichede/issues
|
|
31
|
+
Project-URL: Upstream R package, https://github.com/kaishumason/NicheDE
|
|
32
|
+
Project-URL: Upstream (omicverse), https://github.com/Starlitnightly/omicverse
|
|
33
|
+
Keywords: spatial-transcriptomics,niche-DE,nicheDE,cell-cell-interaction,differential-expression,ligand-receptor,visium,scanpy,anndata
|
|
34
|
+
Classifier: Development Status :: 4 - Beta
|
|
35
|
+
Classifier: Intended Audience :: Science/Research
|
|
36
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
37
|
+
Classifier: Operating System :: OS Independent
|
|
38
|
+
Classifier: Programming Language :: Python :: 3
|
|
39
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
40
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
41
|
+
Requires-Python: >=3.9
|
|
42
|
+
Description-Content-Type: text/markdown
|
|
43
|
+
License-File: LICENSE
|
|
44
|
+
Requires-Dist: numpy>=1.23
|
|
45
|
+
Requires-Dist: scipy>=1.10
|
|
46
|
+
Requires-Dist: pandas>=1.5
|
|
47
|
+
Requires-Dist: anndata>=0.9
|
|
48
|
+
Requires-Dist: joblib>=1.2
|
|
49
|
+
Requires-Dist: threadpoolctl>=3.0
|
|
50
|
+
Provides-Extra: dev
|
|
51
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
52
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
53
|
+
Requires-Dist: ruff; extra == "dev"
|
|
54
|
+
Requires-Dist: pyyaml; extra == "dev"
|
|
55
|
+
Requires-Dist: matplotlib; extra == "dev"
|
|
56
|
+
Requires-Dist: jupyter; extra == "dev"
|
|
57
|
+
Requires-Dist: nbconvert; extra == "dev"
|
|
58
|
+
Dynamic: license-file
|
|
59
|
+
|
|
60
|
+
<h1 align="center">py-nichede</h1>
|
|
61
|
+
|
|
62
|
+
<p align="center">
|
|
63
|
+
<b>Niche-differential expression for spatial transcriptomics — a pure-Python port of the R package <code>nicheDE</code>.</b>
|
|
64
|
+
</p>
|
|
65
|
+
|
|
66
|
+
<p align="center">
|
|
67
|
+
<a href="https://pypi.org/project/pynichede/"><img alt="PyPI" src="https://img.shields.io/pypi/v/pynichede.svg"></a>
|
|
68
|
+
<a href="LICENSE"><img alt="License" src="https://img.shields.io/badge/license-MIT-blue.svg"></a>
|
|
69
|
+
<img alt="Python" src="https://img.shields.io/badge/python-3.9%2B-blue.svg">
|
|
70
|
+
</p>
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## What it does
|
|
75
|
+
|
|
76
|
+
Spatial tools can already tell you *which genes vary across a tissue*. Niche-DE
|
|
77
|
+
answers the question one level up:
|
|
78
|
+
|
|
79
|
+
> **Does cell type A express different genes when it sits next to cell type B?**
|
|
80
|
+
|
|
81
|
+
For every gene, Niche-DE regresses the spot-level counts on the interaction
|
|
82
|
+
between (a) the share of that gene's expression attributable to each **index**
|
|
83
|
+
cell type and (b) a kernel-smoothed **effective niche** describing which cell
|
|
84
|
+
types surround the spot, and reports a Wald statistic per
|
|
85
|
+
*(index cell type, niche cell type, gene)* triple. Those statistics are then
|
|
86
|
+
pooled — with Brown's method across interactions and a Cauchy combination across
|
|
87
|
+
kernel bandwidths — into gene-level, cell-type-level and interaction-level
|
|
88
|
+
p-values, and turned into niche marker genes and context-dependent
|
|
89
|
+
ligand–receptor pairs.
|
|
90
|
+
|
|
91
|
+
`py-nichede` is a line-by-line port of the reference R implementation
|
|
92
|
+
([kaishumason/NicheDE](https://github.com/kaishumason/NicheDE)), validated
|
|
93
|
+
against it on the upstream vignette dataset under a parity gate that was
|
|
94
|
+
committed **before** any Python was written.
|
|
95
|
+
|
|
96
|
+
**Paper**: Mason K., Sathe A., Hess P.R., Rong J., Wu C.-Y., Furth E., Susztak K.,
|
|
97
|
+
Levinsohn J., Ji H.P., Zhang N.R. *Niche-DE: niche-differential gene expression
|
|
98
|
+
analysis in spatial transcriptomics data identifies context-dependent cell–cell
|
|
99
|
+
interactions.* **Genome Biology** 25, 14 (2024).
|
|
100
|
+
[PMC10785550](https://pmc.ncbi.nlm.nih.gov/articles/PMC10785550/)
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Install
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
pip install pynichede
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Runtime dependencies are `numpy`, `scipy`, `pandas`, `anndata`, `joblib`,
|
|
111
|
+
`threadpoolctl`. **No R and no `rpy2`.**
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Quick start (class API)
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
import pynichede as nde
|
|
119
|
+
|
|
120
|
+
nd = (
|
|
121
|
+
nde.NicheDE.from_matrices(
|
|
122
|
+
counts_mat=counts, # spots x genes, raw integer counts
|
|
123
|
+
coordinate_mat=coord, # spots x 2
|
|
124
|
+
library_mat=libmat, # cell types x genes (reference profile)
|
|
125
|
+
deconv_mat=deconv, # spots x cell types (e.g. RCTD output)
|
|
126
|
+
sigma=[1, 100, 250], # kernel bandwidths
|
|
127
|
+
)
|
|
128
|
+
.effective_niche(cutoff=0.05)
|
|
129
|
+
.run(num_cores=8, C=150, M=10, gamma=0.8)
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# genes the tumour compartment turns on specifically next to myeloid cells
|
|
133
|
+
nd.genes("I", index="tumor_epithelial", niche="myeloid", positive=True, alpha=0.05)
|
|
134
|
+
|
|
135
|
+
# genes that distinguish a myeloid niche from a stromal one
|
|
136
|
+
nd.markers("tumor_epithelial", "myeloid", "stromal")
|
|
137
|
+
|
|
138
|
+
# context-dependent ligand-receptor pairs
|
|
139
|
+
nd.ligand_receptor("myeloid", "tumor_epithelial",
|
|
140
|
+
ligand_target_matrix=ltm, lr_mat=lr, resolution="spot")
|
|
141
|
+
|
|
142
|
+
adata = nd.to_anndata() # results into .obsm / .varm / .uns
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Starting from an `AnnData` instead:
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
nd = nde.NicheDE.from_anndata(adata, library_mat=libmat, deconv_mat=deconv,
|
|
149
|
+
sigma=[1, 100, 250], spatial_key="spatial")
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Low-level functional API (mirrors R one-to-one)
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
obj = nde.create_nichede_object(counts, coord, libmat, deconv, sigma=[1, 100, 250])
|
|
156
|
+
obj = nde.calculate_effective_niche(obj, cutoff=0.05)
|
|
157
|
+
obj = nde.niche_DE(obj, num_cores=8, C=150, M=10, gamma=0.8)
|
|
158
|
+
res = nde.get_niche_DE_genes(obj, "I", index="tumor_epithelial", niche="myeloid")
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## What's included
|
|
164
|
+
|
|
165
|
+
Every symbol exported by the R `NAMESPACE` has a Python counterpart.
|
|
166
|
+
|
|
167
|
+
| R (`nicheDE`) | Python (`pynichede`) | Notes |
|
|
168
|
+
|---|---|---|
|
|
169
|
+
| `CreateLibraryMatrix` | `create_library_matrix` | |
|
|
170
|
+
| `CreateLibraryMatrixFromSeurat` | `create_library_matrix_from_anndata` | Seurat → AnnData |
|
|
171
|
+
| `CreateNicheDEObject` | `create_nichede_object` | |
|
|
172
|
+
| `CreateNicheDEObjectFromSeurat` | `create_nichede_object_from_anndata` | Seurat → AnnData |
|
|
173
|
+
| `MergeObjects` | `merge_objects` | |
|
|
174
|
+
| `Filter_NDE` | `filter_nde` | |
|
|
175
|
+
| `CalculateEffectiveNiche` | `calculate_effective_niche` | |
|
|
176
|
+
| `CalculateEffectiveNicheLargeScale` | `calculate_effective_niche_large_scale` | see *Known divergences* |
|
|
177
|
+
| `niche_DE` | `niche_DE` | |
|
|
178
|
+
| `niche_DE_no_parallel` | `niche_DE_no_parallel` | |
|
|
179
|
+
| `get_niche_DE_pval_fisher` | `get_niche_DE_pval_fisher` | |
|
|
180
|
+
| `get_niche_DE_pval_raw` | `get_niche_DE_pval_raw` | |
|
|
181
|
+
| `get_niche_DE_genes` | `get_niche_DE_genes` | |
|
|
182
|
+
| `niche_DE_markers` | `niche_DE_markers` | |
|
|
183
|
+
| `niche_LR_spot` | `niche_LR_spot` | |
|
|
184
|
+
| `niche_LR_cell` | `niche_LR_cell` | |
|
|
185
|
+
| `contrast_post` | `contrast_post` | 0-based cell-type indices |
|
|
186
|
+
| `check_colloc` | `check_colloc` | 0-based cell-type indices |
|
|
187
|
+
| `gene_level` | `gene_level` | |
|
|
188
|
+
| `celltype_level` | `celltype_level` | |
|
|
189
|
+
| `gene_level_fisher` | `gene_level_fisher` | |
|
|
190
|
+
| `celltype_level_fisher` | `celltype_level_fisher` | |
|
|
191
|
+
| `T_to_p` | `T_to_p` | |
|
|
192
|
+
| `ultosymmetric` | `ultosymmetric` | |
|
|
193
|
+
| `nb_lik` | `nb_lik` | |
|
|
194
|
+
| `print.Niche_DE` | `NicheDEObject.__repr__` | |
|
|
195
|
+
| *(poolr dependency)* | `mvnconv`, `fisher_generalized` | clean-room, MIT |
|
|
196
|
+
|
|
197
|
+
Two extra dependency-free modules are exposed for other ports to reuse:
|
|
198
|
+
|
|
199
|
+
- `pynichede.rstats` — R-faithful `glm.fit` (LINPACK `dqrdc2` limited-pivot rank
|
|
200
|
+
detection, `NA`-for-aliased coefficients), `optimize` (Brent `fmin`),
|
|
201
|
+
`p.adjust`, type-7 `quantile`, `weighted.mean`.
|
|
202
|
+
- `pynichede.poolr` — Brown's method and `mvnconv`, re-derived from first
|
|
203
|
+
principles rather than vendored (poolr is GPL-2+, this port is MIT).
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Reproducing the R results exactly
|
|
208
|
+
|
|
209
|
+
The parity gate is a runnable test. Generate the R reference, run the Python
|
|
210
|
+
candidate, and assert the pre-registered thresholds:
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
export R_LIBS_USER=/path/to/rlibs # where nicheDE + poolr are installed
|
|
214
|
+
export REF=/tmp/nichede_ref
|
|
215
|
+
|
|
216
|
+
Rscript tests/r_reference_driver.R $REF 0 16 # 0 = full gene set
|
|
217
|
+
Rscript tests/r_reference_supplement.R $REF 0
|
|
218
|
+
python tests/_run_candidate.py $REF 16
|
|
219
|
+
|
|
220
|
+
NICHEDE_REF_DIR=$REF pytest -q # the gate
|
|
221
|
+
python tests/parity_report.py $REF # the numbers
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`pytest -q` without `NICHEDE_REF_DIR` skips the R-dependent tests and runs the
|
|
225
|
+
31 R-free unit tests, so a bare checkout stays green.
|
|
226
|
+
|
|
227
|
+
### Measured parity — canonical fixture
|
|
228
|
+
|
|
229
|
+
10x Visium human liver metastasis (the dataset shipped with the R package):
|
|
230
|
+
**848 spots x 21 708 genes x 7 cell types x 3 kernel bandwidths**.
|
|
231
|
+
|
|
232
|
+
| Output | Class | Metric | Threshold | **Measured** |
|
|
233
|
+
|---|---|---|---|---|
|
|
234
|
+
| `T_stat` (Wald statistic, all 3 kernels) | ordinal | Pearson / Spearman | ≥ 0.99 | **1.000000 / 1.000000** |
|
|
235
|
+
| `betas` | ordinal | Pearson | ≥ 0.99 | **1.000000** |
|
|
236
|
+
| `log_likelihood` | ordinal | Pearson | ≥ 0.99 | **1.000000** |
|
|
237
|
+
| `valid` gene flags | exact | agreement | 1.0 | **1.000000** (4768 / 4788 / 4858) |
|
|
238
|
+
| `nulls` (dropped interactions) | exact | max abs err | 0 | **0** |
|
|
239
|
+
| `num_cells` | deterministic | max abs err | < 1e-8 | **4.7e-14** |
|
|
240
|
+
| `effective_niche` | deterministic | max abs err | < 1e-8 | **2.9e-13** |
|
|
241
|
+
| `pval_pos` gene level | inference | Spearman(-log10 p) / top-50 J | ≥ 0.90 / 0.70 | **0.9966 / 1.000** |
|
|
242
|
+
| `pval_pos` cell-type level | inference | Spearman / top-50 J | ≥ 0.90 / 0.70 | **0.9960 / 0.923** |
|
|
243
|
+
| `pval_pos` interaction level | inference | Spearman / top-50 J | ≥ 0.90 / 0.70 | **1.000000 / 1.000** |
|
|
244
|
+
| `pval_neg` gene level | inference | Spearman / top-50 J | ≥ 0.90 / 0.70 | **0.9960 / 1.000** |
|
|
245
|
+
| `get_niche_DE_genes` output sets | ranked | Jaccard | ≥ 0.70 | **0.938 – 0.972** |
|
|
246
|
+
| `niche_DE_markers` output set | ranked | Jaccard | ≥ 0.70 | **1.000** |
|
|
247
|
+
|
|
248
|
+
Full breakdown, including the `Int = FALSE` branch and the helper functions, in
|
|
249
|
+
[`RECONSTRUCTION_REPORT.md`](RECONSTRUCTION_REPORT.md).
|
|
250
|
+
|
|
251
|
+
### Speed
|
|
252
|
+
|
|
253
|
+
`niche_DE` on the canonical fixture (848 x 21 708 x 7 x 3):
|
|
254
|
+
|
|
255
|
+
| | wall clock |
|
|
256
|
+
|---|---|
|
|
257
|
+
| R `nicheDE::niche_DE`, 16 cores | **852 s** |
|
|
258
|
+
| `pynichede.niche_DE`, 16 cores | **33 s** |
|
|
259
|
+
|
|
260
|
+
≈ **26x faster**, entirely from exact algebraic rewrites and scheduling — no
|
|
261
|
+
approximation.
|
|
262
|
+
|
|
263
|
+
`niche_LR_spot` on the same fixture (579 candidate ligands against the
|
|
264
|
+
16 968 x 579 NicheNet matrix) went from **314.5 s to 3.0 s (105x)** by
|
|
265
|
+
memoising a per-kernel slice that the R source recomputes once per ligand —
|
|
266
|
+
byte-identical output. See [`ITERATION_LOG.md`](ITERATION_LOG.md) and
|
|
267
|
+
[`MATH.md`](MATH.md).
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## Known divergences from the shipped R package
|
|
272
|
+
|
|
273
|
+
These are measured, not hand-waved. See [`MATH.md`](MATH.md) §3.
|
|
274
|
+
|
|
275
|
+
1. **`CalculateEffectiveNicheLargeScale` — Python is correct, R is not.**
|
|
276
|
+
With `Rfast >= 2.1.5.2`, `Rfast::dista()` returns an **all-zero matrix**
|
|
277
|
+
whenever `nrow(xnew) >= 4`, so every Gaussian kernel weight in the tiled path
|
|
278
|
+
collapses to 1. The shipped R `CalculateEffectiveNicheLargeScale` therefore
|
|
279
|
+
disagrees with the shipped R `CalculateEffectiveNiche` by up to **16.4
|
|
280
|
+
z-units** on the canonical fixture. `py-nichede` implements the intended
|
|
281
|
+
algorithm and matches the exact effective niche to `2.9e-13`; it is
|
|
282
|
+
parity-tested against a **repaired** R version.
|
|
283
|
+
|
|
284
|
+
2. **`poolr::mvnconv` — Python is the more accurate side.**
|
|
285
|
+
poolr's shipped `mvnlookup` is a numerically-integrated table stored to four
|
|
286
|
+
decimals; its `chisq1_2` column deviates from its own exact closed form
|
|
287
|
+
`2 rho^2` by up to `6.28e-4`. `py-nichede` re-derives the table exactly (via
|
|
288
|
+
Mehler's formula) instead of vendoring GPL data. Net effect on a pooled
|
|
289
|
+
Brown p-value: **3.06e-4 relative**.
|
|
290
|
+
|
|
291
|
+
3. **Bug-compatible by design.** Four defects in the R source change *which
|
|
292
|
+
genes get reported*, so they are reproduced rather than fixed — including
|
|
293
|
+
the `new_nul` / `var` typo that invalidates a gene whenever `X'WX` has a zero
|
|
294
|
+
diagonal, and the `X[, -null]` dimension drop that invalidates a gene
|
|
295
|
+
whenever exactly one interaction survives the `M` filter. Reproducing them is
|
|
296
|
+
what takes the `valid`-flag agreement from 0.9933 to exactly 1.0.
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## Notebooks
|
|
301
|
+
|
|
302
|
+
| Notebook | For |
|
|
303
|
+
|---|---|
|
|
304
|
+
| [`examples/compare_R_vs_Python.ipynb`](examples/compare_R_vs_Python.ipynb) | reviewers — pipeline-level parity vs R, one figure per gated output |
|
|
305
|
+
| [`examples/tutorial_liver_met_visium.ipynb`](examples/tutorial_liver_met_visium.ipynb) | new users — Python-only walkthrough of every public function |
|
|
306
|
+
| [`examples/function_by_function_R_parity.ipynb`](examples/function_by_function_R_parity.ipynb) | R users migrating — per-function parameter dictionary + side-by-side calls |
|
|
307
|
+
| [`examples/evolution.ipynb`](examples/evolution.ipynb) | auditors — per-iteration narrative of how the port was built |
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## Relationship to omicverse
|
|
312
|
+
|
|
313
|
+
`py-nichede` is a standalone package. It is also the niche-differential-expression
|
|
314
|
+
backend for [omicverse](https://github.com/Starlitnightly/omicverse), filling a
|
|
315
|
+
gap no other Python tool covers: `squidpy` and `liana-py` provide spatially
|
|
316
|
+
variable genes and ligand–receptor scoring, but neither models
|
|
317
|
+
index-cell-type x niche-cell-type interaction effects on expression.
|
|
318
|
+
|
|
319
|
+
It was produced with the
|
|
320
|
+
[Omicverse-RebuildR](https://github.com/omicverse/omicverse-rebuildr) protocol:
|
|
321
|
+
the R source is treated as the executable spec, the parity metric and threshold
|
|
322
|
+
are pre-registered before any Python is written, and every acceleration rewrite
|
|
323
|
+
must ship an admissibility proof.
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
## Citation
|
|
328
|
+
|
|
329
|
+
If you use `py-nichede`, please cite the original method:
|
|
330
|
+
|
|
331
|
+
```bibtex
|
|
332
|
+
@article{mason2024nichede,
|
|
333
|
+
title = {Niche-DE: niche-differential gene expression analysis in spatial
|
|
334
|
+
transcriptomics data identifies context-dependent cell-cell interactions},
|
|
335
|
+
author = {Mason, Kaishu and Sathe, Anuja and Hess, Paul R. and Rong, Jiazhen and
|
|
336
|
+
Wu, Chi-Yun and Furth, Emma and Susztak, Katalin and Levinsohn, Jonathan and
|
|
337
|
+
Ji, Hanlee P. and Zhang, Nancy R.},
|
|
338
|
+
journal = {Genome Biology},
|
|
339
|
+
volume = {25},
|
|
340
|
+
number = {1},
|
|
341
|
+
pages = {14},
|
|
342
|
+
year = {2024},
|
|
343
|
+
doi = {10.1186/s13059-023-03159-6}
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
and, if the Python port itself was load-bearing, link back to this repository.
|
|
348
|
+
|
|
349
|
+
## License
|
|
350
|
+
|
|
351
|
+
MIT, matching the upstream R package. See [LICENSE](LICENSE).
|
|
352
|
+
`pynichede.poolr` is an independent clean-room implementation and contains no
|
|
353
|
+
GPL-licensed code or data from `poolr`.
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
<h1 align="center">py-nichede</h1>
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<b>Niche-differential expression for spatial transcriptomics — a pure-Python port of the R package <code>nicheDE</code>.</b>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://pypi.org/project/pynichede/"><img alt="PyPI" src="https://img.shields.io/pypi/v/pynichede.svg"></a>
|
|
9
|
+
<a href="LICENSE"><img alt="License" src="https://img.shields.io/badge/license-MIT-blue.svg"></a>
|
|
10
|
+
<img alt="Python" src="https://img.shields.io/badge/python-3.9%2B-blue.svg">
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## What it does
|
|
16
|
+
|
|
17
|
+
Spatial tools can already tell you *which genes vary across a tissue*. Niche-DE
|
|
18
|
+
answers the question one level up:
|
|
19
|
+
|
|
20
|
+
> **Does cell type A express different genes when it sits next to cell type B?**
|
|
21
|
+
|
|
22
|
+
For every gene, Niche-DE regresses the spot-level counts on the interaction
|
|
23
|
+
between (a) the share of that gene's expression attributable to each **index**
|
|
24
|
+
cell type and (b) a kernel-smoothed **effective niche** describing which cell
|
|
25
|
+
types surround the spot, and reports a Wald statistic per
|
|
26
|
+
*(index cell type, niche cell type, gene)* triple. Those statistics are then
|
|
27
|
+
pooled — with Brown's method across interactions and a Cauchy combination across
|
|
28
|
+
kernel bandwidths — into gene-level, cell-type-level and interaction-level
|
|
29
|
+
p-values, and turned into niche marker genes and context-dependent
|
|
30
|
+
ligand–receptor pairs.
|
|
31
|
+
|
|
32
|
+
`py-nichede` is a line-by-line port of the reference R implementation
|
|
33
|
+
([kaishumason/NicheDE](https://github.com/kaishumason/NicheDE)), validated
|
|
34
|
+
against it on the upstream vignette dataset under a parity gate that was
|
|
35
|
+
committed **before** any Python was written.
|
|
36
|
+
|
|
37
|
+
**Paper**: Mason K., Sathe A., Hess P.R., Rong J., Wu C.-Y., Furth E., Susztak K.,
|
|
38
|
+
Levinsohn J., Ji H.P., Zhang N.R. *Niche-DE: niche-differential gene expression
|
|
39
|
+
analysis in spatial transcriptomics data identifies context-dependent cell–cell
|
|
40
|
+
interactions.* **Genome Biology** 25, 14 (2024).
|
|
41
|
+
[PMC10785550](https://pmc.ncbi.nlm.nih.gov/articles/PMC10785550/)
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install pynichede
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Runtime dependencies are `numpy`, `scipy`, `pandas`, `anndata`, `joblib`,
|
|
52
|
+
`threadpoolctl`. **No R and no `rpy2`.**
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Quick start (class API)
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
import pynichede as nde
|
|
60
|
+
|
|
61
|
+
nd = (
|
|
62
|
+
nde.NicheDE.from_matrices(
|
|
63
|
+
counts_mat=counts, # spots x genes, raw integer counts
|
|
64
|
+
coordinate_mat=coord, # spots x 2
|
|
65
|
+
library_mat=libmat, # cell types x genes (reference profile)
|
|
66
|
+
deconv_mat=deconv, # spots x cell types (e.g. RCTD output)
|
|
67
|
+
sigma=[1, 100, 250], # kernel bandwidths
|
|
68
|
+
)
|
|
69
|
+
.effective_niche(cutoff=0.05)
|
|
70
|
+
.run(num_cores=8, C=150, M=10, gamma=0.8)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# genes the tumour compartment turns on specifically next to myeloid cells
|
|
74
|
+
nd.genes("I", index="tumor_epithelial", niche="myeloid", positive=True, alpha=0.05)
|
|
75
|
+
|
|
76
|
+
# genes that distinguish a myeloid niche from a stromal one
|
|
77
|
+
nd.markers("tumor_epithelial", "myeloid", "stromal")
|
|
78
|
+
|
|
79
|
+
# context-dependent ligand-receptor pairs
|
|
80
|
+
nd.ligand_receptor("myeloid", "tumor_epithelial",
|
|
81
|
+
ligand_target_matrix=ltm, lr_mat=lr, resolution="spot")
|
|
82
|
+
|
|
83
|
+
adata = nd.to_anndata() # results into .obsm / .varm / .uns
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Starting from an `AnnData` instead:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
nd = nde.NicheDE.from_anndata(adata, library_mat=libmat, deconv_mat=deconv,
|
|
90
|
+
sigma=[1, 100, 250], spatial_key="spatial")
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Low-level functional API (mirrors R one-to-one)
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
obj = nde.create_nichede_object(counts, coord, libmat, deconv, sigma=[1, 100, 250])
|
|
97
|
+
obj = nde.calculate_effective_niche(obj, cutoff=0.05)
|
|
98
|
+
obj = nde.niche_DE(obj, num_cores=8, C=150, M=10, gamma=0.8)
|
|
99
|
+
res = nde.get_niche_DE_genes(obj, "I", index="tumor_epithelial", niche="myeloid")
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## What's included
|
|
105
|
+
|
|
106
|
+
Every symbol exported by the R `NAMESPACE` has a Python counterpart.
|
|
107
|
+
|
|
108
|
+
| R (`nicheDE`) | Python (`pynichede`) | Notes |
|
|
109
|
+
|---|---|---|
|
|
110
|
+
| `CreateLibraryMatrix` | `create_library_matrix` | |
|
|
111
|
+
| `CreateLibraryMatrixFromSeurat` | `create_library_matrix_from_anndata` | Seurat → AnnData |
|
|
112
|
+
| `CreateNicheDEObject` | `create_nichede_object` | |
|
|
113
|
+
| `CreateNicheDEObjectFromSeurat` | `create_nichede_object_from_anndata` | Seurat → AnnData |
|
|
114
|
+
| `MergeObjects` | `merge_objects` | |
|
|
115
|
+
| `Filter_NDE` | `filter_nde` | |
|
|
116
|
+
| `CalculateEffectiveNiche` | `calculate_effective_niche` | |
|
|
117
|
+
| `CalculateEffectiveNicheLargeScale` | `calculate_effective_niche_large_scale` | see *Known divergences* |
|
|
118
|
+
| `niche_DE` | `niche_DE` | |
|
|
119
|
+
| `niche_DE_no_parallel` | `niche_DE_no_parallel` | |
|
|
120
|
+
| `get_niche_DE_pval_fisher` | `get_niche_DE_pval_fisher` | |
|
|
121
|
+
| `get_niche_DE_pval_raw` | `get_niche_DE_pval_raw` | |
|
|
122
|
+
| `get_niche_DE_genes` | `get_niche_DE_genes` | |
|
|
123
|
+
| `niche_DE_markers` | `niche_DE_markers` | |
|
|
124
|
+
| `niche_LR_spot` | `niche_LR_spot` | |
|
|
125
|
+
| `niche_LR_cell` | `niche_LR_cell` | |
|
|
126
|
+
| `contrast_post` | `contrast_post` | 0-based cell-type indices |
|
|
127
|
+
| `check_colloc` | `check_colloc` | 0-based cell-type indices |
|
|
128
|
+
| `gene_level` | `gene_level` | |
|
|
129
|
+
| `celltype_level` | `celltype_level` | |
|
|
130
|
+
| `gene_level_fisher` | `gene_level_fisher` | |
|
|
131
|
+
| `celltype_level_fisher` | `celltype_level_fisher` | |
|
|
132
|
+
| `T_to_p` | `T_to_p` | |
|
|
133
|
+
| `ultosymmetric` | `ultosymmetric` | |
|
|
134
|
+
| `nb_lik` | `nb_lik` | |
|
|
135
|
+
| `print.Niche_DE` | `NicheDEObject.__repr__` | |
|
|
136
|
+
| *(poolr dependency)* | `mvnconv`, `fisher_generalized` | clean-room, MIT |
|
|
137
|
+
|
|
138
|
+
Two extra dependency-free modules are exposed for other ports to reuse:
|
|
139
|
+
|
|
140
|
+
- `pynichede.rstats` — R-faithful `glm.fit` (LINPACK `dqrdc2` limited-pivot rank
|
|
141
|
+
detection, `NA`-for-aliased coefficients), `optimize` (Brent `fmin`),
|
|
142
|
+
`p.adjust`, type-7 `quantile`, `weighted.mean`.
|
|
143
|
+
- `pynichede.poolr` — Brown's method and `mvnconv`, re-derived from first
|
|
144
|
+
principles rather than vendored (poolr is GPL-2+, this port is MIT).
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Reproducing the R results exactly
|
|
149
|
+
|
|
150
|
+
The parity gate is a runnable test. Generate the R reference, run the Python
|
|
151
|
+
candidate, and assert the pre-registered thresholds:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
export R_LIBS_USER=/path/to/rlibs # where nicheDE + poolr are installed
|
|
155
|
+
export REF=/tmp/nichede_ref
|
|
156
|
+
|
|
157
|
+
Rscript tests/r_reference_driver.R $REF 0 16 # 0 = full gene set
|
|
158
|
+
Rscript tests/r_reference_supplement.R $REF 0
|
|
159
|
+
python tests/_run_candidate.py $REF 16
|
|
160
|
+
|
|
161
|
+
NICHEDE_REF_DIR=$REF pytest -q # the gate
|
|
162
|
+
python tests/parity_report.py $REF # the numbers
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`pytest -q` without `NICHEDE_REF_DIR` skips the R-dependent tests and runs the
|
|
166
|
+
31 R-free unit tests, so a bare checkout stays green.
|
|
167
|
+
|
|
168
|
+
### Measured parity — canonical fixture
|
|
169
|
+
|
|
170
|
+
10x Visium human liver metastasis (the dataset shipped with the R package):
|
|
171
|
+
**848 spots x 21 708 genes x 7 cell types x 3 kernel bandwidths**.
|
|
172
|
+
|
|
173
|
+
| Output | Class | Metric | Threshold | **Measured** |
|
|
174
|
+
|---|---|---|---|---|
|
|
175
|
+
| `T_stat` (Wald statistic, all 3 kernels) | ordinal | Pearson / Spearman | ≥ 0.99 | **1.000000 / 1.000000** |
|
|
176
|
+
| `betas` | ordinal | Pearson | ≥ 0.99 | **1.000000** |
|
|
177
|
+
| `log_likelihood` | ordinal | Pearson | ≥ 0.99 | **1.000000** |
|
|
178
|
+
| `valid` gene flags | exact | agreement | 1.0 | **1.000000** (4768 / 4788 / 4858) |
|
|
179
|
+
| `nulls` (dropped interactions) | exact | max abs err | 0 | **0** |
|
|
180
|
+
| `num_cells` | deterministic | max abs err | < 1e-8 | **4.7e-14** |
|
|
181
|
+
| `effective_niche` | deterministic | max abs err | < 1e-8 | **2.9e-13** |
|
|
182
|
+
| `pval_pos` gene level | inference | Spearman(-log10 p) / top-50 J | ≥ 0.90 / 0.70 | **0.9966 / 1.000** |
|
|
183
|
+
| `pval_pos` cell-type level | inference | Spearman / top-50 J | ≥ 0.90 / 0.70 | **0.9960 / 0.923** |
|
|
184
|
+
| `pval_pos` interaction level | inference | Spearman / top-50 J | ≥ 0.90 / 0.70 | **1.000000 / 1.000** |
|
|
185
|
+
| `pval_neg` gene level | inference | Spearman / top-50 J | ≥ 0.90 / 0.70 | **0.9960 / 1.000** |
|
|
186
|
+
| `get_niche_DE_genes` output sets | ranked | Jaccard | ≥ 0.70 | **0.938 – 0.972** |
|
|
187
|
+
| `niche_DE_markers` output set | ranked | Jaccard | ≥ 0.70 | **1.000** |
|
|
188
|
+
|
|
189
|
+
Full breakdown, including the `Int = FALSE` branch and the helper functions, in
|
|
190
|
+
[`RECONSTRUCTION_REPORT.md`](RECONSTRUCTION_REPORT.md).
|
|
191
|
+
|
|
192
|
+
### Speed
|
|
193
|
+
|
|
194
|
+
`niche_DE` on the canonical fixture (848 x 21 708 x 7 x 3):
|
|
195
|
+
|
|
196
|
+
| | wall clock |
|
|
197
|
+
|---|---|
|
|
198
|
+
| R `nicheDE::niche_DE`, 16 cores | **852 s** |
|
|
199
|
+
| `pynichede.niche_DE`, 16 cores | **33 s** |
|
|
200
|
+
|
|
201
|
+
≈ **26x faster**, entirely from exact algebraic rewrites and scheduling — no
|
|
202
|
+
approximation.
|
|
203
|
+
|
|
204
|
+
`niche_LR_spot` on the same fixture (579 candidate ligands against the
|
|
205
|
+
16 968 x 579 NicheNet matrix) went from **314.5 s to 3.0 s (105x)** by
|
|
206
|
+
memoising a per-kernel slice that the R source recomputes once per ligand —
|
|
207
|
+
byte-identical output. See [`ITERATION_LOG.md`](ITERATION_LOG.md) and
|
|
208
|
+
[`MATH.md`](MATH.md).
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Known divergences from the shipped R package
|
|
213
|
+
|
|
214
|
+
These are measured, not hand-waved. See [`MATH.md`](MATH.md) §3.
|
|
215
|
+
|
|
216
|
+
1. **`CalculateEffectiveNicheLargeScale` — Python is correct, R is not.**
|
|
217
|
+
With `Rfast >= 2.1.5.2`, `Rfast::dista()` returns an **all-zero matrix**
|
|
218
|
+
whenever `nrow(xnew) >= 4`, so every Gaussian kernel weight in the tiled path
|
|
219
|
+
collapses to 1. The shipped R `CalculateEffectiveNicheLargeScale` therefore
|
|
220
|
+
disagrees with the shipped R `CalculateEffectiveNiche` by up to **16.4
|
|
221
|
+
z-units** on the canonical fixture. `py-nichede` implements the intended
|
|
222
|
+
algorithm and matches the exact effective niche to `2.9e-13`; it is
|
|
223
|
+
parity-tested against a **repaired** R version.
|
|
224
|
+
|
|
225
|
+
2. **`poolr::mvnconv` — Python is the more accurate side.**
|
|
226
|
+
poolr's shipped `mvnlookup` is a numerically-integrated table stored to four
|
|
227
|
+
decimals; its `chisq1_2` column deviates from its own exact closed form
|
|
228
|
+
`2 rho^2` by up to `6.28e-4`. `py-nichede` re-derives the table exactly (via
|
|
229
|
+
Mehler's formula) instead of vendoring GPL data. Net effect on a pooled
|
|
230
|
+
Brown p-value: **3.06e-4 relative**.
|
|
231
|
+
|
|
232
|
+
3. **Bug-compatible by design.** Four defects in the R source change *which
|
|
233
|
+
genes get reported*, so they are reproduced rather than fixed — including
|
|
234
|
+
the `new_nul` / `var` typo that invalidates a gene whenever `X'WX` has a zero
|
|
235
|
+
diagonal, and the `X[, -null]` dimension drop that invalidates a gene
|
|
236
|
+
whenever exactly one interaction survives the `M` filter. Reproducing them is
|
|
237
|
+
what takes the `valid`-flag agreement from 0.9933 to exactly 1.0.
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## Notebooks
|
|
242
|
+
|
|
243
|
+
| Notebook | For |
|
|
244
|
+
|---|---|
|
|
245
|
+
| [`examples/compare_R_vs_Python.ipynb`](examples/compare_R_vs_Python.ipynb) | reviewers — pipeline-level parity vs R, one figure per gated output |
|
|
246
|
+
| [`examples/tutorial_liver_met_visium.ipynb`](examples/tutorial_liver_met_visium.ipynb) | new users — Python-only walkthrough of every public function |
|
|
247
|
+
| [`examples/function_by_function_R_parity.ipynb`](examples/function_by_function_R_parity.ipynb) | R users migrating — per-function parameter dictionary + side-by-side calls |
|
|
248
|
+
| [`examples/evolution.ipynb`](examples/evolution.ipynb) | auditors — per-iteration narrative of how the port was built |
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## Relationship to omicverse
|
|
253
|
+
|
|
254
|
+
`py-nichede` is a standalone package. It is also the niche-differential-expression
|
|
255
|
+
backend for [omicverse](https://github.com/Starlitnightly/omicverse), filling a
|
|
256
|
+
gap no other Python tool covers: `squidpy` and `liana-py` provide spatially
|
|
257
|
+
variable genes and ligand–receptor scoring, but neither models
|
|
258
|
+
index-cell-type x niche-cell-type interaction effects on expression.
|
|
259
|
+
|
|
260
|
+
It was produced with the
|
|
261
|
+
[Omicverse-RebuildR](https://github.com/omicverse/omicverse-rebuildr) protocol:
|
|
262
|
+
the R source is treated as the executable spec, the parity metric and threshold
|
|
263
|
+
are pre-registered before any Python is written, and every acceleration rewrite
|
|
264
|
+
must ship an admissibility proof.
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## Citation
|
|
269
|
+
|
|
270
|
+
If you use `py-nichede`, please cite the original method:
|
|
271
|
+
|
|
272
|
+
```bibtex
|
|
273
|
+
@article{mason2024nichede,
|
|
274
|
+
title = {Niche-DE: niche-differential gene expression analysis in spatial
|
|
275
|
+
transcriptomics data identifies context-dependent cell-cell interactions},
|
|
276
|
+
author = {Mason, Kaishu and Sathe, Anuja and Hess, Paul R. and Rong, Jiazhen and
|
|
277
|
+
Wu, Chi-Yun and Furth, Emma and Susztak, Katalin and Levinsohn, Jonathan and
|
|
278
|
+
Ji, Hanlee P. and Zhang, Nancy R.},
|
|
279
|
+
journal = {Genome Biology},
|
|
280
|
+
volume = {25},
|
|
281
|
+
number = {1},
|
|
282
|
+
pages = {14},
|
|
283
|
+
year = {2024},
|
|
284
|
+
doi = {10.1186/s13059-023-03159-6}
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
and, if the Python port itself was load-bearing, link back to this repository.
|
|
289
|
+
|
|
290
|
+
## License
|
|
291
|
+
|
|
292
|
+
MIT, matching the upstream R package. See [LICENSE](LICENSE).
|
|
293
|
+
`pynichede.poolr` is an independent clean-room implementation and contains no
|
|
294
|
+
GPL-licensed code or data from `poolr`.
|