Tutorial
========
.. _Tutorial:
This is a step-by-step tutorial for **TARDIS** analysis.
*If you are new to* **TARDIS**, *we recommend you to follow* `this tutorial `_.*
Also, if you are new to spatial perturbation analysis, we recommend you to read the following paper:
`Uncovering Spatially Resolved Functional Genomics with CRISPR Screen Sequencing `_.
.. admonition:: Reference
**SPAC-seq**, the spatial transcriptomics based CRISPR screening technique, enables the direct
linkage of genetic perturbation with spatially defined cellular microenvironments by
integrating sgRNA reads with spatial transcriptomic profiles.
**TARDIS** represents the first dedicated software package designed specifically for spatial CRISPR screen analysis.
Preparations
------------
Before everything, prepare your spatial CRISPR screen data.
.. note::
Although **TARDIS** is tested on multiple sequencing based spatial transcriptomics platforms,
including **BGI Stereo-seq**, **10X Genomics Visium**, and **10X Genomics Visium HD**,
and may not be limited to these platforms, it is still recommended to ensure that the data meets the following requirements:
- The data is spatially resolved. (e.g. Spatial barcodes are present)
- The data **CAN BE** from multiple tissues. (However, it is recommended to be preprocessed separately, and then combined into one AnnData object.)
- The data should contain **guide annotation** (e.g. Perturb-view) or **guide UMI count** matrix. (e.g. SPAC-seq)
TARDIS mainly integrates two forms of data to perform statistical analysis:
- **Guide Mapping Data**: Spatially resolved guide annotation or guide UMI count matrix.
- **Spatial Transcriptomics Data**: Spatial transcriptomics data.
In this tutorial, we will use the data from `Uncovering Spatially Resolved Functional Genomics with CRISPR Screen Sequencing `_.
The open-source data is available at `Official SPAC-seq data repository `_.
In this set of data, we use two sets of data:
- BGI Stereo-seq data of a MC38 tumor, T cell infiltration perturbation library. (Library on SPAC-seq data repo: 'Day7_rep1')
- Preprocessing and filtering
- Bottom-up niche independent ranking of guides
- 10X Genomics Visium HD of subcutaneous mouse MC38 tumor, metastatic tumor cell perturbation library. (Library on SPAC-seq data repo: 'Subq')
- Preprocessing and filtering
- Clone calling for tumor models
- Top-down niche dependent enrichment of guides
After downloading the data (or obtaining your own data), you can check on the data by loading the AnnData object.
.. tip::
Remember to move the data to the directory where you are running the code.
Jupyter notebook is recommended for this tutorial.
.. code-block:: ipython3
import tardis_spac as td
.. note::
Import **TARDIS** using python, you can utilize scanpy, squidpy, numpy, matplotlib, seaborn, and pandas.
scanpy and squidpy are required for spatial clustering analysis, numpy is required for numerical operations,
matplotlib and seaborn are required for visualization, and pandas is required for data manipulation.
Infiltrated T cell library
--------------------------
In this section, we will perform **TARDIS** analysis on the infiltrated T cell library.
This data is from **BGI Stereo-seq** platform.
Basic information of the data:
- The sample is sliced from a MC38 tumor, with perturbation of T cell injected, disected and sequenced on day 7 of tumor growth.
- The data contains 68 guides, with each perturbation of gene 2 different guide, 2 guides for non-targeting control.
**TARDIS** aims to pinpoint guides that have significant spatial difference of guide to *non-targeting control* guide, which reflects functional effect of the perturbed gene.
Loading and preprocessing
~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block::
import tardis_spac as td
fdata = td.utils.load_data('Day7_rep1.guide.gem', bin_size=100)
.. parsed-literal::
AnnData object with n_obs × n_vars = 68003 × 68
obsm: 'spatial'
.. note::
The AnnData object contains the tissue Spatial Transcriptomics data and guide-targeting data.
The `obsm['spatial']` is the spatial coordinates of the data, which is used for spatial clustering analysis.
The `var_names` is the variable names of the data, which is used for guide distribution analysis.
The `obs_names` is the observation names of the data, which is used to contain the bin information
More information about the AnnData object can be found at [here](https://scanpy.readthedocs.io/en/stable/api/scanpy.AnnData.html).
In poly-A based SPAC-seq, guide count matrix is stored in 'gem' file.
A gem file is a table file derived from the 'gef' file, which is the output of the **BGI SAW** software. (See `here `_)
A gem file is a tab-separated file with the following columns:
- `index`: the identical index of the guide detection
- `guide`: the guide name
- `x`: the x coordinate of the guide detection
- `y`: the y coordinate of the guide detection
- `MIDCount`: the guide count
- `ExonCount`: the detected exon count, usually same to `MIDCount`
We can read the gem file using pandas.
.. note::
`gem` file can be directly read by our module `td.utils.load_bin()`
however, we will show you how to read the file using pandas for preprocessing.
Filtering and Quality Control
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Spatial perturbation can be highly arbitrary if we cannot perform valid
preprocessing and filtering of low quality guides and bins. Refer to `Uncovering Spatially Resolved Functional Genomics with CRISPR Screen Sequencing `_
for difference between filtered and unfiltered guide distribution.
**TARDIS** performs filtering with validation panels with the following methods.
.. code-block::
# perform quality check from BGI stereo-seq GEM output
td.preprocess.filter_qc_bins('Day7_rep1.guide.gem')
.. image:: ../_images/qc_guide_bins.png
:align: center
The function processes a GEM file containing guide reads and performs filtering based on the specified parameters:
1. Reads the GEM file and optionally filters for guides with a specific prefix
2. Removes bins with guide counts below the threshold if specified
3. Handles bins with multiple guides according to the assign_pattern:
- 'max': Keeps only the guide with highest count in each bin
- 'drop': Removes all bins that have multiple guides
- 'all': Keeps all guides in multi-guide bins
4. Optionally binarizes the counts (sets all to 1)
5. Returns filtered DataFrame or saves to file
.. code:: ipython3
import scanpy as sc
import anndata as ad
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
from scipy import sparse
from tqdm import tqdm
.. note::
Above is a common practice to filter and QC the guide from *GEM* file.
Here we read in the guide and RNA data from the h5ad file, which can be downloaded from the SPAC-seq data repository.
.. code:: ipython3
guide_adata = sc.read_h5ad('/home/wpy/stereoseq/tutorial/DATA/Day7_rep1.guide.h5')
rna_adata = sc.read_h5ad('/home/wpy/stereoseq/tutorial/DATA/Day7_rep1.h5')
.. code:: ipython3
rna_adata
.. parsed-literal::
AnnData object with n_obs × n_vars = 567178 × 13177
obs: 'marker', 'n_genes'
var: 'mt', 'mt-', 'gm', 'Rb', 'rik', 'n_cells'
obsm: 'spatial'
.. code:: ipython3
guide_adata
.. parsed-literal::
AnnData object with n_obs × n_vars = 568003 × 34
obs: 'marker'
obsm: 'spatial'
.. code:: ipython3
guide_adata.obsm['spatial'] = np.concat([np.zeros((guide_adata.obsm['spatial'].shape[0], 1)), guide_adata.obsm['spatial'][:, 1].reshape(-1, 1), guide_adata.obsm['spatial'][:, 0].reshape(-1, 1)], axis=1)
.. code:: ipython3
filtered_guide_adata = filter_guide_reads_h5(
guide_adata,
guide_prefix='sg',
binarilize=True,
assign_pattern='max',
filter_threshold=1,
)
.. code:: ipython3
_, ax = plt.subplots(figsize=(5, 5))
td.utils.plot_spatial_guides(filtered_guide_adata, scale_factor=1, s=3, ax=ax)
ax.invert_yaxis()
plt.show()
.. image:: ../_images/tutorial_t_6_0.png
.. code:: ipython3
filtered_guide_adata.var['n_total_counts'] = filtered_guide_adata.X.toarray().sum(axis=0)
.. code:: ipython3
sc.pl.highest_expr_genes(filtered_guide_adata, n_top=20, show=True)
.. image:: ../_images/tutorial_t_8_0.png
A robust CRISPR screening should have a good distribution of the guides with high expression.
.. note::
For robust ranking of spatially specific guides, an appropriate guide filter is essential.
Based on data quality and cell type, we recommend a filter threshold of 200 for poly-A based SPAC-seq on T cells, as an example.
.. code:: ipython3
td.utils.plot_guide_gene_summary(filtered_guide_adata)
.. image:: ../_images/tutorial_t_9_0.png
.. code:: ipython3
filtered_guide_adata = td.utils.combine_guide_replicates(filtered_guide_adata)
We can visualize the spatial distribution of the guides with high expression using :py:func:`td.utils.plot_spatial_guides()`.
Here we visualize the spatial distribution of the guides 'sgZc3h12a' and 'sgnon-targeting'.
.. code:: ipython3
_, axs = plt.subplots(1, 2, figsize=(11, 5))
td.utils.plot_spatial_guides(filtered_guide_adata[filtered_guide_adata[:, 'sgZc3h12a'].X > 0], scale_factor=1, s=3, ax=axs[0])
axs[0].set_title('sgZc3h12a')
axs[0].invert_yaxis()
td.utils.plot_spatial_guides(filtered_guide_adata[filtered_guide_adata[:, 'sgnon-targeting'].X > 0], scale_factor=1, s=3, ax=axs[1])
axs[1].set_title('sgnon-targeting')
axs[1].invert_yaxis()
plt.show()
.. image:: ../_images/tutorial_t_11_0.png
Kullback-Leibler divergence test
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In cluster independent analysis, we perform KL divergence test to determine the guide specificity compared to non-targeting guide.
Cluster independent means that we would like to know the guide specificity compared to wild type T cells.
.. note::
KL Distance Ranking is generally a method to model distribution of guides that have low spatial resolution or the spatial encoding is not the essential feature.
As KL Distance Ranking dicards the spatial relationship between locations.
.. code:: ipython3
td.stats.kl_divergence(
filtered_guide_adata,
reference_guide='sgnon-targeting',
result_field='kl_div',
n_permutations=10000
)
.. parsed-literal::
/home/wpy/miniconda3/envs/tardis/lib/python3.14/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
from .autonotebook import tqdm as notebook_tqdm
KL divergence permutations: 100%|██████████| 32/32 [01:00<00:00, 1.90s/it]
.. code:: ipython3
td.utils.plot_top_kde(
filtered_guide_adata,
result_field='kl_div',
sgnt_label='sgnon-targeting',
top_n=2,
)
.. image:: ../_images/tutorial_t_13_0.png
We can check the distribution of the guides with high KL distance.
This function :py:func:`plot_ranking_scatter()` is a simple function to plot the KL divergence test result using scatter plot
to demonstrate the distribution of the guides with high KL distance.
.. code:: ipython3
td.utils.plot_ranking_scatter(filtered_guide_adata, 'sgnon-targeting', result_field='kl_div')
.. image:: ../_images/tutorial_t_14_0.png
All KL distance results are stored in the :py:attr:`adata.var` attribute named 'kl.div' by default.
.. warning::
KL divergence test requires reference guide. Make sure to set the reference guide correctly using the `reference_guide` parameter.
The reference guide can be set to 'sum' or 'ntc' (non-targeting control guide).
Perturbed subcutaneous tumor model
------------------------------------
In this section, we will perform perturbed subcutaneous tumor model analysis.
We will use the perturbed subcutaneous tumor model data.
.. note::
In this part, we **DID NOT** perform *Cellcharter* analysis for spaitally awared clustering.
Rather, we used *graphclust* clustering from Spaceranger output.
.. code:: ipython3
import tardis_spac as td
import scanpy as sc
import anndata as ad
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
from scipy import sparse
from tqdm import tqdm
The following h5 files can be downloaded from above link.
.. code:: ipython3
guide_adata = sc.read_h5ad('filtered_guide_bc_matrix.h5')
rna_adata = sc.read_h5ad('filtered_gene_bc_matrix.h5')
.. parsed-literal::
/path/to/anndata/_core/anndata.py:1884: UserWarning: Variable names are not unique. To make them unique, call `.var_names_make_unique`.
utils.warn_names_duplicates("var")
Make sure to make the variable names unique.
.. code:: ipython3
rna_adata.var_names_make_unique()
.. code:: ipython3
rna_adata
.. parsed-literal::
AnnData object with n_obs × n_vars = 632032 × 19059
obs: 'in_tissue', 'array_row', 'array_col'
var: 'gene_ids', 'feature_types', 'genome'
obsm: 'spatial'
.. code:: ipython3
guide_adata
.. parsed-literal::
AnnData object with n_obs × n_vars = 632032 × 1520
obs: 'in_tissue', 'array_row', 'array_col'
var: 'gene_ids', 'feature_types', 'genome'
obsm: 'spatial'
**TARDIS** provides a function :py:func:`td.utils.plot.plot_spatial_guides()` to plot the spatial guides.
.. note::
*Image* can be provided together with scale_factor to plot the spatial guides on the HE for reference.
.. code:: ipython3
_, ax = plt.subplots(figsize=(5, 5))
td.utils.plot.plot_spatial_guides(guide_adata, scale_factor=scalefactors, s=1, ax=ax)
plt.show()
.. image:: ../_images/tutorial_tumor_5_0.png
General quanlity control
~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code:: ipython3
guide_adata.var['n_total_counts'] = guide_adata.X.toarray().sum(axis=0)
.. code:: ipython3
sc.pl.highest_expr_genes(guide_adata, n_top=20, show=True)
.. parsed-literal::
/tmp/ipykernel_39694/2812346091.py:1: UserWarning: Some cells have zero counts
sc.pl.highest_expr_genes(guide_adata, n_top=20, show=True)
.. image:: ../_images/tutorial_tumor_7_1.png
A robust CRISPR screening should have a good distribution of the guides with high expression.
.. code:: ipython3
td.utils.plot_guide_gene_summary(guide_adata)
.. image:: ../_images/tutorial_tumor_8_0.png
Clone calling for tumor Models
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In tumor models, we would like to call the clones from the spatial guides.
To identify clonal perturbations that locally expanded in the metastatic screen
(defined as spatially proximal tumor bins with the same perturbation),
ensity-based spatial clustering of applications with noise (DBSCAN) and kernel density estimation (KDE) were performed for each perturbation within each tissue section.
**TARDIS** provides a function :py:func:`td.utils.dbscan_density_region()` to perform dbscan density region clustering.
.. note::
For recovery of larget clones, try raising the *eps* parameter.
For recovery of small clones, try lowering the *min_samples* parameter.
Remember to tune *eps* according to the spatial resolution of the platform.
For example, for 10x Visium data, *eps* should be set to 5-10. (*~8 pixels per 2um bin*)
.. code:: ipython3
perturb_data, perturb_points_df = td.utils.dbscan_density_region(
guide_adata,
"sgBcam_2",
eps=20,
min_samples=10,
label="0",
mode="most",
density_level=7,
)
.. parsed-literal::
Number of Clusters (excluding noise): 25
We can visualize the clone calling result.
.. code:: ipython3
_, ax = plt.subplots(figsize=(5, 5))
sns.scatterplot(
data=perturb_points_df,
x="pxl_row_in_fullres",
y="pxl_col_in_fullres",
s=2,
edgecolor="none",
ax=ax,
)
plt.axis('off')
plt.show()
.. image:: ../_images/tutorial_tumor_11_0.png
Here we call top 50 guides with high expression.
.. code:: ipython3
top_clones = guide_adata.var['n_total_counts'].nlargest(50).index.tolist()
perturb_points_merge_df = pd.DataFrame()
perturb_data_list = []
for clone in tqdm(top_clones):
perturb_data, perturb_points_df = td.utils.dbscan_density_region(
guide_adata,
clone,
eps=20,
min_samples=10,
label="0",
mode="most",
density_level=7,
)
perturb_data = perturb_data[perturb_data.obs['dbscan_cluster'] != '-1'].copy()
perturb_data_list.append(perturb_data)
perturb_points_df['clone'] = clone
perturb_points_merge_df = pd.concat([perturb_points_merge_df, perturb_points_df])
perturb_points_merge_df.groupby('clone').size().sort_values(ascending=False)
.. parsed-literal::
100%|██████████| 50/50 [02:54<00:00, 3.49s/it]
.. parsed-literal::
clone
sgBcam_2 26779
sgApp_1 12063
sgCks1b_2 11448
sgTff3_1 10704
...
sgAnk_1 1284
dtype: int64
Visualize the clone calling result for top 50 guides with high expression.
.. code:: ipython3
_, ax = plt.subplots(figsize=(5, 5))
sns.scatterplot(
data=perturb_points_merge_df,
x="pxl_row_in_fullres",
y="pxl_col_in_fullres",
hue="clone",
palette="gist_ncar",
s=0.2,
edgecolor="none",
ax=ax,
legend=False,
)
plt.axis('off')
plt.show()
.. image:: ../_images/tutorial_tumor_13_0.png
Niche specific analysis
~~~~~~~~~~~~~~~~~~~~~~~~~
A tumor clone can be identified to be specificly distributed in a particular niche.
.. code:: ipython3
rna_adata.obs['graphclust'] = pd.read_csv('/path/to/spaceranger/output/outs/binned_outputs/square_008um/analysis/clustering/gene_expression_graphclust/clusters.csv', index_col=0)['Cluster'].astype(str)
.. code:: ipython3
_, ax = plt.subplots(figsize=(5, 5))
sns.scatterplot(
data=rna_adata.obs,
x='array_col',
y='array_row',
hue='graphclust',
palette='tab20b',
s=0.2,
edgecolor="none",
ax=ax,
legend=False,
)
plt.axis('off')
plt.show()
.. image:: ../_images/tutorial_tumor_15_0.png
Define the niche by differential gene expression analysis.
.. code:: ipython3
sc.pp.normalize_total(rna_adata, inplace=True, target_sum=1e4)
sc.pp.log1p(rna_adata)
sc.tl.rank_genes_groups(rna_adata, 'graphclust', method='t-test')
sc.pl.rank_genes_groups(rna_adata, n_genes=25, sharey=False)
.. image:: ../_images/tutorial_tumor_16_0.png
.. code:: ipython3
guide_adata.obs['graphclust'] = rna_adata.obs['graphclust']
.. code:: ipython3
perturb_data_merge = sc.concat(perturb_data_list)
perturb_data_merge = guide_adata[perturb_data_merge.obs_names.unique()].copy()
.. parsed-literal::
/path/to/anndata/_core/anndata.py:1882: UserWarning: Observation names are not unique. To make them unique, call `.obs_names_make_unique`.
utils.warn_names_duplicates("obs")
Create a pseudo-guide, that is the sum of all the guides in the clone.
.. warning::
This is a very simple way to create a pseudo-guide, and may not be very accurate.
Due to limited detection of 'sgNontargeting' guide, we performed this simple method to create a pseudo-guide.
It is recommended to use the 'sgNontargeting' guide for reference if detected.
.. code:: ipython3
if hasattr(perturb_data_merge.X, "toarray"):
bin_sums = np.ravel(perturb_data_merge.X.sum(axis=1))
else:
bin_sums = np.ravel(np.sum(perturb_data_merge.X, axis=1))
pseudo_adata = ad.AnnData(
X=bin_sums.reshape(-1, 1),
obs=perturb_data_merge.obs.copy(),
var=pd.DataFrame(index=['sgPseudo'])
)
.. code:: ipython3
pseudo_adata = sc.concat([pseudo_adata, perturb_data_merge], axis=1)
.. code:: ipython3
pseudo_adata.obs['graphclust'] = rna_adata.obs['graphclust']
Calculate the Aitchison distance between the reference guide and the guides in the clone.
See :py:func:`td.stats.aitchison_distance()` for more details.
.. code:: ipython3
td.stats.aitchison_distance(
pseudo_adata,
'graphclust',
result_field='aitchison_dist',
reference_guide='sgPseudo',
n_permutations=None,
)
.. code:: ipython3
td.utils.plot.plot_top_kde(
pseudo_adata,
result_field='aitchison_dist',
sgnt_label='sgPseudo',
top_n=2,
)
.. image:: ../_images/tutorial_tumor_23_0.png
**TARDIS** provides permutation test to calculate the p-value of the Aitchison distance.
.. note::
The *p_swap* parameter is the probability of swapping the guide labels.
It is recommended to set to 0.5 for balanced data.
For unbalanced data, it is recommended to set to 0.1-0.3.
.. code:: ipython3
filtered_guide_data = pseudo_adata[:, pseudo_adata.var_names.isin(top_clones + ['sgPseudo']).tolist()].copy()
filtered_guide_data.X = filtered_guide_data.X.astype(np.int64)
td.stats.aitchison_distance(
filtered_guide_data,
'graphclust',
result_field='aitchison_dist',
reference_guide='sgPseudo',
p_swap=0.5,
n_permutations=10000,
)
.. parsed-literal::
sgArf6_1: 100%|██████████| 10000/10000 [00:07<00:00, 1251.27it/s]
sgRab8a_1: 100%|██████████| 10000/10000 [00:08<00:00, 1247.24it/s]
sgCks1b_2: 100%|██████████| 10000/10000 [00:08<00:00, 1246.28it/s]
sgAgr3_2: 100%|██████████| 10000/10000 [00:08<00:00, 1246.10it/s]
...
sgTff3_1: 100%|██████████| 10000/10000 [00:08<00:00, 1247.87it/s]
Visualize the Aitchison distance scatter plot.
.. code:: ipython3
td.utils.plot_aitchison_dist_scatter(filtered_guide_data, 'sgPseudo')
.. image:: ../_images/tutorial_tumor_25_0.png
.. [1] He, P., Williams, B.A., Trout, D. et al. The changing mouse embryo transcriptome at whole tissue and single-cell resolution. Nature 583, 760–767 (2020).