Clustering
Load, filter, embed
Section titled “Load, filter, embed”import numpy as npimport scanpy as scimport matplotlib.pyplot as plt
sc.settings.verbosity = 3np.random.seed(42)
adata = sc.read_10x_mtx("/opt/data/pbmc3k", var_names="gene_symbols", make_unique=True)adata.layers["counts"] = adata.X.copy()adata.var["mt"] = adata.var_names.str.startswith("MT-")sc.pp.calculate_qc_metrics(adata, qc_vars=["mt"], percent_top=None, log1p=False, inplace=True)
def is_outlier(adata, metric, nmads): """Flag cells whose metric sits more than nmads MADs from the median.
Args: adata: AnnData object holding the metric in .obs. metric: Column of adata.obs to test. nmads: Number of median absolute deviations for the cutoff.
Returns: Boolean Series, True where the cell is an outlier. """ values = adata.obs[metric] median = values.median() mad = (values - median).abs().median() return (values < median - nmads * mad) | (values > median + nmads * mad)
adata.obs["log1p_total_counts"] = np.log1p(adata.obs["total_counts"])adata.obs["log1p_n_genes_by_counts"] = np.log1p( adata.obs["n_genes_by_counts"])adata.obs["outlier"] = ( is_outlier(adata, "log1p_total_counts", 5) | is_outlier(adata, "log1p_n_genes_by_counts", 5) | is_outlier(adata, "pct_counts_mt", 3))adata = adata[~adata.obs["outlier"]].copy()sc.pp.filter_genes(adata, min_cells=3)
sc.pp.normalize_total(adata, target_sum=1e4)sc.pp.log1p(adata)sc.pp.highly_variable_genes(adata, layer="counts", n_top_genes=2000, flavor="seurat_v3")adata.raw = adataadata = adata[:, adata.var["highly_variable"]]sc.pp.scale(adata, max_value=10)sc.tl.pca(adata, n_comps=50, random_state=42)sc.pp.neighbors(adata, n_neighbors=15, n_pcs=30, random_state=42)sc.tl.umap(adata, random_state=42)print(adata.shape)(2428, 2000)Leiden clustering
Section titled “Leiden clustering”# Leiden community detection on the KNN graph, the clustering# sc-best-practices recommends. The igraph flavour is the current# default backend.sc.tl.leiden(adata, resolution=0.5, key_added="leiden", random_state=42, flavor="igraph", n_iterations=2, directed=False)
print(adata.obs["leiden"].value_counts().sort_index())leiden0 10991 4562 4123 3154 146Name: count, dtype: int64sc.pl.umap(adata, color="leiden", show=False)plt.savefig("outputs/clustering_leiden.png", dpi=150, bbox_inches="tight")plt.close()
Testing resolutions
Section titled “Testing resolutions”# Try multiple resolutions. Higher resolution splits more populations# apart; sc-best-practices uses these sweeps to focus on# substructures.for resolution in (0.2, 0.4, 0.6, 0.8, 1.0): sc.tl.leiden(adata, resolution=resolution, key_added=f"leiden_{resolution}", random_state=42, flavor="igraph", n_iterations=2, directed=False) n_clusters = adata.obs[f"leiden_{resolution}"].nunique() print(f"resolution {resolution}: {n_clusters} clusters")resolution 0.2: 4 clustersresolution 0.4: 5 clustersresolution 0.6: 6 clustersresolution 0.8: 8 clustersresolution 1.0: 8 clustersComparing the partitions
Section titled “Comparing the partitions”from sklearn.metrics import adjusted_rand_score
# The adjusted Rand index measures agreement between two partitions,# corrected for chance. Agreement above 0.9 means the resolutions# describe almost the same structure.for resolution in (0.2, 0.4, 0.6, 0.8, 1.0): ari = adjusted_rand_score(adata.obs["leiden"], adata.obs[f"leiden_{resolution}"]) print(f"ARI of {resolution} against 0.5: {ari:.3f}")ARI of 0.2 against 0.5: 0.889ARI of 0.4 against 0.5: 0.990ARI of 0.6 against 0.5: 0.987ARI of 0.8 against 0.5: 0.656ARI of 1.0 against 0.5: 0.653sc.pl.umap(adata, color=["leiden_0.2", "leiden_0.4", "leiden_0.8"], show=False)plt.savefig("outputs/clustering_resolutions.png", dpi=150, bbox_inches="tight")plt.close()