| |
| """miRNA-target analysis: test whether miRNA-targeted genes have higher gamma. |
| |
| Uses TargetScan 8.0 predictions to identify miRNA-target relationships, |
| then tests whether predicted targets have systematically higher degradation |
| rates (gamma) than non-targets using Mann-Whitney U tests. |
| |
| This addresses Aim 4 of the research plan: post-transcriptional regulatory networks. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import json |
| import sys |
| from pathlib import Path |
|
|
| import matplotlib |
| matplotlib.use("Agg") |
| import matplotlib.pyplot as plt |
| import numpy as np |
| import pandas as pd |
| from scipy import stats |
|
|
| sys.path.insert(0, str(Path(__file__).parent)) |
| from _common import set_figure_style |
|
|
| import scptr |
|
|
| OUTPUT_DIR = Path(__file__).parent.parent / "output" / "mirna_analysis" |
|
|
|
|
| def save_fig(fig, name, subdir="figures"): |
| out_dir = OUTPUT_DIR / subdir |
| out_dir.mkdir(parents=True, exist_ok=True) |
| path = out_dir / f"{name}.png" |
| fig.savefig(path, dpi=150, bbox_inches="tight") |
| plt.close(fig) |
| print(f" Saved: {path}") |
|
|
|
|
| def run_mirna_analysis(adata, dataset_name, mirna_targets): |
| """Run miRNA-gamma correlation analysis on a dataset.""" |
| print(f"\n{'='*60}") |
| print(f"miRNA ANALYSIS: {dataset_name}") |
| print(f"{'='*60}") |
|
|
| |
| import copy |
| adata = copy.deepcopy(adata) |
| scptr.pp.filter_genes(adata) |
| scptr.pp.normalize_layers(adata) |
| scptr.pp.neighbors(adata, n_neighbors=30) |
| scptr.pp.smooth_layers(adata) |
| scptr.tl.estimate_beta(adata) |
| scptr.tl.estimate_gamma(adata) |
|
|
| |
| print(f" Running miRNA-gamma correlation...") |
| result_df = scptr.tl.mirna_gamma_correlation( |
| adata, mirna_targets, n_top_targets=200, min_cells_expressing=50 |
| ) |
|
|
| if len(result_df) == 0: |
| print(f" No miRNA families with sufficient targets found.") |
| return None |
|
|
| |
| n_tested = len(result_df) |
| n_sig = (result_df["fdr"] < 0.05).sum() |
| n_sig_10 = (result_df["fdr"] < 0.10).sum() |
| n_enriched = (result_df["fold_enrichment"] > 1.0).sum() |
|
|
| print(f"\n Results:") |
| print(f" miRNA families tested: {n_tested}") |
| print(f" Significant (FDR < 0.05): {n_sig} ({100*n_sig/n_tested:.1f}%)") |
| print(f" Significant (FDR < 0.10): {n_sig_10} ({100*n_sig_10/n_tested:.1f}%)") |
| print(f" Enriched (fold > 1.0): {n_enriched} ({100*n_enriched/n_tested:.1f}%)") |
| print(f" Median fold enrichment: {result_df['fold_enrichment'].median():.3f}") |
|
|
| |
| top_sig = result_df[result_df["fdr"] < 0.10].head(20) |
| if len(top_sig) > 0: |
| print(f"\n Top significant miRNAs (FDR < 0.10):") |
| for _, row in top_sig.iterrows(): |
| print(f" {row['representative_mirna']:>25s} " |
| f"n_targets={row['n_targets_in_data']:3d} " |
| f"fold={row['fold_enrichment']:.2f} " |
| f"p={row['mannwhitney_p']:.2e} " |
| f"FDR={row['fdr']:.3f}") |
|
|
| |
| top_effect = result_df.nlargest(10, "fold_enrichment") |
| print(f"\n Top miRNAs by fold enrichment:") |
| for _, row in top_effect.iterrows(): |
| print(f" {row['representative_mirna']:>25s} " |
| f"fold={row['fold_enrichment']:.2f} " |
| f"FDR={row['fdr']:.3f}") |
|
|
| |
| gamma = np.median(adata.layers["gamma"], axis=0) |
| gene_names_upper = [g.upper() for g in adata.var_names] |
|
|
| all_target_genes = set() |
| for _, row in mirna_targets.iterrows(): |
| all_target_genes.add(str(row["gene_symbol"]).upper()) |
|
|
| informative = (adata.layers["gamma"] > 0).mean(axis=0) >= 0.1 |
| target_gamma = [] |
| nontarget_gamma = [] |
| for i, g in enumerate(gene_names_upper): |
| if not informative[i]: |
| continue |
| if g in all_target_genes: |
| target_gamma.append(gamma[i]) |
| else: |
| nontarget_gamma.append(gamma[i]) |
|
|
| if len(target_gamma) >= 10 and len(nontarget_gamma) >= 10: |
| u, p = stats.mannwhitneyu(target_gamma, nontarget_gamma, alternative="greater") |
| print(f"\n Aggregate test (all targets vs non-targets):") |
| print(f" Target genes in data: {len(target_gamma)}") |
| print(f" Non-target genes: {len(nontarget_gamma)}") |
| print(f" Target median gamma: {np.median(target_gamma):.6f}") |
| print(f" Non-target median gamma: {np.median(nontarget_gamma):.6f}") |
| print(f" Fold: {np.median(target_gamma) / (np.median(nontarget_gamma) + 1e-8):.3f}") |
| print(f" Mann-Whitney p: {p:.2e}") |
|
|
| |
| res_dir = OUTPUT_DIR / "results" |
| res_dir.mkdir(parents=True, exist_ok=True) |
| result_df.to_csv(res_dir / f"mirna_gamma_{dataset_name}.csv", index=False) |
|
|
| |
| fig, axes = plt.subplots(1, 3, figsize=(16, 5)) |
|
|
| |
| neg_log_p = -np.log10(result_df["mannwhitney_p"].clip(lower=1e-50)) |
| sig_mask = result_df["fdr"] < 0.05 |
| axes[0].scatter(result_df["fold_enrichment"][~sig_mask], neg_log_p[~sig_mask], |
| s=10, alpha=0.3, color="gray", label="NS") |
| axes[0].scatter(result_df["fold_enrichment"][sig_mask], neg_log_p[sig_mask], |
| s=20, alpha=0.7, color="red", label=f"FDR<0.05 (n={sig_mask.sum()})") |
| axes[0].axhline(y=-np.log10(0.05), color="blue", linestyle="--", alpha=0.5) |
| axes[0].axvline(x=1.0, color="black", linestyle="--", alpha=0.3) |
| axes[0].set_xlabel("Fold enrichment (target/non-target gamma)") |
| axes[0].set_ylabel("-log10(p)") |
| axes[0].set_title(f"miRNA Target Enrichment ({dataset_name})") |
| axes[0].legend() |
|
|
| |
| axes[1].hist(result_df["fold_enrichment"], bins=30, color="steelblue", |
| edgecolor="black", linewidth=0.5) |
| axes[1].axvline(x=1.0, color="red", linestyle="--", label="No enrichment") |
| axes[1].axvline(x=result_df["fold_enrichment"].median(), color="green", |
| linestyle="--", label=f"Median={result_df['fold_enrichment'].median():.2f}") |
| axes[1].set_xlabel("Fold enrichment") |
| axes[1].set_ylabel("Count") |
| axes[1].set_title("Distribution of Fold Enrichments") |
| axes[1].legend() |
|
|
| |
| if len(target_gamma) >= 10: |
| box_data = [target_gamma, nontarget_gamma] |
| bp = axes[2].boxplot(box_data, labels=["miRNA\ntargets", "Non-\ntargets"], |
| patch_artist=True) |
| bp["boxes"][0].set_facecolor("coral") |
| bp["boxes"][1].set_facecolor("lightblue") |
| axes[2].set_ylabel("Median gamma per gene") |
| axes[2].set_title(f"Aggregate: targets vs non-targets\np={p:.2e}") |
| axes[2].set_yscale("symlog", linthresh=0.001) |
|
|
| fig.suptitle(f"miRNA-Gamma Analysis: {dataset_name}", fontsize=13, y=1.02) |
| fig.tight_layout() |
| save_fig(fig, f"mirna_analysis_{dataset_name}") |
|
|
| return { |
| "n_families_tested": n_tested, |
| "n_significant_005": int(n_sig), |
| "n_significant_010": int(n_sig_10), |
| "n_enriched": int(n_enriched), |
| "median_fold_enrichment": float(result_df["fold_enrichment"].median()), |
| "aggregate_p": float(p) if len(target_gamma) >= 10 else None, |
| } |
|
|
|
|
| def main(): |
| set_figure_style() |
| OUTPUT_DIR.mkdir(parents=True, exist_ok=True) |
|
|
| |
| print("=" * 60) |
| print("LOADING TARGETSCAN PREDICTIONS") |
| print("=" * 60) |
|
|
| cache_dir = Path(__file__).parent.parent / ".cache" / "targetscan" |
| try: |
| mirna_targets = scptr.tl.load_targetscan_predictions( |
| species_id=9606, |
| min_context_score=-0.2, |
| cache_dir=cache_dir, |
| ) |
| print(f" Loaded {len(mirna_targets)} human miRNA-target predictions") |
| print(f" miRNA families: {mirna_targets['mirna_family'].nunique()}") |
| print(f" Target genes: {mirna_targets['gene_symbol'].nunique()}") |
| except FileNotFoundError as e: |
| print(f" ERROR: {e}") |
| print(" Please download TargetScan data first.") |
| sys.exit(1) |
|
|
| |
| try: |
| mirna_targets_mouse = scptr.tl.load_targetscan_predictions( |
| species_id=10090, |
| min_context_score=-0.2, |
| cache_dir=cache_dir, |
| ) |
| print(f" Loaded {len(mirna_targets_mouse)} mouse miRNA-target predictions") |
| print(f" miRNA families: {mirna_targets_mouse['mirna_family'].nunique()}") |
| print(f" Target genes: {mirna_targets_mouse['gene_symbol'].nunique()}") |
| except Exception as e: |
| print(f" Mouse predictions not available: {e}") |
| mirna_targets_mouse = mirna_targets |
|
|
| |
| print("\n" + "=" * 60) |
| print("LOADING DATASETS") |
| print("=" * 60) |
|
|
| adata_pan = scptr.datasets.pancreas() |
| adata_dg = scptr.datasets.dentate_gyrus() |
|
|
| |
| try: |
| adata_sci = scptr.datasets.sci_fate() |
| except Exception: |
| adata_sci = None |
|
|
| |
| all_results = {} |
|
|
| |
| all_results["pancreas"] = run_mirna_analysis( |
| adata_pan, "pancreas", mirna_targets_mouse |
| ) |
|
|
| |
| all_results["dentate_gyrus"] = run_mirna_analysis( |
| adata_dg, "dentate_gyrus", mirna_targets_mouse |
| ) |
|
|
| |
| if adata_sci is not None: |
| all_results["sci_fate"] = run_mirna_analysis( |
| adata_sci, "sci_fate", mirna_targets |
| ) |
|
|
| |
| res_dir = OUTPUT_DIR / "results" |
| res_dir.mkdir(parents=True, exist_ok=True) |
| with open(res_dir / "mirna_summary.json", "w") as f: |
| json.dump(all_results, f, indent=2) |
|
|
| |
| print(f"\n{'='*60}") |
| print("miRNA ANALYSIS SUMMARY") |
| print(f"{'='*60}") |
| print(f"{'Dataset':>15s} {'Tested':>7s} {'Sig(5%)':>7s} {'Sig(10%)':>8s} " |
| f"{'Enriched':>8s} {'Med.Fold':>8s} {'Agg.p':>10s}") |
| for name, res in all_results.items(): |
| if res is None: |
| continue |
| print(f"{name:>15s} {res['n_families_tested']:>7d} " |
| f"{res['n_significant_005']:>7d} {res['n_significant_010']:>8d} " |
| f"{res['n_enriched']:>8d} {res['median_fold_enrichment']:>8.3f} " |
| f"{res['aggregate_p']:>10.2e}" if res['aggregate_p'] else "") |
|
|
| print(f"\nResults saved to: {OUTPUT_DIR.resolve()}") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|