Skip to content

Commit 9777a4e

Browse files
feat(matplotlib): implement line-win-probability (#5087)
## Implementation: `line-win-probability` - matplotlib Implements the **matplotlib** version of `line-win-probability`. **File:** `plots/line-win-probability/implementations/matplotlib.py` **Parent Issue:** #4418 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/23342814407)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 3210f1b commit 9777a4e

2 files changed

Lines changed: 375 additions & 0 deletions

File tree

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
""" pyplots.ai
2+
line-win-probability: Win Probability Chart
3+
Library: matplotlib 3.10.8 | Python 3.14.3
4+
Quality: 92/100 | Created: 2026-03-20
5+
"""
6+
7+
import matplotlib.patches as mpatches
8+
import matplotlib.patheffects as pe
9+
import matplotlib.pyplot as plt
10+
import matplotlib.ticker as mticker
11+
import numpy as np
12+
13+
14+
# Data - simulated NFL game: Eagles vs Cowboys
15+
np.random.seed(42)
16+
17+
# Game plays (0 to ~120 plays)
18+
n_plays = 120
19+
plays = np.arange(n_plays + 1)
20+
21+
# Build win probability with realistic scoring events
22+
win_prob = np.full(n_plays + 1, 0.50)
23+
24+
# Scoring events: (play_number, probability_shift, label)
25+
scoring_events = [
26+
(8, 0.12, "PHI Field Goal (3-0)"),
27+
(22, -0.10, "DAL Touchdown (7-3)"),
28+
(35, 0.15, "PHI Touchdown (10-7)"),
29+
(48, 0.08, "PHI Field Goal (13-7)"),
30+
(58, -0.18, "DAL Touchdown (14-13)"),
31+
(72, 0.14, "PHI Touchdown (20-14)"),
32+
(85, -0.06, "DAL Field Goal (20-17)"),
33+
(95, 0.12, "PHI Touchdown (27-17)"),
34+
(110, -0.05, "DAL Field Goal (27-20)"),
35+
]
36+
37+
# Generate smooth probability curve with scoring jumps
38+
prob = 0.50
39+
noise = np.random.normal(0, 0.012, n_plays + 1)
40+
event_indices = {e[0]: (e[1], e[2]) for e in scoring_events}
41+
42+
for i in range(1, n_plays + 1):
43+
if i in event_indices:
44+
prob += event_indices[i][0]
45+
prob += noise[i]
46+
# Mean reversion toward current level
47+
prob = np.clip(prob, 0.02, 0.98)
48+
win_prob[i] = prob
49+
50+
# Force convergence to final outcome: Eagles win
51+
for i in range(105, n_plays + 1):
52+
t = (i - 105) / (n_plays - 105)
53+
win_prob[i] = win_prob[105] * (1 - t**2) + 1.0 * t**2
54+
55+
# Quarter boundaries (roughly 30 plays each)
56+
quarter_boundaries = [0, 30, 60, 90, n_plays]
57+
quarter_labels = ["Q1", "Q2", "Q3", "Q4"]
58+
59+
# Colors
60+
eagles_green = "#004C54"
61+
cowboys_navy = "#003594"
62+
baseline_color = "#444444"
63+
64+
# Plot
65+
fig, ax = plt.subplots(figsize=(16, 9))
66+
67+
# Fill above/below 50%
68+
ax.fill_between(plays, win_prob, 0.5, where=(win_prob >= 0.5), color=eagles_green, alpha=0.3, interpolate=True)
69+
ax.fill_between(plays, win_prob, 0.5, where=(win_prob < 0.5), color=cowboys_navy, alpha=0.45, interpolate=True)
70+
71+
# Win probability line - color changes based on which team leads
72+
for i in range(len(plays) - 1):
73+
color = eagles_green if win_prob[i] >= 0.5 else cowboys_navy
74+
ax.plot(plays[i : i + 2], win_prob[i : i + 2], color=color, linewidth=3, zorder=3, solid_capstyle="round")
75+
76+
# 50% baseline
77+
ax.axhline(y=0.5, color=baseline_color, linewidth=1.5, linestyle="--", alpha=0.5, zorder=2)
78+
79+
# Quarter dividers
80+
for qb in quarter_boundaries[1:-1]:
81+
ax.axvline(x=qb, color="#999999", linewidth=1, linestyle=":", alpha=0.4)
82+
83+
# Quarter labels
84+
for i, label in enumerate(quarter_labels):
85+
mid = (quarter_boundaries[i] + quarter_boundaries[i + 1]) / 2
86+
ax.text(mid, 0.03, label, ha="center", va="center", fontsize=16, color="#888888", fontweight="medium")
87+
88+
# Annotate key scoring events
89+
annotation_events = [
90+
(8, "FG 3-0"),
91+
(22, "TD 7-3"),
92+
(35, "TD 10-7"),
93+
(58, "TD 14-13"),
94+
(72, "TD 20-14"),
95+
(95, "TD 27-17"),
96+
]
97+
98+
for play_idx, label in annotation_events:
99+
wp = win_prob[play_idx]
100+
offset_y = 0.06 if wp >= 0.5 else -0.06
101+
txt = ax.annotate(
102+
label,
103+
xy=(play_idx, wp),
104+
xytext=(play_idx, wp + offset_y),
105+
fontsize=12,
106+
fontweight="bold",
107+
ha="center",
108+
va="center",
109+
color="#222222",
110+
arrowprops={"arrowstyle": "-", "color": "#999999", "linewidth": 0.8},
111+
zorder=4,
112+
)
113+
txt.set_path_effects([pe.withStroke(linewidth=3, foreground="white")])
114+
115+
# Scatter dots on scoring events for visibility
116+
for play_idx, _ in annotation_events:
117+
ax.plot(
118+
play_idx,
119+
win_prob[play_idx],
120+
"o",
121+
color=eagles_green if win_prob[play_idx] >= 0.5 else cowboys_navy,
122+
markersize=7,
123+
zorder=5,
124+
markeredgecolor="white",
125+
markeredgewidth=1,
126+
)
127+
128+
# Style
129+
ax.set_xlim(0, n_plays)
130+
ax.set_ylim(0, 1)
131+
ax.set_yticks([0, 0.25, 0.5, 0.75, 1.0])
132+
ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda x, _: f"{x:.0%}"))
133+
134+
# Subtle y-axis gridlines for easier probability reading
135+
ax.yaxis.grid(True, alpha=0.15, linewidth=0.8, color="#888888")
136+
ax.set_axisbelow(True)
137+
ax.set_xlabel("Play Number", fontsize=20)
138+
ax.set_ylabel("Win Probability", fontsize=20)
139+
ax.set_title(
140+
"Eagles 27 – Cowboys 20 · line-win-probability · matplotlib · pyplots.ai", fontsize=24, fontweight="medium"
141+
)
142+
ax.tick_params(axis="both", labelsize=16)
143+
ax.spines["top"].set_visible(False)
144+
ax.spines["right"].set_visible(False)
145+
146+
# Legend
147+
eagles_patch = mpatches.Patch(color=eagles_green, alpha=0.4, label="Eagles")
148+
cowboys_patch = mpatches.Patch(color=cowboys_navy, alpha=0.5, label="Cowboys")
149+
ax.legend(handles=[eagles_patch, cowboys_patch], fontsize=16, loc="upper left", framealpha=0.8, edgecolor="none")
150+
151+
plt.tight_layout()
152+
plt.savefig("plot.png", dpi=300, bbox_inches="tight")
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
library: matplotlib
2+
specification_id: line-win-probability
3+
created: '2026-03-20T12:30:49Z'
4+
updated: '2026-03-20T12:44:26Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 23342814407
7+
issue: 4418
8+
python_version: 3.14.3
9+
library_version: 3.10.8
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/line-win-probability/matplotlib/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/line-win-probability/matplotlib/plot_thumb.png
12+
preview_html: null
13+
quality_score: 92
14+
review:
15+
strengths:
16+
- Excellent spec compliance — all required features (50% baseline, team color fills,
17+
scoring annotations, quarter markers, final score) present and well-implemented
18+
- Strong data storytelling through color fills, annotated scoring events, and natural
19+
probability convergence
20+
- Professional visual polish with path effects, custom team colors, and refined
21+
typography
22+
- Clean, reproducible code with appropriate complexity
23+
weaknesses:
24+
- Segment-by-segment line plotting loop is less efficient than using LineCollection
25+
- Fill area visibility slightly reduced in narrow Cowboys-lead regions
26+
image_description: The plot displays a win probability chart for an NFL game between
27+
the Eagles and Cowboys. The x-axis shows "Play Number" (0–120) and the y-axis
28+
shows "Win Probability" (0%–100%). A dashed horizontal line marks the 50% baseline.
29+
The area above 50% is filled with a muted teal-green (Eagles) and below 50% with
30+
navy blue (Cowboys). The main probability line changes color based on which team
31+
leads. Six key scoring events are annotated with bold labels (FG 3-0, TD 7-3,
32+
TD 10-7, TD 14-13, TD 20-14, TD 27-17) connected by thin lines to small dots on
33+
the curve, each with a white stroke for readability. Quarter boundaries (Q1–Q4)
34+
are shown as dotted vertical lines with labels at the bottom. The title reads
35+
"Eagles 27 – Cowboys 20 · line-win-probability · matplotlib · pyplots.ai". A legend
36+
in the upper left identifies the team color fills. The probability converges sharply
37+
to 100% at game end. Top and right spines are removed; a subtle y-axis grid aids
38+
reading.
39+
criteria_checklist:
40+
visual_quality:
41+
score: 29
42+
max: 30
43+
items:
44+
- id: VQ-01
45+
name: Text Legibility
46+
score: 8
47+
max: 8
48+
passed: true
49+
comment: 'All font sizes explicitly set: title 24pt, labels 20pt, ticks 16pt,
50+
annotations 12pt, quarter labels 16pt'
51+
- id: VQ-02
52+
name: No Overlap
53+
score: 6
54+
max: 6
55+
passed: true
56+
comment: All annotations well-spaced with white path effects ensuring readability
57+
- id: VQ-03
58+
name: Element Visibility
59+
score: 5
60+
max: 6
61+
passed: true
62+
comment: Line and markers clear; fill areas thin in narrow Cowboys-lead regions
63+
- id: VQ-04
64+
name: Color Accessibility
65+
score: 4
66+
max: 4
67+
passed: true
68+
comment: Teal green vs navy blue fully distinguishable for all color vision
69+
types
70+
- id: VQ-05
71+
name: Layout & Canvas
72+
score: 4
73+
max: 4
74+
passed: true
75+
comment: 16:9 figure with tight_layout, plot fills canvas well
76+
- id: VQ-06
77+
name: Axis Labels & Title
78+
score: 2
79+
max: 2
80+
passed: true
81+
comment: Descriptive labels with percentage formatting on y-axis
82+
design_excellence:
83+
score: 16
84+
max: 20
85+
items:
86+
- id: DE-01
87+
name: Aesthetic Sophistication
88+
score: 6
89+
max: 8
90+
passed: true
91+
comment: Team-specific palette, color-changing line, path effects, approaching
92+
broadcast quality
93+
- id: DE-02
94+
name: Visual Refinement
95+
score: 5
96+
max: 6
97+
passed: true
98+
comment: Spines removed, subtle grid, dotted quarter dividers, white stroke
99+
on annotations
100+
- id: DE-03
101+
name: Data Storytelling
102+
score: 5
103+
max: 6
104+
passed: true
105+
comment: Clear game narrative through fills, annotations, and convergence
106+
spec_compliance:
107+
score: 15
108+
max: 15
109+
items:
110+
- id: SC-01
111+
name: Plot Type
112+
score: 5
113+
max: 5
114+
passed: true
115+
comment: Correct win probability line chart
116+
- id: SC-02
117+
name: Required Features
118+
score: 4
119+
max: 4
120+
passed: true
121+
comment: 'All spec features present: baseline, fills, annotations, score,
122+
quarters'
123+
- id: SC-03
124+
name: Data Mapping
125+
score: 3
126+
max: 3
127+
passed: true
128+
comment: X=play number, Y=win probability, full range shown
129+
- id: SC-04
130+
name: Title & Legend
131+
score: 3
132+
max: 3
133+
passed: true
134+
comment: Title includes score and follows spec-id format; legend correct
135+
data_quality:
136+
score: 15
137+
max: 15
138+
items:
139+
- id: DQ-01
140+
name: Feature Coverage
141+
score: 6
142+
max: 6
143+
passed: true
144+
comment: Shows FG/TD events, lead changes, momentum swings, convergence
145+
- id: DQ-02
146+
name: Realistic Context
147+
score: 5
148+
max: 5
149+
passed: true
150+
comment: Eagles vs Cowboys NFL game with realistic 27-20 final score
151+
- id: DQ-03
152+
name: Appropriate Scale
153+
score: 4
154+
max: 4
155+
passed: true
156+
comment: Realistic probability shifts and natural convergence
157+
code_quality:
158+
score: 10
159+
max: 10
160+
items:
161+
- id: CQ-01
162+
name: KISS Structure
163+
score: 3
164+
max: 3
165+
passed: true
166+
comment: Clean imports-data-plot-save flow, no functions or classes
167+
- id: CQ-02
168+
name: Reproducibility
169+
score: 2
170+
max: 2
171+
passed: true
172+
comment: np.random.seed(42) set
173+
- id: CQ-03
174+
name: Clean Imports
175+
score: 2
176+
max: 2
177+
passed: true
178+
comment: All imports used
179+
- id: CQ-04
180+
name: Code Elegance
181+
score: 2
182+
max: 2
183+
passed: true
184+
comment: Appropriate complexity, no fake UI
185+
- id: CQ-05
186+
name: Output & API
187+
score: 1
188+
max: 1
189+
passed: true
190+
comment: Saves as plot.png with dpi=300
191+
library_mastery:
192+
score: 7
193+
max: 10
194+
items:
195+
- id: LM-01
196+
name: Idiomatic Usage
197+
score: 4
198+
max: 5
199+
passed: true
200+
comment: Good ax-based usage; segment loop less idiomatic than LineCollection
201+
- id: LM-02
202+
name: Distinctive Features
203+
score: 3
204+
max: 5
205+
passed: true
206+
comment: fill_between with interpolate, patheffects, FuncFormatter, mpatches
207+
legend
208+
verdict: APPROVED
209+
impl_tags:
210+
dependencies: []
211+
techniques:
212+
- annotations
213+
- custom-legend
214+
- manual-ticks
215+
patterns:
216+
- data-generation
217+
- iteration-over-groups
218+
dataprep:
219+
- cumulative-sum
220+
styling:
221+
- alpha-blending
222+
- grid-styling
223+
- edge-highlighting

0 commit comments

Comments
 (0)