Skip to content

Repository files navigation

SpatialDE

SpatialDE is a method to identify genes which significantly depend on spatial coordinates in non-linear and non-parametric ways. The intended applications are spatially resolved RNA-sequencing from e.g. Spatial Transcriptomics, or in situ gene expression measurements from e.g. SeqFISH or MERFISH.

Additionally, SpatialDE provides automatic expression histology, a method that groups genes into common spatial patterns (and conversely reveal histological patterns based on gene coexpression).

This repository contains both the implementations of our methods, as well as case studies in applying it.

The key features of our method are

  • Unsupervised - No need to define spatial regions
  • Non-parametric and non-linear expression patterns
  • Automatic histology based on spatially coexpressed genes
  • Extremely fast - Transcriptome wide tests takes only a few minutes on normal computers

The primary implementation is as a Python 3 package, and can be installed from the command line by

$ pip install spatialde

(This should only take a minute or so on a typical system)

To see usage example of SpatialDE either keep reading, or look in the Analysis directory. The following examples are provided:

BreastCancer
Transcriptome wide study on breast cancer tissue from Spatial Transcriptomics.
Frog
A time course of RNA-seq ("1-d space") of Xenopus development.
MERFISH
Expression from single cells in a region of an osteoblast culture using the MERFISH technology with 140 probes.
MouseOB
Spatial Transcriptomics assay of a slice of Mouse Olfactory Bulb. (Also see below)
SeqFISH
Expression counts of single cells from mouse hippocampus using the SeqFISH technology with 249 probes.

If you wish to look at the data used or run the notebooks and scripts from start to finish, the data needs to be fetched using git lfs, a plugin to git for managing large files. Installation instructions are available on the projects website. Once git lfs is installed and you have cloned this repository, data can be downloaded by running git lfs pull from inside any repository directory.

If you are having problems using git lfs, all the data are available on figshare here: https://figshare.com/articles/software/SpatialDE/17065217

Below follows a typical usage example in interactive form.

SpatialDE significance test example use

%pylabinlineimportpandasaspdrcParams['axes.spines.right'] =FalsercParams['axes.spines.top'] =FalseimportNaiveDEimportSpatialDE
Populating the interactive namespace from numpy and matplotlib

As an example, let us look at spatially dependent gene expression in Mouse Olfactory Bulb using a data set published in Stahl et al 2016. With the authors method, hundreds of locations on a tissue slice can be sampled at once, and gene expression is measured by sequencing in an unbiased whole-transcriptome manner.

counts=pd.read_csv('Analysis/MouseOB/data/Rep11_MOB_0.csv', index_col=0)
counts=counts.T[counts.sum(0) >=3].T# Filter practically unobserved genesprint(counts.shape)
counts.iloc[:5, :5]
(262, 14859)
Nrf1Zbtb5Ccnl1Lrrfip1Bbs1
16.92x9.01511121
16.945x11.07500322
16.97x10.11801100
16.939x12.13210104
16.949x13.05500030
sample_info=pd.read_csv('Analysis/MouseOB/MOB_sample_info.csv', index_col=0)
counts=counts.loc[sample_info.index] # Align count matrix with metadata tablesample_info.head(5)
.dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; }
xytotal_counts
16.92x9.01516.9209.01518790
16.945x11.07516.94511.07536990
16.97x10.11816.97010.11812471
16.939x12.13216.93912.13222703
16.949x13.05516.94913.05518641

We can plot the x and y coordinates in the sample info table to see which locations of the tissue slice has been sampled.

figsize(6, 4)
plt.scatter(sample_info['x'], sample_info['y'], c='k');
plt.axis('equal');

README_files/README_7_0.png

Our method assumes normally distributed noise, but the data we are using is from expression counts, and empirically seems to follow a negative binomial distribution. We use technique by Anscombe to approximately transform the data to normal distributed noise.

Secondly, library size or sequencing depth of the spatial samples will bias the expression of every gene. We use linear regression to account for this effect before performing the spatial test.

norm_expr=NaiveDE.stabilize(counts.T).Tresid_expr=NaiveDE.regress_out(sample_info, norm_expr.T, 'np.log(total_counts)').T

For the sake of this example, let's just run the test on 1000 random genes. This should just take a few seconds. With our very fast implementation, testing all 14,000 genes takes about 10 minutes.

sample_resid_expr=resid_expr.sample(n=1000, axis=1, random_state=1)
X=sample_info[['x', 'y']]
results=SpatialDE.run(X, sample_resid_expr)
INFO:root:Performing DE test
INFO:root:Pre-calculating USU^T = K's ...
INFO:root:Done: 0.11s
INFO:root:Fitting gene models
INFO:root:Model 1 of 10
INFO:root:Model 2 of 10
INFO:root:Model 3 of 10
INFO:root:Model 4 of 10
INFO:root:Model 5 of 10
INFO:root:Model 6 of 10
INFO:root:Model 7 of 10
INFO:root:Model 8 of 10
INFO:root:Model 9 of 10
INFO:root:Model 10 of 10

The result will be a DataFrame with P-values and other relevant values for each gene.

The most important columns are

  • g - The name of the gene
  • pval - The P-value for spatial differential expression
  • qval - Significance after correcting for multiple testing
  • l - A parameter indicating the distance scale a gene changes expression over
results.head().T
.dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; }
01234
FSV0.9999552.0597e-092.0597e-092.0597e-092.0597e-09
M44444
g2410016O06RikArpp19Srsf7Wbp7Cpsf3l
l0.4020010.4020010.4020010.4020010.402001
max_delta4.53999e-054.85165e+084.85165e+084.85165e+084.85165e+08
max_ll-52.2589-107.685-114.477-112.664-49.1672
max_mu_hat-0.826851-2.21845-6.67811-2.250440.146089
max_s2_t_hat0.6669851.04203e-089.22126e-081.07257e-082.20142e-10
modelSESESESESE
n260260260260260
s2_FSV1.943420.25378847.29450.3633884.48293
s2_logdelta6.81931e+084.3315e+168.07194e+186.20209e+167.65119e+17
time0.001341820.001044990.0009949210.0009999280.00106692
BIC126.761237.613251.196247.571120.577
max_ll_null-53.706-107.686-114.478-112.665-49.1681
LLR1.447150.0009640070.0009640110.0009640070.00096401
pval0.2289860.9752310.9752310.9752310.975231
qval0.9752310.9752310.9752310.9752310.975231
results.sort_values('qval').head(10)[['g', 'l', 'qval']]
.dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; }
glqval
890Kcnh31.9076090.001512
772Pcp41.1351900.013843
736Igfbp21.1351900.013843
800Gng131.9076090.022632
646Naaa0.6755350.051705
749Map1b1.1351900.051705
826Gng41.9076090.051705
724Fmo11.1351900.096710
714Slc38a31.1351900.096710
712Hpcal41.1351900.107360

We detected a few spatially differentially expressed genes, Cck and Ptn for example.

A simple way to visualize these genes is by plotting the x and y coordinates as above, but letting the color correspond to expression level.

figsize(10, 3)
fori, ginenumerate(['Kcnh3', 'Pcp4', 'Igfbp2']):
plt.subplot(1, 3, i+1)
plt.scatter(sample_info['x'], sample_info['y'], c=norm_expr[g]);
plt.title(g)
plt.axis('equal')
plt.colorbar(ticks=[]);

README_files/README_16_0.png

For reference, we can compare these to genes which are not spatially DE

results.sort_values('qval').tail(10)[['g', 'l', 'qval']]
.dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; }
glqval
334Tmem700.4020010.975231
335Rnf200.4020010.975231
336Zfp85-rs10.4020010.975231
337C1qtnf70.4020010.975231
338Ap4b10.4020010.975231
339Psma40.4020010.975231
340Aldh3b10.4020010.975231
341Hdx0.4020010.975231
328Zfp1130.4020010.975231
999Preb9.0521380.975231
figsize(10, 3)
fori, ginenumerate(['Myo9b', 'Sc4mol', 'Phf11b']):
plt.subplot(1, 3, i+1)
plt.scatter(sample_info['x'], sample_info['y'], c=norm_expr[g]);
plt.title(g)
plt.axis('equal')
plt.colorbar(ticks=[]);

README_files/README_19_0.png

In regular differential expression analysis, we usually investigate the relation between significance and effect size by so called volcano plots. We don't have the concept of fold change in our case, but we can investigate the fraction of variance explained by spatial variation.

figsize(5, 4)
plt.yscale('log')
plt.scatter(results['FSV'], results['qval'], c='black')
plt.axhline(0.05, c='black', lw=1, ls='--');
plt.gca().invert_yaxis();
plt.xlabel('Fraction spatial variance')
plt.ylabel('Adj. P-value');

README_files/README_21_0.png

Automatic expression histology

To perform automatic expression histology (AEH), the genes should be filtered by SpatialDE significance. For this example, let us use a very weak threshold. But in typical use, filter by qval < 0.05

sign_results=results.query('qval < 0.05')

AEH requires two parameters: the number of patterns, and the characteristic lengthscale for histological patterns.

For some guidance in picking the lengthscale l we can look at the optimal lengthscale for the signficant genes.

sign_results['l'].value_counts()
1.135190 11
1.907609 4
0.675535 4
3.205604 1
Name: l, dtype: int64

Here we see that the lengthscale on average is ~1.5, to use some extra spatial covariance, we put this paramater to l = 1.8.

For the number of patterns, we try C = 3.

histology_results, patterns=SpatialDE.aeh.spatial_patterns(X, resid_expr, sign_results, C=3, l=1.8, verbosity=1)
iter 0, ELBO: -9.48e+08
iter 1, ELBO: -4.20e+08, delta_ELBO: 5.28e+08
iter 2, ELBO: -4.20e+08, delta_ELBO: 7.63e+02
iter 3, ELBO: -4.20e+08, delta_ELBO: 2.07e+02
iter 4, ELBO: -4.20e+08, delta_ELBO: 8.03e+01
iter 5, ELBO: -4.20e+08, delta_ELBO: 3.40e+00
iter 6, ELBO: -4.20e+08, delta_ELBO: 6.62e-02
iter 7, ELBO: -4.20e+08, delta_ELBO: 2.75e-03
iter 8, ELBO: -4.20e+08, delta_ELBO: 3.96e-03
iter 9, ELBO: -4.20e+08, delta_ELBO: 7.49e-05
Converged on iter 9

After fitting the AEH model, the function returns two DataFrames, one with pattern membership information for each gene:

histology_results.head()
.dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; }
gmembershippattern
564AI5934421.01
619Arhgef91.01
6326330403K07Rik1.01
646Naaa1.00
712Hpcal41.02

And one with realizations for the underlying expression for each histological pattern.

We can visualize this underlying expression in the tissue context as we would for any individual gene.

figsize(10, 3)
foriinrange(3):
plt.subplot(1, 3, i+1)
plt.scatter(sample_info['x'], sample_info['y'], c=patterns[i]);
plt.axis('equal')
plt.title('Pattern {} - {} genes'.format(i, histology_results.query('pattern == @i').shape[0] ))
plt.colorbar(ticks=[]);

README_files/README_31_0.png

It is usually interesting to see what the coexpressed genes determining a histological pattern are:

foriinhistology_results.sort_values('pattern').pattern.unique():
print('Pattern {}'.format(i))
print(', '.join(histology_results.query('pattern == @i').sort_values('membership')['g'].tolist()))
print()
Pattern 0
Naaa, Aebp1, Mfap3l, Fmo1, 2810002D19Rik, Gng13
Pattern 1
Map2, Arhgef9, AI593442, 6330403K07Rik, Slc38a3, Igfbp2, Nmb, Map1b
Pattern 2
Hpcal4, Snap25, Pcp4, Gng4, Ppfia2, Kcnh3

About

Test genes for Spatial Variation

Resources

Stars

189 stars

Watchers

11 watching

Forks

Releases

Packages

Used by

Contributors

Languages