Repeat purchase rate¶
What share of customers come back for a second order?
Current value (customers case data): 72.4% - ✓ On target. Open the case.
| Field | Value |
|---|---|
| Definition | Customers with two or more distinct invoices in the window divided by customers with at least one. |
| Formula | count(customers with invoices >= 2) / count(customers) |
| Unit | % |
| Granularity | Customer; window of 12 months. |
| Direction | Higher is better ↑ |
| Target and thresholds | ≥ 60.0% (amber ≥ 50.0%) |
| Parameters | – |
| Owner | CRM Lead |
| Source | Transactions (UCI Online Retail II) |
| Input | table sales, columns customer_id, invoice_id |
| Recommended chart | bar - By acquisition cohort, against the target line. |
| Related KPIs | Customer retention (quarter over quarter), Revenue at risk (RFM "At risk") |
How to read it¶
Is the first purchase turning into a relationship? Customers acquired at the end of the window have had less time to repeat - compare like with like.
Common misreadings¶
- Counting invoice lines instead of distinct invoices makes every multi-item basket a "repeat".
Worked example (from the test suite)¶
| customer_id | invoice_id |
|---|---|
| a | i1 |
| a | i1 |
| a | i2 |
| b | i3 |
| b | i3 |
| c | i4 |
Expected result: 33.33%. Distinct invoices: a = 2 (i1, i2), b = 1 (two lines of i3), c = 1. 1 of 3 customers repeats = 0.3333. Counting lines would wrongly give 2/3.
This example is asserted in tests/test_formulas.py against the Python function and in tests/test_sql.py against the SQL below.
Implementations¶
Python function (the single implementation used by the site):
@kpi("repeat_purchase_rate")
def repeat_purchase_rate(df: pd.DataFrame) -> float:
orders = df.groupby("customer_id")["invoice_id"].nunique()
return safe_ratio((orders >= 2).sum(), len(orders))
Reference SQL (tested on SQLite):
SELECT CAST(SUM(CASE WHEN n >= 2 THEN 1 ELSE 0 END) AS REAL) / COUNT(*)
FROM (SELECT customer_id, COUNT(DISTINCT invoice_id) AS n FROM sales GROUP BY customer_id)