MouseBrain Downstream Tutorial

This notebook expands the downstream analysis after the Microglia stGP fit: spatial proximity tests, cell-type shell enrichment, regression, and pathway/signature enrichment.

[ ]:
import json
import os
import sys
import time
from pathlib import Path

import matplotlib
matplotlib.use("Agg")
import numpy as np
import pandas as pd
import scanpy as sc
from IPython.display import display

PROJECT_DIR = Path.cwd()
if PROJECT_DIR.name != "RealData_MouseBrainMERFISH":
    PROJECT_DIR = Path("/home/byual/stGP/RealData_MouseBrainMERFISH")
os.chdir(PROJECT_DIR)
sys.path.insert(0, str(PROJECT_DIR))

import utils as u
from plots import set_nature_style

set_nature_style()
print(f"Working directory: {PROJECT_DIR}")
print(f"QC data: {u.DATA_QC}")
print(f"Proximity results: {u.RESULTS_PROXIMITY}")
print(f"Enrichment results: {u.RESULTS_ENRICHMENT}")

Working directory: /import/home4/byual/stGP-0529/RealData_MouseBrainMERFISH
QC data: data/qc/aging_coronal_qc.h5ad
Proximity results: Results/proximity
Enrichment results: Results/enrichment
[2]:
CELLTYPE = "Microglia"
PROGRAMS = [2]       # Fig. 5 focuses on stGP2. Use None to run every program.
N_PERM = 100
SEED = 0
SKIP_EXISTING = True

GENE_SET_COLLECTIONS = None  # None = all canonical GO + cell-type signature sets.
PADJ_THRESHOLD = 0.1
N_TOP = 6
TOP_PER_PROGRAM = 5

safe_ct = u.safe_name(CELLTYPE)
prox_csv_dir = u.RESULTS_PROXIMITY / safe_ct
prox_fig_dir = u.FIGURES_ROOT / safe_ct / "proximity"
enrich_csv_dir = u.RESULTS_ENRICHMENT / safe_ct
enrich_fig_dir = u.FIGURES_ROOT / safe_ct / "enrichment"
for path in [prox_csv_dir, prox_fig_dir, enrich_csv_dir, enrich_fig_dir]:
    path.mkdir(parents=True, exist_ok=True)

display(pd.DataFrame([{
    "celltype": CELLTYPE,
    "programs": "all" if PROGRAMS is None else PROGRAMS,
    "n_perm": N_PERM,
    "skip_existing": SKIP_EXISTING,
    "proximity_csv_dir": str(prox_csv_dir),
    "proximity_fig_dir": str(prox_fig_dir),
    "enrichment_csv_dir": str(enrich_csv_dir),
    "enrichment_fig_dir": str(enrich_fig_dir),
}]))

celltype programs n_perm skip_existing proximity_csv_dir proximity_fig_dir enrichment_csv_dir enrichment_fig_dir
0 Microglia [2] 100 True Results/proximity/Microglia Figures/Microglia/proximity Results/enrichment/Microglia Figures/Microglia/enrichment

1. Load target stGP scores and all-cell QC coordinates

Proximity tests use the target cell type’s stGP spatial residual b and all cell coordinates/cell-type labels from the QC AnnData.

[3]:
adata_target = u.load_target_data(CELLTYPE, u.RESULTS_STGP)
target = u.extract_target_arrays(adata_target)
regions = sorted(set(target["region"].tolist()))

print(f"Target: {len(target['age']):,} {CELLTYPE} cells")
print(f"Programs: {target['program_labels']}")
print(f"Regions: {regions}")

adata_all = sc.read_h5ad(str(u.DATA_QC))
glob = u.extract_global_arrays(adata_all)
print(f"All QC cells: {adata_all.n_obs:,}")

display(pd.DataFrame({
    "program": target["program_labels"],
    "b_mean": target["B"].mean(axis=0),
    "b_sd": target["B"].std(axis=0),
}))

Target: 52,417 Microglia cells
Programs: ['stGP1', 'stGP2', 'stGP3', 'stGP4']
Regions: ['CC/ACO', 'CTX', 'STR', 'VEN']
All QC cells: 981,750
program b_mean b_sd
0 stGP1 -0.000025 2.349388
1 stGP2 0.109022 2.114654
2 stGP3 -0.133021 2.165237
3 stGP4 0.887728 2.202204

2. Abundance over age

Before proximity testing, check whether target/effectors change strongly in abundance across age.

[4]:
df_counts, df_abund = u.compute_abundance_check(CELLTYPE, glob)
df_counts.to_csv(prox_csv_dir / "abundance_per_slice.csv", index=False)
df_abund.to_csv(prox_csv_dir / "abundance_summary.csv", index=False)
u.render_abundance(CELLTYPE, df_counts, df_abund, prox_fig_dir)

display(df_abund.sort_values("spearman_rho", ascending=False))
print(f"Wrote abundance figure: {prox_fig_dir / 'abundance_check.png'}")

celltype total n_slices spearman_rho spearman_p
0 T cell 1008 20 0.645113 2.130693e-03
1 Microglia 52417 20 0.373825 1.044513e-01
2 Neuroblast 3260 20 -0.939850 7.860443e-10
3 NSC 2448 20 -0.964635 7.227150e-12
Wrote abundance figure: Figures/Microglia/proximity/abundance_check.png

3. Matched near-vs-far proximity and permutation null

For each program, compare target cells near an effector cell type against far target cells within the same age slice. A spatial permutation null keeps effector counts fixed per slice.

[5]:
if PROGRAMS is None:
    program_indices = list(range(target["n_programs"]))
else:
    program_indices = [p - 1 for p in PROGRAMS if 0 < p <= target["n_programs"]]
effectors = [c for c in u.ALL_CELLTYPES if c != CELLTYPE]

program_context = {}
for k in program_indices:
    k_label = target["program_labels"][k]
    sub_csv = prox_csv_dir / k_label
    sub_fig = prox_fig_dir / k_label
    sub_csv.mkdir(parents=True, exist_ok=True)
    sub_fig.mkdir(parents=True, exist_ok=True)
    match_path = sub_csv / "matched_effects.csv"
    perm_path = sub_csv / "permutation_null.csv"

    print(f"\n--- {CELLTYPE} {k_label}: matched proximity ---")
    if SKIP_EXISTING and match_path.exists() and perm_path.exists():
        df_match = pd.read_csv(match_path)
        df_perm = pd.read_csv(perm_path)
        print(f"loaded cached matched/permutation tables from {sub_csv}")
    else:
        t0 = time.perf_counter()
        df_match = u.compute_matched_effect_table(target, glob, regions, effectors)
        df_match.to_csv(match_path, index=False)
        u.make_standalone_matched_heatmap(df_match, CELLTYPE, k_label, sub_fig / "matched_heatmap.png")
        print(f"matched table done in {time.perf_counter() - t0:.1f}s")

        t0 = time.perf_counter()
        df_perm, _, _ = u.compute_permutation_null(
            target, glob, regions, effectors, k, n_perm=N_PERM, seed=SEED,
        )
        df_perm.to_csv(perm_path, index=False)
        u.make_standalone_effector_ranking(df_match, df_perm, CELLTYPE, k_label, sub_fig / "effector_ranking.png")
        print(f"permutation null done in {time.perf_counter() - t0:.1f}s")

    sig_effectors = u._select_significant_effectors(df_match, k_label, q_threshold=u.SIG_Q_THRESHOLD)
    if not sig_effectors:
        sig_effectors = [e for e in u.DEFAULT_PERM_EFFECTORS if e != CELLTYPE][:4]
    print(f"selected effectors: {sig_effectors}")

    program_context[k] = dict(
        label=k_label, sub_csv=sub_csv, sub_fig=sub_fig,
        df_match=df_match, df_perm=df_perm, sig_effectors=sig_effectors,
    )

display(pd.concat([
    ctx["df_match"].assign(focus_program=ctx["label"])
    for ctx in program_context.values()
], ignore_index=True).head())


--- Microglia stGP2: matched proximity ---
loaded cached matched/permutation tables from Results/proximity/Microglia/stGP2
selected effectors: ['Endothelial', 'NSC', 'Astrocyte', 'Neuroblast', 'Ependymal', 'T cell', 'Neuron-Excitatory', 'Oligodendrocyte', 'Neuron-MSN', 'Pericyte', 'Macrophage', 'OPC']
effector program k effect se z p n_near n_far n_blocks q_bh focus_program
0 Astrocyte stGP1 0 -0.333182 0.201157 -1.656328 9.765550e-02 1122 130 2 1.103932e-01 stGP2
1 Astrocyte stGP2 1 2.640462 0.134043 19.698552 2.218769e-86 1122 130 2 5.244364e-86 stGP2
2 Astrocyte stGP3 2 1.079732 0.089434 12.072961 1.467561e-33 1122 130 2 2.312520e-33 stGP2
3 Astrocyte stGP4 3 -1.990231 0.182373 -10.913000 9.990450e-28 1122 130 2 1.484295e-27 stGP2
4 Endothelial stGP1 0 -3.665859 0.245810 -14.913354 2.698483e-50 5482 86 6 5.197079e-50 stGP2

4. Distance decay, age stratification, shell enrichment, and regression

These analyses characterize whether proximity effects are distance-dependent, age-dependent, enriched/depleted in a local shell, and explainable by age/region/cell-type density predictors.

[6]:
program_summaries = []
for k, ctx in program_context.items():
    k_label = ctx["label"]
    sub_csv = ctx["sub_csv"]
    sub_fig = ctx["sub_fig"]
    sig_effectors = ctx["sig_effectors"]
    df_match = ctx["df_match"]
    df_perm = ctx["df_perm"]
    print(f"\n--- {CELLTYPE} {k_label}: secondary proximity analyses ---")

    secondary_paths = {
        "decay": sub_csv / "distance_decay.csv",
        "age": sub_csv / "age_stratification.csv",
        "enrich": sub_csv / "proximity_enrichment.csv",
        "coefs": sub_csv / "variance_decomposition_coefs_full.csv",
        "meta": sub_csv / "variance_decomposition_meta.json",
    }
    if SKIP_EXISTING and all(p.exists() for p in secondary_paths.values()):
        df_decay = pd.read_csv(secondary_paths["decay"])
        df_age = pd.read_csv(secondary_paths["age"])
        df_enrich = pd.read_csv(secondary_paths["enrich"])
        df_coefs = pd.read_csv(secondary_paths["coefs"])
        var_meta = json.loads(secondary_paths["meta"].read_text())
        demo_age = u.pick_demo_slice(target, k, glob)
        print(f"loaded cached secondary tables from {sub_csv}")
    else:
        df_decay = u.compute_distance_decay(target, glob, regions, sig_effectors, k)
        df_decay.to_csv(secondary_paths["decay"], index=False)
        u.make_standalone_distance_decay(df_decay, CELLTYPE, k_label, sub_fig / "distance_decay.png")

        df_age = u.compute_age_stratification(target, glob, regions, sig_effectors, k)
        df_age.to_csv(secondary_paths["age"], index=False)
        u.make_standalone_age_stratification(df_age, CELLTYPE, k_label, sub_fig / "age_stratification.png")

        df_enrich, _, _, _ = u.compute_proximity_enrichment(target, glob, regions, k)
        df_enrich.to_csv(secondary_paths["enrich"], index=False)
        u.make_standalone_enrichment(df_enrich, CELLTYPE, k_label, sub_fig / "proximity_enrichment.png")

        df_coefs, var_meta = u.compute_variance_decomposition(target, glob, regions, k, CELLTYPE)
        df_coefs.to_csv(secondary_paths["coefs"], index=False)
        secondary_paths["meta"].write_text(json.dumps(var_meta, indent=2))
        u.make_standalone_variance(df_coefs, var_meta, CELLTYPE, k_label, sub_fig / "regression.png")

        demo_age = u.pick_demo_slice(target, k, glob)
        for eff_name, eff_tag in [("T cell", "Tcell"), ("NSC", "NSC")]:
            if eff_name == CELLTYPE:
                continue
            eff_demo_age = u.pick_demo_slice(target, k, glob, prefer_old_with_effector=eff_name)
            u.make_standalone_spatial(
                target, glob, k, eff_demo_age, CELLTYPE, k_label,
                sub_fig / f"spatial_example_{eff_tag}.png", effector=eff_name,
            )
        u.make_standalone_near_far_violins(
            target, glob, k, CELLTYPE, k_label, sig_effectors, sub_fig / "near_far_violins.png",
        )

    summary = u._summarise_program(df_match, df_perm, CELLTYPE, k_label, demo_age, target, sig_effectors, var_meta)
    program_summaries.append(summary)
    print(f"secondary outputs are available in {sub_csv} and {sub_fig}")

display(pd.DataFrame(program_summaries))


--- Microglia stGP2: secondary proximity analyses ---
loaded cached secondary tables from Results/proximity/Microglia/stGP2
secondary outputs are available in Results/proximity/Microglia/stGP2 and Figures/Microglia/proximity/stGP2
target_celltype program demo_age n_target_cells n_predictors R2_full sig_effectors top_pro_aging_effector top_pro_aging_effect top_pro_rejuv_effector top_pro_rejuv_effect n_perm_pass perm_pass_effectors
0 Microglia stGP2 34.5 52417 17 0.4008 [Endothelial, NSC, Astrocyte, Neuroblast, Epen... NSC 3.390692 Endothelial -4.832034 7 Astrocyte|Endothelial|Ependymal|Neuroblast|NSC...

5. Downstream high-b vs low-b cell-type enrichment

This section asks which surrounding cell types are enriched around high spatial residual b cells, both across all slices and within each slice.

[7]:
for k, ctx in program_context.items():
    k_label = ctx["label"]
    out_csv = ctx["sub_csv"] / "downstream"
    out_fig = ctx["sub_fig"] / "downstream"
    out_csv.mkdir(parents=True, exist_ok=True)
    out_fig.mkdir(parents=True, exist_ok=True)
    df_match_prog = ctx["df_match"][ctx["df_match"]["program"] == k_label].copy()

    print(f"\n--- {CELLTYPE} {k_label}: downstream high-b/low-b enrichment ---")
    downstream_paths = {
        "all": out_csv / "all_slices_enrichment.csv",
        "slice_enrich": out_csv / "per_slice_enrichment.csv",
        "slice_match": out_csv / "per_slice_matched_effects.csv",
        "summary": out_csv / "downstream_summary.json",
    }
    if SKIP_EXISTING and all(p.exists() for p in downstream_paths.values()):
        df_all = pd.read_csv(downstream_paths["all"])
        df_slice_enrich = pd.read_csv(downstream_paths["slice_enrich"])
        df_slice_match = pd.read_csv(downstream_paths["slice_match"])
        downstream_summary = json.loads(downstream_paths["summary"].read_text())
        print(f"loaded cached downstream tables from {out_csv}")
    else:
        counts_by_eff = u._shell_counts_by_effector(target, glob, effectors)
        df_all = u.compute_downstream_all_slices_enrichment(
            target, glob, effectors, k, counts_by_eff=counts_by_eff,
        )
        df_slice_enrich = u.compute_downstream_per_slice_enrichment(
            target, glob, effectors, k, counts_by_eff=counts_by_eff,
        )
        df_slice_match = u.compute_downstream_per_slice_matched_effects(target, glob, effectors, k)

        df_all.to_csv(downstream_paths["all"], index=False)
        df_slice_enrich.to_csv(downstream_paths["slice_enrich"], index=False)
        df_slice_match.to_csv(downstream_paths["slice_match"], index=False)

        downstream_summary = u.summarise_downstream(
            df_all, df_slice_enrich, df_slice_match, df_match_prog, CELLTYPE, k_label,
        )
        downstream_paths["summary"].write_text(json.dumps(downstream_summary, indent=2))

        u.make_downstream_all_slices_figure(
            df_all, df_match_prog, CELLTYPE, k_label, out_fig / "all_slices_celltype_enrichment.png",
        )
        u.make_downstream_heatmap(
            df_slice_enrich, value_col="log2fc", q_col="q_bh_by_slice",
            title=f"{CELLTYPE} {k_label}: per-slice high-b shell enrichment",
            cbar_label="log2 shell enrichment",
            savepath=out_fig / "per_slice_enrichment_heatmap.png",
        )
        u.make_downstream_heatmap(
            df_slice_match, value_col="effect", q_col="q_bh_by_slice",
            title=f"{CELLTYPE} {k_label}: per-slice matched proximity effect",
            cbar_label="Delta median b (near - far)",
            savepath=out_fig / "per_slice_matched_effect_heatmap.png",
        )

        by_ct_dir = out_fig / "by_celltype"
        for eff in effectors:
            eff_dir = by_ct_dir / u.safe_name(eff)
            eff_dir.mkdir(parents=True, exist_ok=True)
            u.make_downstream_effector_trend(
                df_slice_enrich, df_slice_match, eff, CELLTYPE, k_label, eff_dir / "slice_trend.png",
            )
            demo_age = u.pick_demo_slice(target, k, glob, prefer_old_with_effector=eff)
            u.make_downstream_effector_spatial(
                target, glob, k, demo_age, CELLTYPE, k_label, eff, eff_dir / "spatial_overlay.png",
            )

    ctx["downstream_summary"] = downstream_summary
    print(downstream_summary)


--- Microglia stGP2: downstream high-b/low-b enrichment ---
loaded cached downstream tables from Results/proximity/Microglia/stGP2/downstream
{'target_celltype': 'Microglia', 'program': 'stGP2', 'n_effectors_tested': 13, 'n_slices_tested': 20, 'n_all_slice_enrichment_sig': 12, 'top_all_slice_enriched_effector': 'Oligodendrocyte', 'top_all_slice_depleted_effector': 'Neuron-Excitatory', 'n_per_slice_enrichment_sig': 156, 'n_per_slice_matched_sig': 4, 'n_global_matched_sig': 12, 'global_matched_sig_effectors': 'T cell|NSC|Neuroblast|Macrophage|Ependymal|Pericyte|OPC|Endothelial|Astrocyte|Neuron-MSN|Oligodendrocyte|Neuron-Excitatory'}

6. Cell-type summary tables

Concatenate per-program downstream summaries into cell-type and all-cell-type overview tables.

[8]:
downstream_summary_df = u.compile_celltype_downstream_summary(
    CELLTYPE,
    csv_dir=prox_csv_dir,
    fig_dir=prox_fig_dir,
    program_labels=[program_context[k]["label"] for k in program_context],
)

summary = dict(
    target_celltype=CELLTYPE,
    n_target_cells=int(len(target["age"])),
    n_programs=int(target["n_programs"]),
    regions=regions,
    n_downstream_summary_rows=int(len(downstream_summary_df)),
    programs=program_summaries,
)
(prox_csv_dir / "summary.json").write_text(json.dumps(summary, indent=2))

master_rows = []
for ct in u.ALL_CELLTYPES:
    sj = u.RESULTS_PROXIMITY / u.safe_name(ct) / "summary.json"
    if sj.exists():
        s = json.loads(sj.read_text())
        master_rows.extend([p for p in s.get("programs", []) if "error" not in p])
if master_rows:
    pd.DataFrame(master_rows).to_csv(u.RESULTS_PROXIMITY / "summary_all_celltypes.csv", index=False)

display(downstream_summary_df.head())
print(f"Wrote proximity summary: {prox_csv_dir / 'summary.json'}")

program k effector hi_threshold lo_threshold n_hi n_lo hi_mean lo_mean log2fc p q_bh R_in R_out high_pct low_pct min_group valid
0 stGP2 1 T cell 1.048398 -1.247125 13105 13105 0.026326 0.006410 0.436226 1.136039e-34 2.461419e-34 20.0 50.0 75.0 25.0 30 True
1 stGP2 1 NSC 1.048398 -1.247125 13105 13105 0.047005 0.003129 0.868570 6.716685e-29 1.091461e-28 20.0 50.0 75.0 25.0 30 True
2 stGP2 1 Neuroblast 1.048398 -1.247125 13105 13105 0.077757 0.006791 1.169656 7.208538e-44 1.874220e-43 20.0 50.0 75.0 25.0 30 True
3 stGP2 1 Macrophage 1.048398 -1.247125 13105 13105 0.038306 0.051431 -0.199912 3.426075e-07 4.453897e-07 20.0 50.0 75.0 25.0 30 True
4 stGP2 1 Ependymal 1.048398 -1.247125 13105 13105 0.074857 0.012514 0.998018 5.836289e-32 1.083882e-31 20.0 50.0 75.0 25.0 30 True
Wrote proximity summary: Results/proximity/Microglia/summary.json

7. Pathway and cell-type signature enrichment

Positive stGP gene weights are tested against the local GMT collections. Per-program bar charts and a combined program-by-term dotplot are written under Figures/<celltype>/enrichment.

[9]:
gene_sets = u._resolve_gene_sets(GENE_SET_COLLECTIONS, u.DEFAULT_GENESETS_DIR)
W = u._load_W(u.RESULTS_STGP, CELLTYPE)
programs = W.index.astype(str).tolist()
background_genes = W.columns.astype(str).tolist()

print(f"Loaded W: {len(programs)} programs x {len(background_genes)} genes")
print(f"Gene-set collections: {list(gene_sets.keys())}")
display(W.iloc[:, : min(10, W.shape[1])])

Loaded W: 4 programs x 220 genes
Gene-set collections: ['GO Biological process', 'GO Molecular Function', 'GO Cellular Component', 'Cell-type signatures']
Th Cd52 S100a6 Cd3g Tgfb1 Cd79a Tpm4 Ercc1 Trp53 Trh
stGP1 0.0 0.00000 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0
stGP2 0.0 0.03098 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0
stGP3 0.0 0.00000 0.0 0.0 0.047638 0.0 0.0 0.0 0.0 0.0
stGP4 0.0 0.00000 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0
[10]:
all_enrichment_results = []
enrichment_stats = []
for prog in programs:
    stats, per_prog = u._enrich_one_program(
        program=prog,
        weights=W.loc[prog],
        background_genes=background_genes,
        gene_sets=gene_sets,
        celltype=CELLTYPE,
        safe=safe_ct,
        fig_dir=enrich_fig_dir,
        res_dir=enrich_csv_dir,
        padj_threshold=PADJ_THRESHOLD,
        n_top=N_TOP,
    )
    enrichment_stats.append(stats)
    if per_prog is not None:
        all_enrichment_results.append(per_prog)

combined_path = None
if all_enrichment_results:
    combined = pd.concat(all_enrichment_results, ignore_index=True)
    combined.insert(0, "celltype", CELLTYPE)
    combined_path = enrich_csv_dir / f"{safe_ct}_combined_enrichment.csv"
    combined.to_csv(combined_path, index=False)
    u._plot_program_enrichment_dotplot(
        combined,
        out=enrich_fig_dir / f"{safe_ct}_enrichment_program_dotplot.png",
        padj_threshold=PADJ_THRESHOLD,
    )
    display(combined.head())
else:
    combined = pd.DataFrame()

enrich_summary = dict(
    celltype=CELLTYPE,
    safe_name=safe_ct,
    n_programs=len(programs),
    programs=enrichment_stats,
    gene_sets=list(gene_sets.keys()),
    padj_threshold=float(PADJ_THRESHOLD),
    n_top_per_panel=int(N_TOP),
    combined_csv=str(combined_path) if combined_path else None,
)
(enrich_csv_dir / "enrichment_summary.json").write_text(json.dumps(enrich_summary, indent=2))
u.compile_enrichment_master_summary([CELLTYPE], results_root=u.RESULTS_ENRICHMENT, top_per_program=TOP_PER_PROGRAM)

display(pd.DataFrame(enrichment_stats))
print(f"Wrote enrichment summary: {enrich_csv_dir / 'enrichment_summary.json'}")

  [program] stGP1: 10 positive genes ...
  [program] stGP2: 15 positive genes ...
  [program] stGP3: 15 positive genes ...
  [program] stGP4: 15 positive genes ...
celltype program n_genes_input Gene_set Term Overlap P-value Adjusted P-value Odds Ratio Combined Score Genes gene_set
0 Microglia stGP1 10 m5.go.bp.v2026.1.Mm.symbols.gmt Actin filament based process 2/16 0.158439 0.460611 3.985801 7.343384 Pik3r1;Prox1 GO Biological process
1 Microglia stGP1 10 m5.go.bp.v2026.1.Mm.symbols.gmt Actin filament bundle organization 1/2 0.089041 0.356662 22.052632 53.337758 Pik3r1 GO Biological process
2 Microglia stGP1 10 m5.go.bp.v2026.1.Mm.symbols.gmt Actin filament organization 2/7 0.034656 0.346712 10.989305 36.949025 Pik3r1;Prox1 GO Biological process
3 Microglia stGP1 10 m5.go.bp.v2026.1.Mm.symbols.gmt Activation of immune response 2/29 0.388978 0.619581 1.962567 1.853120 Pik3r1;Mog GO Biological process
4 Microglia stGP1 10 m5.go.bp.v2026.1.Mm.symbols.gmt Activation of innate immune response 1/8 0.315020 0.575638 4.284211 4.948775 Pik3r1 GO Biological process

[master] Wrote 80 rows -> Results/enrichment/summary_all_celltypes.csv
program n_pos_genes n_terms_reported runtime_sec status figure
0 stGP1 10 882 0.84 done Figures/Microglia/enrichment/Microglia_stGP1_e...
1 stGP2 15 1033 1.41 done Figures/Microglia/enrichment/Microglia_stGP2_e...
2 stGP3 15 1741 0.60 done Figures/Microglia/enrichment/Microglia_stGP3_e...
3 stGP4 15 1196 0.59 done Figures/Microglia/enrichment/Microglia_stGP4_e...
Wrote enrichment summary: Results/enrichment/Microglia/enrichment_summary.json