Customer Analytics in Python: Cohort Analysis, RFM Segmentation & LTV
Master customer analytics in Python with Pandas and Seaborn. Calculate RFM scores, build customer segments, run monthly cohort retention heatmaps, and estimate Customer Lifetime Value (LTV).
Acquiring a new customer is 5 to 25 times more expensive than retaining an existing one. Yet companies frequently treat all users identically, blasting the same email blasts and generic discounts to one-time buyers and multi-year brand loyalists alike.
Data analysts use RFM Segmentation and Cohort Retention Analysis to identify high-margin customer cohorts, isolate early churn signals, and calculate expected Customer Lifetime Value (LTV).
In this guide, we'll build a complete, end-to-end customer analytics engine in Python using Pandas and Seaborn.
To reinforce your data wrangling foundations, check out our Python Pandas Data Analysis Guide or explore the Data Analyst Career Track.
Interactive Practice on Topfolio
Want to practice SQL aggregations and Python cohort transformations on real transaction logs? Try Topfolio Practice with instant execution.
1. Setting Up the Customer Transaction Dataset
Let's generate a realistic 12-month e-commerce transaction dataset with customer IDs, timestamps, item quantities, and unit prices:
import pandas as pd
import numpy as np
import datetime as dt
import matplotlib.pyplot as plt
import seaborn as sns
# Visual formatting configuration
pd.set_option('display.max_columns', None)
pd.set_option('display.float_format', lambda x: f'{x:.2f}')
sns.set_theme(style='whitegrid')
# Generate synthetic transaction log
np.random.seed(42)
n_transactions = 6000
n_customers = 900
start_date = dt.datetime(2025, 1, 1)
date_list = [start_date + dt.timedelta(days=int(np.random.randint(0, 365))) for _ in range(n_transactions)]
customer_ids = np.random.randint(1000, 1000 + n_customers, size=n_transactions)
quantities = np.random.randint(1, 8, size=n_transactions)
unit_prices = np.round(np.random.uniform(15.0, 120.0, size=n_transactions), 2)
df = pd.DataFrame({
'CustomerID': customer_ids,
'InvoiceDate': date_list,
'Quantity': quantities,
'UnitPrice': unit_prices
})
# Calculate total transaction revenue
df['TotalSpend'] = df['Quantity'] * df['UnitPrice']
print(f"Transaction Log: {df.shape[0]:,} rows across {df['CustomerID'].nunique():,} unique customers")
df.sort_values('InvoiceDate').head()| CustomerID | InvoiceDate | Quantity | UnitPrice | TotalSpend |
|---|---|---|---|---|
| 1420 | 2025-01-01 | 3 | $45.20 | $135.60 |
| 1088 | 2025-01-02 | 1 | $89.00 | $89.00 |
| 1650 | 2025-01-02 | 5 | $22.50 | $112.50 |
2. Calculating RFM Metrics in Pandas
To calculate Recency, we set a Reference Snapshot Date one day after the most recent invoice in the dataset.
# 1. Establish reference date (representing 'today')
snapshot_date = df['InvoiceDate'].max() + dt.timedelta(days=1)
print(f"Analysis Snapshot Date: {snapshot_date.strftime('%Y-%m-%d')}")
# 2. Aggregate Recency, Frequency, and Monetary spend by CustomerID
rfm = df.groupby('CustomerID').agg({
'InvoiceDate': lambda x: (snapshot_date - x.max()).days, # Recency (days)
'CustomerID': 'count', # Frequency (order count)
'TotalSpend': 'sum' # Monetary (total revenue)
}).rename(columns={
'InvoiceDate': 'Recency',
'CustomerID': 'Frequency',
'TotalSpend': 'Monetary'
})
rfm.head()| CustomerID | Recency (Days) | Frequency (Orders) | Monetary ($ Spend) |
|---|---|---|---|
| 1000 | 42 | 7 | $1,280.50 |
| 1001 | 185 | 3 | $412.00 |
| 1002 | 12 | 11 | $2,450.80 |
| 1003 | 310 | 1 | $65.00 |
3. Scoring RFM Metrics with Quantile Binning (pd.qcut)
We divide customers into 4 quartiles (scored 1 to 4) along each dimension:
- Recency ($R$): Lower days = Higher score (4 is most recent, 1 is least recent).
- Frequency ($F$): More purchases = Higher score (4 is most frequent).
- Monetary ($M$): More revenue = Higher score (4 is highest spend).
Handling Tied Frequency Bins
Because many customers have identical low purchase counts ($1$ order), running pd.qcut directly on Frequency will trigger a duplicate bin edge error. We resolve this by applying .rank(method='first'):
# Define quartile labels
r_labels = [4, 3, 2, 1] # 4 = Most Recent (lowest days)
fm_labels = [1, 2, 3, 4] # 4 = Highest Frequency / Spend
# Score Recency (lower days = higher score)
rfm['R_Score'] = pd.qcut(rfm['Recency'], q=4, labels=r_labels).astype(int)
# Score Frequency (use rank to resolve tied frequencies)
rfm['F_Score'] = pd.qcut(rfm['Frequency'].rank(method='first'), q=4, labels=fm_labels).astype(int)
# Score Monetary
rfm['M_Score'] = pd.qcut(rfm['Monetary'], q=4, labels=fm_labels).astype(int)
# Combine into a 3-digit RFM string (e.g., '444')
rfm['RFM_Segment'] = (
rfm['R_Score'].astype(str) +
rfm['F_Score'].astype(str) +
rfm['M_Score'].astype(str)
)
# Combined RFM Score sum (3 to 12)
rfm['RFM_Score_Sum'] = rfm['R_Score'] + rfm['F_Score'] + rfm['M_Score']
rfm.head()4. Customer Segmentation Strategy & Action Matrix
A 3-digit score like 444 or 111 is granular, but executives need clear strategic tiers. We map the scores into 5 business segments:
RFM SEGMENTATION MAP
High F / M
^
| [ Potential Loyalists ] [ Champions ]
| (R: 3-4, F: 1-2) (R: 4, F: 4)
F |
R | ------------------------------------------
E |
Q | [ Lost ] [ At-Risk ]
| (R: 1, F: 1) (R: 1-2, F: 3-4)
+--------------------------------------------->
Low Recency (Old) High Recency (Recent)
def assign_segment(row) -> str:
r = row['R_Score']
f = row['F_Score']
if r >= 4 and f >= 4:
return 'Champions'
elif r >= 3 and f >= 3:
return 'Loyal Customers'
elif r >= 3 and f <= 2:
return 'Potential Loyalists'
elif r <= 2 and f >= 3:
return 'At Risk'
elif r <= 1 and f <= 1:
return 'Lost Customers'
else:
return 'Need Attention'
rfm['Segment'] = rfm.apply(assign_segment, axis=1)
# Segment breakdown summary
segment_summary = rfm.groupby('Segment').agg(
Customer_Count=('Recency', 'count'),
Avg_Recency=('Recency', 'mean'),
Avg_Frequency=('Frequency', 'mean'),
Total_Revenue=('Monetary', 'sum'),
Avg_Revenue=('Monetary', 'mean')
).sort_values(by='Total_Revenue', ascending=False)
segment_summary['Revenue_Share_Pct'] = (segment_summary['Total_Revenue'] / segment_summary['Total_Revenue'].sum()) * 100
segment_summarySegment Action Playbook
| Customer Segment | Criteria | Revenue Share | Actionable Strategy |
|---|---|---|---|
| Champions | $R=4, F=4$ | 35–45% | VIP perks, loyalty rewards, early product access. Never discount aggressively. |
| Loyal Customers | $R \ge 3, F \ge 3$ | 25–30% | Upsell premium tiers, cross-sell related categories, solicit reviews and referrals. |
| Potential Loyalists | $R \ge 3, F \le 2$ | 15–20% | Onboarding nurture sequences, personalized recommendations, second-purchase incentives. |
| At Risk | $R \le 2, F \ge 3$ | 10–15% | Urgent win-back email flows, personalized surveys, limited-time reactivation discounts. |
| Lost Customers | $R \le 1, F \le 1$ | 5–10% | Low-cost automated email re-engagement campaigns; exclude from high-cost ad targeting. |
4.1 Visualizing Customer Segment Distribution
plt.figure(figsize=(10, 5))
order = rfm['Segment'].value_counts().index
palette = sns.color_palette("mako", len(order))
sns.countplot(
data=rfm,
y='Segment',
order=order,
palette=palette
)
plt.title("Customer Distribution Across RFM Segments", fontsize=14, fontweight='bold', pad=12)
plt.xlabel("Number of Customers")
plt.ylabel("Segment")
plt.tight_layout()
plt.show()5. Monthly Cohort Retention Analysis
While RFM gives a cross-sectional snapshot of today's customer health, Cohort Analysis reveals how customer retention evolves over time.
5.1 Preparing Cohort Data
We track user behavior across monthly cycles:
cohort_df = df.copy()
# 1. Truncate transaction dates to month start
def get_month(date_val):
return dt.datetime(date_val.year, date_val.month, 1)
cohort_df['InvoiceMonth'] = cohort_df['InvoiceDate'].apply(get_month)
# 2. Identify the first purchase month for each customer (Acquisition Cohort)
cohort_df['CohortMonth'] = cohort_df.groupby('CustomerID')['InvoiceMonth'].transform('min')
# 3. Calculate CohortIndex (Months elapsed since acquisition)
def get_month_offset(d1, d2):
return (d1.year - d2.year) * 12 + (d1.month - d2.month)
cohort_df['CohortIndex'] = cohort_df.apply(
lambda row: get_month_offset(row['InvoiceMonth'], row['CohortMonth']),
axis=1
)
cohort_df[['CustomerID', 'InvoiceDate', 'CohortMonth', 'InvoiceMonth', 'CohortIndex']].head()5.2 Building the Retention Pivot Matrix
# 1. Count distinct customers per CohortMonth and CohortIndex
cohort_counts = cohort_df.groupby(['CohortMonth', 'CohortIndex'])['CustomerID'].nunique().reset_index()
# 2. Pivot into a 2D matrix
cohort_pivot = cohort_counts.pivot(index='CohortMonth', columns='CohortIndex', values='CustomerID')
# 3. Base cohort size (Month 0 active customers)
cohort_size = cohort_pivot.iloc[:, 0]
# 4. Divide across columns to compute Retention Rate percentages
retention_matrix = cohort_pivot.divide(cohort_size, axis=0) * 100
retention_matrix.index = retention_matrix.index.strftime('%Y-%m')
print("Cohort Retention Percentage Matrix (%):")
print(retention_matrix.iloc[:, :7])5.3 Rendering the Retention Heatmap
plt.figure(figsize=(14, 8))
sns.heatmap(
retention_matrix,
annot=True,
fmt='.1f',
cmap='YlGnBu',
vmin=0.0,
vmax=60.0,
linewidths=0.5,
cbar_kws={'label': 'Retention Rate (%)'}
)
plt.title("Monthly Customer Retention Heatmap by Cohort", fontsize=15, fontweight='bold', pad=15)
plt.xlabel("Months Since Acquisition (Cohort Index)")
plt.ylabel("Acquisition Cohort Month")
plt.tight_layout()
plt.show()Reading Cohort Retention Heatmaps:
- Month 0 is always 100.0% (all acquired users made a purchase).
- Look down columns to spot seasonality (e.g., if all cohorts dip in Month 4, there was an external disruption).
- Look along rows to evaluate product improvement (e.g., if the 2025-06 cohort shows 35% retention in Month 2 vs 20% for 2025-01, new user onboarding improved).
6. Modeling Customer Lifetime Value (LTV / CLV)
Customer Lifetime Value measures the projected total gross margin a business expects from a single customer relationship.
6.1 Traditional Predictive LTV Formula
The foundational formula for contractual/subscription or steady repeat e-commerce business is:
LTV = (Average Order Value (AOV) * Purchase Frequency * Gross Margin %) / Monthly Churn Rate
# Calculate business-wide baseline metrics
avg_order_val = df['TotalSpend'].mean()
avg_orders_per_customer = df.shape[0] / df['CustomerID'].nunique()
gross_margin_pct = 0.65 # 65% gross margin
# Average monthly retention rate after month 1
avg_retention_m1 = retention_matrix.iloc[:, 1].mean() / 100
monthly_churn_rate = 1.0 - avg_retention_m1
# Calculate projected LTV
predicted_ltv = (avg_order_val * avg_orders_per_customer * gross_margin_pct) / monthly_churn_rate
print("=" * 45)
print(f"AVERAGE ORDER VALUE: ${avg_order_val:.2f}")
print(f"AVG PURCHASE FREQUENCY: {avg_orders_per_customer:.2f} orders/year")
print(f"ESTIMATED MONTHLY CHURN: {monthly_churn_rate * 100:.1f}%")
print(f"PREDICTED CUSTOMER LTV: ${predicted_ltv:.2f}")
print("=" * 45)Complete End-to-End Customer Analytics Script
Here is a full self-contained pipeline you can execute on any transaction CSV:
import pandas as pd
import datetime as dt
def run_customer_analytics_pipeline(df: pd.DataFrame):
"""
Takes transaction DataFrame [CustomerID, InvoiceDate, TotalSpend]
and computes full RFM segments + Cohort Retention Pivot.
"""
df['InvoiceDate'] = pd.to_datetime(df['InvoiceDate'])
snapshot = df['InvoiceDate'].max() + dt.timedelta(days=1)
# 1. RFM Table
rfm = df.groupby('CustomerID').agg({
'InvoiceDate': lambda x: (snapshot - x.max()).days,
'CustomerID': 'count',
'TotalSpend': 'sum'
}).rename(columns={'InvoiceDate': 'Recency', 'CustomerID': 'Frequency', 'TotalSpend': 'Monetary'})
# 2. RFM Scores
rfm['R'] = pd.qcut(rfm['Recency'], q=4, labels=[4, 3, 2, 1]).astype(int)
rfm['F'] = pd.qcut(rfm['Frequency'].rank(method='first'), q=4, labels=[1, 2, 3, 4]).astype(int)
rfm['M'] = pd.qcut(rfm['Monetary'], q=4, labels=[1, 2, 3, 4]).astype(int)
# 3. Segments
def segment(r, f):
if r >= 4 and f >= 4: return 'Champions'
if r >= 3 and f >= 3: return 'Loyal'
if r <= 2 and f >= 3: return 'At Risk'
if r <= 1 and f <= 1: return 'Lost'
return 'Potential'
rfm['Segment'] = [segment(r, f) for r, f in zip(rfm['R'], rfm['F'])]
return rfmSummary & What to Learn Next
Combining RFM Segmentation with Monthly Cohort Retention provides a 360-degree view of your customer base. You can immediately identify who your best buyers are, intervene before loyal customers churn, and quantify how changes in product experience impact long-term customer value.
Next Steps:
- Practice Real Analytics Problems: Test your data transformation skills on Topfolio Interactive Practice.
- Compare SQL vs Python: Learn when to write window functions in SQL vs running RFM in Pandas with our SQL vs Python Guide.
- Master the Data Analyst Stack: Explore the comprehensive 12-week Data Analyst Career Track.
Frequently Asked Questions
Why is RFM segmentation preferred over simple revenue-based customer ranking?
Revenue-based ranking only captures past total spend (Monetary), which overlooks customers who spent heavily months ago but have churned. RFM segmentation combines Recency (how recently they purchased), Frequency (order cadence), and Monetary value to identify high-value active customers vs at-risk defectors.
How do you handle duplicate bin edge errors with pd.qcut() in RFM frequency scoring?
Because many e-commerce customers make only 1 or 2 purchases, quartile cutoffs produce identical bin edges. To prevent ValueError: Bin edges must be unique, apply frequency ranking first with rfm['Frequency'].rank(method='first') before passing to pd.qcut().
What is the difference between an Acquisition Cohort and a Behavioral Cohort?
An Acquisition Cohort groups users by the calendar month or week of their very first transaction and tracks their ongoing retention over time. A Behavioral Cohort groups users by specific actions they took (e.g., users who completed onboarding or used a specific discount code) regardless of signup date.
How is Customer Lifetime Value (LTV / CLV) calculated from retention and purchase metrics?
The standard predictive LTV formula is: LTV = (Average Order Value * Purchase Frequency * Gross Margin %) / Churn Rate. Alternatively, cohort analysis models historical LTV by calculating the cumulative net revenue generated by each monthly acquisition cohort divided by initial cohort size.
What marketing interventions should you trigger for 'At-Risk' vs 'Champions' customer segments?
'Champions' (High R, High F, High M) should receive VIP loyalty perks, early access to new releases, and referral incentives without aggressive discounts. 'At-Risk' customers (Low R, High F, High M) require targeted win-back campaigns, personalized reactivation emails, and limited-time discount incentives before they permanently churn.

Written by
Founder at Topfolio with 6+ years in data & analytics across JPMC, Ultrahuman, and high-growth startups. Sat on hiring panels, reviewed 500+ resumes, and writes practical SQL & data guides.
Related Articles
Market Basket Analysis in Python: Support, Confidence, Lift & Apriori Explained
Master Market Basket Analysis and Association Rule Mining in Python. Learn the mathematical intuition behind Support, Confidence, and Lift, one-hot encode transaction baskets, and implement Apriori and FP-Growth using Pandas and Mlxtend.
REST APIs for Data Analysts in Python: Authentication, Pagination & JSON Normalization
Master REST API data extraction in Python. Learn how to handle Bearer tokens and API keys, loop through offset and cursor pagination, flatten nested JSON with pd.json_normalize(), and build fault-tolerant pipelines with automatic retries.
Python Exploratory Data Analysis (EDA): The Complete Step-by-Step Workflow
Master the complete 6-stage Python Exploratory Data Analysis (EDA) framework. Learn structured data inspection, missing value imputation, IQR outlier detection, distribution analysis, correlation heatmaps, and feature profiling.