| |
| """Comprehensive improvements addressing 5 remaining weaknesses. |
| |
| Experiment A: Network Target GO Enrichment (Weakness #1 — network validation) |
| Experiment B: Pathway-Level Cross-Dataset Consistency (Weakness #4 — low gene-level r) |
| Experiment C: Gamma vs Raw u/s on Downstream Tasks (Weakness #2 — marginal advantage) |
| Experiment D: NB Network Split-Half Robustness (Weakness #3 — single patient) |
| Experiment E: Corrected vs Uncorrected Network Quality (Weakness #5 — destabilizing bias) |
| """ |
|
|
| from __future__ import annotations |
|
|
| import json |
| import sys |
| import warnings |
| 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 |
| from scipy.stats import hypergeom |
|
|
| sys.path.insert(0, str(Path(__file__).parent)) |
| from _common import set_figure_style |
|
|
| import scptr |
|
|
| |
| sys.stdout.reconfigure(line_buffering=True) |
|
|
| OUTPUT_DIR = Path(__file__).parent.parent / "output" / "comprehensive_improvements" |
| CACHE_DIR = Path(__file__).parent.parent / ".cache" |
| WEAKNESS_DIR = Path(__file__).parent.parent / "output" / "weakness_fixes" / "results" |
| TIER3_DIR = Path(__file__).parent.parent / "output" / "tier3" / "results" |
|
|
|
|
| def load_go_library(): |
| """Load GO BP gene sets from local cache (no network calls).""" |
| cache_file = CACHE_DIR / "go_bp_2023.json" |
| if cache_file.exists(): |
| with open(cache_file) as f: |
| go_lib = json.load(f) |
| return go_lib |
|
|
| |
| try: |
| import gseapy as gp |
| go_lib = gp.get_library("GO_Biological_Process_2023") |
| with open(cache_file, "w") as f: |
| json.dump(go_lib, f) |
| return go_lib |
| except Exception as e: |
| print(f" Failed to load GO library: {e}") |
| return None |
|
|
|
|
| def hypergeometric_enrichment(gene_list, go_lib, background_size, alpha=0.05): |
| """Run local hypergeometric GO enrichment (no API calls). |
| |
| Returns list of (term, p_value, overlap, term_size) for significant terms. |
| """ |
| gene_set = set(g.upper() for g in gene_list) |
| k = len(gene_set) |
| N = background_size |
|
|
| results = [] |
| for term_name, term_genes in go_lib.items(): |
| term_upper = set(g.upper() for g in term_genes) |
| K = len(term_upper) |
| if K < 5 or K > N * 0.5: |
| continue |
| overlap = gene_set & term_upper |
| x = len(overlap) |
| if x < 2: |
| continue |
| |
| p_val = hypergeom.sf(x - 1, N, K, k) |
| results.append((term_name, p_val, x, K)) |
|
|
| |
| if not results: |
| return [] |
| results.sort(key=lambda r: r[1]) |
| n_tests = len(results) |
| corrected = [] |
| for i, (term, p, overlap, size) in enumerate(results): |
| adj_p = p * n_tests / (i + 1) |
| corrected.append((term, adj_p, overlap, size)) |
|
|
| |
| min_p = 1.0 |
| for i in range(len(corrected) - 1, -1, -1): |
| min_p = min(min_p, corrected[i][1]) |
| corrected[i] = (corrected[i][0], min_p, corrected[i][2], corrected[i][3]) |
|
|
| sig = [(t, p, o, s) for t, p, o, s in corrected if p < alpha] |
| return sig |
|
|
|
|
| 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_pipeline(adata, name): |
| """Run standard scPTR pipeline.""" |
| print(f"\n--- Pipeline: {name} ---") |
| 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) |
| scptr.tl.variance_decomposition(adata) |
| scptr.tl.pt_states(adata) |
| scptr.tl.pt_velocity(adata) |
| print(f" Done: {adata.shape}") |
| return adata |
|
|
|
|
| def get_expression(adata): |
| if hasattr(adata.X, 'toarray'): |
| return adata.X.toarray() |
| return np.asarray(adata.X) |
|
|
|
|
| def get_rbps_in_data(adata): |
| rbp_path = Path(__file__).parent.parent / "src" / "scptr" / "tools" / "data" / "known_rbps.csv" |
| rbps = pd.read_csv(rbp_path)["gene_symbol"].tolist() |
| gene_map = {g.upper(): i for i, g in enumerate(adata.var_names)} |
| result = {} |
| for r in rbps: |
| if r.upper() in gene_map: |
| result[r.upper()] = gene_map[r.upper()] |
| return result |
|
|
|
|
| def get_target_indices(adata, n_targets=200): |
| gamma = adata.layers["gamma"] |
| nonzero_frac = (gamma > 0).mean(axis=0) |
| informative = nonzero_frac >= 0.1 |
| gamma_var = np.var(gamma[:, informative], axis=0) |
| n = min(n_targets, informative.sum()) |
| top_idx = np.argsort(gamma_var)[-n:] |
| return np.where(informative)[0][top_idx] |
|
|
|
|
| |
| |
| |
| def experiment_a_go_enrichment(): |
| """Test whether predicted RBP targets share biological functions (GO enrichment). |
| |
| Uses local hypergeometric tests with cached GO BP gene sets — no API calls. |
| """ |
| print(f"\n{'='*60}") |
| print("EXPERIMENT A: NETWORK TARGET GO ENRICHMENT") |
| print(f"{'='*60}") |
|
|
| res_dir = OUTPUT_DIR / "results" |
| res_dir.mkdir(parents=True, exist_ok=True) |
|
|
| |
| print(" Loading GO Biological Process gene sets (local cache)...") |
| go_lib = load_go_library() |
| if go_lib is None: |
| return None |
| print(f" Loaded {len(go_lib)} GO BP terms") |
|
|
| |
| networks = {} |
| network_files = { |
| "pancreas": WEAKNESS_DIR / "corrected_network_pancreas.csv", |
| "dentate_gyrus": WEAKNESS_DIR / "corrected_network_dentate_gyrus.csv", |
| "neuroblastoma": TIER3_DIR / "neuroblastoma_network_corrected.csv", |
| } |
|
|
| for name, path in network_files.items(): |
| if path.exists(): |
| df = pd.read_csv(path) |
| print(f" {name}: {len(df)} edges") |
| networks[name] = df |
| else: |
| print(f" {name}: file not found at {path}") |
|
|
| |
| |
| BACKGROUND_SIZE = 20000 |
|
|
| all_results = {} |
|
|
| for ds_name, edges_df in networks.items(): |
| print(f"\n --- {ds_name} ---") |
|
|
| rbp_col = "rbp" |
| target_col = "target" |
|
|
| |
| rbp_targets = {} |
| for rbp, grp in edges_df.groupby(rbp_col): |
| targets = set(grp[target_col].tolist()) |
| rbp_targets[rbp] = targets |
|
|
| |
| all_targets = set() |
| for t in rbp_targets.values(): |
| all_targets |= t |
|
|
| |
| eligible_rbps = {r: t for r, t in rbp_targets.items() if len(t) >= 10} |
| print(f" RBPs with >= 10 targets: {len(eligible_rbps)}") |
|
|
| if not eligible_rbps: |
| all_results[ds_name] = {"n_eligible_rbps": 0} |
| continue |
|
|
| |
| rbp_enrichment_results = [] |
| n_with_sig = 0 |
|
|
| for rbp, targets in eligible_rbps.items(): |
| gene_list = list(targets) |
| sig_terms = hypergeometric_enrichment(gene_list, go_lib, BACKGROUND_SIZE) |
| n_sig = len(sig_terms) |
| has_sig = n_sig > 0 |
| if has_sig: |
| n_with_sig += 1 |
|
|
| top_terms = [t[0] for t in sig_terms[:5]] |
|
|
| rbp_enrichment_results.append({ |
| "rbp": rbp, |
| "n_targets": len(targets), |
| "n_sig_terms": n_sig, |
| "has_sig": has_sig, |
| "top_terms": top_terms, |
| }) |
|
|
| frac_with_sig = n_with_sig / max(len(eligible_rbps), 1) |
| print(f" RBPs with >= 1 significant GO term: {n_with_sig}/{len(eligible_rbps)} ({frac_with_sig:.1%})") |
|
|
| |
| known_biology = { |
| "ELAVL1": ["mRNA stability", "mRNA stabilization", "RNA stability"], |
| "RBFOX1": ["neuron", "neuronal", "synap", "axon"], |
| "RBFOX2": ["neuron", "neuronal", "synap", "splicing"], |
| "RBFOX3": ["neuron", "neuronal", "synap"], |
| "SRSF3": ["splic", "mRNA processing", "RNA processing"], |
| "HNRNPA1": ["splic", "mRNA processing", "RNA processing"], |
| "YBX1": ["translation", "mRNA", "RNA"], |
| "CELF2": ["splic", "neuron", "mRNA"], |
| } |
|
|
| concordance_hits = [] |
| for rbp_res in rbp_enrichment_results: |
| rbp = rbp_res["rbp"] |
| if rbp in known_biology and rbp_res["top_terms"]: |
| expected_keywords = known_biology[rbp] |
| all_terms_str = " ".join(rbp_res["top_terms"]).lower() |
| matched = [kw for kw in expected_keywords if kw.lower() in all_terms_str] |
| if matched: |
| concordance_hits.append({"rbp": rbp, "matched_keywords": matched}) |
| print(f" Known biology match: {rbp} -> {matched}") |
|
|
| |
| enriched_term_sets = {} |
| for rbp_res in rbp_enrichment_results: |
| if rbp_res["top_terms"]: |
| enriched_term_sets[rbp_res["rbp"]] = set(rbp_res["top_terms"]) |
|
|
| jaccard_values = [] |
| rbp_list = list(enriched_term_sets.keys()) |
| for i in range(len(rbp_list)): |
| for j in range(i + 1, len(rbp_list)): |
| s1 = enriched_term_sets[rbp_list[i]] |
| s2 = enriched_term_sets[rbp_list[j]] |
| union = s1 | s2 |
| if union: |
| jaccard_values.append(len(s1 & s2) / len(union)) |
|
|
| mean_jaccard = np.mean(jaccard_values) if jaccard_values else 0 |
| print(f" Cross-RBP GO term Jaccard (specificity): {mean_jaccard:.3f} (lower = more specific)") |
|
|
| |
| |
| print(f" Running bootstrap null (100 random genome-wide sets per RBP)...") |
| n_bootstrap = 100 |
| rng = np.random.RandomState(42) |
| |
| genome_genes = set() |
| for genes in go_lib.values(): |
| genome_genes.update(g.upper() for g in genes) |
| genome_genes_list = sorted(genome_genes) |
| bootstrap_fracs = [] |
|
|
| test_rbps = list(eligible_rbps.items())[:min(10, len(eligible_rbps))] |
| for rbp, targets in test_rbps: |
| n_t = len(targets) |
| null_sig_count = 0 |
| for _ in range(n_bootstrap): |
| random_genes = rng.choice(genome_genes_list, |
| size=min(n_t, len(genome_genes_list)), |
| replace=False).tolist() |
| sig_null = hypergeometric_enrichment(random_genes, go_lib, BACKGROUND_SIZE) |
| if sig_null: |
| null_sig_count += 1 |
| bootstrap_fracs.append(null_sig_count / n_bootstrap) |
|
|
| mean_null_frac = np.mean(bootstrap_fracs) if bootstrap_fracs else 0 |
| print(f" Bootstrap null fraction with sig GO term: {mean_null_frac:.3f}") |
| print(f" Enrichment over null: {frac_with_sig / max(mean_null_frac, 0.01):.1f}x") |
|
|
| all_results[ds_name] = { |
| "n_eligible_rbps": len(eligible_rbps), |
| "n_with_sig_go": n_with_sig, |
| "frac_with_sig_go": float(frac_with_sig), |
| "mean_cross_rbp_jaccard": float(mean_jaccard), |
| "n_known_biology_matches": len(concordance_hits), |
| "concordance_hits": concordance_hits, |
| "bootstrap_null_frac": float(mean_null_frac), |
| "per_rbp": rbp_enrichment_results, |
| } |
|
|
| |
| with open(res_dir / "go_enrichment.json", "w") as f: |
| json.dump(all_results, f, indent=2, default=str) |
|
|
| |
| ds_names = list(all_results.keys()) |
| fig, axes = plt.subplots(1, 2, figsize=(12, 5)) |
|
|
| |
| fracs = [all_results[d].get("frac_with_sig_go", 0) for d in ds_names] |
| null_fracs = [all_results[d].get("bootstrap_null_frac", 0) for d in ds_names] |
| x = np.arange(len(ds_names)) |
| width = 0.35 |
| axes[0].bar(x - width / 2, fracs, width, label="Real RBP targets", |
| color="#1976D2", edgecolor="black", linewidth=0.5) |
| axes[0].bar(x + width / 2, null_fracs, width, label="Random gene sets (null)", |
| color="#BDBDBD", edgecolor="black", linewidth=0.5) |
| axes[0].set_xticks(x) |
| axes[0].set_xticklabels(ds_names, fontsize=9) |
| axes[0].set_ylabel("Fraction with >= 1 sig GO term") |
| axes[0].set_title("GO Enrichment: Real vs Random Targets") |
| axes[0].legend(fontsize=8) |
| axes[0].set_ylim(0, 1.1) |
| for i, (f, n) in enumerate(zip(fracs, null_fracs)): |
| axes[0].text(i - width / 2, f + 0.02, f"{f:.0%}", ha="center", fontsize=8) |
| axes[0].text(i + width / 2, n + 0.02, f"{n:.0%}", ha="center", fontsize=8) |
|
|
| |
| jaccards = [all_results[d].get("mean_cross_rbp_jaccard", 0) for d in ds_names] |
| axes[1].bar(x, jaccards, color="#43A047", edgecolor="black", linewidth=0.5) |
| axes[1].set_xticks(x) |
| axes[1].set_xticklabels(ds_names, fontsize=9) |
| axes[1].set_ylabel("Mean Jaccard (lower = more specific)") |
| axes[1].set_title("Cross-RBP GO Term Specificity") |
| for i, j in enumerate(jaccards): |
| axes[1].text(i, j + 0.005, f"{j:.3f}", ha="center", fontsize=9) |
|
|
| fig.suptitle("Experiment A: Network Target GO Enrichment", fontsize=13) |
| fig.tight_layout() |
| save_fig(fig, "experiment_a_go_enrichment") |
|
|
| |
| print(f"\n EXPERIMENT A SUMMARY:") |
| for ds_name, res in all_results.items(): |
| print(f" {ds_name}: {res.get('frac_with_sig_go', 0):.0%} RBPs with sig GO terms " |
| f"(null: {res.get('bootstrap_null_frac', 0):.0%}, " |
| f"concordance: {res.get('n_known_biology_matches', 0)} hits)") |
|
|
| return all_results |
|
|
|
|
| |
| |
| |
| def experiment_b_pathway_consistency(datasets): |
| """Show pathway-level gamma consistency is higher than gene-level.""" |
| print(f"\n{'='*60}") |
| print("EXPERIMENT B: PATHWAY-LEVEL CROSS-DATASET CONSISTENCY") |
| print(f"{'='*60}") |
|
|
| res_dir = OUTPUT_DIR / "results" |
| res_dir.mkdir(parents=True, exist_ok=True) |
|
|
| |
| print(" Loading GO Biological Process gene sets (local cache)...") |
| go_lib = load_go_library() |
| if go_lib is None: |
| return None |
| print(f" Loaded {len(go_lib)} GO BP terms") |
|
|
| |
| gamma_medians = {} |
| for name, adata in datasets.items(): |
| gamma = adata.layers["gamma"] |
| gamma_med = np.median(gamma, axis=0) |
| gamma_medians[name] = pd.Series(gamma_med, index=[g.upper() for g in adata.var_names]) |
|
|
| names = sorted(datasets.keys()) |
| results = [] |
|
|
| for i, name_a in enumerate(names): |
| for name_b in names[i + 1:]: |
| print(f"\n --- {name_a} vs {name_b} ---") |
|
|
| ga = gamma_medians[name_a] |
| gb = gamma_medians[name_b] |
|
|
| |
| shared = sorted(set(ga.index) & set(gb.index)) |
| if len(shared) < 50: |
| continue |
|
|
| |
| ga_shared = ga[shared].values |
| gb_shared = gb[shared].values |
| valid = np.isfinite(ga_shared) & np.isfinite(gb_shared) |
| r_gene, p_gene = stats.spearmanr(ga_shared[valid], gb_shared[valid]) |
| print(f" Gene-level Spearman r: {r_gene:.4f} (n={valid.sum()})") |
|
|
| |
| |
| pathway_gamma_a = [] |
| pathway_gamma_b = [] |
| pathway_names = [] |
| pathway_sizes = [] |
|
|
| for term_name, term_genes in go_lib.items(): |
| |
| term_genes_upper = set(g.upper() for g in term_genes) |
| term_shared = term_genes_upper & set(shared) |
|
|
| if len(term_shared) < 10: |
| continue |
|
|
| genes_list = sorted(term_shared) |
| idx = [shared.index(g) for g in genes_list] |
|
|
| mean_a = np.mean(ga_shared[idx]) |
| mean_b = np.mean(gb_shared[idx]) |
|
|
| if np.isfinite(mean_a) and np.isfinite(mean_b): |
| pathway_gamma_a.append(mean_a) |
| pathway_gamma_b.append(mean_b) |
| pathway_names.append(term_name) |
| pathway_sizes.append(len(term_shared)) |
|
|
| if len(pathway_gamma_a) < 20: |
| print(f" Too few pathways with >= 10 shared genes: {len(pathway_gamma_a)}") |
| continue |
|
|
| r_pathway, p_pathway = stats.spearmanr(pathway_gamma_a, pathway_gamma_b) |
| print(f" Pathway-level Spearman r: {r_pathway:.4f} (n={len(pathway_gamma_a)} pathways)") |
| print(f" Improvement: {r_pathway:.3f} vs {r_gene:.3f} (gene-level)") |
|
|
| results.append({ |
| "pair": f"{name_a} vs {name_b}", |
| "gene_level_r": float(r_gene), |
| "gene_level_p": float(p_gene), |
| "n_shared_genes": int(valid.sum()), |
| "pathway_level_r": float(r_pathway), |
| "pathway_level_p": float(p_pathway), |
| "n_pathways": len(pathway_gamma_a), |
| "mean_pathway_size": float(np.mean(pathway_sizes)), |
| }) |
|
|
| |
| with open(res_dir / "pathway_consistency.json", "w") as f: |
| json.dump(results, f, indent=2) |
|
|
| |
| if results: |
| fig, ax = plt.subplots(figsize=(8, 5)) |
| pairs = [r["pair"] for r in results] |
| gene_rs = [r["gene_level_r"] for r in results] |
| pathway_rs = [r["pathway_level_r"] for r in results] |
|
|
| x = np.arange(len(pairs)) |
| width = 0.35 |
| ax.bar(x - width / 2, gene_rs, width, label="Gene-level", |
| color="#E53935", edgecolor="black", linewidth=0.5) |
| ax.bar(x + width / 2, pathway_rs, width, label="Pathway-level", |
| color="#1976D2", edgecolor="black", linewidth=0.5) |
| ax.set_xticks(x) |
| ax.set_xticklabels([p.replace(" vs ", "\nvs\n") for p in pairs], fontsize=8) |
| ax.set_ylabel("Spearman r") |
| ax.set_title("Gamma Consistency: Gene vs Pathway Level") |
| ax.legend() |
| for i, (g, p) in enumerate(zip(gene_rs, pathway_rs)): |
| ax.text(i - width / 2, g + 0.01, f"{g:.3f}", ha="center", fontsize=8) |
| ax.text(i + width / 2, p + 0.01, f"{p:.3f}", ha="center", fontsize=8) |
|
|
| fig.tight_layout() |
| save_fig(fig, "experiment_b_pathway_consistency") |
|
|
| print(f"\n EXPERIMENT B SUMMARY:") |
| for r in results: |
| print(f" {r['pair']}: gene r={r['gene_level_r']:.3f} -> pathway r={r['pathway_level_r']:.3f} " |
| f"({r['n_pathways']} pathways)") |
|
|
| return results |
|
|
|
|
| |
| |
| |
| def experiment_c_gamma_advantage(datasets): |
| """Demonstrate gamma's downstream task advantage over raw u/s ratio.""" |
| print(f"\n{'='*60}") |
| print("EXPERIMENT C: GAMMA vs RAW U/S ON DOWNSTREAM TASKS") |
| print(f"{'='*60}") |
|
|
| from sklearn.decomposition import PCA |
| from sklearn.cluster import KMeans |
| from sklearn.metrics import silhouette_score |
|
|
| res_dir = OUTPUT_DIR / "results" |
| res_dir.mkdir(parents=True, exist_ok=True) |
|
|
| all_results = {} |
|
|
| for ds_name, adata in datasets.items(): |
| if ds_name == "scifate": |
| continue |
| print(f"\n --- {ds_name} ---") |
|
|
| gamma = adata.layers["gamma"] |
| Ms = adata.layers["Ms"] |
| Mu = adata.layers["Mu"] |
|
|
| |
| reliable = Ms >= 0.01 |
| smooth_ratio = np.where(reliable, Mu / np.where(reliable, Ms, 1.0), 0.0) |
|
|
| |
| for gi in range(smooth_ratio.shape[1]): |
| col = smooth_ratio[:, gi] |
| pos = col[col > 0] |
| if len(pos) > 10: |
| cap = np.percentile(pos, 99) |
| smooth_ratio[:, gi] = np.clip(col, 0, cap) |
|
|
| |
| gene_medians = np.median(smooth_ratio, axis=0) |
| pos_medians = gene_medians[gene_medians > 0] |
| if len(pos_medians) > 0: |
| global_cap = 10 * np.percentile(pos_medians, 99) |
| smooth_ratio = np.clip(smooth_ratio, 0, global_cap) |
|
|
| print(f" Gamma shape: {gamma.shape}, max={gamma.max():.4f}") |
| print(f" Smooth ratio shape: {smooth_ratio.shape}, max={smooth_ratio.max():.4f}") |
|
|
| |
| clusters = adata.obs.get("clusters", adata.obs.get("cell_type")) |
| if clusters is None: |
| print(f" No cluster labels found, skipping") |
| continue |
| clusters = clusters.astype(str) |
|
|
| |
| print(f"\n Task 1: Invisible State Discovery") |
|
|
| invisible_results = {"gamma": [], "smooth_ratio": []} |
|
|
| for method_name, layer_data in [("gamma", gamma), ("smooth_ratio", smooth_ratio)]: |
| for cluster_name in sorted(clusters.unique()): |
| mask = (clusters == cluster_name).values |
| n_cells = mask.sum() |
| if n_cells < 50: |
| continue |
|
|
| sub = layer_data[mask] |
| n_pcs = min(15, n_cells - 1, sub.shape[1] - 1) |
| pca = PCA(n_components=n_pcs, random_state=42) |
| pcs = pca.fit_transform(sub) |
|
|
| best_k, best_sil, best_labels = 1, -1, np.zeros(n_cells, dtype=int) |
| for k in [2, 3]: |
| if n_cells < k * 10: |
| continue |
| km = KMeans(n_clusters=k, random_state=42, n_init=10) |
| labels = km.fit_predict(pcs) |
| if min(np.bincount(labels)) < 10: |
| continue |
| sil = silhouette_score(pcs, labels) |
| if sil > best_sil: |
| best_k, best_sil, best_labels = k, sil, labels |
|
|
| |
| expr_sub = get_expression(adata)[mask] |
| n_expr_pcs = min(15, n_cells - 1, expr_sub.shape[1] - 1) |
| pca_expr = PCA(n_components=n_expr_pcs, random_state=42) |
| expr_pcs = pca_expr.fit_transform(expr_sub) |
|
|
| if best_k > 1: |
| sil_method = best_sil |
| sil_expr = silhouette_score(expr_pcs, best_labels) |
| else: |
| sil_method = 0 |
| sil_expr = 0 |
|
|
| is_invisible = sil_method > 0.1 and sil_expr < 0.1 |
|
|
| invisible_results[method_name].append({ |
| "cluster": cluster_name, |
| "n_cells": n_cells, |
| "sil_method": float(sil_method), |
| "sil_expr": float(sil_expr), |
| "invisibility": float(sil_method - sil_expr), |
| "is_invisible": is_invisible, |
| }) |
|
|
| |
| gamma_invisible = sum(1 for r in invisible_results["gamma"] if r["is_invisible"]) |
| ratio_invisible = sum(1 for r in invisible_results["smooth_ratio"] if r["is_invisible"]) |
| gamma_mean_invis = np.mean([r["invisibility"] for r in invisible_results["gamma"]]) |
| ratio_mean_invis = np.mean([r["invisibility"] for r in invisible_results["smooth_ratio"]]) |
|
|
| print(f" Gamma: {gamma_invisible} invisible states, mean invisibility={gamma_mean_invis:.3f}") |
| print(f" Smooth ratio: {ratio_invisible} invisible states, mean invisibility={ratio_mean_invis:.3f}") |
|
|
| |
| print(f"\n Task 2: Cell-Type Variance Explained (eta-squared)") |
|
|
| cluster_labels = clusters.values |
| unique_clusters = np.unique(cluster_labels) |
|
|
| def compute_eta_squared(data, labels, unique_labels): |
| """Compute eta-squared (fraction of variance explained by groups).""" |
| n = data.shape[0] |
| grand_mean = data.mean(axis=0) |
| ss_total = np.sum((data - grand_mean) ** 2, axis=0) |
|
|
| ss_between = np.zeros(data.shape[1]) |
| for cl in unique_labels: |
| mask_cl = labels == cl |
| n_cl = mask_cl.sum() |
| if n_cl == 0: |
| continue |
| group_mean = data[mask_cl].mean(axis=0) |
| ss_between += n_cl * (group_mean - grand_mean) ** 2 |
|
|
| eta_sq = ss_between / np.clip(ss_total, 1e-10, None) |
| return eta_sq |
|
|
| eta_gamma = compute_eta_squared(gamma, cluster_labels, unique_clusters) |
| eta_ratio = compute_eta_squared(smooth_ratio, cluster_labels, unique_clusters) |
|
|
| |
| informative = (gamma > 0).mean(axis=0) >= 0.1 |
| eta_gamma_info = eta_gamma[informative] |
| eta_ratio_info = eta_ratio[informative] |
|
|
| gamma_wins = (eta_gamma_info > eta_ratio_info).sum() |
| ratio_wins = (eta_ratio_info > eta_gamma_info).sum() |
| total = len(eta_gamma_info) |
|
|
| print(f" Gamma eta-sq > smooth ratio: {gamma_wins}/{total} ({100*gamma_wins/total:.1f}%)") |
| print(f" Mean eta-sq — gamma: {eta_gamma_info.mean():.4f}, smooth ratio: {eta_ratio_info.mean():.4f}") |
|
|
| |
| w_stat, w_p = stats.wilcoxon(eta_gamma_info, eta_ratio_info) |
| print(f" Wilcoxon signed-rank p: {w_p:.2e}") |
|
|
| all_results[ds_name] = { |
| "invisible_states": { |
| "gamma_n_invisible": gamma_invisible, |
| "smooth_ratio_n_invisible": ratio_invisible, |
| "gamma_mean_invisibility": float(gamma_mean_invis), |
| "smooth_ratio_mean_invisibility": float(ratio_mean_invis), |
| "per_cluster": invisible_results, |
| }, |
| "eta_squared": { |
| "gamma_wins": int(gamma_wins), |
| "ratio_wins": int(ratio_wins), |
| "n_genes": int(total), |
| "gamma_mean": float(eta_gamma_info.mean()), |
| "ratio_mean": float(eta_ratio_info.mean()), |
| "wilcoxon_p": float(w_p), |
| }, |
| } |
|
|
| |
| with open(res_dir / "gamma_advantage.json", "w") as f: |
| json.dump(all_results, f, indent=2, default=str) |
|
|
| |
| fig, axes = plt.subplots(1, 2, figsize=(13, 5)) |
|
|
| |
| ds_labels = list(all_results.keys()) |
| gamma_invis = [all_results[d]["invisible_states"]["gamma_n_invisible"] for d in ds_labels] |
| ratio_invis = [all_results[d]["invisible_states"]["smooth_ratio_n_invisible"] for d in ds_labels] |
| x = np.arange(len(ds_labels)) |
| width = 0.35 |
| axes[0].bar(x - width / 2, gamma_invis, width, label="scPTR gamma", |
| color="#1976D2", edgecolor="black", linewidth=0.5) |
| axes[0].bar(x + width / 2, ratio_invis, width, label="Smooth u/s ratio (no beta)", |
| color="#E53935", edgecolor="black", linewidth=0.5) |
| axes[0].set_xticks(x) |
| axes[0].set_xticklabels(ds_labels, fontsize=9) |
| axes[0].set_ylabel("Number of invisible states") |
| axes[0].set_title("Invisible State Discovery") |
| axes[0].legend(fontsize=8) |
| for i, (g, r) in enumerate(zip(gamma_invis, ratio_invis)): |
| axes[0].text(i - width / 2, g + 0.1, str(g), ha="center", fontsize=9) |
| axes[0].text(i + width / 2, r + 0.1, str(r), ha="center", fontsize=9) |
|
|
| |
| gamma_means = [all_results[d]["eta_squared"]["gamma_mean"] for d in ds_labels] |
| ratio_means = [all_results[d]["eta_squared"]["ratio_mean"] for d in ds_labels] |
| axes[1].bar(x - width / 2, gamma_means, width, label="scPTR gamma", |
| color="#1976D2", edgecolor="black", linewidth=0.5) |
| axes[1].bar(x + width / 2, ratio_means, width, label="Smooth u/s ratio", |
| color="#E53935", edgecolor="black", linewidth=0.5) |
| axes[1].set_xticks(x) |
| axes[1].set_xticklabels(ds_labels, fontsize=9) |
| axes[1].set_ylabel("Mean eta-squared") |
| axes[1].set_title("Cell-Type Variance Explained") |
| axes[1].legend(fontsize=8) |
| for i, (g, r) in enumerate(zip(gamma_means, ratio_means)): |
| axes[1].text(i - width / 2, g + 0.001, f"{g:.4f}", ha="center", fontsize=8) |
| axes[1].text(i + width / 2, r + 0.001, f"{r:.4f}", ha="center", fontsize=8) |
|
|
| fig.suptitle("Experiment C: Gamma vs Smooth Ratio Downstream Tasks", fontsize=13) |
| fig.tight_layout() |
| save_fig(fig, "experiment_c_gamma_advantage") |
|
|
| print(f"\n EXPERIMENT C SUMMARY:") |
| for ds_name, res in all_results.items(): |
| inv = res["invisible_states"] |
| eta = res["eta_squared"] |
| print(f" {ds_name}: invisible states gamma={inv['gamma_n_invisible']} " |
| f"vs ratio={inv['smooth_ratio_n_invisible']}; " |
| f"eta-sq gamma={eta['gamma_mean']:.4f} vs ratio={eta['ratio_mean']:.4f} " |
| f"(p={eta['wilcoxon_p']:.2e})") |
|
|
| return all_results |
|
|
|
|
| |
| |
| |
| def experiment_d_nb_robustness(): |
| """Show NB network is internally robust via split-half cross-validation.""" |
| print(f"\n{'='*60}") |
| print("EXPERIMENT D: NB NETWORK SPLIT-HALF ROBUSTNESS") |
| print(f"{'='*60}") |
|
|
| import scanpy as sc |
|
|
| res_dir = OUTPUT_DIR / "results" |
| res_dir.mkdir(parents=True, exist_ok=True) |
|
|
| |
| h5ad_path = CACHE_DIR / "neuroblastoma.h5ad" |
| if not h5ad_path.exists(): |
| print(f" NB data not found at {h5ad_path}") |
| return None |
|
|
| print(" Loading neuroblastoma dataset...") |
| adata_full = sc.read_h5ad(str(h5ad_path)) |
| sc.pp.filter_genes(adata_full, min_cells=50) |
| adata_full.layers["raw_spliced"] = adata_full.layers["spliced"].copy() |
| adata_full.layers["raw_unspliced"] = adata_full.layers["unspliced"].copy() |
| print(f" Full dataset: {adata_full.shape}") |
|
|
| def run_nb_pipeline(adata): |
| """Run scPTR pipeline on NB data.""" |
| 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) |
| return adata |
|
|
| def infer_network(adata): |
| """Run partial-correlation network inference (library-size corrected).""" |
| gamma = adata.layers["gamma"] |
| expr = get_expression(adata) |
| rbps = get_rbps_in_data(adata) |
| n_cells = adata.n_obs |
|
|
| |
| lib_size = expr.sum(axis=1) |
| lib_rank = stats.rankdata(lib_size) |
| lib_rank_centered = lib_rank - lib_rank.mean() |
| lib_ss = np.dot(lib_rank_centered, lib_rank_centered) |
|
|
| if lib_ss < 1e-10: |
| return pd.DataFrame() |
|
|
| |
| informative = (gamma > 0).mean(axis=0) >= 0.1 |
| if informative.sum() < 20: |
| return pd.DataFrame() |
| gamma_var = np.var(gamma[:, informative], axis=0) |
| n_targets = min(200, informative.sum()) |
| top_var_idx = np.argsort(gamma_var)[-n_targets:] |
| info_indices = np.where(informative)[0] |
| target_indices = info_indices[top_var_idx] |
|
|
| |
| gamma_resid_map = {} |
| for ti in target_indices: |
| t_gamma = gamma[:, ti] |
| if np.std(t_gamma) < 1e-8: |
| continue |
| t_rank = stats.rankdata(t_gamma) |
| t_rank_c = t_rank - t_rank.mean() |
| slope = np.dot(lib_rank_centered, t_rank_c) / lib_ss |
| resid = t_rank - slope * lib_rank |
| resid_c = resid - resid.mean() |
| resid_std = np.sqrt(np.dot(resid_c, resid_c)) |
| if resid_std > 1e-8: |
| gamma_resid_map[ti] = (resid_c, resid_std) |
|
|
| edges = [] |
| for rbp_upper, rbp_idx in rbps.items(): |
| rbp_expr = expr[:, rbp_idx] |
| if np.std(rbp_expr) < 1e-6: |
| continue |
|
|
| rbp_rank = stats.rankdata(rbp_expr) |
| rbp_rank_c = rbp_rank - rbp_rank.mean() |
| slope_rbp = np.dot(lib_rank_centered, rbp_rank_c) / lib_ss |
| rbp_resid = rbp_rank - slope_rbp * lib_rank |
| rbp_resid_c = rbp_resid - rbp_resid.mean() |
| rbp_resid_std = np.sqrt(np.dot(rbp_resid_c, rbp_resid_c)) |
| if rbp_resid_std < 1e-8: |
| continue |
|
|
| for ti in target_indices: |
| if ti not in gamma_resid_map: |
| continue |
| g_resid_c, g_resid_std = gamma_resid_map[ti] |
| r_corr = np.dot(rbp_resid_c, g_resid_c) / (rbp_resid_std * g_resid_std) |
| r_corr = np.clip(r_corr, -1.0, 1.0) |
| df = n_cells - 3 |
| t_val = r_corr * np.sqrt(df / (1 - r_corr ** 2 + 1e-12)) |
| p_corr = 2 * stats.t.sf(abs(t_val), df) |
|
|
| if p_corr < 0.05 / (len(rbps) * n_targets): |
| edges.append({ |
| "rbp": rbp_upper, |
| "target": adata.var_names[ti], |
| "r": float(r_corr), |
| }) |
|
|
| return pd.DataFrame(edges) if edges else pd.DataFrame(columns=["rbp", "target", "r"]) |
|
|
| def get_top_hubs(edges_df, n=20): |
| if len(edges_df) == 0: |
| return [] |
| hub_counts = edges_df.groupby("rbp").size().sort_values(ascending=False) |
| return list(hub_counts.head(n).index) |
|
|
| |
| print("\n Running full-data pipeline...") |
| adata_full_processed = adata_full.copy() |
| adata_full_processed = run_nb_pipeline(adata_full_processed) |
| full_edges = infer_network(adata_full_processed) |
| full_hubs = get_top_hubs(full_edges, n=20) |
| full_hub_counts = full_edges.groupby("rbp").size() if len(full_edges) > 0 else pd.Series(dtype=int) |
| print(f" Full data: {len(full_edges)} edges, top hubs: {full_hubs[:5]}") |
|
|
| |
| n_replicates = 5 |
| rng = np.random.RandomState(42) |
| n_cells = adata_full.n_obs |
|
|
| replicate_results = [] |
|
|
| for rep_i in range(n_replicates): |
| print(f"\n Replicate {rep_i + 1}/{n_replicates}...") |
|
|
| |
| perm = rng.permutation(n_cells) |
| half1_idx = perm[:n_cells // 2] |
| half2_idx = perm[n_cells // 2:] |
|
|
| half_hubs = [] |
| half_hub_counts_list = [] |
|
|
| for half_name, cell_idx in [("half1", half1_idx), ("half2", half2_idx)]: |
| adata_half = adata_full[cell_idx].copy() |
| |
| adata_half.layers["spliced"] = adata_half.layers["raw_spliced"].copy() |
| adata_half.layers["unspliced"] = adata_half.layers["raw_unspliced"].copy() |
|
|
| try: |
| adata_half = run_nb_pipeline(adata_half) |
| edges_half = infer_network(adata_half) |
| hubs = get_top_hubs(edges_half, n=20) |
| hub_counts = edges_half.groupby("rbp").size() if len(edges_half) > 0 else pd.Series(dtype=int) |
| print(f" {half_name}: {len(edges_half)} edges, {len(hubs)} hubs") |
| except Exception as e: |
| print(f" {half_name}: pipeline failed: {e}") |
| hubs = [] |
| hub_counts = pd.Series(dtype=int) |
|
|
| half_hubs.append(set(hubs)) |
| half_hub_counts_list.append(hub_counts) |
|
|
| |
| if half_hubs[0] and half_hubs[1]: |
| union = half_hubs[0] | half_hubs[1] |
| intersection = half_hubs[0] & half_hubs[1] |
| jaccard = len(intersection) / len(union) if union else 0 |
|
|
| |
| shared_rbps = sorted(set(half_hub_counts_list[0].index) & set(half_hub_counts_list[1].index)) |
| if len(shared_rbps) >= 5: |
| c1 = [half_hub_counts_list[0].get(r, 0) for r in shared_rbps] |
| c2 = [half_hub_counts_list[1].get(r, 0) for r in shared_rbps] |
| r_hub, p_hub = stats.spearmanr(c1, c2) |
| else: |
| r_hub, p_hub = np.nan, np.nan |
|
|
| |
| jaccard_h1_full = len(half_hubs[0] & set(full_hubs)) / len(half_hubs[0] | set(full_hubs)) if (half_hubs[0] | set(full_hubs)) else 0 |
| jaccard_h2_full = len(half_hubs[1] & set(full_hubs)) / len(half_hubs[1] | set(full_hubs)) if (half_hubs[1] | set(full_hubs)) else 0 |
|
|
| print(f" Half-half Jaccard (top-20 hubs): {jaccard:.3f}") |
| print(f" Hub count Spearman r: {r_hub:.3f}") |
| print(f" Half1-vs-full Jaccard: {jaccard_h1_full:.3f}, Half2-vs-full: {jaccard_h2_full:.3f}") |
|
|
| replicate_results.append({ |
| "replicate": rep_i + 1, |
| "jaccard_half_half": float(jaccard), |
| "hub_count_spearman_r": float(r_hub) if not np.isnan(r_hub) else None, |
| "jaccard_half1_full": float(jaccard_h1_full), |
| "jaccard_half2_full": float(jaccard_h2_full), |
| "n_shared_rbps": len(shared_rbps), |
| "overlap_hubs": sorted(intersection), |
| }) |
| else: |
| replicate_results.append({ |
| "replicate": rep_i + 1, |
| "jaccard_half_half": 0, |
| "hub_count_spearman_r": None, |
| "jaccard_half1_full": 0, |
| "jaccard_half2_full": 0, |
| }) |
|
|
| |
| jaccards = [r["jaccard_half_half"] for r in replicate_results] |
| hub_rs = [r["hub_count_spearman_r"] for r in replicate_results if r["hub_count_spearman_r"] is not None] |
|
|
| mean_jaccard = np.mean(jaccards) |
| std_jaccard = np.std(jaccards) |
| mean_hub_r = np.mean(hub_rs) if hub_rs else np.nan |
|
|
| print(f"\n SUMMARY:") |
| print(f" Mean Jaccard (top-20 hubs): {mean_jaccard:.3f} +/- {std_jaccard:.3f}") |
| print(f" Mean hub count Spearman r: {mean_hub_r:.3f}") |
|
|
| results = { |
| "full_data_n_edges": len(full_edges), |
| "full_data_top_hubs": full_hubs, |
| "n_replicates": n_replicates, |
| "mean_jaccard": float(mean_jaccard), |
| "std_jaccard": float(std_jaccard), |
| "mean_hub_count_r": float(mean_hub_r) if not np.isnan(mean_hub_r) else None, |
| "replicates": replicate_results, |
| } |
|
|
| with open(res_dir / "nb_split_half.json", "w") as f: |
| json.dump(results, f, indent=2, default=str) |
|
|
| |
| fig, axes = plt.subplots(1, 2, figsize=(12, 5)) |
|
|
| |
| axes[0].bar(range(1, n_replicates + 1), jaccards, color="#1976D2", |
| edgecolor="black", linewidth=0.5) |
| axes[0].axhline(y=mean_jaccard, color="red", linestyle="--", |
| label=f"Mean={mean_jaccard:.3f}") |
| axes[0].set_xlabel("Replicate") |
| axes[0].set_ylabel("Jaccard similarity (top-20 hubs)") |
| axes[0].set_title("Split-Half Hub Consistency") |
| axes[0].legend() |
| axes[0].set_ylim(0, 1) |
|
|
| |
| if hub_rs: |
| axes[1].bar(range(1, len(hub_rs) + 1), hub_rs, color="#43A047", |
| edgecolor="black", linewidth=0.5) |
| axes[1].axhline(y=mean_hub_r, color="red", linestyle="--", |
| label=f"Mean={mean_hub_r:.3f}") |
| axes[1].set_xlabel("Replicate") |
| axes[1].set_ylabel("Spearman r (hub target counts)") |
| axes[1].set_title("Split-Half Hub Count Correlation") |
| axes[1].legend() |
| axes[1].set_ylim(-0.5, 1) |
|
|
| fig.suptitle("Experiment D: NB Network Split-Half Robustness", fontsize=13) |
| fig.tight_layout() |
| save_fig(fig, "experiment_d_nb_robustness") |
|
|
| return results |
|
|
|
|
| |
| |
| |
| def experiment_e_correction_quality(go_results): |
| """Compare GO enrichment quality between corrected and uncorrected networks.""" |
| print(f"\n{'='*60}") |
| print("EXPERIMENT E: CORRECTED vs UNCORRECTED NETWORK QUALITY") |
| print(f"{'='*60}") |
|
|
| res_dir = OUTPUT_DIR / "results" |
| res_dir.mkdir(parents=True, exist_ok=True) |
|
|
| |
| raw_nb_path = TIER3_DIR / "neuroblastoma_network_raw.csv" |
| corr_nb_path = TIER3_DIR / "neuroblastoma_network_corrected.csv" |
|
|
| |
| raw_panc_path = Path(__file__).parent.parent / "output" / "gap_analysis" / "results" / "network" / "pancreas" / "network_edges.csv" |
|
|
| networks_to_compare = {} |
|
|
| if raw_nb_path.exists() and corr_nb_path.exists(): |
| raw_nb = pd.read_csv(raw_nb_path) |
| corr_nb = pd.read_csv(corr_nb_path) |
| networks_to_compare["neuroblastoma"] = {"raw": raw_nb, "corrected": corr_nb} |
| print(f" NB raw: {len(raw_nb)} edges, corrected: {len(corr_nb)} edges") |
|
|
| if raw_panc_path.exists(): |
| raw_panc = pd.read_csv(raw_panc_path) |
| corr_panc_path = WEAKNESS_DIR / "corrected_network_pancreas.csv" |
| if corr_panc_path.exists(): |
| corr_panc = pd.read_csv(corr_panc_path) |
| networks_to_compare["pancreas"] = {"raw": raw_panc, "corrected": corr_panc} |
| print(f" Pancreas raw: {len(raw_panc)} edges, corrected: {len(corr_panc)} edges") |
|
|
| if not networks_to_compare: |
| print(" No raw/corrected network pairs found") |
| return None |
|
|
| |
| go_lib = load_go_library() |
| if go_lib is None: |
| return None |
|
|
| BACKGROUND_SIZE = 20000 |
|
|
| all_results = {} |
|
|
| for ds_name, net_pair in networks_to_compare.items(): |
| print(f"\n --- {ds_name} ---") |
|
|
| for method_name, edges_df in net_pair.items(): |
| print(f"\n {method_name} network ({len(edges_df)} edges):") |
|
|
| rbp_col = "rbp" |
| target_col = "target" |
|
|
| |
| rbp_targets = {} |
| for rbp, grp in edges_df.groupby(rbp_col): |
| rbp_key = rbp.upper() if isinstance(rbp, str) else str(rbp) |
| rbp_targets[rbp_key] = set(str(t) for t in grp[target_col]) |
|
|
| eligible = {r: t for r, t in rbp_targets.items() if len(t) >= 10} |
| print(f" RBPs with >= 10 targets: {len(eligible)}") |
|
|
| n_with_sig = 0 |
| for rbp, targets in eligible.items(): |
| gene_list = list(targets) |
| sig_terms = hypergeometric_enrichment(gene_list, go_lib, BACKGROUND_SIZE) |
| if sig_terms: |
| n_with_sig += 1 |
|
|
| frac = n_with_sig / max(len(eligible), 1) |
| print(f" Fraction with sig GO: {n_with_sig}/{len(eligible)} ({frac:.1%})") |
|
|
| key = f"{ds_name}_{method_name}" |
| all_results[key] = { |
| "dataset": ds_name, |
| "method": method_name, |
| "n_edges": len(edges_df), |
| "n_eligible_rbps": len(eligible), |
| "n_with_sig_go": n_with_sig, |
| "frac_with_sig_go": float(frac), |
| } |
|
|
| |
| with open(res_dir / "correction_quality.json", "w") as f: |
| json.dump(all_results, f, indent=2) |
|
|
| |
| print("\n Destabilizing fraction comparison:") |
| for ds_name, net_pair in networks_to_compare.items(): |
| for method_name, edges_df in net_pair.items(): |
| |
| r_col = None |
| for c in ["r", "spearman_r"]: |
| if c in edges_df.columns: |
| r_col = c |
| break |
| if r_col: |
| destab_frac = (edges_df[r_col] > 0).mean() |
| print(f" {ds_name} {method_name}: {destab_frac:.1%} destabilizing") |
|
|
| |
| fig, ax = plt.subplots(figsize=(8, 5)) |
|
|
| labels = [] |
| raw_fracs = [] |
| corr_fracs = [] |
|
|
| for ds_name in networks_to_compare: |
| raw_key = f"{ds_name}_raw" |
| corr_key = f"{ds_name}_corrected" |
| if raw_key in all_results and corr_key in all_results: |
| labels.append(ds_name) |
| raw_fracs.append(all_results[raw_key]["frac_with_sig_go"]) |
| corr_fracs.append(all_results[corr_key]["frac_with_sig_go"]) |
|
|
| if labels: |
| x = np.arange(len(labels)) |
| width = 0.35 |
| ax.bar(x - width / 2, raw_fracs, width, label="Raw (uncorrected)", |
| color="#E53935", edgecolor="black", linewidth=0.5) |
| ax.bar(x + width / 2, corr_fracs, width, label="Library-size corrected", |
| color="#1976D2", edgecolor="black", linewidth=0.5) |
| ax.set_xticks(x) |
| ax.set_xticklabels(labels, fontsize=9) |
| ax.set_ylabel("Fraction of RBPs with sig GO enrichment") |
| ax.set_title("GO Enrichment: Raw vs Corrected Networks") |
| ax.legend() |
| ax.set_ylim(0, 1.1) |
| for i, (r, c) in enumerate(zip(raw_fracs, corr_fracs)): |
| ax.text(i - width / 2, r + 0.02, f"{r:.0%}", ha="center", fontsize=9) |
| ax.text(i + width / 2, c + 0.02, f"{c:.0%}", ha="center", fontsize=9) |
|
|
| fig.tight_layout() |
| save_fig(fig, "experiment_e_correction_quality") |
|
|
| print(f"\n EXPERIMENT E SUMMARY:") |
| for ds_name in networks_to_compare: |
| raw_key = f"{ds_name}_raw" |
| corr_key = f"{ds_name}_corrected" |
| if raw_key in all_results and corr_key in all_results: |
| print(f" {ds_name}: raw GO={all_results[raw_key]['frac_with_sig_go']:.0%} " |
| f"-> corrected GO={all_results[corr_key]['frac_with_sig_go']:.0%}") |
|
|
| return all_results |
|
|
|
|
| |
| |
| |
| def main(): |
| set_figure_style() |
| OUTPUT_DIR.mkdir(parents=True, exist_ok=True) |
| (OUTPUT_DIR / "results").mkdir(parents=True, exist_ok=True) |
| (OUTPUT_DIR / "figures").mkdir(parents=True, exist_ok=True) |
|
|
| |
| go_results = experiment_a_go_enrichment() |
|
|
| |
| correction_results = experiment_e_correction_quality(go_results) |
|
|
| |
| print(f"\n{'='*60}") |
| print("LOADING DATASETS FOR EXPERIMENTS B, C") |
| print(f"{'='*60}") |
|
|
| adata_pan = scptr.datasets.pancreas() |
| adata_pan = run_pipeline(adata_pan, "pancreas") |
|
|
| adata_dg = scptr.datasets.dentate_gyrus() |
| adata_dg = run_pipeline(adata_dg, "dentate_gyrus") |
|
|
| |
| from run_scifate import load_scifate_data, prepare_for_scptr |
| adata_sf_raw = load_scifate_data() |
| adata_sf = prepare_for_scptr(adata_sf_raw) |
| adata_sf = run_pipeline(adata_sf, "scifate") |
|
|
| datasets = { |
| "pancreas": adata_pan, |
| "dentate_gyrus": adata_dg, |
| "scifate": adata_sf, |
| } |
|
|
| |
| pathway_results = experiment_b_pathway_consistency(datasets) |
|
|
| |
| gamma_adv_results = experiment_c_gamma_advantage(datasets) |
|
|
| |
| nb_results = experiment_d_nb_robustness() |
|
|
| |
| print(f"\n{'='*60}") |
| print("COMPREHENSIVE IMPROVEMENTS COMPLETE") |
| print(f"{'='*60}") |
|
|
| print("\n Experiment A (GO Enrichment):") |
| if go_results: |
| for ds, res in go_results.items(): |
| print(f" {ds}: {res.get('frac_with_sig_go', 0):.0%} RBPs enriched " |
| f"(null: {res.get('bootstrap_null_frac', 0):.0%})") |
|
|
| print("\n Experiment B (Pathway Consistency):") |
| if pathway_results: |
| for r in pathway_results: |
| print(f" {r['pair']}: gene r={r['gene_level_r']:.3f} -> " |
| f"pathway r={r['pathway_level_r']:.3f}") |
|
|
| print("\n Experiment C (Gamma Advantage):") |
| if gamma_adv_results: |
| for ds, res in gamma_adv_results.items(): |
| inv = res["invisible_states"] |
| eta = res["eta_squared"] |
| print(f" {ds}: invisible gamma={inv['gamma_n_invisible']} " |
| f"vs ratio={inv['smooth_ratio_n_invisible']}; " |
| f"eta-sq p={eta['wilcoxon_p']:.2e}") |
|
|
| print("\n Experiment D (NB Robustness):") |
| if nb_results: |
| print(f" Mean Jaccard (top-20): {nb_results['mean_jaccard']:.3f} " |
| f"+/- {nb_results['std_jaccard']:.3f}") |
|
|
| print("\n Experiment E (Correction Quality):") |
| if correction_results: |
| for key, res in correction_results.items(): |
| print(f" {key}: {res['frac_with_sig_go']:.0%} sig GO") |
|
|
| print(f"\n All results saved to: {OUTPUT_DIR.resolve()}") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|