API reference¶
Compute¶
- spatial_smooth.smooth(adata, genes, name='signature', *, steps='spatial', layer=None, score='mean_z', subset_key=None, include=None, exclude=None, store_genes=False, auto_embed=True, progress=False, copy=False)[source]¶
Smooth a gene signature through a pipeline of steps and score it per cell.
Warning
The smoothed score is for visualization only. It is spatially autocorrelated by construction, so any statistic computed on it (differential expression, clustering, correlation, a p-value of any kind) will be badly over-confident. Plot
obs[name]; analyseobs[f"{name}_raw"].The one-liner smooths over physical coordinates with a Gaussian kNN kernel:
import spatial_smooth as ss ss.smooth(adata, ["Prox1", "Neurod6"], "hippocampus") ss.pl.signature(adata, "hippocampus")
Choose what to smooth over with
steps:"spatial"(default),"dm"(the expression manifold, viakompot.smooth_expression), or"dm+spatial"to compose both – the spatial step then consumes the manifold-denoised expression. PassStepobjects instead of a shorthand for full control.- Parameters:
adata – Normalised, log-transformed expression with the required
obsmbases.genes (Sequence[str]) – Signature genes. Duplicates are dropped, order preserved. One gene is fine.
name (str) – Base name for the outputs (see the module docstring’s storage contract).
steps (str | Step | Sequence[str | Step] | None) – A shorthand (
"spatial","dm","dm+spatial","spatial+dm","spatial-kde","spatial-gp","none"), a singleStep, or a sequence of either. Applied left to right; each step consumes the previous step’s output.layer (str | None) – Expression layer to read (
None->adata.X). Should be log-normalised.score (str) – Multi-gene combiner:
"mean_z"(default) or"mean".subset_key (str | None) – Optional cell filter applied before smoothing (see
select_cells()). Filtered-out cells neither train nor receive the field. When a filter removes cells the returned object is a new, smaller AnnData – use the return value.include (Iterable | None) – Optional cell filter applied before smoothing (see
select_cells()). Filtered-out cells neither train nor receive the field. When a filter removes cells the returned object is a new, smaller AnnData – use the return value.exclude (Iterable | None) – Optional cell filter applied before smoothing (see
select_cells()). Filtered-out cells neither train nor receive the field. When a filter removes cells the returned object is a new, smaller AnnData – use the return value.store_genes (bool) – Also write the smoothed
(n_obs, n_genes)expression matrix toadata.obsm[f"{name}_smoothed"].auto_embed (bool) – Compute a Palantir diffusion map when a step needs
obsm["DM_EigenVectors"]and it is absent. SetFalseto fail loudly instead.progress (bool) – Show the GP backend’s progress bar.
copy (bool) – Work on a copy and leave the input untouched.
- Returns:
AnnData – The object carrying the result. Identical to the input when
copy=Falseand no cell filter was applied.- Raises:
KeyError – A gene, layer, or
obsmbasis is missing (the message names it).ImportError – An optional backend a step needs is not installed (the message gives the pip line).
See also
spatial_smooth.plot.signaturerender a stored result without recomputing it.
provenanceread back exactly what was run.
- spatial_smooth.compute_diffusion_map(adata, *, obsm_key='DM_EigenVectors', n_components=10, knn=30, n_pca_components=50, use_hvg=False, random_state=0, recompute=False)[source]¶
Compute a Palantir diffusion map and store it in
adata.obsm[obsm_key].A thin wrapper over
palantir.utils.run_pca+palantir.utils.run_diffusion_maps. The diffusion map is the cell-state embedding aKompotGPstep smooths over by default: nearby cells are transcriptionally similar, so smoothing there denoises along biological structure rather than physical position.Idempotent – returns immediately if
obsm_keyalready exists, unlessrecompute.- Parameters:
adata – Normalised, log-transformed expression.
obsm_key (str) – Destination key. The default matches kompot’s expectation.
n_components (int) – Forwarded to Palantir.
knn (int) – Forwarded to Palantir.
n_pca_components (int) – Forwarded to Palantir.
use_hvg (bool) – Forwarded to Palantir.
random_state (int) – Forwarded to Palantir.
recompute (bool) – Recompute even when
obsm_keyis present.
- Returns:
AnnData – The same object, for chaining.
- spatial_smooth.select_cells(adata, obs_key, *, include=None, exclude=None)[source]¶
Boolean mask over
adata.obsselecting the cells to keep.includekeeps only the listed values;excludedrops the listed values; together they apply in that order. Values are compared as strings, so numeric and categorical columns both work.
Steps¶
Smoothing steps – the composable units of a smoothing pipeline.
A step smooths an (n_obs, n_genes) expression matrix over one embedding stored in
adata.obsm. A pipeline is an ordered list of steps: each consumes the previous step’s
output, so [KompotGP(), KnnGaussian()] first denoises along the expression manifold and then
smooths the already-denoised values over physical space.
Three steps ship with the package:
step |
default basis |
engine |
|---|---|---|
|
Gaussian kernel over |
|
|
FFT Nadaraya-Watson on a fine grid (KDEpy) |
|
|
Gaussian-process regression (kompot/mellon) |
Every step is a frozen dataclass: it is a specification, not a fitted object. It carries no
data, is trivially serialisable into adata.uns (Step.to_dict()), and can be reused
across datasets.
String shorthands¶
resolve_steps() turns a shorthand into a pipeline, so the common cases need no imports:
shorthand |
pipeline |
meaning |
|---|---|---|
|
|
spatial smoothing only |
|
|
cell-state smoothing only |
|
|
both, cell-state first |
|
|
both, spatial first |
|
|
spatial, KDE engine |
|
|
spatial, GP engine |
|
|
no smoothing (raw only) |
- class spatial_smooth.steps.KnnGaussian(basis='spatial', k=400, sigma=None, sigma_factor=6.0, workers=-1)[source]¶
Bases:
StepGaussian kernel over the
knearest neighbours – the fast default.A row-stochastic linear smoother (see
spatial_smooth.smoothers): each cell’s value becomes the Gaussian-weighted mean of itsknearest neighbours inbasis. On a full imaging-based slide (~1.6e5 cells) this runs in a second or two, against several minutes for the Gaussian process at comparable output quality – so it is the default for smoothing over physical tissue coordinates.Truncation, and what provenance reports¶
Restricting the kernel to
kneighbours truncates it. Whichever binds first –sigmaor the radius of thek-th neighbour – sets the bandwidth the data actually sees, and because that radius follows a neighbour count it shrinks where cells are dense and grows where they are sparse. The smoother is therefore truncated-Gaussian and implicitly density-adaptive, not strictly fixed-bandwidth.spatial_smooth.provenance()records this rather than hiding it. Alongsidesigma_nominal(the Gaussian’s sigma before truncation – a bandwidth no cell experiences; also written assigma_usedfor backwards compatibility) it storeskernel_mass_retained(the mean fraction of the untruncated Gaussian inside each cell’sk-neighbour radius),sigma_effective(the bandwidth the kernel behaves like, from its weighted second moment) and that quantity’s 1st/99th percentiles across cells. Quote ``sigma_effective`` in a methods section, never ``sigma_nominal``/``sigma_used``. When the retained mass falls belowMIN_KERNEL_MASSaUserWarningnames both numbers and tells you to raisek.The default
k=400keeps ~96% of the mass atsigma_factor=6.0on 2-D tissue, where roughly 210 cells lie within2 * sigma;sigma_effectivethen trackssigma_usedclosely. (k=100retains only ~58%, pulling the effective bandwidth to ~0.6 xsigmawith a ~2.8x spread across cells.)- param basis:
adata.obsmkey to smooth over. Defaults to"spatial".- type basis:
str
- param k:
Neighbours per cell, self included. Not a free performance knob – see above.
- type k:
int
- param sigma:
Bandwidth in coordinate units.
None(default) infers it scale-invariantly assigma_factorx the median nearest-neighbour distance.- type sigma:
float | None
- param sigma_factor:
Multiplier on the median nearest-neighbour distance when
sigmais inferred.- type sigma_factor:
float
- param workers:
Threads for the kd-tree query (
-1= all cores).- type workers:
int
- class spatial_smooth.steps.Kde(basis='spatial', grid_points=1024, bw=None, bw_factor=6.0, min_density_pct=1.0)[source]¶
Bases:
StepFine-grid FFT Nadaraya-Watson smoothing (KDEpy). Two-dimensional bases only.
Comparable in speed to
KnnGaussianand also row-stochastic, but resolution-bound: detail finer than one grid cell is lost. Useful when you want a rendered field rather than a neighbour average, or when point density varies enough that a fixedkmisbehaves.- Parameters:
basis (str) –
adata.obsmkey – must be 2-D.grid_points (int) – Grid resolution per axis.
bw (float | None) – Bandwidth in coordinate units.
None->bw_factorx median NN distance.bw_factor (float) – Multiplier on the median nearest-neighbour distance when
bwis inferred.min_density_pct (float) – Percentile of the uniform-KDE density below which a grid cell counts as empty background and contributes no signal.
- class spatial_smooth.steps.KompotGP(basis='DM_EigenVectors', sigma=1.0, ls=None, ls_factor=10.0, n_landmarks=5000, groupby=None, condition=None, random_state=0)[source]¶
Bases:
StepGaussian-process regression per gene via
kompot.smooth_expression.Fits a GP of each gene over
basisand evaluates it on every cell, so neighbouring cells borrow statistical strength. Slower thanKnnGaussian– the GP solve dominates – but it is the only step that models a length scale explicitly, returns a posterior, and supports fit on one condition, evaluate everywhere viagroupby/condition.Defaults target the cell-state use:
basis="DM_EigenVectors"(a diffusion map of the expression manifold) with kompot’s nativels_factor=10.Length scale is scale-invariant¶
With
ls=None(the default) kompot/mellon infer the length scale empirically from the data’s nearest-neighbour distances –ls_base = geometric_mean(nn_distances) * e**3(mellon.parameters.compute_ls) – and multiply it byls_factor. Becausels_baseis proportional to the point spacing, the samels_factoryields the same smoothing regardless of the coordinates’ absolute scale (microns vs millimetres).Over a diffusion map the native
ls_factor=10is right. Over physical coordinates it is roughly 200x the cell spacing and washes the field into a single global gradient; usels_factor=0.3there (~6 cell spacings, matchingKnnGaussian’s default footprint). The"spatial-gp"shorthand does exactly that.- param basis:
adata.obsmkey to smooth over. Defaults to"DM_EigenVectors".- type basis:
str
- param sigma:
GP noise level.
- type sigma:
float
- param ls:
Explicit length scale in coordinate units; bypasses the empirical path entirely.
- type ls:
float | None
- param ls_factor:
Multiplier on the empirically inferred length scale (used when
ls is None).- type ls_factor:
float
- param n_landmarks:
Inducing points for mellon’s Nystrom approximation. A small effective length scale needs enough landmarks that the landmark spacing stays below it.
- type n_landmarks:
int
- param groupby:
When both are given the GP is fitted only on cells with
adata.obs[groupby] == conditionand evaluated on all cells.- param condition:
When both are given the GP is fitted only on cells with
adata.obs[groupby] == conditionand evaluated on all cells.- param random_state:
Seed for landmark selection.
- type random_state:
int
- class spatial_smooth.steps.Step(basis='spatial')[source]¶
Bases:
objectBase class for a smoothing step. Not used directly.
- Parameters:
basis (str)
Reading results back¶
- spatial_smooth.provenance(adata, name='signature')[source]¶
Read back what
smooth()did, with the pipeline decoded.The returned dict is the stored record with an extra
"steps"entry: the list of step specifications, each including a"resolved"sub-dict of the values actually used (an inferred bandwidth, the kompot version, …).
Plotting¶
Plotting – a thin, transparent wrapper over scanpy and squidpy.
This module never computes anything. It reads the keys spatial_smooth.core.smooth()
wrote into the AnnData (obs[name], obs[f"{name}_raw"],
uns["spatial_smooth"][name]), works out sensible defaults from the recorded provenance, and
hands everything to an existing plotting function. Write a smoothed object to .h5ad, reload
it anywhere, and these calls render it – no kompot, KDEpy or palantir needed, and no
smoothing repeated.
Backends and where your **kwargs go¶
|
underlying call |
when to use it |
|---|---|---|
|
|
tissue coordinates, optional image |
|
|
any |
|
|
Visium-style |
|
squidpy if installed, else scanpy |
– |
Every **kwargs is forwarded verbatim to that function. color is set by this module
(to the raw and smoothed obs columns); passing it raises TypeError rather than being silently
ignored. Everything else – cmap, size, figsize, vmin/vmax, title,
save, ax – is the backend’s own parameter, documented in the backend’s own docstring.
Defaults this module supplies (a perceptually uniform colour map, percentile colour limits where
the backend supports them, a grey na_color) are applied only when you have not passed that
key yourself.
Two conventions this module does normalise, because leaving them to the backend produced plots that differed for the same data:
Orientation. Imaging platforms store cell centroids as image coordinates – origin top-left,
y increasing downward. squidpy.pl.spatial_scatter honours that; scanpy.pl.embedding
treats obsm["spatial"] as an abstract Cartesian embedding and draws y upward, mirroring the
tissue vertically. Whenever the basis being drawn is "spatial", this module inverts the
y-axis and sets equal aspect, so every backend renders the section as the microscope saw it.
``size`` is backend-native and is deliberately not translated. In scanpy it is the marker
area in points squared; in squidpy it is a scale factor on the inferred spot size (default
1.0). A size=6 that looks right in one is nearly invisible in the other, so this module
does not pretend they mean the same thing.
What it does do is pick a sane default for the scanpy backends on a spatial basis, where
scanpy’s own 120000 / n_obs leaves visible gaps between cells and the field reads as speckle
rather than anatomy. default_marker_size() keeps the 1 / n_obs area scaling and raises
the constant, so the section renders continuous. Pass size yourself to override.
- spatial_smooth.plot.signature(adata, name='signature', *, raw=True, backend='auto', basis=None, **kwargs)[source]¶
Plot a stored smoothing result – raw next to smoothed, by default.
Reads only what
spatial_smooth.smooth()stored. Nothing is recomputed, so this works on a reloaded.h5adin an environment without the smoothing backends installed.- Parameters:
adata – Object carrying a result named
name.name (str) – The result to plot.
raw (bool) – Show the unsmoothed score alongside the smoothed one (two panels).
backend (str) –
"auto","squidpy","scanpy", or"scanpy-spatial". See the module docstring for what each delegates to.basis (str | None) –
adata.obsmkey to lay the cells out on. Defaults to the last smoothing step’s basis when that is"spatial", else the first ofspatial/X_umap/X_pcapresent.**kwargs – Forwarded verbatim to the backend’s plotting function.
- Returns:
Whatever the backend returns (usually
Nonewhen it shows, or axes whenshow=False).- Raises:
KeyError – If no result named
nameis stored (the message says how to create it).
- spatial_smooth.plot.compare(adata, names, *, raw=False, backend='auto', basis=None, **kwargs)[source]¶
Plot several stored results side by side – e.g. one panel per smoothing pipeline.
compare(adata, ["sig_spatial", "sig_dm", "sig_both"])puts the three composition modes on a common canvas. Withraw=Truethe first panel is the (shared) unsmoothed score.
Smoothing kernels¶
Low-level smoothing kernels.
Each function here maps (coords, values) -> smoothed values and knows nothing about
AnnData. Two of them are linear and row-stochastic – they replace each
point’s value by a weighted average of its neighbours’ values with weights summing to one:
knn_gaussian_operator()– a sparse Gaussian kernel over theknearest neighbours.smooth_matrix_kde()– a fine-grid FFT Nadaraya-Watson estimator.
Row-stochasticity is the property that makes the package’s scoring contract exact: a constant
field is left unchanged, so smoothing the individual genes and then combining them into a
z-scored signature score gives exactly the same answer as combining first and smoothing the
score (see spatial_smooth.core.smooth()). The Gaussian-process smoother
(KompotGP) is linear but not row-stochastic, so it is applied
per gene.
All bandwidths default to a scale-invariant setting: a multiple of the median
nearest-neighbour distance of coords. The same factor therefore produces the same amount of
smoothing whether coordinates are in microns, millimetres, or arbitrary embedding units.
- spatial_smooth.smoothers.knn_gaussian_operator(coords, *, k=400, sigma=None, sigma_factor=6.0, workers=-1, return_info=False)[source]¶
Build the sparse row-stochastic Gaussian kNN smoothing operator
W.W[i, j] = exp(-d(i, j)**2 / (2 * sigma**2))for theknearest neighboursjofi(self included), each row normalised to sum to one. ApplyingWto any field replaces every value with the Gaussian-weighted mean of itsknearest neighbours.It costs one
cKDTreequery (O(n log n)) plus a sparse mat-vec (O(n * k)) per field – orders of magnitude cheaper than a Gaussian-process fit, and the recommended default for smoothing over physical tissue coordinates.- Parameters:
coords –
(n, d)array of coordinates (physical positions, or any embedding).k (int) – Neighbours per point, including the point itself. Capped at
n. See the truncation note below –kis not a free performance knob, it changes the estimator.sigma (float | None) – Gaussian bandwidth in coordinate units.
None(default) sets it scale-invariantly tosigma_factor * median_nn_distance(coords).sigma_factor (float) – Multiplier on the median nearest-neighbour distance when
sigmais inferred. The default6.0is ~6 cell spacings.workers (int) – Threads for the kd-tree query (
-1uses all cores; cap via the environment on a shared machine).return_info (bool) – Also return a diagnostics dict (see below).
- Returns:
W (scipy.sparse.csr_matrix) –
(n, n)row-stochastic operator.sigma (float) – The nominal bandwidth: the
sigmaof the Gaussian before truncation.info (dict, only if
return_info) –kernel_mass_retained– the mean fraction of the untruncated 2-D Gaussian’s mass that falls inside each point’sk-neighbour radius. This assumes locally uniform point density, so it is an estimate of the discrete retained-weight fraction rather than an exact accounting; it is biased slightly low (conservative).sigma_effective– the mean bandwidth actually applied, recovered from the weighted second moment (sqrt(E_w[d**2] / 2)); and its 1st/99th percentiles across points.
Notes
Truncation makes the kernel density-adaptive. Neighbours beyond the
k-th get zero weight even wheresigmawould give them a real one, so the effective bandwidth is set by whichever ofsigmaand thek-neighbour radius binds first. Because the radius is fixed by a neighbour count, it shrinks in dense regions and grows in sparse ones: this is a truncated-Gaussian kNN smoother, not a strictly fixed-bandwidth one.The default
k=400withsigma_factor=6.0retains ~96% of the kernel mass on 2-D tissue (~210 points lie within2 * sigmaat typical imaging-assay densities), so the truncation is mild andsigmaandsigma_effectivenearly agree. Loweringktightens the smoother and reduces the effective bandwidth: atk=100only ~58% of the mass survives,sigma_effectivefalls to ~0.6 *sigma, and it varies ~2.8x across cells with local density. Quotesigma_effective, notsigma, in a methods section –spatial_smooth.provenance()records both.
- spatial_smooth.smoothers.smooth_matrix_knn_gaussian(coords, matrix, *, k=400, sigma=None, sigma_factor=6.0, workers=-1)[source]¶
Apply
knn_gaussian_operator()to every column ofmatrix.See that function’s Notes on
k-truncation: the kernel is truncated-Gaussian and implicitly density-adaptive, and the returnedsigmais the nominal one.
- spatial_smooth.smoothers.smooth_field_knn_gaussian(coords, values, **kwargs)[source]¶
1-D convenience wrapper around
smooth_matrix_knn_gaussian().Returns
(smoothed_values, sigma_used)wheresmoothed_valueshas shape(n,).
- spatial_smooth.smoothers.smooth_matrix_kde(coords, matrix, *, grid_points=1024, bw=None, bw_factor=6.0, min_density_pct=1.0, workers=-1)[source]¶
Fine-grid Nadaraya-Watson smoothing via FFT-KDE (KDEpy).
Estimates the smoothed field on a regular
grid_points x grid_pointsgrid as the ratio of two FFT-accelerated kernel density estimates – one weighted by the field, one uniform – then bilinearly interpolates back to each input point. The FFT is what makes a fine grid cheap: cost isO(grid_points**2 log grid_points)regardless of how the points cluster.The estimator is affine, so negative values (z-scored signatures) are handled exactly by shifting the field non-negative for the KDE and shifting back afterwards.
- Parameters:
coords –
(n, 2)coordinates. Two-dimensional only – this is a tissue-plane smoother.matrix –
(n, g)field(s) to smooth.grid_points (int) – Grid resolution per axis.
bw (float | None) – Kernel bandwidth in coordinate units.
None->bw_factor * median_nn_distance.bw_factor (float) – Multiplier on the median nearest-neighbour distance when
bwis inferred.min_density_pct (float) – Grid cells whose uniform-KDE density falls below this percentile (of the positive densities) are treated as empty background: they are excluded from the Nadaraya-Watson ratio and then backfilled with the field’s median before interpolation, which stops NaNs bleeding inward from the tissue edge. Background therefore contributes the median, not nothing – it cannot invent structure, but it is not absent either.
workers (int)
- Returns:
(numpy.ndarray, float) – The
(n, g)smoothed matrix and the bandwidth used, in the caller’s coordinate units.
Notes
The estimate is computed in units of the median nearest-neighbour distance. KDEpy solves for its kernel’s practical support numerically, and that solve is not scale-free – it raises on a bandwidth of a few hundred microns even though such a bandwidth is perfectly ordinary on a tissue section. Rescaling makes the smoother genuinely invariant to the coordinate units.
A constant column is returned unchanged. Smoothing a constant field is the identity for any row-stochastic estimator, and the Nadaraya-Watson ratio cannot express it: KDEpy normalises its weights by their sum, so an all-zero weight vector (which a constant column becomes after the affine shift) divides by zero. A gene absent from the selected cells is routine, so this is a real input, not a pathological one.
- spatial_smooth.smoothers.smooth_field_kde(coords, values, **kwargs)[source]¶
1-D convenience wrapper around
smooth_matrix_kde().
Environment¶
- spatial_smooth.check_dependencies(verbose=True)[source]¶
Report which dependencies are importable and at which version.
Prints a table and returns
{package: version or None}. Call it at the top of a script or notebook to surface gaps before the analysis starts, each with the exactpip installline that fixes it.