{"spec_id":"funnel-meta-analysis","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\nfunnel-meta-analysis: Meta-Analysis Funnel Plot for Publication Bias\nLibrary: plotnine 0.15.5 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-06-10\n\"\"\"\n\nimport os\nimport sys\n\nimport numpy as np\nimport pandas as pd\n\n\n# Work around naming conflict with plotnine.py script and plotnine package\nscript_dir = os.path.dirname(os.path.abspath(__file__))\nif script_dir in sys.path:\n    sys.path.remove(script_dir)\nif \"\" in sys.path:\n    sys.path.remove(\"\")\nif \".\" in sys.path:\n    sys.path.remove(\".\")\n\nfrom plotnine import (\n    aes,\n    annotate,\n    element_blank,\n    element_line,\n    element_rect,\n    element_text,\n    geom_line,\n    geom_point,\n    geom_polygon,\n    geom_vline,\n    ggplot,\n    guide_legend,\n    guides,\n    labs,\n    scale_color_manual,\n    scale_shape_manual,\n    scale_size_continuous,\n    scale_x_continuous,\n    scale_y_reverse,\n    theme,\n    theme_minimal,\n)\n\n\n# Theme 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 — brand green for inside funnel, semantic red for outside/bias\nBRAND = \"#009E73\"\nOUTLIER_COLOR = \"#AE3030\"  # matte red — semantic anchor for bad/outlier/bias\n\n# Data — 15 RCTs comparing drug vs placebo (log odds ratios)\nnp.random.seed(42)\nn_studies = 15\ntrue_effect = 0.3\n\nstd_errors = np.concatenate(\n    [np.random.uniform(0.05, 0.15, 5), np.random.uniform(0.15, 0.30, 5), np.random.uniform(0.30, 0.50, 5)]\n)\neffect_sizes = true_effect + np.random.normal(0, std_errors)\n\n# Add publication bias: shift imprecise (high-SE) studies toward positive\nbias_mask = std_errors > 0.35\neffect_sizes[bias_mask] += np.random.uniform(0.04, 0.12, bias_mask.sum())\n\n# Clip to keep x-axis balanced and avoid extreme outliers\neffect_sizes = np.clip(effect_sizes, -0.55, 0.78)\n\nsummary_effect = float(np.average(effect_sizes, weights=1 / std_errors**2))\n\n# Inverse-variance weight for point sizing\nweights = 1 / std_errors**2\nweight_normalized = weights / weights.max()\n\n# Classify each study by funnel region\nlower_ci = summary_effect - 1.96 * std_errors\nupper_ci = summary_effect + 1.96 * std_errors\noutside_funnel = (effect_sizes < lower_ci) | (effect_sizes > upper_ci)\nregion = np.where(outside_funnel, \"Outside funnel\", \"Inside funnel\")\n\nstudies_df = pd.DataFrame(\n    {\n        \"effect_size\": effect_sizes,\n        \"std_error\": std_errors,\n        \"weight\": weight_normalized,\n        \"region\": pd.Categorical(region, categories=[\"Inside funnel\", \"Outside funnel\"]),\n    }\n)\n\n# Funnel boundary lines (pseudo 95% CI around pooled effect)\nse_max = 0.55\nse_range = np.linspace(0, se_max, 200)\nfunnel_lines = pd.DataFrame(\n    {\n        \"effect\": np.concatenate([summary_effect - 1.96 * se_range, summary_effect + 1.96 * se_range]),\n        \"se\": np.concatenate([se_range, se_range]),\n        \"side\": [\"lower\"] * len(se_range) + [\"upper\"] * len(se_range),\n    }\n)\n\n# Funnel polygon for shaded interior region\nfunnel_poly = pd.DataFrame(\n    {\"x\": [summary_effect, summary_effect - 1.96 * se_max, summary_effect + 1.96 * se_max], \"y\": [0.0, se_max, se_max]}\n)\n\n# Title\ntitle = \"funnel-meta-analysis · python · plotnine · anyplot.ai\"\n\n# Plot\nplot = (\n    ggplot()\n    + geom_polygon(funnel_poly, aes(x=\"x\", y=\"y\"), fill=BRAND, alpha=0.08)\n    + geom_line(funnel_lines, aes(x=\"effect\", y=\"se\", group=\"side\"), color=INK_SOFT, linetype=\"dashed\", size=0.7)\n    + geom_vline(xintercept=summary_effect, color=BRAND, size=1.2)\n    + geom_vline(xintercept=0, color=INK_MUTED, linetype=\"dotted\", size=0.7)\n    + geom_point(\n        studies_df,\n        aes(x=\"effect_size\", y=\"std_error\", size=\"weight\", color=\"region\", shape=\"region\"),\n        alpha=0.85,\n        stroke=0.3,\n    )\n    + scale_size_continuous(range=(2.5, 8), guide=None)\n    + scale_color_manual(values={\"Inside funnel\": BRAND, \"Outside funnel\": OUTLIER_COLOR}, name=\"\")\n    + scale_shape_manual(values={\"Inside funnel\": \"o\", \"Outside funnel\": \"^\"}, name=\"\")\n    + guides(color=guide_legend(override_aes={\"shape\": [\"o\", \"^\"]}), shape=False)\n    + annotate(\n        \"text\",\n        x=summary_effect + 0.45,\n        y=0.08,\n        label=\"Asymmetry suggests\\npublication bias\",\n        size=11,\n        color=OUTLIER_COLOR,\n        fontstyle=\"italic\",\n        ha=\"center\",\n    )\n    + annotate(\n        \"text\",\n        x=summary_effect + 0.02,\n        y=0.01,\n        label=f\"Pooled effect = {summary_effect:.2f}\",\n        size=10,\n        color=BRAND,\n        ha=\"left\",\n        va=\"top\",\n    )\n    + annotate(\"text\", x=-0.02, y=0.01, label=\"Null\", size=10, color=INK_MUTED, ha=\"right\", va=\"top\")\n    + scale_y_reverse(limits=(0.60, -0.02))\n    + scale_x_continuous(breaks=np.arange(-0.6, 1.2, 0.2).round(1).tolist())\n    + labs(x=\"Log Odds Ratio\", y=\"Standard Error\", color=\"\", title=title)\n    + theme_minimal()\n    + theme(\n        figure_size=(8, 4.5),\n        plot_title=element_text(size=12, weight=\"bold\", color=INK),\n        axis_title=element_text(size=10, color=INK),\n        axis_text=element_text(size=8, color=INK_SOFT),\n        panel_grid_major_y=element_line(color=INK, size=0.3, alpha=0.12),\n        panel_grid_major_x=element_blank(),\n        panel_grid_minor=element_blank(),\n        panel_background=element_rect(fill=PAGE_BG, color=\"none\"),\n        plot_background=element_rect(fill=PAGE_BG, color=\"none\"),\n        legend_position=(0.12, 0.12),\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n        legend_text=element_text(size=8, color=INK_SOFT),\n        legend_title=element_blank(),\n        axis_line=element_line(color=INK_SOFT, size=0.5),\n    )\n)\n\n# Save\nplot.save(f\"plot-{THEME}.png\", dpi=400, width=8, height=4.5, units=\"in\")\n"}