| |
| """Final publication fixes: address critical reviewer concerns. |
| |
| 1. Fix Fisher's exact test bug in hub consistency (contingency table was wrong) |
| 2. Pathway specificity analysis (scPTR vs unspliced-only: unique vs generic pathways) |
| 3. Per-cell sci-fate: stratify by expression level to show where scPTR advantage is largest |
| """ |
|
|
| 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 |
|
|
| OUTPUT_DIR = Path(__file__).parent.parent / "output" / "final_fixes" |
| PROJECT_ROOT = Path(__file__).parent.parent |
|
|
|
|
| def save_fig(fig, name, subdir="figures"): |
| if fig is None: |
| return |
| 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 fix_hub_fisher(): |
| """Recompute Fisher's exact using only shared RBPs as the universe.""" |
| print("\n" + "=" * 60) |
| print("1. CORRECTED FISHER'S EXACT FOR HUB CONSISTENCY") |
| print("=" * 60) |
|
|
| res_dir = OUTPUT_DIR / "results" |
| res_dir.mkdir(parents=True, exist_ok=True) |
|
|
| |
| hub_files = { |
| "pancreas": PROJECT_ROOT / "output" / "gap_analysis" / "results" / "network" / "pancreas" / "rbp_hub_counts.csv", |
| "dentate_gyrus": PROJECT_ROOT / "output" / "gap_analysis" / "results" / "network" / "dentate_gyrus" / "rbp_hub_counts.csv", |
| } |
|
|
| hub_counts = {} |
| for name, path in hub_files.items(): |
| if path.exists(): |
| df = pd.read_csv(path) |
| count_col = [c for c in df.columns if c != "rbp"][0] |
| hub_counts[name] = pd.Series(df[count_col].values, index=df["rbp"].values) |
|
|
| |
| nb_net_path = PROJECT_ROOT / "output" / "tier3" / "results" / "neuroblastoma_network_corrected.csv" |
| if nb_net_path.exists(): |
| nb_net = pd.read_csv(nb_net_path) |
| hub_counts["neuroblastoma"] = nb_net.groupby("rbp").size().sort_values(ascending=False) |
|
|
| |
| hub_upper = {} |
| for name, series in hub_counts.items(): |
| hub_upper[name] = pd.Series(series.values, index=[g.upper() for g in series.index]) |
|
|
| names = sorted(hub_upper.keys()) |
| results = [] |
|
|
| print("\n Corrected Fisher's exact (universe = shared RBPs only):") |
| for i, name_a in enumerate(names): |
| for j in range(len(names)): |
| if i == j: |
| continue |
| name_b = names[j] |
| shared = set(hub_upper[name_a].index) & set(hub_upper[name_b].index) |
| n_shared = len(shared) |
| if n_shared < 5: |
| continue |
|
|
| |
| shared_a = hub_upper[name_a].reindex(list(shared)).dropna().sort_values(ascending=False) |
| shared_b = hub_upper[name_b].reindex(list(shared)).dropna().sort_values(ascending=False) |
|
|
| |
| k_a = min(5, n_shared // 3) |
| k_b = min(10, n_shared // 2) |
|
|
| top_a = set(shared_a.index[:k_a]) |
| top_b = set(shared_b.index[:k_b]) |
|
|
| |
| a_and_b = len(top_a & top_b) |
| a_not_b = len(top_a - top_b) |
| b_not_a = len(top_b - top_a) |
| neither = n_shared - a_and_b - a_not_b - b_not_a |
|
|
| table = [[a_and_b, a_not_b], [b_not_a, neither]] |
| odds_ratio, fisher_p = stats.fisher_exact(table, alternative="greater") |
| print(f" Top-{k_a} {name_a} in top-{k_b} {name_b}: " |
| f"{a_and_b}/{k_a} overlap, OR={odds_ratio:.2f}, p={fisher_p:.4f} " |
| f"(n_shared={n_shared})") |
|
|
| results.append({ |
| "source": name_a, "target": name_b, |
| "k_source": k_a, "k_target": k_b, |
| "overlap": a_and_b, "n_shared": n_shared, |
| "odds_ratio": float(odds_ratio), "fisher_p": float(fisher_p), |
| }) |
|
|
| with open(res_dir / "corrected_hub_fisher.json", "w") as f: |
| json.dump(results, f, indent=2) |
|
|
| return results |
|
|
|
|
| |
| |
| |
| def pathway_specificity(): |
| """Analyze whether scPTR finds different or more specific pathways |
| than unspliced-only, rather than just counting totals.""" |
| print("\n" + "=" * 60) |
| print("2. PATHWAY SPECIFICITY ANALYSIS") |
| print("=" * 60) |
|
|
| res_dir = OUTPUT_DIR / "results" |
| res_dir.mkdir(parents=True, exist_ok=True) |
|
|
| |
| coherence_csv = PROJECT_ROOT / "output" / "comprehensive_fixes" / "results" / "coherence_ablation.csv" |
| pathways_csv = PROJECT_ROOT / "output" / "comprehensive_fixes" / "results" / "coherence_ablation_pathways.csv" |
|
|
| coherence_df = pd.read_csv(coherence_csv) |
| pathways_df = pd.read_csv(pathways_csv) |
|
|
| |
| generic_pathways = { |
| "ribosome", "oxidative phosphorylation", "thermogenesis", |
| "huntington disease", "alzheimer disease", "parkinson disease", |
| "non-alcoholic fatty liver disease", "cardiac muscle contraction", |
| "diabetic cardiomyopathy", "chemical carcinogenesis", |
| "metabolic pathways", "carbon metabolism", |
| } |
|
|
| |
| tissue_specific = { |
| "pancreas": { |
| "protein processing in endoplasmic reticulum", "autophagy", |
| "insulin secretion", "insulin signaling pathway", |
| "pancreatic secretion", "maturity onset diabetes", |
| "unfolded protein response", "protein folding", |
| }, |
| "dentate_gyrus": { |
| "synaptic vesicle cycle", "long-term potentiation", |
| "glutamatergic synapse", "gabaergic synapse", |
| "axon guidance", "neurotrophin signaling pathway", |
| "dopaminergic synapse", "serotonergic synapse", |
| }, |
| } |
|
|
| results = {} |
|
|
| for dataset in ["pancreas", "dentate_gyrus"]: |
| print(f"\n--- {dataset} ---") |
| ds_paths = pathways_df[pathways_df["dataset"] == dataset] |
| ds_coherence = coherence_df[coherence_df["dataset"] == dataset] |
|
|
| expected_set = tissue_specific.get(dataset, set()) |
|
|
| for method in ["scPTR_gamma", "raw_u_s_ratio", "unspliced_only"]: |
| method_paths = ds_paths[ds_paths["method"] == method] |
| all_terms = [t.lower() for t in method_paths["pathway"].values] |
|
|
| n_total = len(all_terms) |
| n_generic = sum(1 for t in all_terms |
| if any(g in t for g in generic_pathways)) |
| n_tissue = sum(1 for t in all_terms |
| if any(ts in t for ts in expected_set)) |
| n_specific = n_total - n_generic |
|
|
| |
| other_methods = [m for m in ["scPTR_gamma", "raw_u_s_ratio", "unspliced_only"] |
| if m != method] |
| other_terms = set() |
| for om in other_methods: |
| om_paths = ds_paths[ds_paths["method"] == om] |
| other_terms |= set(t.lower() for t in om_paths["pathway"].values) |
|
|
| unique_terms = [t for t in all_terms if t not in other_terms] |
| n_unique = len(unique_terms) |
|
|
| |
| mc = ds_coherence[ds_coherence["method"] == method] |
|
|
| key = f"{dataset}_{method}" |
| results[key] = { |
| "dataset": dataset, "method": method, |
| "n_total_pathways": n_total, |
| "n_generic": n_generic, |
| "n_tissue_specific": n_tissue, |
| "n_non_generic": n_specific, |
| "n_unique_to_method": n_unique, |
| "generic_fraction": n_generic / max(n_total, 1), |
| "mean_invisibility": float(mc["invisibility"].mean()) if len(mc) > 0 else 0, |
| "mean_diff_genes": float(mc["n_diff_genes"].mean()) if len(mc) > 0 else 0, |
| } |
|
|
| print(f" {method:<20s}: {n_total} total, {n_generic} generic, " |
| f"{n_tissue} tissue-specific, {n_unique} unique") |
|
|
| |
| print("\n Per-cluster: do all methods find the same expected pathways?") |
| clusters_tested = coherence_df["cluster"].unique() |
| agreement_data = [] |
|
|
| for cluster in clusters_tested: |
| cluster_data = coherence_df[coherence_df["cluster"] == cluster] |
| for _, row in cluster_data.iterrows(): |
| agreement_data.append({ |
| "cluster": cluster, |
| "dataset": row["dataset"], |
| "method": row["method"], |
| "n_expected": row["n_expected_pathways"], |
| "n_sig": row["n_sig_pathways"], |
| "n_diff_genes": row["n_diff_genes"], |
| }) |
|
|
| agreement_df = pd.DataFrame(agreement_data) |
|
|
| |
| print("\n Per-cluster winner (most expected pathways):") |
| winner_counts = {"scPTR_gamma": 0, "raw_u_s_ratio": 0, "unspliced_only": 0, "tie": 0} |
|
|
| for cluster in clusters_tested: |
| cl = agreement_df[agreement_df["cluster"] == cluster] |
| if len(cl) == 0: |
| continue |
| max_expected = cl["n_expected"].max() |
| winners = cl[cl["n_expected"] == max_expected]["method"].tolist() |
| if len(winners) == 1: |
| winner_counts[winners[0]] += 1 |
| else: |
| winner_counts["tie"] += 1 |
|
|
| for method, count in winner_counts.items(): |
| print(f" {method}: wins {count}/{len(clusters_tested)} clusters") |
|
|
| results["winner_counts"] = winner_counts |
|
|
| with open(res_dir / "pathway_specificity.json", "w") as f: |
| json.dump(results, f, indent=2, default=str) |
|
|
| |
| fig, axes = plt.subplots(1, 2, figsize=(12, 5)) |
|
|
| |
| methods = ["scPTR_gamma", "raw_u_s_ratio", "unspliced_only"] |
| method_labels = ["scPTR\ngamma", "Raw u/s\nratio", "Unspliced\nonly"] |
| x = np.arange(len(methods)) |
|
|
| for di, dataset in enumerate(["pancreas", "dentate_gyrus"]): |
| offset = di * 0.35 - 0.175 |
| generics = [] |
| tissues = [] |
| others = [] |
| for m in methods: |
| key = f"{dataset}_{m}" |
| if key in results: |
| r = results[key] |
| generics.append(r["n_generic"]) |
| tissues.append(r["n_tissue_specific"]) |
| others.append(r["n_non_generic"] - r["n_tissue_specific"]) |
| else: |
| generics.append(0) |
| tissues.append(0) |
| others.append(0) |
|
|
| color_generic = "lightgray" if di == 0 else "silver" |
| color_tissue = "steelblue" if di == 0 else "darkorange" |
| color_other = "lightblue" if di == 0 else "moccasin" |
|
|
| axes[0].bar(x + offset, tissues, 0.3, label=f"{dataset} tissue-specific", |
| color=color_tissue, edgecolor="black", linewidth=0.3) |
| axes[0].bar(x + offset, others, 0.3, bottom=tissues, |
| label=f"{dataset} other", color=color_other, |
| edgecolor="black", linewidth=0.3) |
| axes[0].bar(x + offset, generics, 0.3, |
| bottom=[t + o for t, o in zip(tissues, others)], |
| label=f"{dataset} generic", color=color_generic, |
| edgecolor="black", linewidth=0.3) |
|
|
| axes[0].set_xticks(x) |
| axes[0].set_xticklabels(method_labels) |
| axes[0].set_ylabel("Number of top pathways") |
| axes[0].set_title("Pathway Composition by Method") |
| axes[0].legend(fontsize=6, ncol=2) |
|
|
| |
| cats = list(winner_counts.keys()) |
| vals = [winner_counts[c] for c in cats] |
| colors_bar = ["steelblue", "orange", "lightblue", "gray"] |
| axes[1].bar(cats, vals, color=colors_bar, edgecolor="black", linewidth=0.5) |
| axes[1].set_ylabel("Number of clusters won") |
| axes[1].set_title("Per-Cluster: Most Expected Pathways") |
| for i, v in enumerate(vals): |
| axes[1].text(i, v + 0.2, str(v), ha="center", fontsize=10, fontweight="bold") |
|
|
| fig.suptitle("Pathway Specificity Analysis", fontsize=13, y=1.02) |
| fig.tight_layout() |
| save_fig(fig, "pathway_specificity") |
|
|
| return results |
|
|
|
|
| |
| |
| |
| def percell_stratified(): |
| """Show that scPTR's advantage over raw u/s increases for |
| low-expression genes, where smoothing matters most.""" |
| print("\n" + "=" * 60) |
| print("3. PER-CELL SCI-FATE STRATIFIED BY EXPRESSION LEVEL") |
| print("=" * 60) |
|
|
| res_dir = OUTPUT_DIR / "results" |
| res_dir.mkdir(parents=True, exist_ok=True) |
|
|
| from run_scifate import load_scifate_data, prepare_for_scptr |
| import scptr |
|
|
| adata_raw = load_scifate_data() |
| adata = prepare_for_scptr(adata_raw) |
|
|
| 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" Pipeline complete: {adata.shape}") |
|
|
| gamma = adata.layers["gamma"] |
| u_layer = adata.layers.get("Mu", adata.layers.get("unspliced")) |
| s_layer = adata.layers.get("Ms", adata.layers.get("spliced")) |
| u = u_layer.toarray() if hasattr(u_layer, 'toarray') else np.asarray(u_layer) |
| s = s_layer.toarray() if hasattr(s_layer, 'toarray') else np.asarray(s_layer) |
|
|
| raw_ratio = np.zeros_like(gamma) |
| s_safe = np.where(s > 0.01, s, 1.0) |
| raw_ratio = u / s_safe |
| raw_ratio[s < 0.01] = 0 |
|
|
| |
| total_raw = np.asarray(adata_raw.X.toarray() if hasattr(adata_raw.X, 'toarray') else adata_raw.X) |
| new_raw = np.asarray(adata_raw.layers["new"].toarray() if hasattr(adata_raw.layers["new"], 'toarray') else adata_raw.layers["new"]) |
| old_raw = total_raw - new_raw |
|
|
| raw_gene_map = {g: i for i, g in enumerate(adata_raw.var_names)} |
| filtered_in_raw = [raw_gene_map[g] for g in adata.var_names if g in raw_gene_map] |
| genes_in_both = [g for g in adata.var_names if g in raw_gene_map] |
| gene_idx_in_filtered = [list(adata.var_names).index(g) for g in genes_in_both] |
|
|
| gt_new = new_raw[:, filtered_in_raw] |
| gt_old = old_raw[:, filtered_in_raw] |
| gt_total = total_raw[:, filtered_in_raw] |
| gt_ratio = np.zeros_like(gt_new, dtype=float) |
| valid_gt = gt_old > 0.1 |
| gt_ratio[valid_gt] = gt_new[valid_gt] / gt_old[valid_gt] |
| gt_ratio[~valid_gt] = np.nan |
|
|
| gamma_matched = gamma[:, gene_idx_in_filtered] |
| raw_matched = raw_ratio[:, gene_idx_in_filtered] |
|
|
| |
| gene_mean_expr = gt_total.mean(axis=0) |
| terciles = np.percentile(gene_mean_expr[gene_mean_expr > 0], [33, 67]) |
|
|
| strata = { |
| "low": gene_mean_expr <= terciles[0], |
| "medium": (gene_mean_expr > terciles[0]) & (gene_mean_expr <= terciles[1]), |
| "high": gene_mean_expr > terciles[1], |
| } |
|
|
| n_cells = adata.n_obs |
| results = {} |
|
|
| for stratum_name, gene_mask in strata.items(): |
| n_genes_stratum = gene_mask.sum() |
| print(f"\n --- {stratum_name} expression ({n_genes_stratum} genes) ---") |
|
|
| gamma_corrs = [] |
| raw_corrs = [] |
|
|
| for i in range(n_cells): |
| gt_i = gt_ratio[i, gene_mask] |
| gamma_i = gamma_matched[i, gene_mask] |
| raw_i = raw_matched[i, gene_mask] |
|
|
| valid = np.isfinite(gt_i) & (gt_i > 0) & (gamma_i > 0) & (raw_i > 0) |
| if valid.sum() >= 10: |
| r_g, _ = stats.spearmanr(gamma_i[valid], gt_i[valid]) |
| r_r, _ = stats.spearmanr(raw_i[valid], gt_i[valid]) |
| gamma_corrs.append(r_g) |
| raw_corrs.append(r_r) |
|
|
| gamma_corrs = np.array(gamma_corrs) |
| raw_corrs = np.array(raw_corrs) |
|
|
| |
| finite_mask = np.isfinite(gamma_corrs) & np.isfinite(raw_corrs) |
| gamma_corrs = gamma_corrs[finite_mask] |
| raw_corrs = raw_corrs[finite_mask] |
|
|
| if len(gamma_corrs) < 10: |
| print(f" Skipped: only {len(gamma_corrs)} valid cells after NaN filtering") |
| continue |
|
|
| mean_g = np.mean(gamma_corrs) |
| mean_r = np.mean(raw_corrs) |
| gamma_wins = (gamma_corrs > raw_corrs).sum() |
| n_valid = len(gamma_corrs) |
| w_stat, w_p = stats.wilcoxon(gamma_corrs, raw_corrs, alternative="greater") |
|
|
| print(f" scPTR gamma: mean r = {mean_g:.4f}") |
| print(f" Raw u/s: mean r = {mean_r:.4f}") |
| print(f" Advantage: {mean_g - mean_r:.4f}") |
| print(f" gamma wins: {gamma_wins}/{n_valid} ({100*gamma_wins/n_valid:.1f}%)") |
| print(f" Wilcoxon p: {w_p:.2e}") |
|
|
| results[stratum_name] = { |
| "n_genes": int(n_genes_stratum), |
| "n_valid_cells": int(n_valid), |
| "mean_gamma_corr": float(mean_g), |
| "mean_raw_corr": float(mean_r), |
| "advantage": float(mean_g - mean_r), |
| "gamma_wins_frac": float(gamma_wins / n_valid), |
| "wilcoxon_p": float(w_p), |
| } |
|
|
| with open(res_dir / "percell_stratified.json", "w") as f: |
| json.dump(results, f, indent=2) |
|
|
| |
| fig, axes = plt.subplots(1, 2, figsize=(11, 5)) |
|
|
| strata_order = ["low", "medium", "high"] |
| strata_labels = ["Low\nexpr", "Medium\nexpr", "High\nexpr"] |
|
|
| |
| gamma_means = [results.get(s, {}).get("mean_gamma_corr", 0) for s in strata_order] |
| raw_means = [results.get(s, {}).get("mean_raw_corr", 0) for s in strata_order] |
| x = np.arange(len(strata_order)) |
| axes[0].bar(x - 0.15, gamma_means, 0.3, label="scPTR gamma", |
| color="steelblue", edgecolor="black", linewidth=0.5) |
| axes[0].bar(x + 0.15, raw_means, 0.3, label="Raw u/s", |
| color="salmon", edgecolor="black", linewidth=0.5) |
| axes[0].set_xticks(x) |
| axes[0].set_xticklabels(strata_labels) |
| axes[0].set_ylabel("Mean per-cell Spearman r") |
| axes[0].set_title("Per-Cell Correlation by Expression Level") |
| axes[0].legend() |
|
|
| |
| advantages = [results.get(s, {}).get("advantage", 0) for s in strata_order] |
| p_values = [results.get(s, {}).get("wilcoxon_p", 1) for s in strata_order] |
| colors = ["steelblue" if a > 0 else "salmon" for a in advantages] |
| axes[1].bar(strata_labels, advantages, color=colors, edgecolor="black", linewidth=0.5) |
| axes[1].set_ylabel("scPTR advantage (gamma r - raw r)") |
| axes[1].set_title("scPTR Advantage by Expression Level") |
| axes[1].axhline(0, color="gray", linestyle="--", alpha=0.3) |
| for i, (a, p) in enumerate(zip(advantages, p_values)): |
| sig = "***" if p < 0.001 else "**" if p < 0.01 else "*" if p < 0.05 else "ns" |
| axes[1].text(i, a + 0.001 if a > 0 else a - 0.002, |
| f"{a:.4f}\n({sig})", ha="center", fontsize=9) |
|
|
| fig.suptitle("scPTR Advantage Stratified by Gene Expression Level", |
| fontsize=13, y=1.02) |
| fig.tight_layout() |
| save_fig(fig, "percell_stratified") |
|
|
| return 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) |
|
|
| all_results = {} |
|
|
| |
| all_results["hub_fisher"] = fix_hub_fisher() |
|
|
| |
| all_results["pathway_specificity"] = pathway_specificity() |
|
|
| |
| all_results["percell_stratified"] = percell_stratified() |
|
|
| with open(OUTPUT_DIR / "results" / "all_final_fixes.json", "w") as f: |
| json.dump(all_results, f, indent=2, default=str) |
|
|
| print("\n" + "=" * 60) |
| print("ALL FINAL FIXES COMPLETE") |
| print("=" * 60) |
| print(f"Results saved to: {OUTPUT_DIR.resolve()}") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|