Case study · Data · Spark

Customer Churn Lakehouse

Which customers are about to stop buying? This lakehouse ingests a year of real retail transactions day by day, cleans them behind data-quality gates, and feeds a churn model evaluated on a month it never saw.

PySparkDelta LakeSpark MLMLflow
Screenshot of the Customer Churn Lakehouse demo
5.4×faster lookups after Z-ordering
0.749 AUCvs 0.684 for a recency rule
541,909Real transactions processed
5.7 s vs 10.6 sDaily incremental load vs full rebuild

The problem

Retention teams need a ranked list of at-risk customers they can trust, which means the data behind it has to be complete, de-duplicated and reproducible, and the model has to be tested the way it will be used: on the future.

Architecture

How data moves through the system, left to right.

Landing305 daily CSV dropsone per trading day
Bronzeraw strings + lineageappend, idempotent
Silvertyped, de-duplicatedMERGE on line_id
Goldpoint-in-time features90-day churn label
Spark MLrule · LR · GBTMLflow tracked

Key decisions

Bronze keeps everything as strings

A malformed upstream value is stored and inspectable, never lost at ingestion.

try_cast in silver

Spark 4's ANSI mode fails a whole batch on one bad value; try_cast turns it into NULL and quarantines that one row with a reason.

MERGE on a content hash

line_id hashes the whole line and the MERGE is pruned to the batch's months, so re-sent data never double-counts revenue: replaying 5 days inserted 0 rows.

Quality gate after every batch

Row reconciliation (in = inserted + duplicates + already loaded + quarantined), unique keys, non-null checks and share warnings, stored as a Delta table.

Out-of-time evaluation

Train on March–July snapshots, select on August, report once on September. Deterministic training lets CI check the metrics.

Results

MeasureResult
Real transactions processed541,909
Daily incremental load vs full rebuild5.7 s vs 10.6 s (46% less work)
Z-order: files read per lookup400 → 1 (5.4× faster)
Churn model ROC AUC0.749 GBT vs 0.684 recency rule
Precision in riskiest 10%69% (base rate 44%)
Critical data-quality checkspassed 36/36 batches

UCI Online Retail dataset (1 Dec 2010 – 9 Dec 2011). Timings from one run on a 2-vCPU machine in Spark local mode; the Z-order benchmark uses a 20× scaled copy because 537K rows is too small for file layout to matter.

What I learned & what's next

  • Gradient-boosted trees and logistic regression were within noise of each other, so the tree model's extra complexity buys little.
  • Seasonality is confounded with the snapshots: churn is lowest for September, whose 90-day window covers Christmas.
  • A stale Delta history metric once reported rows inserted on a replay; comparing table versions fixed the reconciliation.