Cross-Selling
Cross-selling is encouraging a customer to buy complementary or related products alongside the one they are already buying. The goal is a bigger basket, a higher average order value, and ultimately more lifetime value.
“Frequently bought together — laptop, bag, mouse” is the retail form; selling a credit card to a checking-account holder is the banking one. It differs from upselling, which sells a higher tier of the same thing: iPhone to iPhone Pro is an upsell, iPhone plus AirPods or AppleCare is a cross-sell.
Three benefits are usually claimed: more revenue per customer, better stickiness because customers holding several products stay longer, and in SaaS the opening of new use cases through additional modules. The second one is the one worth testing, because the evidence normally offered for it does not support it.
The retention claim, and where it comes from
The following hypothetical simulation creates customers whose underlying quality affects both product holdings and churn. It compares the observed association with the effect built into that data-generating model.
Each of 400,000 customers has a latent quality drawn from a standard normal. The number of products held is one plus two independent draws, each more likely for higher-quality customers. Monthly churn is 6.0% minus 1.8 points per unit of quality, minus 1.0 point for each product beyond the first, clipped to between 0.5% and 30%. The 1.0 point is the effect built into the data.
import numpy as np
from scipy.stats import norm
rng = np.random.default_rng(0)
n = 400_000
quality = norm.ppf(rng.random(n))
n_products = (1 + (rng.random(n) < 1 / (1 + np.exp(-1.2 * quality)))
+ (rng.random(n) < 1 / (1 + np.exp(-1.2 * (quality - 1.5))))).astype(int)
true_effect = 0.010 # one extra product lowers churn by 1.0 point, before clipping
churn = 0.060 - 0.018 * quality - true_effect * (n_products - 1)
churn = np.clip(churn, 0.005, 0.30)
for k in (1, 2, 3):
held = n_products == k
print(f"{k} product(s): {held.sum():,} customers, monthly churn {churn[held].mean():.4f}, "
f"vs one product {churn[held].mean() - churn[n_products == 1].mean():+.4f}")
1 product(s): 176,219 customers, monthly churn 0.0702, vs one product +0.0000
2 product(s): 170,697 customers, monthly churn 0.0454, vs one product -0.0248
3 product(s): 53,084 customers, monthly churn 0.0222, vs one product -0.0480
In this simulation, three-product customers churn 4.80 percentage points less than one-product customers. That comparison spans two additional products, or 2.40 points per product. The data-generating coefficient is 1.00 point per product before clipping churn probabilities; clipping makes some individual effects smaller. Comparing the raw two-product gap directly with the one-product coefficient would overstate the discrepancy.
Customers with higher latent quality both buy more products and churn less. To separate the two, compare customers of similar quality. The code below splits them into three quality bands and, within each, takes the gap between one-product and three-product customers divided by two.
edges = (-1.0, 0.0, 1.0, 3.0)
for lo, hi in zip(edges[:-1], edges[1:]):
band = (quality >= lo) & (quality < hi)
means = [churn[band & (n_products == k)].mean() for k in (1, 2, 3)]
print(f"quality {lo:+.0f} to {hi:+.0f}: churn with 1 / 2 / 3 products "
f"{means[0]:.4f} / {means[1]:.4f} / {means[2]:.4f}, "
f"gap per product {(means[0] - means[2]) / 2:.4f}")
quality -1 to +0: churn with 1 / 2 / 3 products 0.0690 / 0.0573 / 0.0459, gap per product 0.0116
quality +0 to +1: churn with 1 / 2 / 3 products 0.0531 / 0.0415 / 0.0298, gap per product 0.0117
quality +1 to +3: churn with 1 / 2 / 3 products 0.0363 / 0.0243 / 0.0121, gap per product 0.0121
Within quality bands, the gap per additional product is 1.16–1.21 points, versus 2.40 points in the raw comparison. Residual confounding and clipping remain. Randomize eligibility for a cross-sell campaign and compare all assigned customers, including non-buyers, to estimate the campaign effect; comparing buyers with non-buyers after assignment does not preserve randomization.
The business case uses a common one-additional-product scale. Convert a churn reduction into money with a constant-churn lifetime value, ARPU times margin divided by churn, for 10,000 eligible customers, 12% take-up, \$40 monthly ARPU, a 75% margin and 6% baseline churn. Three assumed churn reductions are compared.
reach, take, arpu, margin, base_churn = 10_000, 0.12, 40.0, 0.75, 0.06
def ltv(churn):
return arpu * margin / churn
for label, cut in (("raw gap per product, 2.40 points", 0.0240),
("coefficient scenario, 1.0 point", 0.0100),
("no retention effect at all", 0.0)):
value = reach * take * (ltv(base_churn - cut) - ltv(base_churn))
print(f"{label}: {value:,.2f}")
raw gap per product, 2.40 points: 400,000.00
coefficient scenario, 1.0 point: 120,000.00
no retention effect at all: 0.00
A 2.40-point assumed churn reduction gives \$400,000, whereas 1.00 point gives \$120,000, about 3.33 times less. Both are sensitivity scenarios for 10,000 eligible customers, 12% take-up and the stated constant-churn LTV model. Neither is an experimentally identified campaign value, and the 1-point coefficient is not an exact population effect after clipping.
Recommendation lift works from assumed purchase associations. Take five items ranked by popularity, each with a base rate (its marginal purchase probability) and an assumed lift. The probability of buying the item given a purchase of X is the base rate times the lift.
| Item rank by popularity | Base rate | Probability given X | Lift (assumed) |
|---|---|---|---|
| 1 | 0.30 | 0.30 | 1.0x |
| 2 | 0.18 | 0.47 | 2.6x |
| 3 | 0.12 | 0.49 | 4.1x |
| 4 | 0.08 | 0.48 | 6.0x |
| 5 | 0.06 | 0.57 | 9.5x |
A fifth-ranked item has probability 0.57 conditional on purchase of X, against marginal probability 0.06. This is association-rule lift, not a measured conversion gain from recommending that item. Test a recommendation policy against a popularity policy on the same randomized eligible population before attributing incremental purchases to it.
How it is done, and where it helps
The mechanisms are bundling, recommendation widgets, add-ons and accessories, account expansion across a B2B relationship, and loyalty incentives for adding to a basket. The examples run across industries: camera to memory card and tripod; project management to time tracking or reporting; mortgage to home insurance; hotel booking to airport transfer.
Relevant offers may reduce the decline risk discussed under upselling. Purchase history supplies targeting hypotheses; randomized tests of policy and placement determine whether those hypotheses add contribution after offer costs and retention effects.
Measure the retention effect over a suitable follow-up period. A holdout estimates the tested campaign in its population and period; it does not settle the effect of every subsequent campaign.
References
- SciPy documentation,
scipy.stats.norm. docs.scipy.org
Discover more from Insightful Data Lab
Subscribe to get the latest posts sent to your email.
