Skip to content

Commit 9fe4b5c

Browse files
feat(bokeh): implement line-retention-cohort
1 parent 0dd291f commit 9fe4b5c

1 file changed

Lines changed: 111 additions & 0 deletions

File tree

  • plots/line-retention-cohort/implementations
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
"""pyplots.ai
2+
line-retention-cohort: User Retention Curve by Cohort
3+
Library: bokeh | Python 3.13
4+
Quality: pending | Created: 2026-03-16
5+
"""
6+
7+
import numpy as np
8+
from bokeh.io import export_png, output_file, save
9+
from bokeh.models import ColumnDataSource, Legend, Span
10+
from bokeh.plotting import figure
11+
12+
13+
# Data
14+
np.random.seed(42)
15+
weeks = np.arange(0, 13)
16+
17+
cohorts = {
18+
"Jan 2025": {"size": 1245, "decay": 0.18},
19+
"Feb 2025": {"size": 1380, "decay": 0.16},
20+
"Mar 2025": {"size": 1520, "decay": 0.14},
21+
"Apr 2025": {"size": 1410, "decay": 0.12},
22+
"May 2025": {"size": 1680, "decay": 0.10},
23+
}
24+
25+
retention_data = {}
26+
for cohort, params in cohorts.items():
27+
base = 100 * np.exp(-params["decay"] * weeks)
28+
noise = np.random.normal(0, 1.5, len(weeks))
29+
retention = np.clip(base + noise, 0, 100)
30+
retention[0] = 100.0
31+
retention_data[cohort] = retention
32+
33+
# Plot
34+
colors = ["#8FAFC1", "#7B9DB7", "#5A8BA8", "#306998", "#1A4D6E"]
35+
line_widths = [3, 3.5, 4, 4.5, 5]
36+
alphas = [0.55, 0.65, 0.75, 0.85, 1.0]
37+
38+
p = figure(
39+
width=4800,
40+
height=2700,
41+
title="line-retention-cohort · bokeh · pyplots.ai",
42+
x_axis_label="Weeks Since Signup",
43+
y_axis_label="Retention Rate (%)",
44+
)
45+
46+
legend_items = []
47+
for i, (cohort, params) in enumerate(cohorts.items()):
48+
source = ColumnDataSource(data={"week": weeks, "retention": retention_data[cohort]})
49+
label = f"{cohort} (n={params['size']:,})"
50+
51+
line = p.line(
52+
x="week", y="retention", source=source, line_width=line_widths[i], line_color=colors[i], line_alpha=alphas[i]
53+
)
54+
scatter = p.scatter(
55+
x="week",
56+
y="retention",
57+
source=source,
58+
size=12 + i * 2,
59+
fill_color=colors[i],
60+
fill_alpha=alphas[i],
61+
line_color="white",
62+
line_width=2,
63+
)
64+
legend_items.append((label, [line, scatter]))
65+
66+
# Reference line at 20% retention threshold
67+
threshold = Span(location=20, dimension="width", line_color="#999999", line_dash="dashed", line_width=2, line_alpha=0.7)
68+
p.add_layout(threshold)
69+
70+
# Legend
71+
legend = Legend(items=legend_items, location="top_right")
72+
legend.label_text_font_size = "20pt"
73+
legend.glyph_height = 30
74+
legend.glyph_width = 30
75+
legend.spacing = 12
76+
legend.padding = 20
77+
legend.background_fill_alpha = 0.8
78+
legend.border_line_alpha = 0.3
79+
p.add_layout(legend)
80+
81+
# Style
82+
p.title.text_font_size = "42pt"
83+
p.xaxis.axis_label_text_font_size = "32pt"
84+
p.yaxis.axis_label_text_font_size = "32pt"
85+
p.xaxis.major_label_text_font_size = "24pt"
86+
p.yaxis.major_label_text_font_size = "24pt"
87+
88+
p.y_range.start = 0
89+
p.y_range.end = 105
90+
p.x_range.start = -0.3
91+
p.x_range.end = 12.3
92+
93+
p.ygrid.grid_line_alpha = 0.2
94+
p.ygrid.grid_line_dash = "dashed"
95+
p.xgrid.grid_line_alpha = 0
96+
97+
p.background_fill_color = "#fafafa"
98+
p.border_fill_color = "white"
99+
100+
p.axis.axis_line_width = 2
101+
p.axis.axis_line_color = "#333333"
102+
p.axis.major_tick_line_width = 2
103+
p.axis.minor_tick_line_width = 0
104+
105+
p.toolbar_location = None
106+
107+
# Save
108+
export_png(p, filename="plot.png")
109+
110+
output_file("plot.html")
111+
save(p)

0 commit comments

Comments
 (0)