{"spec_id":"funnel-meta-analysis","library":"seaborn","language":"python","code":"\"\"\" anyplot.ai\nfunnel-meta-analysis: Meta-Analysis Funnel Plot for Publication Bias\nLibrary: seaborn 0.13.2 | Python 3.13.13\nQuality: 87/100 | Updated: 2026-06-10\n\"\"\"\n\nimport os\nimport sys\n\n\n# Remove script directory from sys.path to prevent sibling .py files from\n# shadowing installed packages (e.g. matplotlib.py → import matplotlib conflict)\n_script_dir = os.path.dirname(os.path.abspath(__file__))\nif _script_dir in sys.path:\n    sys.path.remove(_script_dir)\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nimport seaborn as sns\nfrom matplotlib.lines import Line2D\n\n\n# Theme-adaptive chrome tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\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\"]\n\n# Seaborn theme with theme-adaptive chrome\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_SOFT,\n        \"grid.alpha\": 0.2,\n        \"grid.linewidth\": 0.6,\n        \"axes.grid\": False,\n        \"legend.facecolor\": ELEVATED_BG,\n        \"legend.edgecolor\": INK_SOFT,\n        \"font.family\": \"sans-serif\",\n    },\n)\nsns.set_context(\"notebook\", font_scale=1.0)\n\n# Data\nnp.random.seed(42)\n\nstudies = [\n    \"Adams 2018\",\n    \"Baker 2019\",\n    \"Chen 2017\",\n    \"Diaz 2020\",\n    \"Evans 2016\",\n    \"Fischer 2021\",\n    \"Garcia 2019\",\n    \"Hughes 2018\",\n    \"Ibrahim 2020\",\n    \"Jones 2017\",\n    \"Kim 2021\",\n    \"Lee 2019\",\n    \"Martinez 2020\",\n    \"Novak 2018\",\n    \"O'Brien 2022\",\n]\nn_studies = len(studies)\ntrue_effect = -0.35\n\nstd_errors = np.concatenate(\n    [np.random.uniform(0.05, 0.15, 5), np.random.uniform(0.15, 0.30, 6), np.random.uniform(0.30, 0.50, 4)]\n)\neffect_sizes = true_effect + np.random.normal(0, 1, n_studies) * std_errors\neffect_sizes[-2] += 0.25\neffect_sizes[-1] += 0.30\n\nweights = 1 / std_errors**2\nsummary_effect = np.average(effect_sizes, weights=weights)\n\ndf = pd.DataFrame({\"effect_size\": effect_sizes, \"std_error\": std_errors, \"study\": studies, \"weight\": weights})\n\ndf[\"precision\"] = pd.cut(\n    df[\"std_error\"], bins=[0, 0.15, 0.30, 1.0], labels=[\"High precision\", \"Moderate precision\", \"Low precision\"]\n)\n\n# Plot — landscape 3200×1800 px (hard rule: figsize=(8, 4.5), dpi=400, no bbox_inches='tight')\nfig, ax = plt.subplots(figsize=(8, 4.5), dpi=400)\n\nse_range = np.linspace(0, 0.55, 300)\nci_left = summary_effect - 1.96 * se_range\nci_right = summary_effect + 1.96 * se_range\n\n# Pseudo 95% CI funnel region\nax.fill_betweenx(se_range, ci_left, ci_right, color=INK_MUTED, alpha=0.10)\nax.plot(ci_left, se_range, color=INK_SOFT, linewidth=1.2, linestyle=\"--\", alpha=0.5)\nax.plot(ci_right, se_range, color=INK_SOFT, linewidth=1.2, linestyle=\"--\", alpha=0.5)\n\n# Reference lines: summary effect (structural INK neutral) and null effect\nax.axvline(x=summary_effect, color=INK, linewidth=2.0, alpha=0.85, zorder=4)\nax.axvline(x=0, color=INK_SOFT, linewidth=1.2, linestyle=\":\", alpha=0.5, zorder=3)\n\n# Scatter by precision tier — Imprint palette; high precision → green (semantic: quality/good)\ntier_palette = {\n    \"High precision\": IMPRINT_PALETTE[0],\n    \"Moderate precision\": IMPRINT_PALETTE[1],\n    \"Low precision\": IMPRINT_PALETTE[2],\n}\nsns.scatterplot(\n    data=df,\n    x=\"effect_size\",\n    y=\"std_error\",\n    hue=\"precision\",\n    size=\"weight\",\n    sizes=(60, 280),\n    palette=tier_palette,\n    edgecolor=\"white\",\n    linewidth=0.8,\n    alpha=0.85,\n    zorder=5,\n    ax=ax,\n    legend=False,\n)\n\n# Seaborn rugplot for marginal effect size distribution — idiomatic seaborn feature\nsns.rugplot(data=df, x=\"effect_size\", height=0.04, color=INK_SOFT, alpha=0.65, ax=ax)\n\n# Annotate two most imprecise (lower-right outlier) studies\n# Martinez 2020 is rightmost — use left-aligned offset to avoid right canvas edge\noutliers = df.nlargest(2, \"std_error\")\nfor _, row in outliers.iterrows():\n    x_offset = -75 if row[\"study\"] == \"Martinez 2020\" else 10\n    ax.annotate(\n        row[\"study\"],\n        xy=(row[\"effect_size\"], row[\"std_error\"]),\n        xytext=(x_offset, -3),\n        textcoords=\"offset points\",\n        fontsize=8,\n        fontstyle=\"italic\",\n        color=INK_MUTED,\n    )\n\n# Axis style\nax.invert_yaxis()\nax.set_xlabel(\"Log Odds Ratio (Drug vs Placebo)\", fontsize=10)\nax.set_ylabel(\"Standard Error\", fontsize=10)\nax.set_title(\n    \"funnel-meta-analysis · python · seaborn · anyplot.ai\", fontsize=12, fontweight=\"medium\", pad=10, color=INK\n)\nax.tick_params(axis=\"both\", labelsize=8)\n\n# X-axis: asymmetric limits covering the full funnel extent at max SE plus data range\nx_min = min(summary_effect - 1.96 * 0.55, df[\"effect_size\"].min()) - 0.08\nx_max = max(summary_effect + 1.96 * 0.55, df[\"effect_size\"].max()) + 0.08\nax.set_xlim(x_min, x_max)\n\nsns.despine(ax=ax)\n\n# Y-axis-only grid (style guide preference)\nax.yaxis.grid(True, alpha=0.2, linewidth=0.8, color=INK_SOFT)\nax.xaxis.grid(False)\n\n# Legend\nlegend_elements = [\n    Line2D([0], [0], color=INK, linewidth=2.0, alpha=0.85, label=f\"Summary effect ({summary_effect:.2f})\"),\n    Line2D([0], [0], color=INK_SOFT, linewidth=1.2, linestyle=\":\", alpha=0.5, label=\"Null effect (0)\"),\n    Line2D([0], [0], marker=\"o\", color=\"w\", markerfacecolor=IMPRINT_PALETTE[0], markersize=7, label=\"High precision\"),\n    Line2D(\n        [0], [0], marker=\"o\", color=\"w\", markerfacecolor=IMPRINT_PALETTE[1], markersize=6, label=\"Moderate precision\"\n    ),\n    Line2D([0], [0], marker=\"o\", color=\"w\", markerfacecolor=IMPRINT_PALETTE[2], markersize=5, label=\"Low precision\"),\n]\nax.legend(handles=legend_elements, fontsize=8, frameon=False, loc=\"lower left\")\n\n# Size encoding note and weighted-mean apex label for information density\nax.text(\n    0.98,\n    0.03,\n    \"Circle size ∝ study weight\",\n    transform=ax.transAxes,\n    ha=\"right\",\n    va=\"bottom\",\n    fontsize=7,\n    color=INK_MUTED,\n    fontstyle=\"italic\",\n)\nax.annotate(\n    f\"WM = {summary_effect:.2f}\",\n    xy=(summary_effect, 0.01),\n    xytext=(summary_effect + 0.12, 0.06),\n    fontsize=7,\n    color=INK_SOFT,\n    arrowprops={\"arrowstyle\": \"-\", \"color\": INK_MUTED, \"lw\": 0.7},\n)\n\n# Save — no bbox_inches; figsize×dpi produces exact 3200×1800 target\n# Use __file__-relative path so script runs correctly from any working directory\noutput_dir = os.path.dirname(os.path.abspath(__file__))\nplt.savefig(os.path.join(output_dir, f\"plot-{THEME}.png\"), dpi=400, facecolor=PAGE_BG)\nplt.close()\n"}