Chapter 26 — Financial, Marketing, and Customer Analytics
This chapter applies data science to three business domains: finance (returns and risk), marketing (funnels and CAC/LTV), and customers (RFM segmentation and churn). Each section turns a business question into a quantitative analysis.
Learning Objectives
- Compute and plot financial returns and volatility.
- Calculate a Sharpe ratio.
- Build a marketing conversion funnel.
- Compute CAC, LTV, and the LTV:CAC ratio.
- Perform RFM customer segmentation.
- Train and evaluate a churn-prediction model.
Prerequisites / Imports
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, classification_report
sns.set_theme(style='whitegrid')
1 Financial Analytics: Simulated Price Series
We simulate a year of daily prices using geometric Brownian motion.
rng = np.random.default_rng(42)
n = 252
daily_ret = rng.normal(0.0005, 0.012, n)
price = 100 * np.cumprod(1 + daily_ret)
dates = pd.date_range('2026-01-01', periods=n, freq='B')
s = pd.Series(price, index=dates)
plt.figure(figsize=(10,4))
s.plot(); plt.title('Simulated daily price'); plt.ylabel('price ($)')
plt.show()
2 Returns and Volatility
Returns are percentage changes; volatility is their standard deviation.
returns = s.pct_change().dropna()
print('mean daily return:', round(float(returns.mean()), 5))
print('daily volatility:', round(float(returns.std()), 4))
print('annualized volatility:', round(float(returns.std()*np.sqrt(252)), 3))
plt.figure(figsize=(10,4))
returns.plot(alpha=0.7); plt.title('Daily returns'); plt.show()
mean daily return: -0.0001 daily volatility: 0.0113 annualized volatility: 0.179
3 Rolling Metrics and the Sharpe Ratio
A 20-day rolling volatility and the Sharpe ratio (return per unit of risk).
roll_vol = returns.rolling(20).std()*np.sqrt(252)
plt.figure(figsize=(10,4))
roll_vol.plot(color='darkred'); plt.title('20-day annualized volatility'); plt.show()
sharpe = round(float(returns.mean()/returns.std()*np.sqrt(252)), 3)
print('annualized Sharpe ratio:', sharpe)
annualized Sharpe ratio: -0.137
4 Marketing Analytics: A Conversion Funnel
A funnel tracks how prospects drop off between stages.
stages = ['Visited','Signed up','Activated','Subscribed','Renewed']
users = [10000, 6200, 3500, 2100, 1500]
funnel = pd.DataFrame({'stage': stages, 'users': users})
funnel['conv_rate'] = (funnel['users']/funnel['users'].iloc[0]*100).round(1)
funnel
| stage | users | conv_rate | |
|---|---|---|---|
| 0 | Visited | 10000 | 100.0 |
| 1 | Signed up | 6200 | 62.0 |
| 2 | Activated | 3500 | 35.0 |
| 3 | Subscribed | 2100 | 21.0 |
| 4 | Renewed | 1500 | 15.0 |
fig, ax = plt.subplots(figsize=(9,4))
ax.barh(funnel['stage'][::-1], funnel['users'][::-1], color='mediumseagreen')
ax.set_title('Marketing conversion funnel'); ax.set_xlabel('users')
plt.show()
5 CAC, LTV, and the LTV:CAC Ratio
Customer Acquisition Cost (CAC) vs Lifetime Value (LTV); a ratio above 3 is healthy.
cac = 120
ltv = 480
print(f'LTV:CAC ratio = {ltv/cac:.1f}')
print('Healthy' if ltv/cac >= 3 else 'Needs improvement')
LTV:CAC ratio = 4.0 Healthy
6 Customer Analytics: RFM Segmentation
RFM = Recency, Frequency, Monetary value. We build it from synthetic transactions.
rng = np.random.default_rng(5)
tx = pd.DataFrame({
'customer': rng.choice([f'C{i:03d}' for i in range(100)], 600),
'date': pd.date_range('2026-01-01', periods=600, freq='D'),
'amount': rng.normal(60, 25, 600).round(2),
})
snapshot = tx['date'].max()
rfm = tx.groupby('customer').agg(
recency=('date', lambda d: (snapshot - d.max()).days),
frequency=('customer', 'count'),
monetary=('amount', 'sum'),
)
rfm.columns = ['recency','frequency','monetary']
rfm.head()
| recency | frequency | monetary | |
|---|---|---|---|
| customer | |||
| C000 | 190 | 6 | 317.95 |
| C001 | 8 | 9 | 622.49 |
| C002 | 122 | 6 | 291.21 |
| C003 | 3 | 8 | 563.41 |
| C004 | 35 | 9 | 473.77 |
7 Scoring RFM Segments
Rank each metric into quartiles and combine into an RFM segment code.
r_labels = range(4, 0, -1) # lower recency days is better
f_labels = range(1, 5)
m_labels = range(1, 5)
rfm['R'] = pd.qcut(rfm['recency'], 4, labels=r_labels)
rfm['F'] = pd.qcut(rfm['frequency'], 4, labels=f_labels)
rfm['M'] = pd.qcut(rfm['monetary'], 4, labels=m_labels)
rfm['RFM'] = rfm['R'].astype(str) + rfm['F'].astype(str) + rfm['M'].astype(str)
print(rfm['RFM'].value_counts().head(8))
RFM 111 11 222 7 444 7 434 6 112 5 344 5 211 5 311 5 Name: count, dtype: int64
8 Churn Prediction
We synthesize customer features and a churn label, then train a logistic model.
rng = np.random.default_rng(3)
customers = pd.DataFrame({
'tenure_months': rng.integers(1, 60, 500),
'monthly_spend': rng.normal(50, 20, 500).round(2),
'support_tickets': rng.poisson(1.0, 500),
})
logit = -1.0 + 0.04*customers['tenure_months'] - 0.02*customers['monthly_spend'] + 0.5*customers['support_tickets']
prob = 1/(1+np.exp(-logit))
customers['churn'] = (rng.random(500) < prob).astype(int)
print('churn rate:', round(float(customers['churn'].mean()), 3))
customers.head()
churn rate: 0.432
| tenure_months | monthly_spend | support_tickets | churn | |
|---|---|---|---|---|
| 0 | 48 | 29.79 | 2 | 1 |
| 1 | 6 | 34.24 | 1 | 0 |
| 2 | 11 | 48.85 | 1 | 0 |
| 3 | 14 | 95.92 | 2 | 0 |
| 4 | 11 | 46.43 | 1 | 1 |
X = customers[['tenure_months','monthly_spend','support_tickets']]
y = customers['churn']
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.3, random_state=42)
model = LogisticRegression(max_iter=1000).fit(Xtr, ytr)
pred = model.predict(Xte)
print('accuracy:', round(accuracy_score(yte, pred), 3))
print(classification_report(yte, pred, zero_division=0))
accuracy: 0.64
precision recall f1-score support
0 0.65 0.79 0.71 85
1 0.62 0.45 0.52 65
accuracy 0.64 150
macro avg 0.63 0.62 0.62 150
weighted avg 0.64 0.64 0.63 150
Case Study: Business Summary Scorecard
Combine one insight from each domain into a one-screen scorecard for executives.
scorecard = pd.DataFrame({
'Metric': ['Annualized Sharpe', 'Funnel overall conversion', 'LTV:CAC', 'Top RFM segment share', 'Churn model accuracy'],
'Value': [str(sharpe),
f"{funnel['users'].iloc[-1]/funnel['users'].iloc[0]*100:.1f}%",
f"{ltv/cac:.1f}x",
f"{(rfm['RFM']=='444').mean()*100:.1f}%",
round(accuracy_score(yte, pred), 3)],
})
scorecard
| Metric | Value | |
|---|---|---|
| 0 | Annualized Sharpe | -0.137 |
| 1 | Funnel overall conversion | 15.0% |
| 2 | LTV:CAC | 4.0x |
| 3 | Top RFM segment share | 7.0% |
| 4 | Churn model accuracy | 0.64 |
Exercises
- Simulate a price series and plot it.
- Compute daily returns and annualized volatility.
- Calculate the Sharpe ratio for a return series.
- Build a 5-stage conversion funnel and plot it.
- Compute LTV:CAC for CAC=$200 and LTV=$700.
- Build an RFM table from a synthetic transactions dataset.
- Score customers into RFM segments and find the largest segment.
- Train a logistic churn model and report accuracy.
- Explain why the Sharpe ratio is useful.
- Write three business recommendations from your analyses.
Python Data Science: From Foundations to Applications — Chapter 26