{"spec_id":"forest-basic","library":"seaborn","language":"python","code":"\"\"\" anyplot.ai\nforest-basic: Meta-Analysis Forest Plot\nLibrary: seaborn 0.13.2 | Python 3.13.13\nQuality: 95/100 | Updated: 2026-05-11\n\"\"\"\n\nimport os\n\nimport matplotlib.patches as mpatches\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nimport seaborn as sns\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\"\nBRAND = \"#009E73\"  # Okabe-Ito position 1\n\n# Configure seaborn theme before creating figure\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.10,\n    },\n)\n\n# Data: Meta-analysis of treatment effect (mean difference) from 10 studies\nnp.random.seed(42)\n\nstudies = [\n    \"Smith et al. 2018\",\n    \"Johnson et al. 2019\",\n    \"Williams et al. 2019\",\n    \"Brown et al. 2020\",\n    \"Davis et al. 2020\",\n    \"Miller et al. 2021\",\n    \"Wilson et al. 2021\",\n    \"Moore et al. 2022\",\n    \"Taylor et al. 2022\",\n    \"Anderson et al. 2023\",\n]\n\n# Effect sizes (mean differences) - some favor treatment, some favor control\neffect_sizes = [-0.45, 0.12, -0.28, -0.52, 0.05, -0.38, -0.15, -0.42, -0.22, -0.35]\nci_widths = [0.35, 0.28, 0.42, 0.25, 0.55, 0.32, 0.38, 0.30, 0.45, 0.28]\nci_lower = [e - w for e, w in zip(effect_sizes, ci_widths, strict=True)]\nci_upper = [e + w for e, w in zip(effect_sizes, ci_widths, strict=True)]\nweights = [12.5, 8.2, 6.8, 14.1, 5.5, 10.3, 7.9, 11.8, 6.2, 9.7]\n\n# Calculate pooled estimate (weighted mean)\npooled_effect = np.average(effect_sizes, weights=weights)\npooled_se = np.sqrt(1 / np.sum([w / (ci_w**2) for w, ci_w in zip(weights, ci_widths, strict=True)]))\npooled_ci_lower = pooled_effect - 1.96 * pooled_se\npooled_ci_upper = pooled_effect + 1.96 * pooled_se\n\ndf = pd.DataFrame(\n    {\"study\": studies, \"effect\": effect_sizes, \"ci_lower\": ci_lower, \"ci_upper\": ci_upper, \"weight\": weights}\n)\n\n# Sort by effect size\ndf = df.sort_values(\"effect\", ascending=True).reset_index(drop=True)\n\n# Create figure\nfig, ax = plt.subplots(figsize=(16, 9), facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\n\n# Y positions for studies (leave space at bottom for pooled estimate)\ny_positions = np.arange(len(df)) + 1.5\n\n# Plot confidence intervals as horizontal lines\nfor i, (_, row) in enumerate(df.iterrows()):\n    ax.hlines(y=y_positions[i], xmin=row[\"ci_lower\"], xmax=row[\"ci_upper\"], color=BRAND, linewidth=2.5, zorder=1)\n\n# Plot point estimates using seaborn\nsns.scatterplot(\n    data=df,\n    x=\"effect\",\n    y=y_positions,\n    size=\"weight\",\n    sizes=(100, 500),\n    color=BRAND,\n    edgecolor=PAGE_BG,\n    linewidth=1.5,\n    legend=False,\n    ax=ax,\n    zorder=2,\n)\n\n# Add study labels on the left\nfor i, (_, row) in enumerate(df.iterrows()):\n    ax.text(-1.2, y_positions[i], row[\"study\"], fontsize=14, va=\"center\", ha=\"left\", fontweight=\"medium\", color=INK)\n\n# Add effect size values on the right\nfor i, (_, row) in enumerate(df.iterrows()):\n    ax.text(\n        0.95,\n        y_positions[i],\n        f\"{row['effect']:.2f} [{row['ci_lower']:.2f}, {row['ci_upper']:.2f}]\",\n        fontsize=12,\n        va=\"center\",\n        ha=\"left\",\n        family=\"monospace\",\n        color=INK_SOFT,\n    )\n\n# Draw pooled estimate diamond\ndiamond_y = 0.3\ndiamond_height = 0.4\ndiamond = mpatches.Polygon(\n    [\n        [pooled_effect, diamond_y],\n        [pooled_ci_lower, diamond_y + diamond_height / 2],\n        [pooled_effect, diamond_y + diamond_height],\n        [pooled_ci_upper, diamond_y + diamond_height / 2],\n    ],\n    closed=True,\n    facecolor=\"#954477\",\n    edgecolor=BRAND,\n    linewidth=2,\n    zorder=3,\n)\nax.add_patch(diamond)\n\n# Add pooled estimate label\nax.text(\n    -1.2,\n    diamond_y + diamond_height / 2,\n    \"Pooled Estimate\",\n    fontsize=14,\n    va=\"center\",\n    ha=\"left\",\n    fontweight=\"bold\",\n    color=INK,\n)\nax.text(\n    0.95,\n    diamond_y + diamond_height / 2,\n    f\"{pooled_effect:.2f} [{pooled_ci_lower:.2f}, {pooled_ci_upper:.2f}]\",\n    fontsize=12,\n    va=\"center\",\n    ha=\"left\",\n    family=\"monospace\",\n    fontweight=\"bold\",\n    color=INK_SOFT,\n)\n\n# Vertical reference line at null effect (0)\nax.axvline(x=0, color=INK_SOFT, linestyle=\"--\", linewidth=2, zorder=0, alpha=0.7)\n\n# Separator line above pooled estimate\nax.axhline(y=1.0, color=INK_SOFT, linewidth=1.5, zorder=0, alpha=0.3)\n\n# Styling\nax.set_xlim(-1.3, 1.5)\nax.set_ylim(-0.3, len(df) + 2)\nax.set_xlabel(\"Mean Difference (Treatment - Control)\", fontsize=20, color=INK, fontweight=\"medium\")\nax.set_ylabel(\"\")\nax.set_title(\"forest-basic · seaborn · anyplot.ai\", fontsize=24, fontweight=\"bold\", pad=20, color=INK)\n\n# Remove y-axis ticks (study names are shown as text)\nax.set_yticks([])\n\n# Style x-axis ticks\nax.tick_params(axis=\"x\", labelsize=16, colors=INK_SOFT)\n\n# Add annotation for interpretation\nax.text(\n    -0.65, len(df) + 1.5, \"← Favors Treatment\", fontsize=14, ha=\"center\", va=\"center\", color=BRAND, fontweight=\"medium\"\n)\nax.text(\n    0.65, len(df) + 1.5, \"Favors Control →\", fontsize=14, ha=\"center\", va=\"center\", color=INK_SOFT, fontweight=\"medium\"\n)\n\n# Adjust grid\nax.grid(axis=\"x\", alpha=0.1, linestyle=\"--\", color=INK, linewidth=0.8)\nax.grid(axis=\"y\", visible=False)\n\n# Remove top and right spines\nsns.despine(left=True)\n\nplt.tight_layout()\nplt.savefig(f\"plot-{THEME}.png\", dpi=300, bbox_inches=\"tight\", facecolor=PAGE_BG)\n"}