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

Prerequisites / Imports

In [1]:
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.

In [1]:
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.

In [1]:
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).

In [1]:
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.

In [1]:
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
In [1]:
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.

In [1]:
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.

In [1]:
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.

In [1]:
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.

In [1]:
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
In [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.

In [1]:
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

  1. Simulate a price series and plot it.
  2. Compute daily returns and annualized volatility.
  3. Calculate the Sharpe ratio for a return series.
  4. Build a 5-stage conversion funnel and plot it.
  5. Compute LTV:CAC for CAC=$200 and LTV=$700.
  6. Build an RFM table from a synthetic transactions dataset.
  7. Score customers into RFM segments and find the largest segment.
  8. Train a logistic churn model and report accuracy.
  9. Explain why the Sharpe ratio is useful.
  10. Write three business recommendations from your analyses.

Python Data Science: From Foundations to Applications — Chapter 26