Cohort Analysis
Overview
Cohort analysis tracks groups of users with shared characteristics over time, revealing patterns in retention, engagement, and lifetime value.
When to Use
- Measuring user retention rates and identifying when users churn
- Analyzing customer lifetime value (LTV) and payback periods
- Comparing performance across different user acquisition channels or campaigns
- Understanding how product changes affect different user groups over time
- Tracking engagement patterns and identifying early warning signs of churn
- Evaluating the long-term impact of onboarding improvements or feature releases
Core Concepts
- Cohort: Group of users sharing a characteristic (signup date, region, etc.)
- Cohort Size: Initial group size
- Retention Rate: Percentage remaining active
- Churn Rate: Percentage who left
- Retention Curve: How cohort degrades over time
Cohort Types
- Acquisition Date: Users grouped by signup period
- Behavioral: Users grouped by actions taken
- Revenue: Users grouped by purchase value
- Geographic: Users grouped by location
- Demographic: Users grouped by characteristics
Implementation with Python
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
# Create sample user lifecycle data
np.random.seed(42)
# Generate user data
n_users = 5000
users = []
for user_id in range(n_users):
signup_month = np.random.choice(range(1, 13))
lifetime_months = np.random.poisson(6) + 1
for month in range(1, lifetime_months + 1):
users.append({
'user_id': user_id,
'signup_month': signup_month,
'month': month,
'active': 1,
})
df = pd.DataFrame(users)
# Add derived columns
df['cohort_month'] = df['signup_month']
df['cohort_age'] = df['month'] # Could be day, week, etc.
df['date'] = pd.to_datetime('2023-01-01') + pd.to_timedelta(df['signup_month'] * 30, unit='D')
print("User Data Summary:")
print(df.head(10))
# 1. Cohort Table (Retention Matrix)
cohort_data = df.groupby(['cohort_month', 'cohort_age']).agg({
'user_id': 'nunique'
}).reset_index()
cohort_data.columns = ['cohort_month', 'cohort_age', 'unique_users']
# Create pivot table
cohort_pivot = cohort_data.pivot(index='cohort_month', columns='cohort_age', values='unique_users')
print("\nCohort Sizes (Raw User Counts):")
print(cohort_pivot)
# 2. Cohort Retention (as percentage of cohort size)
cohort_size = cohort_pivot.iloc[:, 0]
retention_table = cohort_pivot.divide(cohort_size, axis=0) * 100
print("\nCohort Retention Rate (%):")
print(retention_table.round(1))
# 3. Visualize Retention Matrix
fig, axes = plt.subplots(2, 1, figsize=(14, 8))
# Heatmap of raw counts
sns.heatmap(cohort_pivot, annot=True, fmt='g', cmap='YlOrRd', ax=axes[0],
cbar_kws={'label': 'User Count'})
axes[0].set_title('Cohort Sizes - User Counts')
axes[0].set_xlabel('Cohort Age (Months)')
axes[0].set_ylabel('Cohort Month')
# Heatmap of retention rates
sns.heatmap(retention_table, annot=True, fmt='.0f', cmap='RdYlGn', vmin=0, vmax=100,
ax=axes[1], cbar_kws={'label': 'Retention %'})
axes[1].set_title('Cohort Retention Rates (%)')
axes[1].set_xlabel('Cohort Age (Months)')
axes[1].set_ylabel('Cohort Month')
plt.tight_layout()
plt.show()
# 4. Retention Curve
fig, ax = plt.subplots(figsize=(12, 6))
# Plot retention curves for each cohort
for cohort_month in cohort_pivot.index[:8]: # First 8 cohorts
cohort_retention = retention_table.loc[cohort_month]
ax.plot(cohort_retention.index, cohort_retention.values, marker='o', label=f'Cohort {cohort_month}')
ax.set_xlabel('Cohort Age (Months)')
ax.set_ylabel('Retention Rate (%)')
ax.set_title('Retention Curves by Cohort')
ax.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
ax.grid(True, alpha=0.3)
ax.set_ylim([0, 105])
plt.tight_layout()
plt.show()
# 5. Average Retention Curve
fig, ax = plt.subplots(figsize=(10, 6))
# Calculate average retention at each age
avg_retention = retention_table.mean()
ax.plot(avg_retention.index, avg_retention.values, marker='o', linewidth=2, markersize=8, color='navy')
ax.fill_between(avg_retention.index, avg_retention.values, alpha=0.3, color='navy')
# Add confidence interval
std_retention = retention_table.std()
ax.fill_between(std_retention.index,
avg_retention - std_retention,
avg_retention + std_retention,
alpha=0.2, color='navy', label='±1 Std Dev')
ax.set_xlabel('Cohort Age (Months)')
ax.set_ylabel('Retention Rate (%)')
ax.set_title('Average Retention Curve with Confidence Band')
ax.legend()
ax.grid(True, alpha=0.3)
ax.set_ylim([0, 105])
plt.tight_layout()
plt.show()
# 6. Churn Rate
churn_rate = 100 - retention_table
print("\nChurn Rates (%):")
print(churn_rate.round(1).head())
# 7. Revenue Cohort Analysis
# Add revenue data
np.random.seed(42)
df['revenue'] = np.random.exponential(50, len(df))
# Revenue by cohort
revenue_data = df.groupby(['cohort_month', 'cohort_age']).agg({
'revenue': 'sum',
'user_id': 'nunique'
}).reset_index()
revenue_data['revenue_per_user'] = revenue_data['revenue'] / revenue_data['user_id']
revenue_pivot = revenue_data.pivot(index='cohort_month', columns='cohort_age', values='revenue')
rpu_pivot = revenue_data.pivot(index='cohort_month', columns='cohort_age', values='revenue_per_user')
# Visualize revenue
fig, axes = plt.subplots(2, 1, figsize=(14, 8))
sns.heatmap(revenue_pivot, annot=True, fmt='.0f', cmap='YlGnBu', ax=axes[0],
cbar_kws={'label': 'Total Revenue ($)'})
axes[0].set_title('Total Revenue by Cohort')
axes[0].set_xlabel('Cohort Age (Months)')
axes[0].set_ylabel('Cohort Month')
sns.heatmap(rpu_pivot, annot=True, fmt='.2f', cmap='YlGnBu', ax=axes[1],
cbar_kws={'label': 'Revenue per User ($)'})
axes[1].set_title('Revenue per User by Cohort')
axes[1].set_xlabel('Cohort Age (Months)')
axes[1].set_ylabel('Cohort Month')
plt.tight_layout()
plt.show()
# 8. Lifetime Value Calculation
df['month_since_signup'] = df['cohort_age']
ltv_data = df.groupby('user_id').agg({
'revenue': 'sum',
'cohort_month': 'first',
'month_since_signup': 'max',
}).reset_index()
ltv_data.columns = ['user_id', 'lifetime_value', 'cohort_month', 'lifetime_months']
# Average LTV by cohort
ltv_by_cohort = ltv_data.groupby('cohort_month')['lifetime_value'].agg(['mean', 'median', 'std'])
print("\nLifetime Value by Cohort:")
print(ltv_by_cohort.round(2))
fig, ax = plt.subplots(figsize=(10, 6))
ltv_by_cohort['mean'].plot(kind='bar', ax=ax, color='skyblue', edgecolor='black')
ax.set_title('Average Lifetime Value by Cohort')
ax.set_xlabel('Cohort Month')
ax.set_ylabel('Lifetime Value ($)')
ax.grid(True, alpha=0.3, axis='y')
plt.tight_layout()
plt.show()
# 9. Cohort Composition Over Time
fig, ax = plt.subplots(figsize=(12, 6))
# Active users per month by cohort
active_by_month = df.groupby(['date', 'cohort_month']).size().reset_index(name='active_users')
pivot_active = active_by_month.pivot(index='date', columns='cohort_month', values='active_users')
pivot_active.plot(ax=ax, marker='o')
ax.set_title('Active Users Per Month by Cohort')
ax.set_xlabel('Month')
ax.set_ylabel('Active Users')
ax.legend(title='Cohort Month', bbox_to_anchor=(1.05, 1))
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# 10. Cohort Summary Metrics
summary_metrics = pd.DataFrame({
'Cohort Month': cohort_size.index,
'Initial Size': cohort_size.values,
'Month 1 Retention': retention_table.iloc[:, 0].values,
'Month 3 Retention': retention_table.iloc[:, min(2, retention_table.shape[1]-1)].values,
'Avg LTV': ltv_by_cohort['mean'].values,
})
print("\nCohort Summary Metrics:")
print(summary_metrics.round(2))
# 11. Visualization comparison
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
# Month 1 vs Month 3 retention
ax_plot = axes[0]
months = ['Month 1', 'Month 3']
month_1_ret = retention_table.iloc[:, 0].mean()
month_3_ret = retention_table.iloc[:, min(2, retention_table.shape[1]-1)].mean()
ax_plot.bar(months, [month_1_ret, month_3_ret], color=['#1f77b4', '#ff7f0e'], edgecolor='black')
ax_plot.set_ylabel('Retention Rate (%)')
ax_plot.set_title('Average Retention by Milestone')
ax_plot.set_ylim([0, 100])
for i, v in enumerate([month_1_ret, month_3_ret]):
ax_plot.text(i, v + 2, f'{v:.1f}%', ha='center')
# Cohort size trend
axes[1].plot(cohort_size.index, cohort_size.values, marker='o', linewidth=2, markersize=8)
axes[1].set_xlabel('Cohort Month')
axes[1].set_ylabel('Cohort Size')
axes[1].set_title('Cohort Sizes Over Time')
axes[1].grid(True, alpha=0.3)
# LTV trend
axes[2].plot(ltv_by_cohort.index, ltv_by_cohort['mean'].values, marker='o', linewidth=2, markersize=8, color='green')
axes[2].set_xlabel('Cohort Month')
axes[2].set_ylabel('Average Lifetime Value ($)')
axes[2].set_title('LTV Trend by Cohort')
axes[2].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print("\nCohort analysis complete!")
Key Metrics
- Retention Rate: % of cohort active
- Churn Rate: % of cohort lost
- Day/Month 1 Retention: Early engagement
- Lifetime Value: Total revenue per user
- Payback Period: Time to recover CAC
Insights to Look For
- Early retention predictors
- Differences between cohorts
- Seasonal patterns
- Engagement degradation
- Revenue trends
Deliverables
- Cohort retention matrix
- Retention curve visualization
- Churn rate analysis
- Lifetime value calculations
- Revenue per cohort
- Executive summary with insights
- Actionable recommendations
1---2name: cohort-analysis3description: Track and analyze user cohorts over time, calculate retention rates, and identify behavioral patterns for customer lifecycle and retention analysis4---5
6# Cohort Analysis
7
8## Overview
9
10Cohort analysis tracks groups of users with shared characteristics over time, revealing patterns in retention, engagement, and lifetime value.
11
12## When to Use
13
14- Measuring user retention rates and identifying when users churn
15- Analyzing customer lifetime value (LTV) and payback periods
16- Comparing performance across different user acquisition channels or campaigns
17- Understanding how product changes affect different user groups over time
18- Tracking engagement patterns and identifying early warning signs of churn
19- Evaluating the long-term impact of onboarding improvements or feature releases
20
21## Core Concepts
22
23- **Cohort**: Group of users sharing a characteristic (signup date, region, etc.)
24- **Cohort Size**: Initial group size
25- **Retention Rate**: Percentage remaining active
26- **Churn Rate**: Percentage who left
27- **Retention Curve**: How cohort degrades over time
28
29## Cohort Types
30
31- **Acquisition Date**: Users grouped by signup period
32- **Behavioral**: Users grouped by actions taken
33- **Revenue**: Users grouped by purchase value
34- **Geographic**: Users grouped by location
35- **Demographic**: Users grouped by characteristics
36
37## Implementation with Python
38
39```python
40import pandas as pd
41import numpy as np
42import matplotlib.pyplot as plt
43import seaborn as sns
44
45# Create sample user lifecycle data
46np.random.seed(42)
47
48# Generate user data
49n_users = 5000
50users = []
51
52for user_id in range(n_users):
53 signup_month = np.random.choice(range(1, 13))
54 lifetime_months = np.random.poisson(6) + 1
55
56 for month in range(1, lifetime_months + 1):
57 users.append({
58 'user_id': user_id,
59 'signup_month': signup_month,
60 'month': month,
61 'active': 1,
62 })
63
64df = pd.DataFrame(users)
65
66# Add derived columns
67df['cohort_month'] = df['signup_month']
68df['cohort_age'] = df['month'] # Could be day, week, etc.
69df['date'] = pd.to_datetime('2023-01-01') + pd.to_timedelta(df['signup_month'] * 30, unit='D')
70
71print("User Data Summary:")
72print(df.head(10))
73
74# 1. Cohort Table (Retention Matrix)
75cohort_data = df.groupby(['cohort_month', 'cohort_age']).agg({
76 'user_id': 'nunique'
77}).reset_index()
78cohort_data.columns = ['cohort_month', 'cohort_age', 'unique_users']
79
80# Create pivot table
81cohort_pivot = cohort_data.pivot(index='cohort_month', columns='cohort_age', values='unique_users')
82
83print("\nCohort Sizes (Raw User Counts):")
84print(cohort_pivot)
85
86# 2. Cohort Retention (as percentage of cohort size)
87cohort_size = cohort_pivot.iloc[:, 0]
88retention_table = cohort_pivot.divide(cohort_size, axis=0) * 100
89
90print("\nCohort Retention Rate (%):")
91print(retention_table.round(1))
92
93# 3. Visualize Retention Matrix
94fig, axes = plt.subplots(2, 1, figsize=(14, 8))
95
96# Heatmap of raw counts
97sns.heatmap(cohort_pivot, annot=True, fmt='g', cmap='YlOrRd', ax=axes[0],
98 cbar_kws={'label': 'User Count'})
99axes[0].set_title('Cohort Sizes - User Counts')
100axes[0].set_xlabel('Cohort Age (Months)')
101axes[0].set_ylabel('Cohort Month')
102
103# Heatmap of retention rates
104sns.heatmap(retention_table, annot=True, fmt='.0f', cmap='RdYlGn', vmin=0, vmax=100,
105 ax=axes[1], cbar_kws={'label': 'Retention %'})
106axes[1].set_title('Cohort Retention Rates (%)')
107axes[1].set_xlabel('Cohort Age (Months)')
108axes[1].set_ylabel('Cohort Month')
109
110plt.tight_layout()
111plt.show()
112
113# 4. Retention Curve
114fig, ax = plt.subplots(figsize=(12, 6))
115
116# Plot retention curves for each cohort
117for cohort_month in cohort_pivot.index[:8]: # First 8 cohorts
118 cohort_retention = retention_table.loc[cohort_month]
119 ax.plot(cohort_retention.index, cohort_retention.values, marker='o', label=f'Cohort {cohort_month}')
120
121ax.set_xlabel('Cohort Age (Months)')
122ax.set_ylabel('Retention Rate (%)')
123ax.set_title('Retention Curves by Cohort')
124ax.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
125ax.grid(True, alpha=0.3)
126ax.set_ylim([0, 105])
127
128plt.tight_layout()
129plt.show()
130
131# 5. Average Retention Curve
132fig, ax = plt.subplots(figsize=(10, 6))
133
134# Calculate average retention at each age
135avg_retention = retention_table.mean()
136ax.plot(avg_retention.index, avg_retention.values, marker='o', linewidth=2, markersize=8, color='navy')
137ax.fill_between(avg_retention.index, avg_retention.values, alpha=0.3, color='navy')
138
139# Add confidence interval
140std_retention = retention_table.std()
141ax.fill_between(std_retention.index,
142 avg_retention - std_retention,
143 avg_retention + std_retention,
144 alpha=0.2, color='navy', label='±1 Std Dev')
145
146ax.set_xlabel('Cohort Age (Months)')
147ax.set_ylabel('Retention Rate (%)')
148ax.set_title('Average Retention Curve with Confidence Band')
149ax.legend()
150ax.grid(True, alpha=0.3)
151ax.set_ylim([0, 105])
152
153plt.tight_layout()
154plt.show()
155
156# 6. Churn Rate
157churn_rate = 100 - retention_table
158print("\nChurn Rates (%):")
159print(churn_rate.round(1).head())
160
161# 7. Revenue Cohort Analysis
162# Add revenue data
163np.random.seed(42)
164df['revenue'] = np.random.exponential(50, len(df))
165
166# Revenue by cohort
167revenue_data = df.groupby(['cohort_month', 'cohort_age']).agg({
168 'revenue': 'sum',
169 'user_id': 'nunique'
170}).reset_index()
171revenue_data['revenue_per_user'] = revenue_data['revenue'] / revenue_data['user_id']
172
173revenue_pivot = revenue_data.pivot(index='cohort_month', columns='cohort_age', values='revenue')
174rpu_pivot = revenue_data.pivot(index='cohort_month', columns='cohort_age', values='revenue_per_user')
175
176# Visualize revenue
177fig, axes = plt.subplots(2, 1, figsize=(14, 8))
178
179sns.heatmap(revenue_pivot, annot=True, fmt='.0f', cmap='YlGnBu', ax=axes[0],
180 cbar_kws={'label': 'Total Revenue ($)'})
181axes[0].set_title('Total Revenue by Cohort')
182axes[0].set_xlabel('Cohort Age (Months)')
183axes[0].set_ylabel('Cohort Month')
184
185sns.heatmap(rpu_pivot, annot=True, fmt='.2f', cmap='YlGnBu', ax=axes[1],
186 cbar_kws={'label': 'Revenue per User ($)'})
187axes[1].set_title('Revenue per User by Cohort')
188axes[1].set_xlabel('Cohort Age (Months)')
189axes[1].set_ylabel('Cohort Month')
190
191plt.tight_layout()
192plt.show()
193
194# 8. Lifetime Value Calculation
195df['month_since_signup'] = df['cohort_age']
196ltv_data = df.groupby('user_id').agg({
197 'revenue': 'sum',
198 'cohort_month': 'first',
199 'month_since_signup': 'max',
200}).reset_index()
201ltv_data.columns = ['user_id', 'lifetime_value', 'cohort_month', 'lifetime_months']
202
203# Average LTV by cohort
204ltv_by_cohort = ltv_data.groupby('cohort_month')['lifetime_value'].agg(['mean', 'median', 'std'])
205
206print("\nLifetime Value by Cohort:")
207print(ltv_by_cohort.round(2))
208
209fig, ax = plt.subplots(figsize=(10, 6))
210ltv_by_cohort['mean'].plot(kind='bar', ax=ax, color='skyblue', edgecolor='black')
211ax.set_title('Average Lifetime Value by Cohort')
212ax.set_xlabel('Cohort Month')
213ax.set_ylabel('Lifetime Value ($)')
214ax.grid(True, alpha=0.3, axis='y')
215plt.tight_layout()
216plt.show()
217
218# 9. Cohort Composition Over Time
219fig, ax = plt.subplots(figsize=(12, 6))
220
221# Active users per month by cohort
222active_by_month = df.groupby(['date', 'cohort_month']).size().reset_index(name='active_users')
223pivot_active = active_by_month.pivot(index='date', columns='cohort_month', values='active_users')
224
225pivot_active.plot(ax=ax, marker='o')
226ax.set_title('Active Users Per Month by Cohort')
227ax.set_xlabel('Month')
228ax.set_ylabel('Active Users')
229ax.legend(title='Cohort Month', bbox_to_anchor=(1.05, 1))
230ax.grid(True, alpha=0.3)
231
232plt.tight_layout()
233plt.show()
234
235# 10. Cohort Summary Metrics
236summary_metrics = pd.DataFrame({
237 'Cohort Month': cohort_size.index,
238 'Initial Size': cohort_size.values,
239 'Month 1 Retention': retention_table.iloc[:, 0].values,
240 'Month 3 Retention': retention_table.iloc[:, min(2, retention_table.shape[1]-1)].values,
241 'Avg LTV': ltv_by_cohort['mean'].values,
242})
243
244print("\nCohort Summary Metrics:")
245print(summary_metrics.round(2))
246
247# 11. Visualization comparison
248fig, axes = plt.subplots(1, 3, figsize=(15, 4))
249
250# Month 1 vs Month 3 retention
251ax_plot = axes[0]
252months = ['Month 1', 'Month 3']
253month_1_ret = retention_table.iloc[:, 0].mean()
254month_3_ret = retention_table.iloc[:, min(2, retention_table.shape[1]-1)].mean()
255ax_plot.bar(months, [month_1_ret, month_3_ret], color=['#1f77b4', '#ff7f0e'], edgecolor='black')
256ax_plot.set_ylabel('Retention Rate (%)')
257ax_plot.set_title('Average Retention by Milestone')
258ax_plot.set_ylim([0, 100])
259for i, v in enumerate([month_1_ret, month_3_ret]):
260 ax_plot.text(i, v + 2, f'{v:.1f}%', ha='center')
261
262# Cohort size trend
263axes[1].plot(cohort_size.index, cohort_size.values, marker='o', linewidth=2, markersize=8)
264axes[1].set_xlabel('Cohort Month')
265axes[1].set_ylabel('Cohort Size')
266axes[1].set_title('Cohort Sizes Over Time')
267axes[1].grid(True, alpha=0.3)
268
269# LTV trend
270axes[2].plot(ltv_by_cohort.index, ltv_by_cohort['mean'].values, marker='o', linewidth=2, markersize=8, color='green')
271axes[2].set_xlabel('Cohort Month')
272axes[2].set_ylabel('Average Lifetime Value ($)')
273axes[2].set_title('LTV Trend by Cohort')
274axes[2].grid(True, alpha=0.3)
275
276plt.tight_layout()
277plt.show()
278
279print("\nCohort analysis complete!")
280```
281
282## Key Metrics
283
284- **Retention Rate**: % of cohort active
285- **Churn Rate**: % of cohort lost
286- **Day/Month 1 Retention**: Early engagement
287- **Lifetime Value**: Total revenue per user
288- **Payback Period**: Time to recover CAC
289
290## Insights to Look For
291
292- Early retention predictors
293- Differences between cohorts
294- Seasonal patterns
295- Engagement degradation
296- Revenue trends
297
298## Deliverables
299
300- Cohort retention matrix
301- Retention curve visualization
302- Churn rate analysis
303- Lifetime value calculations
304- Revenue per cohort
305- Executive summary with insights
306- Actionable recommendations