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.
@@ -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.
@@ -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`.