SKNY (SpatialKNifeY) - tumor microenvironment analysis for single-cell spatial omics
SKNY (SpatialKNifeY) is a Python package for spatial transcriptomics / spatial proteomics analysis of the tumor microenvironment (TME). It works on single-cell spatial omics data from Xenium, CosMx, PhenoCycler and any other platform that can be loaded into an anndata object, so it fits into scanpy, squidpy and stlearn workflows.
SKNY answers questions such as:
Where is the tumor border in this section, and how far is each location or cell from it?
How does gene expression change with distance from the tumor (intra-tumoral vs peri-tumoral)?
Which genes are differentially expressed between the tumor core and the surrounding stroma?
What is the density of immune cells (cells / mm²) in the tumor core, the tumor margin and distant tissue?
How do individual tumor nests (“single tumor microenvironment”) differ from each other?
Published in PLOS Computational Biology (2025): https://doi.org/10.1371/journal.pcbi.1012854 (see Citation).
Two workflows
1. Expression-based tumor contour on a grid (sk.pp.calculate_distance; the method in the paper)
The tissue is binned into a 10 µm grid, the tumor is contoured from marker gene expression (e.g. CDH1, EPCAM, keratins) with image processing, and the shortest-path distance from the contour to every grid bin is calculated. Downstream functions then give:
gene expression as a function of distance from the tumor border (sk.pl.distance_gene_plot),
clustering of all genes by their distance profile (sk.pl.clustering_heatmap),
differentially expressed genes between distance bands (sk.pl.deg_manhattan_plot),
per-tumor-nest (“single-TME”) pseudo-bulk expression inside each nest (sk.pp.convert_indivisual_solid) and in the band around it (sk.pp.convert_indivisual_peri).
2. Cell-label based regions: tumor core / margin / dist (sk.pp.calculate_region; since v0.3.0)
For cell-level data with annotated cell types (scanpy / Xenium 5K / CosMx / PhenoCycler), the tumor is defined by a cell label such as "Malignant". Every cell is assigned to the tumor core (within 15 µm of a tumor cell), the peri-tumoral margin (stroma within 100 µm of the core) or dist (farther tissue), using an all-cell tissue mask so that empty space is never counted. sk.pp.region_density then reports the density (cells / mm²) of any cell group per region, and sk.pl.region_map / sk.pl.region_density_plot draw the results.
Installation
pip install skny
SKNY supports Python 3.8 to 3.13 (tested with numpy 2 and pandas 3) and depends only on anndata, numpy, pandas, scipy, matplotlib, seaborn, plotly, opencv-python, networkx and pillow, so it installs alongside current scanpy / squidpy environments.
Both workflows run with pip install skny alone. Grids made with stlearn.tl.cci.grid are accepted as well (pip install "skny[stlearn]" installs stlearn if you want it).
Quick start
Workflow 1: distance from the expression-based tumor contour (Xenium example)
import skny as sk
# Load a Xenium run (cell x gene counts + centroids in um). Any AnnData with
# obs["imagecol"] / obs["imagerow"] (or obsm["spatial"]) in um works as well.
adata = sk.pp.read_xenium("Xenium_FFPE_Human_Breast_Cancer_Rep1/") # output folder, or
# adata = sk.pp.read_xenium("..._cell_feature_matrix.h5", cells_file="..._cells.csv.gz")
# Bin cells into a 10 um grid (same format as stlearn.tl.cci.grid, no stlearn needed)
grid = sk.pp.grid(adata, bin_um=10)
# Contour the tumor from marker genes and compute the distance of every bin from the contour
grid = sk.pp.calculate_distance(grid, pos_marker_ls=["CDH1"])
sk.pl.preprocessed_images(grid, figsize=(800, None)) # tumor mask, contour, equidistant curves
sk.pl.gene_plot(grid, "KRT15") # expression on the tissue
sk.pl.distance_gene_plot(grid, "KRT15") # expression vs distance from the tumor
sk.pl.clustering_heatmap(grid, std=True) # all genes clustered by distance profile
sk.pl.deg_manhattan_plot(grid) # DEGs between intra- and peri-tumoral bands
# "Single tumor microenvironment": pseudo-bulk expression per tumor nest
solid = sk.pp.convert_indivisual_solid(grid) # inside each nest (-inf, 0] um
peri = sk.pp.convert_indivisual_peri(grid, section=30) # 0 to 30 um band around each nest
Runtime on the public 10x Xenium breast cancer section (167,782 cells, 752 x 547 bins): reading 1 s, gridding 1 s, calculate_distance about 80 s on a laptop.
Workflow 2: tumor core / margin / dist from a cell label
import skny as sk
# adata: one row per cell, coordinates in micrometres in adata.obsm["spatial"],
# cell types in adata.obs["cell_type"], sample id in adata.obs["sample"]
sk.pp.calculate_region(adata, "Malignant", label_key="cell_type", sample_key="sample")
# -> adata.obs["region"] in {"core", "margin", "dist", "outside"}, adata.obs["region_dist_um"]
df = sk.pp.region_density(adata, "immune_subtype", sample_key="sample")
# -> columns: sample, region, group, n, area_mm2, density (cells / mm^2)
sk.pl.region_map(adata, sample="S1", highlight=["T cell", "Myeloid"])
sk.pl.region_density_plot(df, groups=["CD8+ T", "Treg", "M2-like"])
When to use SKNY (and how it relates to scanpy / squidpy)
scanpy handles clustering and differential expression; squidpy handles neighborhood graphs, co-occurrence and ligand-receptor analysis. Neither defines a tumor border or measures distance from it.
Use SKNY when your question is about the spatial relationship to the tumor: expression gradients from the tumor border, immune infiltration into the tumor core vs the margin, comparison of individual tumor nests, or anything stratified by distance from a tumor domain.
SKNY takes and returns AnnData objects, so you can keep using scanpy / squidpy before and after it (e.g. run sc.tl.rank_genes_groups on adata.obs["region"]).
Documentation and tutorials
Documentation: https://skny.readthedocs.io
Tutorials (Jupyter notebooks, public 10x Xenium breast cancer data):
Xenium analysis - workflow 1 end to end
Single-TME analysis - per-tumor-nest analysis
Region analysis - workflow 2 (core / margin / dist, cell density)
API reference: https://skny.readthedocs.io/en/latest/api.html
Machine-readable summary for coding assistants: llms.txt and AGENTS.md
Citation
If you use SKNY, please cite:
Sakai SA, Nomura R, Nagasawa S, Chi S, Suzuki A, Suzuki Y, Imai M, Nakamura Y, Yoshino T, Ishikawa S, Tsuchihara K, Kageyama S-I, Yamashita R. SpatialKNifeY (SKNY): Extending from spatial domain to surrounding area to identify microenvironment features with single-cell spatial omics data. PLOS Computational Biology 21(2): e1012854 (2025). https://doi.org/10.1371/journal.pcbi.1012854
@article{Sakai2025SKNY,
title = {SpatialKNifeY (SKNY): Extending from spatial domain to surrounding area to identify microenvironment features with single-cell spatial omics data},
author = {Sakai, Shunsuke A. and Nomura, Ryosuke and Nagasawa, Satoi and Chi, SungGi and Suzuki, Ayako and Suzuki, Yutaka and Imai, Mitsuho and Nakamura, Yoshiaki and Yoshino, Takayuki and Ishikawa, Shumpei and Tsuchihara, Katsuya and Kageyama, Shun-Ichiro and Yamashita, Riu},
journal = {PLOS Computational Biology},
year = {2025},
volume = {21},
number = {2},
pages = {e1012854},
doi = {10.1371/journal.pcbi.1012854}
}
To cite a specific version of the software, use the Zenodo DOI: https://doi.org/10.5281/zenodo.23907317 (always resolves to the latest release; each release also has its own DOI, e.g. v0.3.1: https://doi.org/10.5281/zenodo.23907318).
A CITATION.cff file is included in the repository (GitHub “Cite this repository” button).
License
MIT. See LICENSE.
History
0.3.2 (2026-11-03)
New skny.pp.read_xenium (10x Xenium reader, h5py only) and skny.pp.grid (10 um gridding with the same output format as stlearn.tl.cci.grid), so the expression-based workflow runs with pip install skny alone; skny.pl.gene_plot replaces stlearn.pl.gene_plot in the tutorials.
Fixed a one-bin offset: stlearn numbers grid bins from grid_0 while SKNY assumed grid_1, which shifted every mask by one bin (10 um). The numbering is now detected (skny.pp.bin_names); results on stlearn grids move by one bin compared with <= 0.3.1.
Tutorials no longer depend on stlearn (the Tumor contour with Xenium data notebook was re-executed with skny 0.3.2); the sdist no longer ships the notebooks (11 MB -> < 1 MB).
0.3.1 (2026-11-03)
Dependencies relaxed: no more pins on numpy / numba; stlearn and scanpy are no longer required (install skny[stlearn] for the grid tutorial). SKNY now runs on Python 3.8 to 3.13 with numpy 2 and pandas 3.
Compatibility fixes for recent matplotlib / pandas (figure buffer access, row-wise z-score, applymap removal, NaN-safe distance-band handling in convert_indivisual_*).
New end-to-end regression test of the grid workflow on synthetic data (tests/test_grid_workflow.py, no external data needed).
Citation information (PLOS Computational Biology 2025) added to the README and a CITATION.cff file; llms.txt and AGENTS.md added for coding assistants.
PyPI metadata (keywords, project URLs, classifiers) and NumPy-style docstrings for all public functions.
Removed the unused skny console script (template leftover).
skny.pp.calculate_region now warns instead of raising when no cell carries the reference label (all cells become not_computed), as covered by the unit tests.
0.3.0 (2026-11-02)
New cell-label based region analysis (a variant of calculate_distance that works on cell-level AnnData instead of marker expression on a grid): skny.pp.calculate_region (tumor core / margin / dist from the Euclidean distance to the nearest reference cell, with an all-cell tissue mask), skny.pp.region_density (cells / mm^2 of any cell group per region), skny.pl.region_map and skny.pl.region_density_plot.
Tutorial notebooks/region_analysis and unit tests tests/test_region.py.
0.2.16 (2024-02-14)
0.2.8 (2024-02-01)
0.2.7 (2024-01-31)
0.2.6 (2024-01-31)
Practice of upload to PyPI.
0.2.5 (2024-01-30)
0.2.4 (2024-01-30)
0.2.3 (2024-01-30)
0.2.2 (2024-01-30)
0.2.1 (2024-01-30)
0.1.2 (2024-01-30)
0.1.1 (2024-01-30)
0.1.0 (2024-01-21)
First release on PyPI.
Metadata
Release files for skny 0.3.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| skny-0.3.2.tar.gz | 55.5 kB | Without provenance | Details for skny-0.3.2.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| skny-0.3.2-py2.py3-none-any.whl | Python 3, Python 2 | none | any | Details for skny-0.3.2-py2.py3-none-any.whl |
Total release size: 98.6 kB
Release files / skny-0.3.2.tar.gz
| Download URL | skny-0.3.2.tar.gz |
|---|---|
| Size | 55.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
43d0b37ea1ce96f0e9b0dcae45c14e15e2ef6cc5a067bed5b38c13d385cd7a9e
|
|
BLAKE2b-256 checksum How to use checksums |
2c8919c432fc426b8b21d402424150baec1236fc0638789285482ad8a1550f0e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.1
|
Release files / skny-0.3.2-py2.py3-none-any.whl
| Download URL | skny-0.3.2-py2.py3-none-any.whl |
|---|---|
| Size | 43.1 kB |
| Tags | Python 2 Python 3 |
|
SHA-256 checksum How to use checksums |
06c7a874bde20c2200261ac5a97f839fe74a251eeed114e8e42b282ebd9d0255
|
|
BLAKE2b-256 checksum How to use checksums |
74e4f54e49228613223e1c9e5d9432a1e880c72c57fe233760d5bad9b0719f62
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.1
|
Security for skny 0.3.2
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.