Tutorial 3 — Visualisation, GSEA, and export#

This tutorial covers:

  1. Similarity heatmap

  2. MP score distributions across experimental groups (violin plots)

  3. MP scores on UMAP

  4. Gene Set Enrichment Analysis (GSEA) using bundled MSigDB gene sets

  5. Exporting the gene signatures to CSV — with and without loading scores

Dataset — Peter Sims lab DMSO vs. panobinostat scRNA-seq cohort.

0 · Imports and data preparation#

[ ]:
import kaleidocell
import scanpy as sc
import pandas as pd

adata = sc.read_h5ad('../data/petersims_example.h5ad')

results_nmf, nmf_convergence = kaleidocell.multi_sample_nmf(
    adata,
    batch_key='Patients',
    test_ranks=[4, 5, 6, 7, 8, 9],
    n_initializations=10,
    seed=42,
)

results_mp = kaleidocell.derive_nmf_metaprograms(results_nmf, top_n_genes=50)
mp_scores = kaleidocell.compute_mp_scores(results_mp, adata)

1 · Similarity heatmap#

Visualise pairwise cosine similarities between all NMF programs. Diagonal blocks correspond to meta-programs; tight blocks indicate well-defined signatures.

[ ]:
kaleidocell.plot_heatmap(results_mp)

2 · Quality metrics#

[ ]:
results_mp['metrics']

3 · MP score distributions across conditions#

show_distribution_over_obs draws one violin plot per MP, grouped by the specified obs column. This is useful for quickly identifying MPs that are differentially active between conditions or donors.

[ ]:
kaleidocell.show_distribution_over_obs(
    mp_scores,
    adata,
    batch_key='Treatment',
    save=False,
)

4 · MP scores on UMAP#

Overlay per-cell MP scores on the UMAP embedding. If no UMAP is present in adata.obsm, it is computed automatically.

Setting weighted=True scales each gene’s expression by its normalised loading weight before summing, giving higher-specificity genes more influence than low-weight genes.

[ ]:
# Standard equal-weight scores
kaleidocell.plot_mp_scores_on_umap(mp_scores, adata, ncols=4)
[ ]:
# Weight-scaled scores — requires results_mp
kaleidocell.plot_mp_scores_on_umap(
    mp_scores, adata,
    ncols=4,
    weighted=True,
    results_mp=results_mp,
)

5 · Gene Set Enrichment Analysis#

kaleidocell ships with several MSigDB gene-set files. Use kaleidocell.files to discover what is available.

[ ]:
# List all bundled gene-set files with descriptions
print(kaleidocell.files)
[ ]:
# Run Enrichr-based GSEA against Hallmarks and GO Biological Process
# Pass the short filename — kaleidocell resolves it to the bundled path automatically
gsea_results, gsea_plots = kaleidocell.run_gsea_pipeline(
    results_mp,
    from_file=[
        'h.all.v2026.1.Hs.symbols.gmt',
        'c5.go.bp.v2026.1.Hs.symbols.gmt',
    ],
    top_n_plot=6,
    plot=False,
    save_csv=False,
)

# Significant terms table
gsea_results.head(20)
[ ]:
# Bar plots of top terms per MP
kaleidocell.plot_gsea_results(gsea_plots, ncols=4)

6 · Export gene signatures to CSV#

The gene signatures live in results_mp['mp_dict'] — a dictionary mapping each MP name to a pd.Series of gene names (index) and loading scores (values), sorted descending by score.

Two export formats are shown below.

6a · Gene names only (no scores)#

Long-format table: one row per gene per MP. Useful for downstream tools that expect a plain gene list per program.

[ ]:
genes_only = pd.DataFrame(
    [
        {"MP": mp_name, "gene": gene}
        for mp_name, gene_series in results_mp["mp_dict"].items()
        for gene in gene_series.index
    ]
)

genes_only.to_csv('../results/03/metaprograms_genes.csv', index=False)
print(f"Saved {len(genes_only)} rows. Preview:")
genes_only.head(10)

6b · Gene names with loading scores#

Same long-format table but with an additional score column containing the consensus NMF loading weight. Higher scores indicate greater specificity to this meta-program.

[ ]:
genes_with_scores = pd.DataFrame(
    [
        {"MP": mp_name, "gene": gene, "score": float(score)}
        for mp_name, gene_series in results_mp["mp_dict"].items()
        for gene, score in gene_series.items()
    ]
)

genes_with_scores.to_csv('../results/03/metaprograms_genes_scores.csv', index=False)
print(f"Saved {len(genes_with_scores)} rows. Preview:")
genes_with_scores.head(10)

6c · Wide format — one column per MP#

Useful when you want to open the file in Excel and compare signatures side-by-side.

[ ]:
wide = pd.DataFrame(
    {
        mp_name: gene_series.index.tolist()
        for mp_name, gene_series in results_mp["mp_dict"].items()
    }
)

wide.to_csv('../results/03/metaprograms_wide.csv', index=False)
wide.head()