Skip to content

Commit 31c1ef6

Browse files
feat(matplotlib): implement forest-basic (#2397)
## Implementation: `forest-basic` - matplotlib Implements the **matplotlib** version of `forest-basic`. **File:** `plots/forest-basic/implementations/matplotlib.py` --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20543281172)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 24fd704 commit 31c1ef6

2 files changed

Lines changed: 135 additions & 0 deletions

File tree

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
""" pyplots.ai
2+
forest-basic: Meta-Analysis Forest Plot
3+
Library: matplotlib 3.10.8 | Python 3.13.11
4+
Quality: 93/100 | Created: 2025-12-27
5+
"""
6+
7+
import matplotlib.patches as mpatches
8+
import matplotlib.pyplot as plt
9+
import numpy as np
10+
11+
12+
# Data: Meta-analysis of RCTs comparing treatment efficacy (standardized mean difference)
13+
studies = [
14+
"Johnson 2018",
15+
"Smith 2019",
16+
"Garcia 2020",
17+
"Williams 2020",
18+
"Brown 2021",
19+
"Davis 2021",
20+
"Miller 2022",
21+
"Wilson 2022",
22+
"Anderson 2023",
23+
"Taylor 2023",
24+
]
25+
26+
# Effect sizes (standardized mean difference) and 95% CIs
27+
effect_sizes = np.array([-0.45, -0.32, -0.58, -0.21, -0.67, -0.38, -0.52, -0.29, -0.41, -0.55])
28+
ci_lower = np.array([-0.78, -0.61, -0.95, -0.48, -1.02, -0.69, -0.88, -0.56, -0.72, -0.91])
29+
ci_upper = np.array([-0.12, -0.03, -0.21, 0.06, -0.32, -0.07, -0.16, -0.02, -0.10, -0.19])
30+
31+
# Study weights (based on sample size / inverse variance)
32+
weights = np.array([8.5, 10.2, 7.8, 11.5, 6.9, 9.3, 8.1, 10.8, 9.7, 7.2])
33+
34+
# Pooled estimate (random effects meta-analysis)
35+
pooled_effect = -0.42
36+
pooled_ci_lower = -0.53
37+
pooled_ci_upper = -0.31
38+
39+
# Create figure
40+
fig, ax = plt.subplots(figsize=(16, 9))
41+
42+
n_studies = len(studies)
43+
y_positions = np.arange(n_studies, 0, -1)
44+
45+
# Normalize weights for marker sizing (scale between 80 and 300)
46+
weight_normalized = (weights - weights.min()) / (weights.max() - weights.min())
47+
marker_sizes = 80 + weight_normalized * 220
48+
49+
# Plot vertical reference line at null effect (0)
50+
ax.axvline(x=0, color="#888888", linestyle="--", linewidth=2, alpha=0.7, zorder=1)
51+
52+
# Plot confidence intervals as horizontal lines
53+
for y, lower, upper in zip(y_positions, ci_lower, ci_upper, strict=True):
54+
ax.hlines(y=y, xmin=lower, xmax=upper, color="#306998", linewidth=3, zorder=2)
55+
56+
# Plot effect size points
57+
ax.scatter(effect_sizes, y_positions, s=marker_sizes, color="#306998", edgecolors="white", linewidths=1.5, zorder=3)
58+
59+
# Plot pooled estimate as diamond
60+
diamond_y = 0
61+
diamond_height = 0.4
62+
63+
# Create diamond shape using polygon
64+
diamond_vertices = np.array(
65+
[
66+
[pooled_effect, diamond_y + diamond_height],
67+
[pooled_ci_upper, diamond_y],
68+
[pooled_effect, diamond_y - diamond_height],
69+
[pooled_ci_lower, diamond_y],
70+
]
71+
)
72+
diamond_patch = mpatches.Polygon(
73+
diamond_vertices, closed=True, facecolor="#FFD43B", edgecolor="#306998", linewidth=2.5, zorder=4
74+
)
75+
ax.add_patch(diamond_patch)
76+
77+
# Add study labels on y-axis
78+
ax.set_yticks(list(y_positions) + [0])
79+
ax.set_yticklabels(studies + ["Pooled Estimate"])
80+
81+
# Styling
82+
ax.set_xlabel("Standardized Mean Difference (95% CI)", fontsize=20)
83+
ax.set_title("forest-basic \u00b7 matplotlib \u00b7 pyplots.ai", fontsize=24)
84+
ax.tick_params(axis="both", labelsize=16)
85+
ax.tick_params(axis="y", length=0)
86+
87+
# Set x-axis limits with padding
88+
x_min = min(ci_lower.min(), pooled_ci_lower) - 0.15
89+
x_max = max(ci_upper.max(), pooled_ci_upper) + 0.15
90+
ax.set_xlim(x_min, x_max)
91+
92+
# Set y-axis limits
93+
ax.set_ylim(-0.8, n_studies + 0.8)
94+
95+
# Add subtle grid for x-axis only
96+
ax.grid(True, axis="x", alpha=0.3, linestyle="--", zorder=0)
97+
ax.set_axisbelow(True)
98+
99+
# Add annotation for "Favors Treatment" and "Favors Control"
100+
ax.text(x_min + 0.05, -0.6, "\u2190 Favors Treatment", fontsize=14, ha="left", va="top", color="#555555")
101+
ax.text(x_max - 0.05, -0.6, "Favors Control \u2192", fontsize=14, ha="right", va="top", color="#555555")
102+
103+
# Remove top and right spines
104+
ax.spines["top"].set_visible(False)
105+
ax.spines["right"].set_visible(False)
106+
ax.spines["left"].set_visible(False)
107+
108+
plt.tight_layout()
109+
plt.savefig("plot.png", dpi=300, bbox_inches="tight")
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
library: matplotlib
2+
specification_id: forest-basic
3+
created: '2025-12-27T19:21:39Z'
4+
updated: '2025-12-27T19:29:13Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20543281172
7+
issue: 0
8+
python_version: 3.13.11
9+
library_version: 3.10.8
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/forest-basic/matplotlib/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/forest-basic/matplotlib/plot_thumb.png
12+
preview_html: null
13+
quality_score: 93
14+
review:
15+
strengths:
16+
- 'Excellent implementation of the forest plot with all key elements: effect sizes,
17+
confidence intervals, weighted markers, and pooled estimate diamond'
18+
- Professional visual design with appropriate color scheme (Python blue for studies,
19+
yellow for pooled)
20+
- Clear Favors Treatment/Control annotations enhance interpretability
21+
- Marker sizes proportional to study weights effectively communicate study importance
22+
- Clean code structure following KISS principles
23+
weaknesses:
24+
- Missing legend explaining that marker size represents study weight
25+
- All effect sizes favor treatment (none crossing the null line to favor control
26+
would make data more illustrative)

0 commit comments

Comments
 (0)