{"spec_id":"ma-differential-expression","library":"seaborn","language":"python","code":"\"\"\" anyplot.ai\nma-differential-expression: MA Plot for Differential Expression\nLibrary: seaborn 0.13.2 | Python 3.13.14\nQuality: 88/100 | Updated: 2026-06-21\n\"\"\"\n\nimport os\nimport sys\n\n\n# Remove the script's own directory from sys.path so that sibling files\n# (matplotlib.py, seaborn.py, etc.) don't shadow installed packages.\ntry:\n    _here = os.path.realpath(os.path.dirname(__file__))\nexcept NameError:\n    _here = os.path.realpath(os.getcwd())\nsys.path = [p for p in sys.path if p and os.path.realpath(p) != _here]\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nimport seaborn as sns\nfrom statsmodels.nonparametric.smoothers_lowess import lowess as sm_lowess\n\n\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\n\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint palette — canonical order, first series always #009E73\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\nANYPLOT_AMBER = \"#DDCC77\"  # warning / caution (outside categorical pool)\n\n# Semantic mapping: up=green (positive/gain), down=matte-red (loss/negative)\nUP_COLOR = IMPRINT_PALETTE[0]  # #009E73 — first Imprint series, semantically \"positive/up\"\nSIG_COLOR = IMPRINT_PALETTE[1]  # #C475FD lavender — significant but sub-threshold fold change\nDOWN_COLOR = IMPRINT_PALETTE[4]  # #AE3030 matte red — semantic anchor for loss/negative\nNSIG_COLOR = INK_MUTED  # theme-adaptive muted gray for background noise\n\nLOESS_COLOR = IMPRINT_PALETTE[2]  # #4467A3 blue — trend overlay\nTHRESHOLD_COLOR = ANYPLOT_AMBER  # #DDCC77 — caution/threshold lines\n\nsns.set_theme(\n    style=\"ticks\",\n    rc={\n        \"figure.facecolor\": PAGE_BG,\n        \"axes.facecolor\": PAGE_BG,\n        \"axes.edgecolor\": INK_SOFT,\n        \"axes.labelcolor\": INK,\n        \"text.color\": INK,\n        \"xtick.color\": INK_SOFT,\n        \"ytick.color\": INK_SOFT,\n        \"grid.color\": INK,\n        \"grid.alpha\": 0.15,\n        \"legend.facecolor\": ELEVATED_BG,\n        \"legend.edgecolor\": INK_SOFT,\n    },\n)\n\n# --- Data ---\nnp.random.seed(42)\nn_genes = 15000\n\nmean_expression = np.random.exponential(scale=3, size=n_genes) + 1\nlog_fold_change = np.random.normal(0, 0.5, n_genes)\n\nn_de = int(n_genes * 0.08)\nde_indices = np.random.choice(n_genes, n_de, replace=False)\nlog_fold_change[de_indices] += np.random.choice([-1, 1], n_de) * np.random.uniform(1.5, 4, n_de)\n\np_values = np.ones(n_genes)\np_values[de_indices] = 10 ** (-np.random.uniform(2, 10, n_de))\np_values[~np.isin(np.arange(n_genes), de_indices)] = np.random.uniform(0.01, 1.0, n_genes - n_de)\n\nsignificant = p_values < 0.05\n\nstatus = np.where(\n    ~significant,\n    \"Not significant\",\n    np.where(log_fold_change > 1, \"Up-regulated\", np.where(log_fold_change < -1, \"Down-regulated\", \"Significant\")),\n)\n\n# Gene names spread across expression range for spatial storytelling\ngene_names = [None] * n_genes\ntop_gene_labels = [\"BRCA1\", \"TP53\", \"MYC\", \"EGFR\", \"VEGFA\", \"IL6\"]\nsig_de_mask = significant & (np.abs(log_fold_change) > 1)\nsig_de_indices = np.where(sig_de_mask)[0]\nsig_de_expr = mean_expression[sig_de_indices]\nsig_de_abs_lfc = np.abs(log_fold_change[sig_de_indices])\n\nexpr_min, expr_max = sig_de_expr.min(), sig_de_expr.max()\nn_labels = len(top_gene_labels)\nexpr_edges = np.linspace(expr_min, expr_max + 0.01, n_labels + 1)\ntop_sig = []\nfor b in range(n_labels):\n    in_bin = (sig_de_expr >= expr_edges[b]) & (sig_de_expr < expr_edges[b + 1])\n    if not np.any(in_bin):\n        continue\n    bin_idx = np.where(in_bin)[0]\n    best = bin_idx[np.argmax(sig_de_abs_lfc[bin_idx])]\n    top_sig.append(sig_de_indices[best])\n\nfor i, idx in enumerate(top_sig[:n_labels]):\n    gene_names[idx] = top_gene_labels[i]\n\ndf = pd.DataFrame(\n    {\n        \"Mean Expression (A)\": mean_expression,\n        \"Log₂ Fold Change (M)\": log_fold_change,\n        \"Status\": pd.Categorical(\n            status, categories=[\"Not significant\", \"Significant\", \"Up-regulated\", \"Down-regulated\"]\n        ),\n        \"gene_name\": gene_names,\n    }\n)\n\nstatus_palette = {\n    \"Not significant\": NSIG_COLOR,\n    \"Significant\": SIG_COLOR,\n    \"Up-regulated\": UP_COLOR,\n    \"Down-regulated\": DOWN_COLOR,\n}\n\n# --- Precompute LOESS + bootstrap CI (seaborn confidence-band pattern) ---\nx_all = df[\"Mean Expression (A)\"].values\ny_all = df[\"Log₂ Fold Change (M)\"].values\nsort_full = np.argsort(x_all)\nx_sorted, y_sorted = x_all[sort_full], y_all[sort_full]\n\n# Full-data LOESS for annotation placement\nloess_full = sm_lowess(y_sorted, x_sorted, frac=0.3, return_sorted=True)\n\n# Bootstrap CI on 2k subsample for speed (pattern mirrors seaborn's CI bands)\nrng_boot = np.random.default_rng(0)\nsub_idx = np.sort(rng_boot.choice(len(x_sorted), 2000, replace=False))\nxs_b, ys_b = x_sorted[sub_idx], y_sorted[sub_idx]\nx_grid = np.linspace(xs_b[0], xs_b[-1], 200)\n\nn_boot = 80\nboot_curves = np.empty((n_boot, len(x_grid)))\nfor bi in range(n_boot):\n    ri = rng_boot.integers(0, len(xs_b), len(xs_b))\n    xr, yr = xs_b[ri], ys_b[ri]\n    s = np.argsort(xr)\n    lw = sm_lowess(yr[s], xr[s], frac=0.3, return_sorted=True)\n    boot_curves[bi] = np.interp(x_grid, lw[:, 0], lw[:, 1])\n\nci_lo = np.percentile(boot_curves, 2.5, axis=0)\nci_hi = np.percentile(boot_curves, 97.5, axis=0)\n\n# --- Plot ---\nfig, ax = plt.subplots(figsize=(8, 4.5), dpi=400)\nfig.patch.set_facecolor(PAGE_BG)\nax.set_facecolor(PAGE_BG)\n\n# LOESS 95% CI band drawn first so data points render on top\nax.fill_between(x_grid, ci_lo, ci_hi, color=LOESS_COLOR, alpha=0.12, zorder=1, label=\"95% CI\")\n\n# Reference lines\nax.axhline(y=0, color=INK_SOFT, linewidth=1.5, alpha=0.6, zorder=2)\nax.axhline(y=1, color=THRESHOLD_COLOR, linewidth=1.2, linestyle=\"--\", alpha=0.85, zorder=2)\nax.axhline(y=-1, color=THRESHOLD_COLOR, linewidth=1.2, linestyle=\"--\", alpha=0.85, zorder=2)\n\n# Background layer: all 15k genes, small + transparent for density\nsns.scatterplot(\n    data=df,\n    x=\"Mean Expression (A)\",\n    y=\"Log₂ Fold Change (M)\",\n    hue=\"Status\",\n    hue_order=[\"Not significant\", \"Significant\", \"Up-regulated\", \"Down-regulated\"],\n    palette=status_palette,\n    size=\"Status\",\n    sizes={\"Not significant\": 6, \"Significant\": 14, \"Up-regulated\": 20, \"Down-regulated\": 20},\n    alpha=0.3,\n    edgecolor=\"none\",\n    legend=\"full\",\n    ax=ax,\n)\n\n# Emphasis layer: DE genes with subtle edge for definition\nde_data = df[df[\"Status\"].isin([\"Up-regulated\", \"Down-regulated\"])]\nsns.scatterplot(\n    data=de_data,\n    x=\"Mean Expression (A)\",\n    y=\"Log₂ Fold Change (M)\",\n    hue=\"Status\",\n    hue_order=[\"Up-regulated\", \"Down-regulated\"],\n    palette={\"Up-regulated\": UP_COLOR, \"Down-regulated\": DOWN_COLOR},\n    s=20,\n    alpha=0.6,\n    edgecolor=ELEVATED_BG,\n    linewidth=0.3,\n    legend=False,\n    ax=ax,\n)\n\n# Threshold annotations (raised to 8pt for mobile legibility)\nxlim = ax.get_xlim()\nx_lbl = xlim[1] * 0.97\nax.text(x_lbl, 1.12, \"2-fold ↑\", fontsize=8, color=THRESHOLD_COLOR, ha=\"right\", fontstyle=\"italic\")\nax.text(x_lbl, -1.28, \"2-fold ↓\", fontsize=8, color=THRESHOLD_COLOR, ha=\"right\", fontstyle=\"italic\")\n\n# LOESS smoothing curve (seaborn-distinctive lowess via regplot)\nsns.regplot(\n    data=df,\n    x=\"Mean Expression (A)\",\n    y=\"Log₂ Fold Change (M)\",\n    lowess=True,\n    scatter=False,\n    line_kws={\"color\": LOESS_COLOR, \"linewidth\": 2.0, \"alpha\": 0.9, \"label\": \"LOESS trend\"},\n    ax=ax,\n)\n\n# In-plot annotation foregrounding the key finding: flat LOESS = no expression bias\nx_annot = float(np.percentile(x_all, 86))\ny_annot = float(np.interp(x_annot, loess_full[:, 0], loess_full[:, 1]))\nax.text(\n    x_annot,\n    y_annot + 0.22,\n    \"No expression bias\",\n    fontsize=8,\n    fontstyle=\"italic\",\n    color=INK_MUTED,\n    ha=\"right\",\n    va=\"bottom\",\n)\n\n# Gene labels spread across expression range with refined offsets and thinner arrowheads\nlabeled = df[df[\"gene_name\"].notna()].copy()\nlabel_positions = []\nfor _, row in labeled.iterrows():\n    x_val = row[\"Mean Expression (A)\"]\n    y_val = row[\"Log₂ Fold Change (M)\"]\n    y_off = -24 if y_val > 0 else 24\n    x_off = 20 if x_val < df[\"Mean Expression (A)\"].median() else -20\n    for px, py in label_positions:\n        if abs(x_val - px) < 2 and abs(y_val - py) < 1:\n            y_off = y_off + (30 if y_off > 0 else -30)\n            break\n    label_positions.append((x_val, y_val))\n    ax.annotate(\n        row[\"gene_name\"],\n        xy=(x_val, y_val),\n        xytext=(x_off, y_off),\n        textcoords=\"offset points\",\n        fontsize=8,\n        fontweight=\"bold\",\n        color=INK,\n        arrowprops={\"arrowstyle\": \"->\", \"color\": INK_SOFT, \"lw\": 0.6, \"connectionstyle\": \"arc3,rad=0.15\"},\n        bbox={\n            \"boxstyle\": \"round,pad=0.2\",\n            \"facecolor\": ELEVATED_BG,\n            \"edgecolor\": INK_SOFT,\n            \"alpha\": 0.92,\n            \"linewidth\": 0.4,\n        },\n    )\n\n# Style\nsns.despine(ax=ax)\nax.set_xlabel(\"Mean Expression (A)\", fontsize=10, color=INK)\nax.set_ylabel(\"Log₂ Fold Change (M)\", fontsize=10, color=INK)\nax.set_title(\n    \"ma-differential-expression · python · seaborn · anyplot.ai\", fontsize=12, fontweight=\"medium\", pad=10, color=INK\n)\nax.tick_params(axis=\"both\", labelsize=8, colors=INK_SOFT)\nax.yaxis.grid(True, alpha=0.15, linewidth=0.6, color=INK)\n\nsns.move_legend(ax, \"upper right\", fontsize=8, framealpha=0.92, title=\"Gene Status\", title_fontsize=8)\n\nfig.subplots_adjust(left=0.10, right=0.97, top=0.91, bottom=0.13)\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\nplt.close()\n"}