Skip to content

Commit 7a7f872

Browse files
feat(seaborn): implement line-win-probability (#5090)
## Implementation: `line-win-probability` - seaborn Implements the **seaborn** version of `line-win-probability`. **File:** `plots/line-win-probability/implementations/seaborn.py` **Parent Issue:** #4418 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/23342814414)* --------- 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 9777a4e commit 7a7f872

2 files changed

Lines changed: 383 additions & 0 deletions

File tree

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
""" pyplots.ai
2+
line-win-probability: Win Probability Chart
3+
Library: seaborn 0.13.2 | Python 3.14.3
4+
Quality: 91/100 | Created: 2026-03-20
5+
"""
6+
7+
import matplotlib.patches as mpatches
8+
import matplotlib.pyplot as plt
9+
import numpy as np
10+
import pandas as pd
11+
import seaborn as sns
12+
13+
14+
# Seaborn theme and context for global styling
15+
sns.set_theme(
16+
style="ticks",
17+
rc={
18+
"axes.facecolor": "#F7F9FC",
19+
"figure.facecolor": "#FFFFFF",
20+
"grid.color": "#D0D8E0",
21+
"grid.alpha": 0.3,
22+
"grid.linewidth": 0.6,
23+
"font.family": "sans-serif",
24+
},
25+
)
26+
sns.set_context("talk", font_scale=1.05, rc={"lines.linewidth": 2.8})
27+
28+
# Palette from seaborn
29+
palette = sns.color_palette(["#306998", "#D4583B"])
30+
home_color = palette[0]
31+
away_color = palette[1]
32+
33+
# Data
34+
np.random.seed(42)
35+
36+
plays = np.arange(0, 121)
37+
win_prob = np.full(len(plays), 0.50)
38+
39+
events = {
40+
8: ("FG Home", 0.07),
41+
22: ("TD Away", -0.15),
42+
35: ("TD Home", 0.18),
43+
48: ("INT Home", 0.10),
44+
55: ("FG Away", -0.08),
45+
65: ("TD Home", 0.16),
46+
78: ("TD Away", -0.14),
47+
85: ("FG Home", 0.09),
48+
95: ("TD Away", -0.20),
49+
105: ("TD Home", 0.22),
50+
115: ("FG Home", 0.08),
51+
}
52+
53+
for i in range(1, len(plays)):
54+
noise = np.random.normal(0, 0.015)
55+
if i in events:
56+
shift = events[i][1]
57+
else:
58+
shift = 0
59+
win_prob[i] = np.clip(win_prob[i - 1] + shift + noise, 0.02, 0.98)
60+
61+
win_prob[-1] = 0.95
62+
63+
df = pd.DataFrame({"play": plays, "win_probability": win_prob})
64+
65+
# Plot
66+
fig, ax = plt.subplots(figsize=(16, 9))
67+
68+
sns.lineplot(data=df, x="play", y="win_probability", color=home_color, linewidth=2.8, ax=ax)
69+
70+
ax.fill_between(plays, win_prob, 0.5, where=(win_prob >= 0.5), color=home_color, alpha=0.2, interpolate=True)
71+
ax.fill_between(plays, win_prob, 0.5, where=(win_prob < 0.5), color=away_color, alpha=0.2, interpolate=True)
72+
73+
ax.axhline(y=0.5, color="#888888", linewidth=1.2, linestyle="--", alpha=0.5)
74+
75+
key_events = {
76+
22: ("TD Away\n7-3", away_color),
77+
35: ("TD Home\n10-7", home_color),
78+
65: ("TD Home\n20-14", home_color),
79+
95: ("TD Away\n23-27", away_color),
80+
105: ("TD Home\n30-27", home_color),
81+
}
82+
83+
annotation_offsets = {22: (-8, 0.10), 35: (0, -0.10), 65: (8, 0.10), 95: (-12, 0.10), 105: (0, -0.12)}
84+
85+
for play_num, (label, color) in key_events.items():
86+
y_val = win_prob[play_num]
87+
x_off, y_off = annotation_offsets[play_num]
88+
ax.annotate(
89+
label,
90+
xy=(play_num, y_val),
91+
xytext=(play_num + x_off, y_val + y_off),
92+
fontsize=13,
93+
fontweight="bold",
94+
color=color,
95+
ha="center",
96+
va="center",
97+
arrowprops={"arrowstyle": "->", "color": color, "lw": 1.5, "connectionstyle": "arc3,rad=0.1"},
98+
)
99+
100+
sns.scatterplot(
101+
x=list(key_events),
102+
y=[win_prob[p] for p in key_events],
103+
color=[key_events[p][1] for p in key_events],
104+
s=100,
105+
zorder=5,
106+
edgecolor="white",
107+
linewidth=1.5,
108+
ax=ax,
109+
legend=False,
110+
)
111+
112+
# Quarter markers
113+
for q, label in [(30, "Q1"), (60, "Q2"), (90, "Q3"), (120, "Q4")]:
114+
ax.axvline(x=q, color="#C0C8D0", linewidth=0.8, linestyle=":", alpha=0.6)
115+
ax.text(q - 15, 0.025, label, fontsize=14, color="#8899AA", ha="center", fontweight="medium")
116+
117+
# Style
118+
ax.set_xlabel("Play Number", fontsize=20)
119+
ax.set_ylabel("Home Win Probability", fontsize=20)
120+
ax.set_title("line-win-probability · seaborn · pyplots.ai\n", fontsize=24, fontweight="medium", pad=4)
121+
ax.text(
122+
0.5,
123+
1.02,
124+
"NFL Game — Home vs Away | Lead changes and momentum shifts across 120 plays",
125+
transform=ax.transAxes,
126+
ha="center",
127+
fontsize=14,
128+
color="#667788",
129+
fontstyle="italic",
130+
)
131+
ax.tick_params(axis="both", labelsize=16)
132+
133+
ax.set_ylim(0, 1)
134+
ax.set_xlim(0, 120)
135+
ax.set_yticks([0, 0.25, 0.5, 0.75, 1.0])
136+
ax.set_yticklabels(["0%", "25%", "50%", "75%", "100%"])
137+
138+
sns.despine(ax=ax)
139+
ax.yaxis.grid(True, alpha=0.2, linewidth=0.6)
140+
141+
home_patch = mpatches.Patch(color=home_color, alpha=0.4, label="Home")
142+
away_patch = mpatches.Patch(color=away_color, alpha=0.4, label="Away")
143+
ax.legend(handles=[home_patch, away_patch], fontsize=16, loc="upper left", frameon=False)
144+
145+
ax.text(
146+
118,
147+
0.015,
148+
"Final: Home 30 – Away 27",
149+
fontsize=15,
150+
ha="right",
151+
color="#556677",
152+
fontweight="semibold",
153+
fontstyle="italic",
154+
bbox={"boxstyle": "round,pad=0.3", "facecolor": "#E8EEF4", "edgecolor": "#C0C8D0", "alpha": 0.8},
155+
)
156+
157+
# Save
158+
plt.tight_layout()
159+
plt.savefig("plot.png", dpi=300, bbox_inches="tight")
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
library: seaborn
2+
specification_id: line-win-probability
3+
created: '2026-03-20T12:32:04Z'
4+
updated: '2026-03-20T12:45:51Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 23342814414
7+
issue: 4418
8+
python_version: 3.14.3
9+
library_version: 0.13.2
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/line-win-probability/seaborn/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/line-win-probability/seaborn/plot_thumb.png
12+
preview_html: null
13+
quality_score: 91
14+
review:
15+
strengths:
16+
- Excellent data storytelling with annotated scoring events showing running score
17+
- Polished design with custom background, cohesive palette, and styled final score
18+
box
19+
- Complete spec compliance — all required features implemented
20+
- Realistic NFL game data with compelling back-and-forth narrative
21+
weaknesses:
22+
- Q4 quarter label is obscured by the Final score box (minor overlap issue)
23+
- Library mastery could be stronger — core visualization relies heavily on matplotlib
24+
rather than seaborn-specific plotting features
25+
image_description: 'The plot displays a win probability line chart for an NFL game
26+
across 120 plays. A dark blue line (#306998) traces the home team''s win probability
27+
from ~50% at kickoff through various scoring events. The area above the 50% dashed
28+
baseline is filled with translucent blue (Home) and below with translucent salmon/red
29+
(#D4583B, Away). Five key events are annotated with curved arrows and bold colored
30+
text showing the scoring play and running score: "TD Away 7-3", "TD Home 10-7",
31+
"TD Home 20-14", "TD Away 23-27", and "TD Home 30-27". Dotted vertical lines mark
32+
Q1, Q2, and Q3 boundaries. The y-axis shows 0%-100% with the 50% reference line.
33+
A styled box at the bottom right displays "Final: Home 30 – Away 27". The title
34+
follows the required format. A legend in the upper left shows Home (blue) and
35+
Away (red) patches. The background is a subtle light blue-gray (#F7F9FC). An italic
36+
subtitle reads "NFL Game — Home vs Away | Lead changes and momentum shifts across
37+
120 plays".'
38+
criteria_checklist:
39+
visual_quality:
40+
score: 29
41+
max: 30
42+
items:
43+
- id: VQ-01
44+
name: Text Legibility
45+
score: 8
46+
max: 8
47+
passed: true
48+
comment: 'All font sizes explicitly set: title 24pt, labels 20pt, ticks 16pt,
49+
annotations 13pt'
50+
- id: VQ-02
51+
name: No Overlap
52+
score: 5
53+
max: 6
54+
passed: true
55+
comment: Q4 quarter label hidden behind Final score box; remaining annotations
56+
well-spaced
57+
- id: VQ-03
58+
name: Element Visibility
59+
score: 6
60+
max: 6
61+
passed: true
62+
comment: Line, fills, and scatter markers all clearly visible with appropriate
63+
sizing
64+
- id: VQ-04
65+
name: Color Accessibility
66+
score: 4
67+
max: 4
68+
passed: true
69+
comment: Blue and salmon provide strong contrast, colorblind-distinguishable
70+
- id: VQ-05
71+
name: Layout & Canvas
72+
score: 4
73+
max: 4
74+
passed: true
75+
comment: Good 16:9 layout, balanced margins, nothing cut off
76+
- id: VQ-06
77+
name: Axis Labels & Title
78+
score: 2
79+
max: 2
80+
passed: true
81+
comment: Descriptive labels with percentage units 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: Custom background, cohesive palette, italic subtitle, styled score
92+
box — clearly above defaults
93+
- id: DE-02
94+
name: Visual Refinement
95+
score: 5
96+
max: 6
97+
passed: true
98+
comment: Spines removed, subtle grid, custom background, polished quarter
99+
markers
100+
- id: DE-03
101+
name: Data Storytelling
102+
score: 5
103+
max: 6
104+
passed: true
105+
comment: Annotations with running score, fill colors for momentum, final score
106+
box — strong narrative
107+
spec_compliance:
108+
score: 15
109+
max: 15
110+
items:
111+
- id: SC-01
112+
name: Plot Type
113+
score: 5
114+
max: 5
115+
passed: true
116+
comment: Correct line chart showing win probability over game progression
117+
- id: SC-02
118+
name: Required Features
119+
score: 4
120+
max: 4
121+
passed: true
122+
comment: 'All features: 50% reference, fill areas, annotations, final score,
123+
quarter markers'
124+
- id: SC-03
125+
name: Data Mapping
126+
score: 3
127+
max: 3
128+
passed: true
129+
comment: X=Play Number, Y=Win Probability, full range displayed
130+
- id: SC-04
131+
name: Title & Legend
132+
score: 3
133+
max: 3
134+
passed: true
135+
comment: Correct title format and Home/Away legend patches
136+
data_quality:
137+
score: 15
138+
max: 15
139+
items:
140+
- id: DQ-01
141+
name: Feature Coverage
142+
score: 6
143+
max: 6
144+
passed: true
145+
comment: Lead changes, momentum swings, multiple event types, both teams scoring
146+
- id: DQ-02
147+
name: Realistic Context
148+
score: 5
149+
max: 5
150+
passed: true
151+
comment: NFL game scenario, neutral sports context, plausible data
152+
- id: DQ-03
153+
name: Appropriate Scale
154+
score: 4
155+
max: 4
156+
passed: true
157+
comment: 120 plays realistic for NFL, probability values in valid range
158+
code_quality:
159+
score: 10
160+
max: 10
161+
items:
162+
- id: CQ-01
163+
name: KISS Structure
164+
score: 3
165+
max: 3
166+
passed: true
167+
comment: Clean imports → data → plot → save flow, no functions or classes
168+
- id: CQ-02
169+
name: Reproducibility
170+
score: 2
171+
max: 2
172+
passed: true
173+
comment: np.random.seed(42) set
174+
- id: CQ-03
175+
name: Clean Imports
176+
score: 2
177+
max: 2
178+
passed: true
179+
comment: All imports used
180+
- id: CQ-04
181+
name: Code Elegance
182+
score: 2
183+
max: 2
184+
passed: true
185+
comment: Appropriate complexity, no fake functionality
186+
- id: CQ-05
187+
name: Output & API
188+
score: 1
189+
max: 1
190+
passed: true
191+
comment: Saves as plot.png with dpi=300
192+
library_mastery:
193+
score: 6
194+
max: 10
195+
items:
196+
- id: LM-01
197+
name: Idiomatic Usage
198+
score: 4
199+
max: 5
200+
passed: true
201+
comment: Good use of seaborn axes-level API, set_theme, set_context, despine
202+
- id: LM-02
203+
name: Distinctive Features
204+
score: 2
205+
max: 5
206+
passed: false
207+
comment: Seaborn features are mainly styling utilities; core viz relies on
208+
matplotlib
209+
verdict: APPROVED
210+
impl_tags:
211+
dependencies: []
212+
techniques:
213+
- annotations
214+
- custom-legend
215+
- manual-ticks
216+
patterns:
217+
- data-generation
218+
- explicit-figure
219+
dataprep:
220+
- cumulative-sum
221+
styling:
222+
- alpha-blending
223+
- grid-styling
224+
- edge-highlighting

0 commit comments

Comments
 (0)