Tutorial 2 — Filtering meta-programs#
Not all meta-programs that come out of the initial NMF are biologically meaningful or statistically robust. This tutorial demonstrates two complementary filtering strategies:
Quality-metric filter (
filter_by_scores) — remove MPs whose within-cluster cohesion metrics fall below thresholds.Differential-activity filter (
filter_for_significant_obs) — keep only MPs whose cell scores differ significantly between an experimental condition and the rest, optionally with effect-size gates (KS statistic, Wasserstein distance).
Both functions are non-destructive: they return a filtered copy of results_mp and a list of MPs to drop, leaving the original object unchanged so you can compare before and after.
Dataset — Peter Sims lab DMSO vs. panobinostat scRNA-seq cohort.
0 · Imports and data preparation#
Run the same NMF + consensus steps as Tutorial 1.
[ ]:
import kaleidocell
import scanpy as sc
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)
mp_scores = kaleidocell.compute_mp_scores(results_mp, adata)
print(f"Starting with {len(results_mp['mp_dict'])} meta-programs.")
1 · Filter by quality metrics#
filter_by_scores removes MPs that fail to meet minimum thresholds on the quality metrics computed during derive_nmf_metaprograms.
``logic`` parameter
"OR"(default) — drop an MP if it fails any threshold."AND"— drop an MP only if it fails all thresholds simultaneously (more lenient).
[ ]:
# Inspect current metric distributions before deciding on thresholds
results_mp['metrics']
[ ]:
to_drop_quality, results_quality = kaleidocell.filter_by_scores(
results_mp,
meanSimilarity=0.2, # within-cluster cosine similarity
silhouette=0.1, # cluster separation
sampleCoverage=0.3, # fraction of samples represented
logic='OR',
)
print(f"MPs flagged by quality filter: {to_drop_quality}")
print(f"Remaining: {len(results_quality['mp_dict'])} MPs")
2 · Filter by differential activity between conditions#
filter_for_significant_obs compares the per-cell MP scores between one condition (value) and all other cells. Multiple-testing correction is applied across MPs.
Available statistical tests | test | Description | |——–|————-| | "mannwhitney" | Mann–Whitney U (default; non-parametric location shift) | | "ttest" | Welch’s t-test (parametric; fast for large n) | | "ks" | Kolmogorov–Smirnov (sensitive to any distributional difference) | | "permutation" | Permutation test (most rigorous; slow) |
Effect-size gates (applied after significance) | Parameter | Description | |———–|————-| | min_ks_stat | Minimum KS statistic (≥ 0.20 recommended) | | emd_null_n_bootstrap | Bootstrap null EMD from the rest group; keeps only MPs above the null |
The function always computes ks_stat and emd columns in the returned stats table, even when the gates are not applied.
[ ]:
# Compare panobinostat-treated cells vs. DMSO controls
to_drop_sig, results_sig, stats = kaleidocell.filter_for_significant_obs(
results_mp,
mp_scores,
adata,
obs='Treatment',
value='Panobinostat',
test='mannwhitney',
correction='fdr_bh',
p_threshold=0.05,
min_ks_stat=0.20, # effect-size gate: drop small-effect MPs
show_only_up_regulated=True, # keep only MPs up in Panobinostat
)
print(f"\nMPs retained: {list(results_sig['mp_dict'].keys())}")
[ ]:
# The stats DataFrame contains one row per MP with all test results
stats[['statistic', 'pvalue_corrected', 'significant', 'ks_stat', 'emd',
'mean_condition', 'mean_rest', 'up_regulated']]
3 · Apply the final drop list manually#
Both filter functions print the exact drop_meta_programs call needed. Run it only after reviewing the lists above.
[ ]:
# Combine the two drop lists (deduplication is handled inside drop_meta_programs)
final_drop = list(dict.fromkeys(to_drop_quality + to_drop_sig))
print(f"Final drop list ({len(final_drop)} MPs): {final_drop}")
results_mp_final = kaleidocell.drop_meta_programs(results_mp, final_drop)
print(f"Retained: {list(results_mp_final['mp_dict'].keys())}")
4 · Generate HTML report for the filtered results#
[ ]:
mp_scores_final = kaleidocell.compute_mp_scores(results_mp_final, adata)
path = kaleidocell.get_html(
results_mp_final,
adata,
mp_scores=mp_scores_final,
obs=['Treatment', 'Patients'],
output_path='../results/02/',
)
print(f"Report: {path}")