I sent you this chapter. Reply on LinkedIn or by email.
The formulas behind the four calculators, and four queries every brand with more than one category should be able to run.
| For | Formula | Notes |
|---|---|---|
| Wallet Allocation Rule | share = (1 − r/(n + 1)) × 2/n | r: your rank; n: brands the customer uses. Keiningham and colleagues. |
| Break-even buyers | launch cost / (spend per buyer × margin) | Divide by active customers for the share. Year one, base only. |
| Adjusted multiple | (A/B) / (C/D) | A, B: outcome-year spend, multi and single. C, D: early-window spend. An upper bound on the causal effect. |
| Contribution per order | v(1−d)(1−r) − vc(1−r) − r×rc − f | v: full-price order value; d: discount; r: share returned; c: product cost share; rc: cost per return; f: fulfillment per order. Per year: orders × this − service. |
| Break-even discount | 1 − (s/o + vc(1−r) + r×rc + f) / (v(1−r)) | s: service cost per year; o: orders per year. |
| Holdout lift | (pT − pH) ± 1.96√(pT(1−pT)/nT + pH(1−pH)/nH) | p: share who bought the new category; T: treated, H: holdout. Incremental buyers: lift × nT. |
-- survey_responses: customer_id, category_spend_12m, brands_used, our_rank
WITH ours AS (
SELECT o.customer_id, SUM(ol.net_amount) AS our_spend_12m
FROM orders o
JOIN order_lines ol ON ol.order_id = o.id
JOIN products p ON p.id = ol.product_id
WHERE p.category = 'coffee' -- the hero's category
AND o.created_at >= CURRENT_DATE - INTERVAL '12 months'
GROUP BY o.customer_id
)
SELECT s.our_rank, s.brands_used, COUNT(*) AS customers,
AVG(LEAST(1, COALESCE(ours.our_spend_12m, 0)
/ NULLIF(s.category_spend_12m, 0))) AS actual_share,
AVG((1 - s.our_rank::numeric / (s.brands_used + 1))
* (2.0 / s.brands_used)) AS rule_share,
SUM(s.category_spend_12m - COALESCE(ours.our_spend_12m, 0)) AS spend_elsewhere
FROM survey_responses s
LEFT JOIN ours USING (customer_id)
WHERE s.brands_used BETWEEN 1 AND 20
AND s.our_rank BETWEEN 1 AND s.brands_used
GROUP BY s.our_rank, s.brands_used
ORDER BY s.brands_used, s.our_rank;
If actual and rule shares roughly agree across rows, rank is a usable tracker. Stated spend is noisy: use medians in the calculator.
-- groups customers by when they first bought outside their first category
WITH lines AS (
SELECT o.customer_id, o.created_at, p.category,
ol.net_amount - COALESCE(rf.amount, 0) AS net
FROM orders o
JOIN order_lines ol ON ol.order_id = o.id
JOIN products p ON p.id = ol.product_id
LEFT JOIN (SELECT order_line_id, SUM(amount) AS amount
FROM refunds GROUP BY order_line_id) rf
ON rf.order_line_id = ol.id
),
firsts AS (
SELECT customer_id, MIN(created_at) AS first_at FROM orders GROUP BY customer_id
),
first_cat AS (
SELECT DISTINCT ON (l.customer_id) l.customer_id, l.category
FROM lines l JOIN firsts f USING (customer_id)
WHERE l.created_at = f.first_at
ORDER BY l.customer_id, l.net DESC
),
crossed AS (
SELECT l.customer_id, MIN(l.created_at) AS crossed_at
FROM lines l JOIN first_cat c USING (customer_id)
WHERE l.category <> c.category
GROUP BY l.customer_id
),
per AS (
SELECT f.customer_id, f.first_at, x.crossed_at,
SUM(l.net) FILTER (WHERE l.created_at < f.first_at + INTERVAL '180 days') AS early,
SUM(l.net) FILTER (WHERE l.created_at >= f.first_at + INTERVAL '365 days'
AND l.created_at < f.first_at + INTERVAL '730 days') AS outcome
FROM firsts f
JOIN lines l USING (customer_id)
LEFT JOIN crossed x USING (customer_id)
WHERE f.first_at < CURRENT_DATE - INTERVAL '730 days'
GROUP BY f.customer_id, f.first_at, x.crossed_at
),
grouped AS (
SELECT *, CASE
WHEN crossed_at IS NULL OR crossed_at >= first_at + INTERVAL '730 days' THEN 'single'
WHEN crossed_at >= first_at + INTERVAL '180 days'
AND crossed_at < first_at + INTERVAL '365 days' THEN 'multi'
END AS grp
FROM per
)
SELECT grp, COUNT(*) AS customers,
AVG(COALESCE(early, 0)) AS early_spend, -- days 0 to 179
AVG(COALESCE(outcome, 0)) AS outcome_spend -- days 365 to 729
FROM grouped
WHERE grp IS NOT NULL
GROUP BY grp;
Customers who crossed in their first 180 days, or in the outcome year, are left out, so both groups’ early windows are clean. The four averages go into the chapter 7 calculator. Syntax is Postgres.
-- last 12 months. order_contribution: one row per order, with customer_id,
-- created_at, categories (array), and contribution = revenue after discounts
-- and refunds, minus product, fulfillment, shipping, payment and return costs.
SELECT c.customer_id,
(SELECT COUNT(DISTINCT cat)
FROM order_contribution c2, unnest(c2.categories) AS cat
WHERE c2.customer_id = c.customer_id
AND c2.created_at >= CURRENT_DATE - INTERVAL '12 months') AS categories,
SUM(c.contribution)
- COALESCE(MAX(t.tickets), 0) * :cost_per_ticket AS contribution
FROM order_contribution c
LEFT JOIN (SELECT customer_id, COUNT(*) AS tickets FROM support_tickets
WHERE created_at >= CURRENT_DATE - INTERVAL '12 months'
GROUP BY customer_id) t USING (customer_id)
WHERE c.created_at >= CURRENT_DATE - INTERVAL '12 months'
GROUP BY c.customer_id;
Wrap it to count customers with negative contribution among those with two or more categories, and their share of all negative contribution. That’s your version of Shah and colleagues’ 10% to 35% and 39% to 88%.
-- flow_assignments: customer_id, arm ('treat' or 'holdout'), qualified_at
WITH w AS (
SELECT a.customer_id, a.arm,
BOOL_OR('cold_brew' = ANY(c.categories)) AS bought_new,
COALESCE(SUM(c.contribution), 0) AS contribution
FROM flow_assignments a
LEFT JOIN order_contribution c
ON c.customer_id = a.customer_id
AND c.created_at >= a.qualified_at
AND c.created_at < a.qualified_at + INTERVAL '90 days'
WHERE a.qualified_at < CURRENT_DATE - INTERVAL '90 days'
GROUP BY a.customer_id, a.arm
)
SELECT arm, COUNT(*) AS customers,
AVG(COALESCE(bought_new, false)::int) AS bought_new_category,
AVG(contribution) AS contribution_per_customer
FROM w GROUP BY arm;
Count every assigned customer, including those who never opened a message. Comparing openers with the holdout brings the selection problem back.
Want to answer the note that sent you here? Reply on LinkedIn or by email.
If this is your store
The free Klaviyo audit reads your own account and scores it. Start with your store’s address. A read-only key gets the full report.
Written by Andrew Lauchner, a growth and retention operator for consumer brands. The paid work is one ninety-day Sprint.
This is one chapter of The Next Category, which is free and readable in full on a single page with no form in front of it.