A retention curve plots, for a cohort of users, what % is still active over time.
A second worked example: two curves, same D1, very different stories
A note-taking app and a habit-tracking app both launch cohorts with 40% D1 retention - a strong start. Six weeks later:
- Note-taking app: 40% (D1) -> 22% (D7) -> 9% (D30) -> 3% (D42), still falling with no sign of leveling off.
- Habit-tracking app: 40% (D1) -> 19% (D7) -> 14% (D30) -> 13% (D42), flat from D30 onward.
Same starting point, opposite diagnosis. The note-taking app is a leaky bucket - whatever's driving that D1 spike (a good onboarding demo, maybe) isn't translating into a habit, and the curve gives no evidence it ever will; the fix has to happen upstream, in what happens right after signup. The habit-tracking app's curve is flat, not high - only 13% stick - but that 13% is real, durable product-market fit for a niche audience, and the fix there is a growth problem (get more of the right users in), not a retention problem.
The number that matters isn't D1. It's whether the curve stops falling. A curve that's still dropping at D42 hasn't told you its real retention rate yet - it's told you a lower bound.
Cohort retention matrix & heatmap in Python
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
# Create cohort activity dataframe (Cohort Month x Months Since Signup)
cohorts = ["2026-01", "2026-02", "2026-03", "2026-04", "2026-05"]
retention_data = []
# Retention profile: Month 0 (100%), M1 (45%), M2 (30%), M3 (25%), M4 (24%)
base_rates = [1.0, 0.45, 0.30, 0.25, 0.24]
for i, c in enumerate(cohorts):
for m in range(len(cohorts) - i):
retention_data.append({
"cohort_month": c,
"month_number": m,
"retention_rate": base_rates[m] + np.random.uniform(-0.02, 0.02)
})
df_ret = pd.DataFrame(retention_data)
pivot_ret = df_ret.pivot(index="cohort_month", columns="month_number", values="retention_rate")
print("--- Cohort Retention Matrix ---")
print((pivot_ret * 100).round(1).astype(str) + "%")
# Visualization
plt.figure(figsize=(8, 4))
sns.heatmap(pivot_ret, annot=True, fmt=".1%", cmap="YlGnBu", cbar=False)
plt.title("User Cohort Retention Heatmap")
plt.xlabel("Months Since Signup")
plt.ylabel("Cohort Month")
plt.tight_layout()
# plt.show()
Takeaway
Stabilization of the curve indicates product-market fit. An asymptote above 20% proves long-term user retention.