Skip to main content

Composable spatial and cell-state smoothing of gene signatures for single-cell and spatial transcriptomics

Project description

spatial-smooth

Composable smoothing of gene-set signatures over space and cell state.

📖 Documentation · Tutorial notebook · Setty Lab

⚠️ For visualization only

This package makes spatial regions easier to see. Its output is a picture, not data.

Smoothing deliberately makes neighbouring cells look like one another — which is what you want when you are trying to spot where a program is active, and what you must never hand to a statistical test. A smoothed score is spatially autocorrelated by construction: the cells stop being independent observations, so differential expression, differential abundance, clustering, correlations and p-values computed on smoothed values will report "significant" structure in pure noise.

Look at the smoothed values. Run the statistics on the raw ones — every call writes adata.obs[f"{name}_raw"] for exactly that purpose — with a method that accounts for spatial dependence.

Every cell is measured independently, so a per-cell signature score is dominated by dropout and sampling noise: a speckle of dots in which a real anatomical region is genuinely hard to spot. Smoothing lets neighbouring cells borrow statistical strength, turning that speckle into a coherent field you can read at a glance.

import spatial_smooth as ss

ss.smooth(adata, ["Prox1", "Neurod6", "Wfs1", "Fibcd1"], "hippocampus")
ss.pl.signature(adata, "hippocampus")

raw versus smoothed hippocampal signature

A four-gene hippocampal signature on a public 10x Xenium mouse-brain section (36,419 cells). Left: the raw mean z-score. Right: after one line of smoothing — the dentate-gyrus C-shape, the CA fields and the cortical layers resolve. 0.9 seconds.


Install

pip install "spatial-smooth[all]"

[all] pulls every optional backend. For a minimal install, drop the extra (pip install spatial-smooth) and add only the extras you need (see below). uv pip install "spatial-smooth[all]" works identically.

Only numpy, scipy, pandas and anndata are required. Everything else is an optional extra, imported lazily and reported with the exact pip install line when missing: dm (kompot), embedding (palantir), plot (scanpy), squidpy, kde (KDEpy). Run ss.check_dependencies() to see where you stand.

Your data needs exactly two things: log-normalised expression in adata.X (or a named layer), and coordinates in adata.obsm["spatial"].


Which neighbours count?

That is the scientific choice, and it is one argument.

ss.smooth(adata, genes, "sig")                     # spatial only   (the default)
ss.smooth(adata, genes, "sig", steps="dm")         # cell state only
ss.smooth(adata, genes, "sig", steps="dm+spatial") # both, in that order

the three composition modes

The same signature, four ways. Spatial smoothing averages over physically adjacent cells. Cell-state smoothing averages over transcriptionally similar cells (a diffusion map), without using position at all. Composed does the manifold first, then the tissue.

A pipeline is an ordered list of steps; each smooths the expression matrix over one embedding and hands the result to the next. Doing just one of the two is a one-element pipeline, not a special case. Pass Step objects instead of a shorthand for full control:

ss.smooth(adata, genes, "sig", store_genes=True, steps=[
    ss.KompotGP(basis="DM_EigenVectors", ls_factor=10.0, n_landmarks=8000),
    ss.KnnGaussian(basis="spatial", k=64, sigma_factor=4.0),
])
shorthand pipeline shorthand pipeline
"spatial" (default) [KnnGaussian()] "spatial+dm" [KnnGaussian(), KompotGP()]
"dm" [KompotGP()] "spatial-kde" [Kde()]
"dm+spatial" [KompotGP(), KnnGaussian()] "spatial-gp" GP over tissue coordinates

Compute once, plot forever

Results are written into the AnnData, and plotting reads only those keys — so an expensive smoothing is done once.

key contents
obs[name] smoothed signature score
obs[f"{name}_raw"] unsmoothed score, same genes and combiner
obsm[f"{name}_smoothed"] (n_obs, n_genes) smoothed expression (store_genes=True)
uns["spatial_smooth"][name] provenance: genes, pipeline, resolved bandwidths, version
adata.write_h5ad("smoothed.h5ad")
# ... later, elsewhere, with neither kompot nor palantir installed:
adata = anndata.read_h5ad("smoothed.h5ad")
ss.provenance(adata, "sig")["steps"]   # exactly what ran, with the bandwidths it resolved
ss.pl.signature(adata, "sig")          # renders in ~60 ms; recomputes nothing

That is a contract, not a hope: a test blocks kompot, KDEpy and palantir at the import machinery, replaces every compute entry point with a function that raises, and then renders a reloaded file.


Plotting wraps scanpy and squidpy

ss.pl.signature sets color from the stored provenance and forwards every other keyword verbatim to squidpy.pl.spatial_scatter, scanpy.pl.embedding, or scanpy.pl.spatial (backend=, default "auto").

ss.pl.signature(adata, "sig", backend="squidpy", cmap="magma", figsize=(6, 6))
ss.pl.compare(adata, ["spatial_only", "dm_only", "composed"], raw=True, ncols=4)

Two conventions are normalised for you, because leaving them to the backend gave different pictures of the same tissue. A spatial basis is always drawn in image convention — y increasing downward, equal aspect — so scanpy and squidpy agree on which way is up. And size means different things per backend — marker area in scanpy, a scale factor in squidpy — so it is documented rather than translated, and the scanpy path gets a density-aware default (plot.default_marker_size) so a dense section renders as tissue rather than speckle. Pass size yourself to override.


Choosing a smoother

step engine full slide (~1.6 × 10⁵ cells) gives you
KnnGaussian Gaussian kernel over k spatial neighbours ~1 s the default; fast, sharp
Kde FFT Nadaraya-Watson on a fine grid ~1 s a rendered field; resolution-bound
KompotGP Gaussian-process regression (kompot/mellon) minutes a length scale, a posterior, fit-on-one-condition

Bandwidths default to a multiple of the median nearest-neighbour distance, so the same factor smooths the same amount whether coordinates are in microns or millimetres.

Quote sigma_effective, never sigma_nominal. KnnGaussian truncates its Gaussian at the k-th neighbour, so the bandwidth the data actually sees is set by whichever of sigma and the k-neighbour radius binds first — and since that radius follows a neighbour count, the kernel is implicitly density-adaptive. provenance() records kernel_mass_retained, sigma_effective and its spread across cells alongside sigma_nominal, and warns when the truncation starts to bite. The default k=400 keeps ~96% of the kernel mass, so the two nearly agree.

One caveat. Over a diffusion map, kompot's native ls_factor=10 is right. Over physical coordinates it is ~200× the cell spacing and collapses the field into a single global gradient. Use ls_factor≈0.3 there — which is what the "spatial-gp" shorthand does.

See Concepts for the composition and scoring semantics, and why gene-level smoothing costs nothing in correctness.

License

MIT. See LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

spatial_smooth-0.1.0.tar.gz (52.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

spatial_smooth-0.1.0-py3-none-any.whl (34.1 kB view details)

Uploaded Python 3

File details

Details for the file spatial_smooth-0.1.0.tar.gz.

File metadata

  • Download URL: spatial_smooth-0.1.0.tar.gz
  • Upload date:
  • Size: 52.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.12

File hashes

Hashes for spatial_smooth-0.1.0.tar.gz
Algorithm Hash digest
SHA256 deef3ace41225a828b30bafa53e4a30b7da0f1a877aa51d93758b266cc80ad0d
MD5 eba53041b28c64de77283095a2e51413
BLAKE2b-256 71a1953cceb0c1771a263dc517611b6db4ac70a151af66aa23210c4d200826f6

See more details on using hashes here.

File details

Details for the file spatial_smooth-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: spatial_smooth-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 34.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.12

File hashes

Hashes for spatial_smooth-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ae09732c903b4cfa8679dd6d55d29e532f66e0f83fa984a4e093198dc16c1712
MD5 bb81edd95826f27deba3fa1f2085ea4c
BLAKE2b-256 489d1070cf0f33ca532660aa0eb6fd03fe4554ae44cc07b444e31dbcfd5a41a6

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page