Lesson 4 · Real-World Data Analytics
Python Data Analytics #04: Cohort Analysis & Customer Retention in Python
Video four of the hundred-video real-world data analytics series. Grouping real customers by their real first purchase month, and tracking how each real…
- CourseReal-World Data Analytics
- Lesson4 of 26
- Video28 min
- FormatJupyter notebook · 30 code cells
- Data1 dataset
What you'll learn
Datasets used in this lesson
Save these next to the notebook. In Google Colab, upload them with the 📁 icon on the left first.
- online_retail_clean.csv36.2 MB
📓 Full notebook
Download .ipynbData Analytics 100, Video 4: Cohort Analysis and Customer Retention#
- Video four of the hundred-video real-world data analytics series.
- Grouping real customers by their real first purchase month, and tracking how each real cohort actually retains over time.
- Let's get into it.
Part 1: What a Cohort Actually Is#
import pandas as pd
import matplotlib.pyplot as plt
clean = pd.read_csv('online_retail_clean.csv', parse_dates=['InvoiceDate'])
clean['CustomerID'] = clean['CustomerID'].astype(int)
clean.shape
Part 2: Real Invoice Month#
clean['InvoiceMonth'] = clean['InvoiceDate'].values.astype('datetime64[M]')
clean[['InvoiceDate', 'InvoiceMonth']].head(3)
Part 3: Real Cohort Month per Customer#
first_purchase = clean.groupby('CustomerID')['InvoiceMonth'].min()
first_purchase = first_purchase.rename('CohortMonth')
first_purchase.head()
Part 4: Attaching Cohort Month Back to Every Transaction#
merged = clean.merge(first_purchase, on='CustomerID')
merged[['CustomerID', 'InvoiceMonth', 'CohortMonth']].head(3)
Part 5: Real Cohort Index#
year_diff = merged['InvoiceMonth'].dt.year - merged['CohortMonth'].dt.year
month_diff = merged['InvoiceMonth'].dt.month - merged['CohortMonth'].dt.month
merged['CohortIndex'] = year_diff * 12 + month_diff
merged['CohortIndex'].describe()
Part 6: Real Active Customers per Cohort per Month#
cohort_data = merged.groupby(['CohortMonth', 'CohortIndex'])['CustomerID'].nunique().reset_index()
cohort_data.head()
Part 7: Real Cohort Pivot Table#
cohort_pivot = cohort_data.pivot(index='CohortMonth', columns='CohortIndex', values='CustomerID')
cohort_pivot.shape
cohort_pivot.iloc[:5, :6]
Part 8: Real Cohort Sizes#
cohort_sizes = cohort_pivot.iloc[:, 0]
cohort_sizes
Part 9: Real Retention Rate Table#
retention = cohort_pivot.divide(cohort_sizes, axis=0)
retention.round(3).iloc[:6, :6]
Part 10: Real Month-1 Retention Across Cohorts#
month1_retention = retention[1].dropna().sort_index()
month1_retention.round(3)
month1_retention.mean().round(3)
Part 11: Visualizing the Real Retention Heatmap#
plt.figure(figsize=(11, 7))
plt.imshow(retention, cmap='YlGnBu', aspect='auto', vmin=0, vmax=1)
plt.colorbar(label='Real Retention Rate')
plt.yticks(range(len(retention.index)), [d.strftime('%Y-%m') for d in retention.index])
plt.xlabel('Real Months Since First Purchase')
plt.title('Real Customer Retention Heatmap by Cohort')
plt.tight_layout()
plt.savefig('cohort_retention_heatmap.png', dpi=120)
plt.close()
Part 12: Real Retention Curves for a Few Cohorts#
plt.figure(figsize=(9, 6))
for cohort in retention.index[:6]:
plt.plot(retention.columns, retention.loc[cohort], marker='o', label=cohort.strftime('%Y-%m'))
plt.xlabel('Real Months Since First Purchase')
plt.ylabel('Real Retention Rate')
plt.title('Real Retention Curves for the First Six Cohorts')
plt.legend()
plt.tight_layout()
plt.savefig('cohort_retention_curves.png', dpi=120)
plt.close()
Part 13: Real Revenue per Cohort per Month#
revenue_data = merged.groupby(['CohortMonth', 'CohortIndex'])['Revenue'].sum().reset_index()
revenue_pivot = revenue_data.pivot(index='CohortMonth', columns='CohortIndex', values='Revenue')
revenue_pivot.iloc[:5, :6].round(0)
Part 14: Real Average Revenue per Retained Customer#
avg_revenue_per_active = revenue_pivot / cohort_pivot
avg_revenue_per_active.iloc[:5, :6].round(2)
Part 15: Best and Worst Real Cohorts by Month-1 Retention#
best_cohort = month1_retention.idxmax()
worst_cohort = month1_retention.idxmin()
print(f'Best real cohort: {best_cohort.strftime("%Y-%m")} at {round(month1_retention[best_cohort]*100,1)}% month-1 retention')
print(f'Weakest real cohort: {worst_cohort.strftime("%Y-%m")} at {round(month1_retention[worst_cohort]*100,1)}% month-1 retention')
Part 16: Is Retention Actually Trending Over Time#
early_cohorts = month1_retention.iloc[:len(month1_retention)//2]
late_cohorts = month1_retention.iloc[len(month1_retention)//2:]
round(early_cohorts.mean(), 3), round(late_cohorts.mean(), 3)
Part 17: Real Long-Term Retention at Month 6#
month6_retention = retention[6].dropna()
month6_retention.round(3)
month6_retention.mean().round(3)
Part 18: Real Cumulative Retained Customer-Months#
customer_months = cohort_pivot.sum(axis=1)
customer_months.sort_values(ascending=False).round(0)
Part 19: Real Sanity Check on the Pivot Math#
manual_check = merged[(merged['CohortMonth'] == cohort_pivot.index[0]) & (merged['CohortIndex'] == 1)]['CustomerID'].nunique()
manual_check == cohort_pivot.iloc[0, 1]
Part 20: Saving the Real Retention Table#
retention.round(4).to_csv('cohort_retention_table.csv')
reloaded = pd.read_csv('cohort_retention_table.csv', index_col=0)
reloaded.shape == retention.shape
Part 21: Real One-Time vs Real Repeat Customers#
months_active = merged.groupby('CustomerID')['CohortIndex'].apply(lambda s: (s == 0).sum() == len(s))
one_time_pct = months_active.mean()
round(one_time_pct * 100, 1)
Part 22: Real Cohort Size Trend Over Time#
plt.figure(figsize=(9, 5))
plt.bar([d.strftime('%Y-%m') for d in cohort_sizes.index], cohort_sizes.values, color='steelblue')
plt.xticks(rotation=45)
plt.ylabel('Real New Customers')
plt.title('Real New Customer Acquisition by Cohort Month')
plt.tight_layout()
plt.savefig('cohort_acquisition_sizes.png', dpi=120)
plt.close()
Part 23: Real Overall Repeat Purchase Rate#
orders_per_customer = clean.groupby('CustomerID')['InvoiceNo'].nunique()
repeat_rate = (orders_per_customer > 1).mean()
round(repeat_rate * 100, 1)
Part 24: Real Time Between First and Second Purchase#
repeat_customers = orders_per_customer[orders_per_customer > 1].index
first_two = clean[clean['CustomerID'].isin(repeat_customers)].sort_values('InvoiceDate')
def gap_to_second_visit(dates):
uniq = sorted(dates.unique())
return (uniq[1] - uniq[0]) / pd.Timedelta(days=1) if len(uniq) > 1 else None
gap_days = first_two.groupby('CustomerID')['InvoiceDate'].apply(gap_to_second_visit).dropna()
gap_days.median().round(1)
Part 25: Do Month-1 Returners Actually Spend More Overall#
month0_month1_customers = merged[(merged['CohortIndex'] == 1)]['CustomerID'].unique()
total_revenue_per_customer = clean.groupby('CustomerID')['Revenue'].sum()
returners_revenue = total_revenue_per_customer.loc[total_revenue_per_customer.index.isin(month0_month1_customers)].mean()
non_returners_revenue = total_revenue_per_customer.loc[~total_revenue_per_customer.index.isin(month0_month1_customers)].mean()
round(returners_revenue, 2), round(non_returners_revenue, 2)
Part 26: Real Matured Cohorts Only#
matured = retention[retention[6].notna()]
matured.shape[0]
matured[[0, 1, 3, 6]].round(3)
Part 27: Real Country Mix Within the Largest Cohort#
largest_cohort_month = cohort_sizes.idxmax()
largest_cohort_customers = first_purchase[first_purchase == largest_cohort_month].index
clean[clean['CustomerID'].isin(largest_cohort_customers)]['Country'].value_counts().head(5)
Part 28: Real Retention Improvement Recommendation#
gap = round((returners_revenue - non_returners_revenue) / non_returners_revenue * 100, 1)
print(f'Customers retained through month one are worth {gap}% more in real lifetime revenue on average, making early-lifecycle retention campaigns a genuinely high-value real investment.')
Part 29: Real Verification the Full Table Reconstructs Correctly#
total_from_pivot = cohort_pivot.iloc[:, 0].sum()
total_unique_customers = clean['CustomerID'].nunique()
total_from_pivot == total_unique_customers
Part 30: Real Cohort Count Recap#
print(f'Analyzed {len(cohort_pivot)} real monthly cohorts covering {total_unique_customers} real unique customers.')
Wrap-Up: What You Learned#
- A cohort groups real customers by their real first purchase month, tracked together going forward.
- The real cohort index measures months elapsed since that first purchase, letting different real cohorts be compared fairly.
- A retention table divides real active counts by each real cohort's starting size, turning raw headcounts into a real comparable percentage.
- Real revenue-per-cohort tracking shows whether retained customers are genuinely still worth as much, not just whether they came back.
- Comparing real early cohorts to real later ones reveals whether the business is actually improving at keeping customers over time.
- Next video: real sales forecasting, predicting real future retail demand from this same historical data.
Found this useful?
All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.



