Skip to content

Commit 57d2daa

Browse files
feat(matplotlib): implement heatmap-cohort-retention (#4943)
## Implementation: `heatmap-cohort-retention` - matplotlib Implements the **matplotlib** version of `heatmap-cohort-retention`. **File:** `plots/heatmap-cohort-retention/implementations/matplotlib.py` **Parent Issue:** #4570 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/23165008020)* --------- 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 c07a105 commit 57d2daa

2 files changed

Lines changed: 367 additions & 0 deletions

File tree

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
""" pyplots.ai
2+
heatmap-cohort-retention: Cohort Retention Heatmap
3+
Library: matplotlib 3.10.8 | Python 3.14.3
4+
Quality: 92/100 | Created: 2026-03-16
5+
"""
6+
7+
import matplotlib.colors as mcolors
8+
import matplotlib.patches as mpatches
9+
import matplotlib.pyplot as plt
10+
import numpy as np
11+
12+
13+
# Data
14+
np.random.seed(42)
15+
cohort_labels = [
16+
"Jan 2024",
17+
"Feb 2024",
18+
"Mar 2024",
19+
"Apr 2024",
20+
"May 2024",
21+
"Jun 2024",
22+
"Jul 2024",
23+
"Aug 2024",
24+
"Sep 2024",
25+
"Oct 2024",
26+
]
27+
cohort_sizes = [1200, 1350, 980, 1100, 1450, 1280, 1050, 1320, 1180, 1400]
28+
n_cohorts = len(cohort_labels)
29+
n_periods = n_cohorts
30+
31+
# Generate realistic retention data with meaningful variation across cohorts
32+
# Some cohorts retain much better (e.g., May launch campaign), some churn faster
33+
retention = np.full((n_cohorts, n_periods), np.nan)
34+
# Per-cohort decay multipliers: <1 = better retention, >1 = worse retention
35+
decay_profiles = [1.0, 1.15, 1.3, 1.1, 0.55, 0.65, 1.2, 0.85, 1.05, 0.75]
36+
37+
for i in range(n_cohorts):
38+
max_periods = n_periods - i
39+
retention[i, 0] = 100.0
40+
for j in range(1, max_periods):
41+
base_drop = (15 * np.exp(-0.25 * j) + 1.5) * decay_profiles[i]
42+
noise = np.random.uniform(-2, 2)
43+
retention[i, j] = max(retention[i, j - 1] - base_drop - noise, 5)
44+
45+
# Find the best-performing cohort (highest average retention) for emphasis
46+
# Require at least 4 periods to qualify as "best" cohort
47+
avg_retention = [np.nanmean(retention[i, 1 : n_periods - i]) if n_periods - i >= 4 else 0.0 for i in range(n_cohorts)]
48+
best_cohort = int(np.argmax(avg_retention))
49+
50+
# Plot
51+
fig, ax = plt.subplots(figsize=(16, 9))
52+
53+
# Perceptually-uniform colormap
54+
cmap = plt.cm.viridis
55+
norm = mcolors.Normalize(vmin=0, vmax=100)
56+
57+
# Draw heatmap cells using FancyBboxPatch for rounded corners
58+
for i in range(n_cohorts):
59+
for j in range(n_periods):
60+
if np.isnan(retention[i, j]):
61+
continue
62+
val = retention[i, j]
63+
color = cmap(norm(val))
64+
rect = mpatches.FancyBboxPatch(
65+
(j - 0.47, i - 0.47),
66+
0.94,
67+
0.94,
68+
boxstyle=mpatches.BoxStyle.Round(pad=0, rounding_size=0.08),
69+
facecolor=color,
70+
edgecolor="white",
71+
linewidth=2.5,
72+
)
73+
ax.add_patch(rect)
74+
# Text color: white on dark cells, dark on light cells
75+
luminance = 0.299 * color[0] + 0.587 * color[1] + 0.114 * color[2]
76+
text_color = "white" if luminance < 0.5 else "#1a1a2e"
77+
ax.text(
78+
j,
79+
i,
80+
f"{val:.0f}%",
81+
ha="center",
82+
va="center",
83+
fontsize=15,
84+
fontweight="bold" if i == best_cohort else "medium",
85+
color=text_color,
86+
)
87+
88+
# Style
89+
ax.set_xlim(-0.5, n_periods - 0.5)
90+
ax.set_ylim(n_cohorts - 0.5, -0.5)
91+
ax.set_xticks(range(n_periods))
92+
ax.set_xticklabels([f"Month {p}" for p in range(n_periods)], fontsize=16)
93+
ax.set_yticks(range(n_cohorts))
94+
ytick_labels = []
95+
for idx, (label, size) in enumerate(zip(cohort_labels, cohort_sizes, strict=True)):
96+
text = f"{label} (n={size:,})"
97+
if idx == best_cohort:
98+
text = f"\u2605 {text}"
99+
ytick_labels.append(text)
100+
ax.set_yticklabels(ytick_labels, fontsize=16)
101+
ax.set_xlabel("Months Since Signup", fontsize=20)
102+
ax.set_ylabel("Signup Cohort", fontsize=20)
103+
ax.set_title("heatmap-cohort-retention · matplotlib · pyplots.ai", fontsize=24, fontweight="medium", pad=20)
104+
105+
# Highlight best cohort row with a subtle border
106+
highlight_rect = mpatches.FancyBboxPatch(
107+
(-0.55, best_cohort - 0.55),
108+
n_periods - best_cohort + 0.1,
109+
1.1,
110+
boxstyle=mpatches.BoxStyle.Round(pad=0, rounding_size=0.12),
111+
facecolor="none",
112+
edgecolor="#FFD700",
113+
linewidth=3,
114+
linestyle="--",
115+
zorder=5,
116+
)
117+
ax.add_patch(highlight_rect)
118+
119+
for spine in ax.spines.values():
120+
spine.set_visible(False)
121+
ax.tick_params(axis="both", length=0)
122+
123+
# Colorbar
124+
sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm)
125+
sm.set_array([])
126+
cbar = fig.colorbar(sm, ax=ax, shrink=0.6, aspect=25, pad=0.02)
127+
cbar.set_label("Retention Rate (%)", fontsize=16)
128+
cbar.ax.tick_params(labelsize=16)
129+
cbar.outline.set_visible(False)
130+
131+
plt.tight_layout()
132+
plt.savefig("plot.png", dpi=300, bbox_inches="tight")
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
library: matplotlib
2+
specification_id: heatmap-cohort-retention
3+
created: '2026-03-16T20:48:01Z'
4+
updated: '2026-03-16T21:00:45Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 23165008020
7+
issue: 4570
8+
python_version: 3.14.3
9+
library_version: 3.10.8
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/heatmap-cohort-retention/matplotlib/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/heatmap-cohort-retention/matplotlib/plot_thumb.png
12+
preview_html: null
13+
quality_score: 92
14+
review:
15+
strengths:
16+
- Rounded-corner cells via FancyBboxPatch give a modern, polished look distinct
17+
from standard heatmaps
18+
- Best-cohort highlighting (star icon + gold dashed border + bold text) creates
19+
effective data storytelling
20+
- 'Excellent spec compliance: triangular shape, cell annotations, cohort sizes,
21+
colorbar all present'
22+
- Clean, well-structured code with meaningful data variation across cohorts
23+
weaknesses:
24+
- Cell text fontsize (15) is slightly below the recommended 16pt minimum for tick-level
25+
text
26+
- Could explore a warmer sequential colormap that maps more intuitively to good
27+
retention = green
28+
image_description: 'The plot displays a triangular cohort retention heatmap with
29+
10 monthly signup cohorts (Jan 2024 – Oct 2024) on the y-axis and periods (Month
30+
0 – Month 9) on the x-axis. Cells use the viridis colormap — bright yellow-green
31+
for high retention (100%), transitioning through teal to dark blue for low retention
32+
(~33–39%). Each cell contains a bold percentage label with automatic contrast
33+
(white text on dark cells, dark text on light cells). The triangular shape is
34+
correctly formed: Jan 2024 has all 10 periods while Oct 2024 has only 1. Cohort
35+
sizes are shown in parentheses next to each cohort label (e.g., "Jan 2024 (n=1,200)").
36+
The May 2024 cohort is marked with a gold star (★) and surrounded by a gold dashed
37+
border, identifying it as the best-performing cohort with retention of 74% at
38+
Month 5. Cells have rounded corners with white borders, creating a clean, modern
39+
appearance. A vertical colorbar on the right labeled "Retention Rate (%)" maps
40+
the 0–100% range. The title reads "heatmap-cohort-retention · matplotlib · pyplots.ai".'
41+
criteria_checklist:
42+
visual_quality:
43+
score: 29
44+
max: 30
45+
items:
46+
- id: VQ-01
47+
name: Text Legibility
48+
score: 7
49+
max: 8
50+
passed: true
51+
comment: All font sizes explicitly set (title 24, labels 20, ticks 16, cell
52+
text 15). Cell text slightly below 16pt guideline but readable.
53+
- id: VQ-02
54+
name: No Overlap
55+
score: 6
56+
max: 6
57+
passed: true
58+
comment: No overlapping text anywhere. All cell values, axis labels, and tick
59+
labels fully readable.
60+
- id: VQ-03
61+
name: Element Visibility
62+
score: 6
63+
max: 6
64+
passed: true
65+
comment: Cells well-sized with white borders and rounded corners creating
66+
excellent separation.
67+
- id: VQ-04
68+
name: Color Accessibility
69+
score: 4
70+
max: 4
71+
passed: true
72+
comment: Viridis is perceptually-uniform and colorblind-safe.
73+
- id: VQ-05
74+
name: Layout & Canvas
75+
score: 4
76+
max: 4
77+
passed: true
78+
comment: Plot fills canvas well. Colorbar compact and well-positioned. Balanced
79+
margins.
80+
- id: VQ-06
81+
name: Axis Labels & Title
82+
score: 2
83+
max: 2
84+
passed: true
85+
comment: 'Descriptive labels: Months Since Signup, Signup Cohort.'
86+
design_excellence:
87+
score: 16
88+
max: 20
89+
items:
90+
- id: DE-01
91+
name: Aesthetic Sophistication
92+
score: 6
93+
max: 8
94+
passed: true
95+
comment: Rounded-corner cells, white borders, gold star/border highlight,
96+
luminance-based text contrast. Above defaults with intentional design.
97+
- id: DE-02
98+
name: Visual Refinement
99+
score: 5
100+
max: 6
101+
passed: true
102+
comment: All spines removed, tick marks hidden, colorbar outline removed,
103+
generous whitespace between cells.
104+
- id: DE-03
105+
name: Data Storytelling
106+
score: 5
107+
max: 6
108+
passed: true
109+
comment: Best cohort highlighted with star icon and gold dashed border. Clear
110+
focal point guides viewer to key insight.
111+
spec_compliance:
112+
score: 15
113+
max: 15
114+
items:
115+
- id: SC-01
116+
name: Plot Type
117+
score: 5
118+
max: 5
119+
passed: true
120+
comment: Correct triangular cohort retention heatmap.
121+
- id: SC-02
122+
name: Required Features
123+
score: 4
124+
max: 4
125+
passed: true
126+
comment: 'All spec features present: triangular shape, cell annotations, cohort
127+
sizes, colorbar, period labels.'
128+
- id: SC-03
129+
name: Data Mapping
130+
score: 3
131+
max: 3
132+
passed: true
133+
comment: Cohorts on Y, periods on X, retention mapped to color.
134+
- id: SC-04
135+
name: Title & Legend
136+
score: 3
137+
max: 3
138+
passed: true
139+
comment: Title follows exact format. Colorbar serves as legend with proper
140+
label.
141+
data_quality:
142+
score: 14
143+
max: 15
144+
items:
145+
- id: DQ-01
146+
name: Feature Coverage
147+
score: 5
148+
max: 6
149+
passed: true
150+
comment: 'Good variation: fast-churning and high-retention cohorts shown.
151+
Could show slightly more extreme variation.'
152+
- id: DQ-02
153+
name: Realistic Context
154+
score: 5
155+
max: 5
156+
passed: true
157+
comment: SaaS monthly cohort retention is a real, neutral business analytics
158+
scenario.
159+
- id: DQ-03
160+
name: Appropriate Scale
161+
score: 4
162+
max: 4
163+
passed: true
164+
comment: 'Realistic retention values: 100% at signup, decaying to 30-40%.
165+
Cohort sizes 980-1450 plausible.'
166+
code_quality:
167+
score: 10
168+
max: 10
169+
items:
170+
- id: CQ-01
171+
name: KISS Structure
172+
score: 3
173+
max: 3
174+
passed: true
175+
comment: 'Clean linear structure: imports, data, plot, style, save.'
176+
- id: CQ-02
177+
name: Reproducibility
178+
score: 2
179+
max: 2
180+
passed: true
181+
comment: np.random.seed(42) set at start.
182+
- id: CQ-03
183+
name: Clean Imports
184+
score: 2
185+
max: 2
186+
passed: true
187+
comment: 'All imports used: mcolors, mpatches, plt, np.'
188+
- id: CQ-04
189+
name: Code Elegance
190+
score: 2
191+
max: 2
192+
passed: true
193+
comment: Appropriate complexity. FancyBboxPatch justified for rounded-corner
194+
aesthetic.
195+
- id: CQ-05
196+
name: Output & API
197+
score: 1
198+
max: 1
199+
passed: true
200+
comment: Saves as plot.png with dpi=300, bbox_inches=tight. No deprecated
201+
API.
202+
library_mastery:
203+
score: 8
204+
max: 10
205+
items:
206+
- id: LM-01
207+
name: Idiomatic Usage
208+
score: 4
209+
max: 5
210+
passed: true
211+
comment: Uses Axes methods, FancyBboxPatch, ScalarMappable, mcolors.Normalize
212+
correctly. Manual heatmap for rounded corners.
213+
- id: LM-02
214+
name: Distinctive Features
215+
score: 4
216+
max: 5
217+
passed: true
218+
comment: FancyBboxPatch with BoxStyle.Round, luminance-based text color, ScalarMappable
219+
for standalone colorbar are distinctively matplotlib.
220+
verdict: APPROVED
221+
impl_tags:
222+
dependencies: []
223+
techniques:
224+
- colorbar
225+
- annotations
226+
- patches
227+
- manual-ticks
228+
patterns:
229+
- data-generation
230+
- matrix-construction
231+
- iteration-over-groups
232+
dataprep: []
233+
styling:
234+
- custom-colormap
235+
- edge-highlighting

0 commit comments

Comments
 (0)