Upstream Steps
This Notebook
Metacell calculation by SEACells (following this tutorial)
import os
import sys
import numpy as np
import pandas as pd
import scanpy as sc
import pickle
#Plotting
import matplotlib.pyplot as plt
import matplotlib
import seaborn as sns
#utils
import ipynbname
from datetime import datetime
# SeaCell
import SEACells
#import custom functions
sys.path.append('../')
import functions as fn
findfont: Font family ['Raleway'] not found. Falling back to DejaVu Sans. findfont: Font family ['Lato'] not found. Falling back to DejaVu Sans.
# Some plotting aesthetics
%matplotlib inline
sns.set_style('ticks')
matplotlib.rcParams['figure.figsize'] = [3.5, 3.5]
matplotlib.rcParams['figure.dpi'] = 100
print("Scanpy version: ", sc.__version__)
print("Pandas version: ", pd.__version__)
print("SEACell version: ", SEACells.__version__)
Scanpy version: 1.9.3 Pandas version: 2.0.0 SEACell version: 0.3.3
sc.settings.verbosity = 3
sc.settings.set_figure_params(dpi=80)
input_file = '../../../../DataDir/ExternalData/SingleCellData/Castaldi_adata.h5ad'
print(datetime.now())
2026-03-05 18:11:47.822083
adata = sc.read(input_file)
adata
AnnData object with n_obs × n_vars = 14913 × 33538
obs: 'dataset', 'cellID', 'cellID_newName', 'n_genes_by_counts', 'log1p_n_genes_by_counts', 'total_counts', 'log1p_total_counts', 'total_counts_mt', 'log1p_total_counts_mt', 'pct_counts_mt', 'total_counts_ribo', 'log1p_total_counts_ribo', 'pct_counts_ribo', 'stage', 'type', 'id_stage', 'cellID_newName_type', 'S_score', 'G2M_score', 'phase', 'leidenAnnotated', 'leiden_1.2', 'endpoint_GlutamatergicNeurons_late', 'endpoint_GlutamatergicNeurons_early', 'endpoint_MigratingNeurons', 'endpoint_OuterRadialGliaAstrocytes', 'endpoint_Interneurons', 'endpoint_Interneurons_GAD2', 'endpoint_CajalR_like', 'Exc_Lineage', 'endpoint_GlutamatergicNeurons_both'
var: 'highly_variable', 'means', 'dispersions', 'dispersions_norm'
uns: 'cellID_colors', 'cellID_newName_colors', 'cellID_newName_type_colors', 'dataset_colors', 'draw_graph', 'harmony', 'hvg', 'leidenAnnotated_colors', 'log1p', 'pca', 'stage_colors', 'type_colors', 'umap'
obsm: 'X_draw_graph_fa_harmony', 'X_pca', 'X_pca_harmony', 'X_umap_harmony', 'X_umap_nocorr'
varm: 'PCs'
layers: 'counts', 'lognorm'
obsp: 'harmony_connectivities', 'harmony_distances', 'pca_connectivities', 'pca_distances'
print(adata.X[40:45, 40:45])
(3, 3) 0.9110727
sc.pl.embedding(adata, basis="X_umap_harmony", color=['leidenAnnotated', 'dataset'], ncols=1)
/usr/local/lib/python3.8/dist-packages/scanpy/plotting/_tools/scatterplots.py:392: UserWarning: No data for colormapping provided via 'c'. Parameters 'cmap' will be ignored cax = scatter( /usr/local/lib/python3.8/dist-packages/scanpy/plotting/_tools/scatterplots.py:392: UserWarning: No data for colormapping provided via 'c'. Parameters 'cmap' will be ignored cax = scatter(
sc.pl.embedding(adata, basis="X_draw_graph_fa_harmony", color=['leidenAnnotated', 'dataset'], ncols=1)
/usr/local/lib/python3.8/dist-packages/scanpy/plotting/_tools/scatterplots.py:392: UserWarning: No data for colormapping provided via 'c'. Parameters 'cmap' will be ignored cax = scatter( /usr/local/lib/python3.8/dist-packages/scanpy/plotting/_tools/scatterplots.py:392: UserWarning: No data for colormapping provided via 'c'. Parameters 'cmap' will be ignored cax = scatter(
adata.obs.head(3)
| dataset | cellID | cellID_newName | n_genes_by_counts | log1p_n_genes_by_counts | total_counts | log1p_total_counts | total_counts_mt | log1p_total_counts_mt | pct_counts_mt | ... | leiden_1.2 | endpoint_GlutamatergicNeurons_late | endpoint_GlutamatergicNeurons_early | endpoint_MigratingNeurons | endpoint_OuterRadialGliaAstrocytes | endpoint_Interneurons | endpoint_Interneurons_GAD2 | endpoint_CajalR_like | Exc_Lineage | endpoint_GlutamatergicNeurons_both | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| AAACCTGAGAGACTAT-1_DownD250 | DownD250 | MIFF1 | CTL02A | 2586 | 7.858254 | 6358.0 | 8.757627 | 175.0 | 5.170484 | 2.752438 | ... | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | GlutamatergicNeurons_late | 1 |
| AAACCTGCATGGTTGT-1_DownD250 | DownD250 | KOLF | CTL08A | 1372 | 7.224753 | 2540.0 | 7.840313 | 73.0 | 4.304065 | 2.874016 | ... | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | GlutamatergicNeurons_late | 1 |
| AAACCTGTCAGTTAGC-1_DownD250 | DownD250 | 3391B | CTL01 | 1216 | 7.104144 | 2859.0 | 7.958577 | 43.0 | 3.784190 | 1.504022 | ... | 6 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | Other | 0 |
3 rows × 31 columns
Normalization, log-transformation, HVG and dimensionality reduction have been already performed.
We follow the workflow described in the SEACells tutorial
Parameters to be defined:
We then employ the SEACells.core.SEACells function to initialize the model.
## Core parameters
n_SEACells = round(adata.n_obs/60)
print(n_SEACells)
build_kernel_on = 'X_pca_harmony' # key in ad.obsm to use for computing metacells
# This would be replaced by 'X_svd' for ATAC data
## Additional parameters
n_waypoint_eigs = 10 # Number of eigenvalues to consider when initializing metacells
249
#help(SEACells.core.SEACells)
model = SEACells.core.SEACells(adata,
build_kernel_on=build_kernel_on,
n_SEACells=n_SEACells,
n_waypoint_eigs=n_waypoint_eigs,
convergence_epsilon = 1e-5)
Welcome to SEACells!
construct_kernel_matrix function constructs the kernel matrix from data matrix using PCA/SVD and nearest neighbors. Key parameters are:
model.construct_kernel_matrix(n_neighbors=20)
M = model.kernel_matrix
Computing kNN graph using scanpy NN ... computing neighbors
2026-03-05 18:18:07.034962: I tensorflow/core/util/port.cc:110] oneDNN custom operations are on. You may see slightly different numerical results due to floating-point round-off errors from different computation orders. To turn them off, set the environment variable `TF_ENABLE_ONEDNN_OPTS=0`. 2026-03-05 18:18:07.077195: I tensorflow/core/platform/cpu_feature_guard.cc:182] This TensorFlow binary is optimized to use available CPU instructions in performance-critical operations. To enable the following instructions: AVX2 AVX512F AVX512_VNNI FMA, in other operations, rebuild TensorFlow with the appropriate compiler flags. 2026-03-05 18:18:08.073650: W tensorflow/compiler/tf2tensorrt/utils/py_utils.cc:38] TF-TRT Warning: Could not find TensorRT
finished: added to `.uns['neighbors']`
`.obsp['distances']`, distances for each pair of neighbors
`.obsp['connectivities']`, weighted adjacency matrix (0:00:25)
Computing radius for adaptive bandwidth kernel...
0%| | 0/14913 [00:00<?, ?it/s]
Making graph symmetric... Parameter graph_construction = union being used to build KNN graph... Computing RBF kernel...
0%| | 0/14913 [00:00<?, ?it/s]
Building similarity LIL matrix...
0%| | 0/14913 [00:00<?, ?it/s]
Constructing CSR matrix...
# Initialize archetypes
model.initialize_archetypes()
Building kernel on X_pca_harmony
Computing diffusion components from X_pca_harmony for waypoint initialization ...
Determing nearest neighbor graph...
computing neighbors
finished: added to `.uns['neighbors']`
`.obsp['distances']`, distances for each pair of neighbors
`.obsp['connectivities']`, weighted adjacency matrix (0:00:04)
Done.
Sampling waypoints ...
Done.
Selecting 238 cells from waypoint initialization.
Initializing residual matrix using greedy column selection
Initializing f and g...
100%|██████████| 21/21 [00:00<00:00, 32.11it/s]
Selecting 11 cells from greedy initialization.
adata.obsm['X_umap'] = adata.obsm['X_umap_harmony'].copy()
# Plot the initilization to ensure they are spread across phenotypic space
SEACells.plot.plot_initialization(adata, model)
We apply the model.fit function.
model.fit(min_iter=10, max_iter=100)
model
Randomly initialized A matrix. Setting convergence threshold at 0.00239 Starting iteration 1. Completed iteration 1. Starting iteration 10. Completed iteration 10. Starting iteration 20. Completed iteration 20. Starting iteration 30. Completed iteration 30. Converged after 35 iterations.
<SEACells.core.SEACells at 0x15544874de50>
# Plot model convergence
model.plot_convergence()
#model.A_
plt.figure(figsize=(4,3))
sns.distplot((model.A_.T > 0.1).sum(axis=1), kde=False)
plt.title(f'Non-trivial (> 0.1) assignments per cell')
plt.xlabel('# Non-trivial SEACell Assignments')
plt.ylabel('# Cells')
plt.show()
plt.figure(figsize=(4,3))
b = np.partition(model.A_.T, -5)
sns.heatmap(np.sort(b[:,-5:])[:, ::-1], cmap='viridis', vmin=0)
plt.title('Strength of top 5 strongest assignments')
plt.xlabel('$n^{th}$ strongest assignment')
plt.show()
/tmp/ipykernel_2843413/2880491417.py:4: UserWarning: `distplot` is a deprecated function and will be removed in seaborn v0.14.0. Please adapt your code to use either `displot` (a figure-level function with similar flexibility) or `histplot` (an axes-level function for histograms). For a guide to updating your code to use the new functions, please see https://gist.github.com/mwaskom/de44147ed2974457ad6372750bbe5751 sns.distplot((model.A_.T > 0.1).sum(axis=1), kde=False)
labels,weights = model.get_soft_assignments()
#labels.head()
We will use hard assignment of each cell to a single metacell to define our metacells for downstream steps. Hard assingment can be retreived in two ways:
adata.obs[['SEACell']].head()
# Alternatively:
# model.get_hard_assignments().head()
| SEACell | |
|---|---|
| index | |
| AAACCTGAGAGACTAT-1_DownD250 | SEACell-186 |
| AAACCTGCATGGTTGT-1_DownD250 | SEACell-28 |
| AAACCTGTCAGTTAGC-1_DownD250 | SEACell-109 |
| AAACGGGCAGCTATTG-1_DownD250 | SEACell-203 |
| AAACGGGGTGCACGAA-1_DownD250 | SEACell-121 |
# By default, ad.raw is used for summarization. Other layers present in the anndata can be specified using the parameter summarize_layer parameter
SEA_adata = SEACells.core.summarize_by_SEACell(adata, SEACells_label='SEACell', summarize_layer='counts', celltype_label='leidenAnnotated')
SEA_adata
100%|██████████| 249/249 [00:02<00:00, 95.46it/s]
AnnData object with n_obs × n_vars = 249 × 33538
obs: 'leidenAnnotated', 'leidenAnnotated_purity'
layers: 'raw'
SEA_adata.obs.head()
| leidenAnnotated | leidenAnnotated_purity | |
|---|---|---|
| SEACell-186 | GlutamatergicNeurons_late | 0.934066 |
| SEACell-28 | GlutamatergicNeurons_late | 0.864865 |
| SEACell-109 | Interneurons | 0.795181 |
| SEACell-203 | MigratingNeurons | 0.818182 |
| SEACell-121 | RadialGliaProgenitors | 0.980392 |
print(SEA_adata.X[40:43, 40:43])
(0, 0) 6.0 (1, 0) 6.0 (2, 0) 11.0
plot_2D(): visualize metacell assignments on UMAP or any 2-dimensional embedding froma adata.obsm. Plots can also be coloured by metacell assignment. The visualization allows to assess the representativeness of the calculated metacells: ability to represent the global structure of the original dataset. Better representation corresponds to a more uniform coverage of the dataset.
plot_SEACell_sizes(): distribution of number of cells assigned to each metacell. Given to the non-uniform distribution of cell type density, metacells are expected to vary in size: larger metacells are retrieved from denser regions, and smaller metacells from sparser ones. However outlier values may require further inspection.
SEACells.plot.plot_2D(adata, key='X_umap', colour_metacells=False)
/usr/local/lib/python3.8/dist-packages/seaborn/relational.py:573: UserWarning: No data for colormapping provided via 'c'. Parameters 'cmap' will be ignored points = ax.scatter(x=x, y=y, **kws) /usr/local/lib/python3.8/dist-packages/seaborn/relational.py:573: UserWarning: No data for colormapping provided via 'c'. Parameters 'cmap' will be ignored points = ax.scatter(x=x, y=y, **kws)
SEACells.plot.plot_2D(adata, key='X_umap', colour_metacells=True)
/usr/local/lib/python3.8/dist-packages/seaborn/relational.py:573: UserWarning: No data for colormapping provided via 'c'. Parameters 'cmap' will be ignored points = ax.scatter(x=x, y=y, **kws) /usr/local/lib/python3.8/dist-packages/seaborn/relational.py:573: UserWarning: No data for colormapping provided via 'c'. Parameters 'cmap' will be ignored points = ax.scatter(x=x, y=y, **kws)
SEACells.plot.plot_SEACell_sizes(adata, bins=10, figsize=(10, 5))
/usr/local/lib/python3.8/dist-packages/SEACells/plot.py:121: UserWarning:
`distplot` is a deprecated function and will be removed in seaborn v0.14.0.
Please adapt your code to use either `displot` (a figure-level function with
similar flexibility) or `histplot` (an axes-level function for histograms).
For a guide to updating your code to use the new functions, please see
https://gist.github.com/mwaskom/de44147ed2974457ad6372750bbe5751
sns.distplot(label_df.groupby('SEACell').count().iloc[:, 0], bins=bins)
| size | |
|---|---|
| SEACell | |
| SEACell-0 | 112 |
| SEACell-1 | 83 |
| SEACell-10 | 33 |
| SEACell-100 | 51 |
| SEACell-101 | 81 |
| ... | ... |
| SEACell-95 | 73 |
| SEACell-96 | 76 |
| SEACell-97 | 21 |
| SEACell-98 | 99 |
| SEACell-99 | 33 |
249 rows × 1 columns
Purity: compute_celltype_purity(ad, col_name) computes the purity of different celltype labels within a SEACell metacell.
Compactness: per-SEAcell variance in diffusion components (typically 'X_pca' for RNA). Lower values of compactness suggest more compact/lower variance metacells.
Separation: distance between a SEACell and its nth_nbr. If cluster is provided as a string, e.g. 'celltype', nearest neighbors are restricted to have the same celltype value. Higher values of separation suggest better distinction between metacells.
SEACell_purity = SEACells.evaluate.compute_celltype_purity(adata, 'leidenAnnotated')
plt.figure(figsize=(6,4))
sns.boxplot(data=SEACell_purity, y='leidenAnnotated_purity')
plt.title('leidenAnnotated Purity')
sns.despine()
plt.show()
plt.close()
SEACell_purity.head()
| leidenAnnotated | leidenAnnotated_purity | |
|---|---|---|
| SEACell | ||
| SEACell-0 | MigratingNeurons | 0.964286 |
| SEACell-1 | GlutamatergicNeurons_late | 0.939759 |
| SEACell-10 | ProliferatingProgenitors | 0.909091 |
| SEACell-100 | RadialGliaProgenitors | 0.764706 |
| SEACell-101 | GlutamatergicNeurons_late | 0.888889 |
compactness = SEACells.evaluate.compactness(adata, 'X_pca_harmony')
plt.figure(figsize=(6,4))
sns.boxplot(data=compactness, y='compactness')
plt.title('Compactness')
sns.despine()
plt.show()
plt.close()
compactness.head()
Determing nearest neighbor graph...
computing neighbors
finished: added to `.uns['neighbors']`
`.obsp['distances']`, distances for each pair of neighbors
`.obsp['connectivities']`, weighted adjacency matrix (0:00:07)
| compactness | |
|---|---|
| SEACell | |
| SEACell-0 | 0.000317 |
| SEACell-1 | 0.000695 |
| SEACell-10 | 0.015297 |
| SEACell-100 | 0.002325 |
| SEACell-101 | 0.000930 |
separation = SEACells.evaluate.separation(adata, 'X_pca_harmony')
plt.figure(figsize=(6,4))
sns.boxplot(data=separation, y='separation')
plt.title('Separation')
sns.despine()
plt.show()
plt.close()
separation.head()
Determing nearest neighbor graph...
computing neighbors
finished: added to `.uns['neighbors']`
`.obsp['distances']`, distances for each pair of neighbors
`.obsp['connectivities']`, weighted adjacency matrix (0:00:07)
| separation | |
|---|---|
| SEACell | |
| SEACell-0 | 0.034274 |
| SEACell-1 | 0.057667 |
| SEACell-10 | 0.373162 |
| SEACell-100 | 0.116281 |
| SEACell-101 | 0.078160 |
SEA_adata.layers['counts'] = SEA_adata.X.copy()
sc.pp.normalize_total(SEA_adata)
sc.pp.log1p(SEA_adata)
SEA_adata.layers['lognorm'] = SEA_adata.X.copy()
sc.pp.highly_variable_genes(SEA_adata, inplace=True)
sc.tl.pca(SEA_adata, use_highly_variable=True)
sc.pp.neighbors(SEA_adata, use_rep='X_pca')
sc.tl.umap(SEA_adata)
normalizing counts per cell
finished (0:00:00)
extracting highly variable genes
finished (0:00:00)
--> added
'highly_variable', boolean vector (adata.var)
'means', float vector (adata.var)
'dispersions', float vector (adata.var)
'dispersions_norm', float vector (adata.var)
computing PCA
on highly variable genes
with n_comps=50
finished (0:00:00)
computing neighbors
finished: added to `.uns['neighbors']`
`.obsp['distances']`, distances for each pair of neighbors
`.obsp['connectivities']`, weighted adjacency matrix (0:00:01)
computing UMAP
finished: added
'X_umap', UMAP coordinates (adata.obsm) (0:00:01)
sc.pl.umap(SEA_adata, color=['leidenAnnotated'], s=75)
/usr/local/lib/python3.8/dist-packages/scanpy/plotting/_tools/scatterplots.py:392: UserWarning: No data for colormapping provided via 'c'. Parameters 'cmap' will be ignored cax = scatter(
sc.pl.umap(SEA_adata, color=['leidenAnnotated_purity'], s=75)
SEA_adata.obs.columns
Index(['leidenAnnotated', 'leidenAnnotated_purity'], dtype='object')
SEA_adata
AnnData object with n_obs × n_vars = 249 × 33538
obs: 'leidenAnnotated', 'leidenAnnotated_purity'
var: 'highly_variable', 'means', 'dispersions', 'dispersions_norm'
uns: 'log1p', 'hvg', 'pca', 'neighbors', 'umap', 'leidenAnnotated_colors'
obsm: 'X_pca', 'X_umap'
varm: 'PCs'
layers: 'raw', 'counts', 'lognorm'
obsp: 'distances', 'connectivities'
SEA_adata.X
<249x33538 sparse matrix of type '<class 'numpy.float64'>' with 2976218 stored elements in Compressed Sparse Row format>
SEA_adata.write('../../../../DataDir/ExternalData/SingleCellData/Castaldi_metacells_new.h5ad')
nb_fname = ipynbname.name()
nb_fname
'SEACellsCastaldi-NEW'
%%bash -s "$nb_fname"
jupyter nbconvert "$1".ipynb --to="python"
jupyter nbconvert "$1".ipynb --to="html"
[NbConvertApp] Converting notebook SEACellsCastaldi-NEW.ipynb to python [NbConvertApp] Writing 11413 bytes to SEACellsCastaldi-NEW.py [NbConvertApp] Converting notebook SEACellsCastaldi-NEW.ipynb to html [NbConvertApp] Writing 2699077 bytes to SEACellsCastaldi-NEW.html
print(datetime.now())
2026-03-05 20:05:18.276663