Method
How raw billing events become the numbers on this site, and the choices made along the way.
Data
The KKBox Churn Prediction Challenge (WSDM Cup 2018): real, anonymised records from Asia's largest music-streaming service. Four sources are used: members (sign-up channel, city, age), transactions (23M payments, renewals and cancellations from 2.4M members, 2015 to March 2017), daily listening logs (410.5M member-days) and Kaggle's own churn labels, which are used only to validate our definition.
The 30 GB listening log is never written to disk: it is streamed out of the archive into DuckDB and collapsed to one row per member per month (scripts/ingest.py). Only aggregates, with no group smaller than 50 members, are published here.
From transactions to subscriptions
Transactions are events, not states. They are collapsed to one event per member per day (a same-day cancellation beats a renewal) in int_billing_events, then stitched into continuous subscription spells with a gaps-and-islands window query (int_billing_events_sequenced): a new spell starts only when coverage has lapsed for more than 30 days. A renewal a week late is not churn followed by a reactivation.
The 30-day grace period is the competition's own churn definition, and our flag agrees with Kaggle's published labels (see Why members churn).
Repairing the raw data
Holes in the billing export. Some payment methods almost vanish from the transaction log for a month or more, then return at full volume. Method 39, for example, drops from ~76K to ~4K transactions a month in February–June 2016, while its members keep listening at their usual rate. Taken at face value, that reads as mass churn followed by mass reactivation. int_billing_data_gaps flags every payment-method-month below 25% of that method's typical volume (23 months across 6 methods). A lapse that overlaps one of these holes, where the member later reappears, is treated as continuous: the renewals were almost certainly made but not exported. This applies to 6.4% of subscription spells. Members who never reappear still count as churned.
Missing plan lengths. 3.7% of transactions record a plan length of zero, yet 99% of them are paid and expire about 31 days later: ordinary monthly payments with the field missing. The length is inferred from the expiry date instead of dropping them.
Start of the data. The log begins in January 2015, so everyone already subscribed then looks new. 2015 is used as warm-up: revenue flows and churn rates are reported from January 2016, and cohorts from March 2015.
Revenue
MRR is measured on the last day of each month: a member counts if a spell covers that date, and contributes the price of their latest paid period normalised to 30 days (amount paid × 30 ÷ plan days). The price comes from an ASOF JOIN in int_paying_months. Amounts stay in New Taiwan dollars rather than applying an invented exchange rate.
Every member-month is classified as new, expansion, contraction, churned, reactivation or retained (fct_subscriber_months). The monthly bridge must satisfy beginning + new + reactivation + expansion − contraction − churn = ending, and a dbt test fails the build if any month is off by more than one cent.
NRR and GRR are reported on a trailing-twelve-month basis: revenue today from members who paid twelve months ago, over what they paid then (GRR caps each member at their old amount). The one-month ratios are kept in the data tables but not headlined, because ~99% a month reads as healthy and is actually ~89% a year.
Churn and the model
fct_renewal_decisions has one row per paid period at the moment it expires. Churned means no further paid period in the same spell. Periods expiring within 30 days of the end of the data are dropped as censored, because their outcome is not yet knowable.
Features only use information from before the decision month: billing settings, tenure, prior churn, and listening in the last full month compared with the three before it. Evaluation is out-of-time: train on 2016, choose models on Q4 2016, report on Jan–Feb 2017 once. A random split would let the model learn from the future.
An explicit cancellation is a legitimate predictor (it happens before expiry), but it is close to the outcome itself. So the site also reports a listening-only model, which measures how much behaviour alone reveals before a member takes any billing action.
Limitations
Month-end snapshots miss members whose whole subscription falls inside one calendar month (e.g. a single 7-day plan); they are in the renewal-decision analysis but not in MRR. Expansion and contraction mostly reflect plan-length and promotional-price changes, not upsell. Listening logs end in February 2017, which bounds the test window. The billing-gap repair is a judgement call: a few months (e.g. March 2016 at ~7% churn) remain elevated after it, and may still contain export problems. Churn-driver charts show associations, and members who choose auto-renew differ from those who don't in ways the data cannot see.
Stack
DuckDB for storage and compute · dbt for the transformation layer and its tests · scikit-learn for the backtest · Next.js static export with ECharts, hosted on Vercel. No server or database runs behind this site; everything is computed at build time. Source on GitHub.