Mục lục bài viết42 mục
- 1.Prerequisites
- 2.Step 1: Define Churn, Time Windows, and Business Objectives
- 2.1Churn Label Definition
- 2.2Observation Windows
- 2.3Objective Tie-In
- 2.4Decision Guardrails
- 2.5Deliverable Snippet for the Deck
- 3.Step 2: Audit Data, Verify Identity Stitching, and Build the Panel
- 3.1Minimum Viable Schema
- 3.2Identity Stitching Rules
- 3.3Coverage and Stability Checks — Run These Before Feature Engineering
- 3.4Daily Customer Panel — Core SQL
- 3.5Privacy Note
- 4.Step 3: Feature Engineering — RFM and Behavioral Signals
- 4.1Core Feature Set
- 4.2Feature Snapshot SQL — Point-in-Time Safe
- 4.3Robustness Tips
- 5.Step 4: Baseline Model and Probability Calibration
- 5.1Why Logistic Regression First
- 5.2Training and Calibration
- 5.3How to Frame This for Executives
- 6.Step 5: Validation — Time-Based Backtest, AUC, and Lift
- 6.1Backtest Setup
- 6.2Metrics to Report
- 6.3Sample Size for the Live A/B Pilot
- 6.4Handling Seasonal Drift in Risk Cutoffs
- 6.5Lift Estimation Template
- 7.Step 6: Risk Segmentation and Action Playbook
- 7.1Segmentation Template
- 7.2Action Playbook
- 7.3Margin Guardrails
- 8.Step 7: Scoring Pipeline and CRM/ESP Deployment
- 8.1Weekly Scoring Script
- 8.2Control Group and Measurement
- 8.3Measurement Dashboard
- 9.Step 8: Presentation Deck — 15 Slides
- 9.1Deck Structure
- 9.2Executive Language Translations
- 9.3Model Card — Required in Appendix
- 10.Common Failure Modes and How to Avoid Them
- 11.Privacy-Approved Signal Brief
- 12.3–4 Week Execution Timeline
Follow this track to build a transparent churn propensity model with actionable segments, expected lift, time-based validation, and a reproducible notebook — ready to present and deploy within 3–4 weeks.
Prerequisites
Confirm these are in place before Week 1:
- Query access to GA4, CRM, and ESP exports (BigQuery or equivalent)
- A business-agreed churn definition and observation window — without this, Week 1 cannot start
- Python 3.10+ environment and SQL query rights
- Permission to set a persistent control group in CRM or ESP
- A light integration slot confirmed in the engineering calendar for Weeks 3–4
On the 3–4 week timeline: This is achievable if GA4/CRM/ESP identity stitching already works and the data warehouse is stable. If identity coverage is below 60% or ETL pipelines are unreliable, plan an additional 1–2 weeks for data remediation before modeling starts. Do not compress the timeline by skipping the coverage check in Step 2.
Expected deliverables:
- A 15-slide deck and reproducible notebook demonstrating a defendable churn model, feature importance, time-split validation, and expected lift
- Scoring integrated into CRM or ESP with a persistent 10% control group and measurable lift vs. control within 2–4 weeks post-deploy
- A privacy-approved signal brief listing 3–5 GDPR/CCPA-compliant predictive signals with a measurement plan
Step 1: Define Churn, Time Windows, and Business Objectives
Label quality determines model credibility. Lock these three up front — changing definitions mid-pilot invalidates backtest comparability and will be challenged in the sign-off meeting.
Churn Label Definition
Subscription businesses: Churn = not renewed by day N after plan end. Common default: N = 30.
Transactional businesses: Churn = no purchase within N days of the last order, where:
N = median_interpurchase_interval × 1.5 to 2.0
Example: if the median interpurchase interval is 21 days, set N = 45. This avoids the common mistake of treating customers as churned before their natural repurchase window has elapsed — which leads to discounting customers who would have returned anyway.
Observation Windows
- Lookback window for features: 30, 60, or 90 days depending on purchase cadence
- Outcome window: the N-day future window used to assign the churn label
Objective Tie-In
- Primary KPI: incremental retained orders or billings within the outcome window
- Secondary KPIs: cost per retained user, margin-safe offer redemption rate, opt-out rate
Decision Guardrails
- Set N so the business has a 2–4 week action window after scoring — if N is too long, CRM acts too late
- Freeze the churn definition and N in a shared document before any SQL runs — do not adjust during the pilot
Deliverable Snippet for the Deck
“We call a user at risk if they have not purchased again by day 45 after their last order. Our model predicts this risk at day 21, giving CRM 24 days to act.”
Step 2: Audit Data, Verify Identity Stitching, and Build the Panel
You do not need a CDP. A thin schema with a stable identity key is enough to ship a pilot.
Minimum Viable Schema
Identity
customer_id: stable internal ID — the primary join key across all systemsemail_sha256: lowercase-trimmed email hashed client-side or in batch
CRM orders: order_id, customer_id, order_ts_utc, order_value, payment_status
GA4 behavioral: customer_id or device_id, event_ts_utc, event_name, page_category, session_id, source_medium
ESP messaging: customer_id or email_sha256, send_ts_utc, message_type, campaign_id, opened, clicked, bounced, unsubscribed
Identity Stitching Rules
- Prefer
customer_idacross all systems - If GA4 lacks
customer_id, map the last knowndevice_idtocustomer_idvia login events in the prior 60 days - Never ingest raw PII into modeling files — use
email_sha256strictly for ESP joins
Coverage and Stability Checks — Run These Before Feature Engineering
-- Coverage: what % of CRM orders have a GA4 session in the prior 30 days?
-- Target: 60%+ for a clean pilot. If below 60%, fix stitching before proceeding.
SELECT
COUNT(DISTINCT o.customer_id) AS orders_with_ga4_session,
COUNT(DISTINCT o.customer_id) * 1.0 / total.cnt AS coverage_rate
FROM crm_orders o
JOIN ga4_events g
ON o.customer_id = g.customer_id
AND g.event_ts_utc BETWEEN TIMESTAMP_SUB(o.order_ts_utc, INTERVAL 30 DAY)
AND o.order_ts_utc
CROSS JOIN (SELECT COUNT(DISTINCT customer_id) AS cnt FROM crm_orders) total;
-- Stability: daily order and session counts over the past 90 days
-- Flag anomalies that indicate schema changes or ETL issues
SELECT DATE(order_ts_utc) AS day, COUNT(*) AS orders
FROM crm_orders
WHERE order_ts_utc >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 90 DAY)
GROUP BY 1 ORDER BY 1;
Daily Customer Panel — Core SQL
-- BigQuery. Adjust window functions for your warehouse dialect.
WITH last_order AS (
SELECT
customer_id,
MAX(order_ts_utc) AS last_order_ts,
COUNTIF(order_ts_utc >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 90 DAY)) AS orders_90d,
SUM(CASE WHEN order_ts_utc >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 90 DAY)
THEN order_value ELSE 0 END) AS revenue_90d
FROM crm_orders
WHERE payment_status = 'paid'
GROUP BY customer_id
),
beh_30d AS (
SELECT
customer_id,
COUNTIF(event_name = 'view_item') AS views_30d,
COUNTIF(event_name = 'add_to_cart') AS atc_30d,
COUNTIF(event_name = 'begin_checkout') AS checkout_30d,
COUNT(DISTINCT session_id) AS sessions_30d
FROM ga4_events
WHERE event_ts_utc >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
AND customer_id IS NOT NULL
GROUP BY customer_id
),
esp_30d AS (
SELECT
customer_id,
COUNTIF(send_ts_utc >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)) AS sends_30d,
COUNTIF(opened AND send_ts_utc >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)) AS opens_30d,
COUNTIF(clicked AND send_ts_utc >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)) AS clicks_30d,
MAX(CAST(unsubscribed AS INT64)) AS ever_unsub
FROM esp_events
GROUP BY customer_id
)
SELECT
COALESCE(l.customer_id, b.customer_id, e.customer_id) AS customer_id,
l.last_order_ts,
l.orders_90d, l.revenue_90d,
b.views_30d, b.atc_30d,
b.checkout_30d, b.sessions_30d,
e.sends_30d, e.opens_30d,
e.clicks_30d, e.ever_unsub
FROM last_order l
FULL OUTER JOIN beh_30d b USING (customer_id)
FULL OUTER JOIN esp_30d e USING (customer_id);
Privacy Note
Keep only non-sensitive behavioral attributes — aggregate counts and recency flags. No demographics or special-category data.
Privacy brief (FL2 requirement): A one-page list of 3–5 signals, each with its source table, retention period, and lawful basis. If legal cannot find a data map entry for a signal, replace it with an aggregated non-PII equivalent before proceeding.
Step 3: Feature Engineering — RFM and Behavioral Signals
Prioritize high-signal, low-cardinality features that are easy to explain to executives and stable across schema changes.
Core Feature Set
RFM
- Recency: days since last order, days since last site session, days since last email open
- Frequency: orders (90d), sessions (30d), email opens (30d)
- Monetary: revenue (90d), average order value (90d)
Funnel intensity
add_to_cart / viewsratio (30d)checkout_starts / add_to_cartratio (30d)
Trend flags
- Sessions (30d) minus sessions (prior 30d) — detects engagement drop
- Open rate (30d) vs. open rate (90d) — detects email fatigue
Friction signals
- Count of failed checkouts or payment errors
- Unsubscribe flag
Feature Snapshot SQL — Point-in-Time Safe
The most common modeling error in churn is look-ahead bias: features or labels that accidentally incorporate data from after the cutoff date. Every feature query must be strictly bounded to <= cutoff_date. Run this check before training:
DECLARE cutoff_date DATE DEFAULT '2026-05-31';
-- Verify no future data leaks into the feature window
SELECT COUNT(*) AS future_rows_should_be_zero
FROM ga4_events
WHERE event_ts_utc > TIMESTAMP(cutoff_date)
AND customer_id IN (SELECT customer_id FROM feature_snapshot);
Feature computation:
DECLARE cutoff_date DATE DEFAULT '2026-05-31';
SELECT
customer_id,
DATE(cutoff_date) AS as_of_date,
TIMESTAMP_DIFF(TIMESTAMP(cutoff_date), last_order_ts, DAY) AS recency_last_order_d,
COALESCE(orders_90d, 0) AS orders_90d,
COALESCE(revenue_90d, 0.0) AS revenue_90d,
COALESCE(sessions_30d, 0) AS sessions_30d,
SAFE_DIVIDE(COALESCE(atc_30d, 0), NULLIF(views_30d, 0)) AS atc_view_ratio_30d,
SAFE_DIVIDE(COALESCE(checkout_30d, 0), NULLIF(atc_30d, 0)) AS checkout_atc_ratio_30d,
SAFE_DIVIDE(COALESCE(opens_30d, 0), NULLIF(sends_30d, 0)) AS open_rate_30d,
COALESCE(ever_unsub, 0) AS ever_unsub
FROM customer_panel_daily
WHERE DATE(last_order_ts) <= cutoff_date;
Label creation — strictly future-only:
WITH future_orders AS (
SELECT customer_id, MIN(order_ts_utc) AS next_order_ts
FROM crm_orders
WHERE order_ts_utc > TIMESTAMP(@cutoff_date)
GROUP BY customer_id
)
SELECT
f.*,
CASE
WHEN fo.next_order_ts IS NULL THEN 1
WHEN TIMESTAMP_DIFF(fo.next_order_ts, TIMESTAMP(@cutoff_date), DAY) > 45 THEN 1
ELSE 0
END AS label_churn_45d
FROM feature_snapshot f
LEFT JOIN future_orders fo USING (customer_id);
Robustness Tips
- Cap extreme counts at the 99th percentile before training — a single outlier user can destabilize logistic regression coefficients
- Log-transform or bin skewed count features (orders, revenue)
- Stick to 10–20 robust features — adding niche signals rarely improves calibration and makes the model harder to defend
Step 4: Baseline Model and Probability Calibration
Why Logistic Regression First
For a 3–4 week pilot with an executive audience, logistic regression is the right starting point: coefficients are directionally interpretable, the model is easy to deploy as SQL or a lightweight batch job, and calibration behavior is well-understood. A more complex model that cannot be explained in a 15-slide deck will face stakeholder resistance that delays deployment longer than the marginal accuracy gain is worth.
If logistic regression reaches AUC ≥ 0.70 with good calibration, ship it. Upgrade to gradient boosting in the next iteration with the live control group data as additional validation evidence.
Training and Calibration
# pip install pandas scikit-learn numpy matplotlib joblib statsmodels
import pandas as pd
import numpy as np
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.calibration import CalibratedClassifierCV, calibration_curve
from sklearn.metrics import roc_auc_score, brier_score_loss
from sklearn.inspection import permutation_importance
from joblib import dump
import matplotlib.pyplot as plt
df = pd.read_csv('train_snapshot.csv').sort_values('as_of_date')
FEATURE_COLS = [
'recency_last_order_d', 'orders_90d', 'revenue_90d', 'sessions_30d',
'atc_view_ratio_30d', 'checkout_atc_ratio_30d',
'sends_30d', 'open_rate_30d', 'ever_unsub'
]
X = df[FEATURE_COLS].fillna(0)
y = df['label_churn_45d'].astype(int)
# Time-based split — never random split for temporal prediction tasks
split_idx = int(0.8 * len(df))
X_train, X_val = X.iloc[:split_idx], X.iloc[split_idx:]
y_train, y_val = y.iloc[:split_idx], y.iloc[split_idx:]
scaler = StandardScaler()
X_train_sc = scaler.fit_transform(X_train)
X_val_sc = scaler.transform(X_val) # fit only on train — no leakage
lr = LogisticRegression(
penalty='l2', C=1.0, solver='liblinear',
max_iter=1000, class_weight='balanced'
)
lr.fit(X_train_sc, y_train)
# Calibrate on the validation fold — not on the training set
cal = CalibratedClassifierCV(lr, method='isotonic', cv='prefit')
cal.fit(X_val_sc, y_val)
pred_val = cal.predict_proba(X_val_sc)[:, 1]
print(f'AUC: {roc_auc_score(y_val, pred_val):.3f}')
print(f'Brier: {brier_score_loss(y_val, pred_val):.3f}')
# Calibration plot — include in deck appendix
prob_true, prob_pred = calibration_curve(y_val, pred_val, n_bins=10)
plt.figure(figsize=(4, 4))
plt.plot(prob_pred, prob_true, marker='o', label='Model')
plt.plot([0, 1], [0, 1], '--', color='gray', label='Perfect calibration')
plt.title('Calibration plot — validation fold')
plt.xlabel('Predicted probability'); plt.ylabel('Observed frequency')
plt.tight_layout(); plt.savefig('calibration.png', dpi=150)
# Feature importance — permutation on validation, not train
perm = permutation_importance(cal, X_val_sc, y_val, n_repeats=10, random_state=42)
pi = pd.Series(perm.importances_mean, index=FEATURE_COLS).sort_values(ascending=False)
print('Permutation importance:\n', pi)
dump({'scaler': scaler, 'model': cal, 'features': FEATURE_COLS}, 'churn_lr_calibrated.joblib')
How to Frame This for Executives
- Replace “AUC 0.74” with: “The model correctly ranks higher-risk customers above lower-risk ones 74% of the time in held-out backtests.”
- Replace “Brier score” with: “When the model predicts 40% churn risk, roughly 40 out of 100 customers in that bucket actually churn — the scores are reliable, not just relative.”
- Calibration is what makes the probability score usable for business decisions (segmentation cutoffs, offer ROI math). If the calibration curve is far from the diagonal, fix it before presenting.
Step 5: Validation — Time-Based Backtest, AUC, and Lift
Backtest Setup
Use at least 6 months of history. Roll multiple cutoff dates monthly:
| Period | Role |
|---|---|
| Months 1–4 | Training data |
| Month 5 | Validation (tune cutoffs here) |
| Month 6 | Held-out test (report here) |
All users observable at the cutoff date must be included — do not drop users who were suppressed by past campaigns without annotating and accounting for this. If major promo events occurred during the backtest window, annotate them in the change log and evaluate model stability across promo vs. non-promo periods separately.
Metrics to Report
- Discrimination: AUC and top-decile capture rate of churners
- Calibration: Brier score and calibration plot
- Expected lift proxy: churn rate in the top risk decile vs. the population average
Always report metrics on the held-out test period, not the validation fold used for calibration tuning.
Sample Size for the Live A/B Pilot
The pilot runs as a two-proportion test within each risk segment. Use proportion_effectsize from statsmodels to compute Cohen’s h correctly — do not pass a raw proportion difference as the effect size directly:
from statsmodels.stats.proportion import proportion_effectsize
from statsmodels.stats.power import NormalIndPower
# Example: baseline repurchase rate 8%, expected with treatment 10%
h = proportion_effectsize(0.10, 0.08) # Cohen's h — correct effect size for proportions
n = NormalIndPower().solve_power(
effect_size=h, alpha=0.05, power=0.8, alternative='two-sided'
)
print(f'Required n per arm: {round(n)}') # ~3,900
Note on the code: The correct approach is to compute Cohen’s h via
proportion_effectsizefirst, then pass that value intosolve_power. Passing raw proportions directly tosolve_powerwithout converting to Cohen’s h will produce incorrect results, sinceNormalIndPowerexpects a standardized effect size, not a probability difference.
Handling Seasonal Drift in Risk Cutoffs
Risk cutoffs frozen on the validation month can become unreliable if the pilot month has a materially different churn rate — common during promotions, holidays, or demand spikes. Before freezing cutoffs for the pilot:
- Compare the validation month’s population-level churn rate to the pilot month’s early-week observed rate
- If the difference exceeds 3–5 percentage points, recompute cutoffs on a sliding 4-week window rather than a single frozen month
- Document the cutoff values and the date they were computed in the score output file — this is required for post-pilot audit
For the first pilot, prefer percentile-based cutoffs (top 20% = high risk) over fixed probability thresholds. Percentile cutoffs are robust to distribution shift because they track the relative ranking of scores rather than their absolute value.
Lift Estimation Template
Important: The numbers below are illustrative placeholders. If your organization has not run a comparable save offer before, the absolute lift and redemption rate are assumptions, not measured baselines. Present these as scenarios with ranges, not point estimates.
Validation month population churn rate (45d): 20%
High-risk segment churn rate (top 20%): 40%
Assumed absolute lift from save offer: 3 pp ← needs precedent or pilot confirmation
Pilot population: 50,000 users
High-risk segment (top 20%): 10,000 users
Expected incremental saves: 10,000 × 0.03 = 300
If average retained order margin = $12: $3,600 incremental margin
If offer cost = $6 × 15% redemption rate: $9,000 offer cost
Net impact (this scenario): −$5,400
→ Present low / mid / high redemption scenarios.
Net-positive outcome requires either higher margin per order,
lower offer cost, or confirmed lift > 3pp from historical data.
The worksheet should always include a scenario where the offer cost exceeds the incremental margin — the board needs to see the downside case.
Step 6: Risk Segmentation and Action Playbook
Segmentation Template
| Segment | Cutoff | Recommended basis |
|---|---|---|
| High risk | Top 20% by score | Percentile on validation month |
| Medium risk | 20th–50th percentile | Percentile on validation month |
| Low risk | Below 50th percentile | — |
Freeze cutoffs on the validation month and document them. If pilot month distribution shifts more than 3–5pp, see the drift handling protocol in Step 5.
Action Playbook
High risk
- Offer: limited-time win-back discount or free shipping
- Channel: email + SMS if opted in
- Cadence: 3 touches over 10 days
- Suppress: unsubscribed users, users with open support tickets, users with unresolved payment errors
Medium risk
- Offer: relevant content, category reminders, low-cost perk — no discount
- Channel: email only
- Cadence: 2 touches over 14 days
Low risk
- No discount. Next-best-offer recommendations or new arrivals only
Suppression rule for non-churn friction: Users with open support tickets or payment gateway errors are not churning — they are blocked. Sending a win-back offer to this segment is counterproductive and will show up as wasted offer cost in the margin analysis. Coordinate with CS and Payments to maintain a suppression list before the pilot launches.
Margin Guardrails
- Do not extend discounts to the low-risk segment
- Set a maximum offer budget per user per week
- Cap weekly touches and honor opt-outs immediately
- Include a margin floor slide in the deck: show at what offer cost and redemption rate the campaign becomes margin-negative
Step 7: Scoring Pipeline and CRM/ESP Deployment
Weekly Scoring Script
import pandas as pd
from joblib import load
art = load('churn_lr_calibrated.joblib')
scaler, model, feats = art['scaler'], art['model'], art['features']
SCORE_DATE = '2026-06-03'
df = pd.read_csv(f'weekly_features_{SCORE_DATE}.csv').fillna(0)
X = scaler.transform(df[feats])
df['churn_prob'] = model.predict_proba(X)[:, 1]
# Percentile-based cutoffs — recompute monthly, document each recompute
hi_pct = df['churn_prob'].quantile(0.80)
med_pct = df['churn_prob'].quantile(0.50)
df['risk_segment'] = pd.cut(
df['churn_prob'],
bins=[-1, med_pct, hi_pct, 1.0],
labels=['low', 'medium', 'high']
)
out = df[['customer_id']].copy()
out['score_date_utc'] = SCORE_DATE
out['churn_prob'] = df['churn_prob'].round(4)
out['risk_segment'] = df['risk_segment'].astype(str)
out['model_version'] = 'lr_v1_2026-05'
out['source_window_days'] = 45
out['hi_cutoff'] = round(hi_pct, 4) # log for audit
out['med_cutoff'] = round(med_pct, 4) # log for audit
out.to_csv(f'churn_scores_{SCORE_DATE}.csv', index=False)
Archive every batch output. Do not overwrite — include score_date_utc and model_version in every file.
Control Group and Measurement
- At first scoring run, randomly assign 10% of each risk segment to a persistent control group — tag them in CRM/ESP as
control_flag = True - Persist control membership for the full 4-week measurement window — do not reassign
- Exclude
control_flag = Trueusers from all campaign journeys - Verify audience counts in CRM/ESP match the score file within 5% — a larger discrepancy indicates a join or upload error
Journey conflict check: When pushing risk_segment attributes to the ESP, confirm this segment is explicitly excluded from existing mass campaigns. Users receiving both a win-back offer and a regular promotional email creates measurement noise and degrades the user experience.
Measurement Dashboard
Track per arm, per segment, weekly:
| Metric | Description |
|---|---|
| Repurchase rate | Primary lift signal |
| Offer redemption rate | Offer economics |
| Unsubscribe / complaint rate | Guardrail |
| Revenue per user | Margin validation |
Measurement window: 30–45 days after first touch, aligned to the churn outcome window.
Step 8: Presentation Deck — 15 Slides
Deck Structure
| Slide | Content |
|---|---|
| 1 | Executive summary: one sentence on expected outcome and decision ask |
| 2 | What we predict: “Who will not purchase in 45 days” and why it matters in revenue terms |
| 3 | Data used: GA4, CRM, ESP — non-sensitive signals, hashed joins, privacy brief reference |
| 4 | Churn definition and timeline: one visual showing observation window, cutoff, outcome window |
| 5 | Approach: logistic regression, weekly scoring, control group — why interpretability over complexity |
| 6 | Backtest credibility: time-based split, AUC in plain language, calibration plot |
| 7 | Segmentation: sizes, percentile cutoffs, which month they were frozen on |
| 8 | Action plan: offers and channels per segment, suppression rules, margin guardrails |
| 9 | Expected impact: lift scenarios (low / mid / high) with assumptions explicit |
| 10 | Pilot design: control share, measurement window, dashboards |
| 11 | Privacy and compliance: lawful basis, retention, opt-out guardrails |
| 12 | Risks and mitigations: data drift, over-offering, privacy complaints, identity coverage |
| 13 | Integration plan: score file format, CRM/ESP wiring, no platform change required |
| 14 | Timeline: 3–4 week Gantt with owners |
| 15 | Decision ask: go/no-go, offer budget cap, named CRM owner |
Executive Language Translations
| Technical | Plain language for slides |
|---|---|
| AUC 0.74 | “The model correctly ranks higher-risk customers above lower-risk ones 74% of the time in held-out backtests.” |
| Brier score / calibration | “Predicted 40% risk means roughly 40 of 100 customers in that group actually churned — the scores reflect reality.” |
| Logistic regression | “A transparent model whose logic can be explained in one sentence per feature.” |
| Control group | “A 10% holdout that lets us measure whether our actions caused the improvement, not just correlated with it.” |
Model Card — Required in Appendix
Include: objective, label definition, time windows, data sources and exclusions, training and validation dates, performance metrics, feature importance, calibration method, fairness/privacy considerations, intended use and misuse cases, retraining frequency, versioning, and contacts.
Common Failure Modes and How to Avoid Them
| Failure | Prevention |
|---|---|
| Definition shifts between train and pilot | Freeze churn N in writing before any SQL runs |
| Look-ahead bias in features | Run the future-data leak check in Step 3 before every training run |
| Backtesting on campaign-suppressed users | Annotate promo periods; exclude or stratify accordingly |
| Over-fitting with too many features | Cap at 10–20 robust RFM and behavioral signals |
| No holdout | Tag the 10% control in CRM/ESP before the first score is pushed |
| Score file without versioning | Always include model_version and score_date_utc — archive every batch |
| Ignoring calibration | Plot calibration before every deployment; fix before launch if the curve drifts far from the diagonal |
| ROI numbers presented as certain | Label lift and redemption assumptions explicitly; always show the downside scenario |
| Journey conflicts | Confirm suppression from mass campaigns before the pilot goes live |
Privacy-Approved Signal Brief
| Signal | Source | Retention | Lawful basis |
|---|---|---|---|
| Days since last session | GA4 | 90 days raw → aggregate | Legitimate interest / consent |
| Sessions count (30d) | GA4 | 90 days raw → aggregate | Legitimate interest / consent |
| Product views + add-to-carts (30d) | GA4 | 90 days raw → aggregate | Legitimate interest / consent |
| Email sends, opens, clicks (30d) | ESP | 90 days raw → aggregate | Legitimate interest |
| Orders count + revenue (90d) | CRM | Per data retention policy | Contract performance |
| Unsubscribe flag | ESP | Indefinite (compliance) | Legal obligation |
Raw event-level data purges after 90 days. Aggregates used for modeling retain no PII. Email joins use email_sha256 only, hashed client-side before transmission.
3–4 Week Execution Timeline
| Days | Actions |
|---|---|
| 1–3 | Freeze churn definition in writing; run coverage and stability checks; confirm identity stitching ≥60% |
| 4–7 | Build daily customer panel SQL; generate feature snapshot at first cutoff date; run look-ahead leak check |
| 8–12 | Train and calibrate model; run time-based backtest; generate AUC, Brier, calibration plot |
| 13–16 | Freeze risk cutoffs on validation month; build action playbook; compute lift scenarios with explicit assumptions |
| 17–20 | Build scoring script; test score file format with CRM/ESP; tag 10% control group; set up measurement dashboard |
| 21–24 | Draft deck; Finance review of margin math and downside scenario; privacy sign-off on signal brief |
| 25–28 | Executive review; incorporate feedback; launch pilot journeys; confirm control excluded from campaigns |
By following this sequence you will have a churn propensity model that is technically sound, calibrated, time-validated, and explainable to a VP audience — with a live control group that produces credible lift evidence within 4 weeks of deployment. The most common failure is not the model: it is moving too fast on a fragile data foundation or presenting ROI assumptions as certainties. Validate the data first, freeze the definitions, and keep the economics honest.