Logistic regression, lift charts and market basket analysis

TL;DR

Difficulty: Intermediate

Most marketing questions end in a yes or a no, and logistic regression is the tool built for them.

  • It models the log odds, so every prediction stays between 0 and 1, and its coefficients become odds ratios.
  • Score the file, cut it into deciles, and mail only the deciles whose response rate beats cost divided by margin.
  • Judge the ranking on a holdout gains chart, never on the training data.
  • With product flags as predictors, the same model does basket analysis and controls for the buyer and the rest of the basket, which association rules cannot.

Most marketing questions end in a yes or a no: will this customer respond to the spring brochure, will the person with the wine hamper add the cheese hamper. I walk through the logit, odds ratios and the S shaped curve, score an invented Cork hamper shop's 20,000 customers into deciles and find the depth where the next decile stops paying, then use the same model for market basket analysis. This is chapter 7 of 21 in my Marketing Analytics series.

The spring campaign that pays for itself, or does not

Every March, Priya at Northgate Hampers, an invented online hamper shop in Cork at northgatehampers.example, has the same conversation with herself. Christmas is over and about 20,000 people on the customer file have bought at least once. Easter and Mother's Day are coming, and a printed spring brochure with a code inside costs roughly €1.20 per household once print, An Post postage and the redeemed discount are added up. Mail all 20,000 and she spends €24,000 before a single hamper leaves the warehouse. Mail nobody and she leaves revenue on the table. Mail "the good customers" by gut feeling and she never finds out what she missed.

The question is not "should I mail". It is "who, and how deep". That is a yes or no outcome per customer: respond or not. This chapter is about the most common outcome in marketing, the binary one, and the tool built for it: logistic regression. I use it twice. First to rank customers for the spring campaign and find the depth where the next brochure stops paying for itself. Then on the order lines, to answer a basket question: given the wine hamper in the basket, how likely is the cheese hamper, and what should the shop recommend.

Northgate Hampers and every number in this chapter are made up so I can show each step. The method is the one I run for real.

Where this chapter sits in the series

This is chapter 7 of 21 in my Marketing Analytics series, and the fourth of part two, the dependent variable techniques, which run from chapter 4 to chapter 10. The previous chapter, Poisson regression, handled counts. The next one, survival analysis, churn and customer lifetime value, adds time: not whether a customer leaves, but when. Logistic regression sits between them as the workhorse. Half of what gets sold as "predictive marketing" is a logistic model with a decent feature table behind it.

Every basic idea used here, from probability to logarithms, is built from scratch on the Foundations page.

Ordinary regression breaks on a yes or no outcome

Code a response as 1 and no response as 0, fit an ordinary straight line, and three things go wrong. The prediction is unbounded, so a very engaged customer gets a predicted response of 1.3, which is not a probability. The errors cannot be normally distributed, because the outcome only takes two values. And every effect is forced to be linear: going from one to two orders adds as much probability as going from nine to 10, which nobody believes.

Logistic regression fixes all three with one idea. Instead of modelling the probability directly, it models the log of the odds. Odds are the probability of the event divided by the probability of it not happening.1 A 20% response rate is odds of 0.20/0.80=0.250.20 / 0.80 = 0.25, 50% is odds of 1, and 90% is odds of 9. Odds run from zero to infinity, and their natural logarithm, the logit, runs from minus infinity to plus infinity, which is exactly what a linear equation likes to produce.

ln⁡(p1−p)=β0+β1x1+β2x2+⋯+βkxk\ln\left(\frac{p}{1-p}\right) = \beta_0 + \beta_1 x_1 + \beta_2 x_2 + \dots + \beta_k x_k

Here pp is the probability that the customer responds, p1−p\frac{p}{1-p} is the odds, ln⁡\ln is the natural logarithm, x1x_1 to xkx_k are the things we know about the customer, β0\beta_0 is the intercept (the log odds for a customer with every xx at zero), and each βj\beta_j is the change in the log odds for one unit more of xjx_j, holding the others constant. Foundations: regression and Foundations: logarithms

Solve that for pp and you get the logistic function, the S shaped curve that gives the method its name:

p=11+e−(β0+β1x1+⋯+βkxk)p = \frac{1}{1 + e^{-(\beta_0 + \beta_1 x_1 + \dots + \beta_k x_k)}}

Here ee is Euler's number, about 2.718, and the bracket in the exponent is the same linear score β0+β1x1+⋯+βkxk\beta_0 + \beta_1 x_1 + \dots + \beta_k x_k as above. Whatever the score is, the function squashes it between 0 and 1. Foundations: regression

Worked example: three scores through the curve

A score of 0 gives p=1/(1+e0)=1/2=0.5p = 1 / (1 + e^{0}) = 1 / 2 = 0.5. A score of 2 gives p=1/(1+e−2)=1/(1+0.135)=0.881p = 1 / (1 + e^{-2}) = 1 / (1 + 0.135) = 0.881. A score of minus 4 gives p=1/(1+e4)=1/(1+54.6)=0.018p = 1 / (1 + e^{4}) = 1 / (1 + 54.6) = 0.018.

The logistic functionThe logistic function turns a linear score into a probability. At a score of minus 6 the probability is 0.002, at minus 2 it is 0.12, at 0 it is 0.5, at 2 it is 0.88 and at 6 it is 0.998. The curve is steepest around zero and flat at both ends.From a linear score to a probability00.511.52-6-4-20246Probability of a yesProbabilityLinear score

The horizontal axis is the linear score, the vertical axis the probability it turns into. The curve is steepest in the middle and flat at both ends, which is a fair description of people: pushing an already loyal customer harder does very little, and neither does shouting at someone who left years ago.

The betas are estimated by maximum likelihood, not least squares,1 so there is no R squared in the usual sense. The diagnostics that matter are different, and I come to them below.

Coefficients become odds ratios

The betas live on the log odds scale, which nobody thinks in. Exponentiate them and you get odds ratios, which people can use:1

ORj=eβj\text{OR}_j = e^{\beta_j}

Here ORj\text{OR}_j is the odds ratio for predictor jj, ee is Euler's number and βj\beta_j is that predictor's coefficient. An odds ratio of 1.5 means one unit more of xjx_j multiplies the odds by 1.5, with everything else held constant. Foundations: regression

An odds ratio of 1.73 for the gift buyer flag means a gift buyer has 1.73 times the odds of responding of a self buyer with otherwise identical characteristics. Odds, not probability. If the self buyer's probability is 3%, their odds are 0.03/0.97=0.0310.03 / 0.97 = 0.031. The gift buyer's odds are 0.031×1.73=0.0540.031 \times 1.73 = 0.054, and their probability 0.054/1.054=5.1%0.054 / 1.054 = 5.1\%.

Worked example: Priya's spring response model

Here is the spring response model, fitted on last year's spring campaign: 20,000 mailed, 788 responded, a base rate of 788/20,000=3.94%788 / 20{,}000 = 3.94\%. Every variable is measured as of the day the brochure went out.

VariableCoefficientStd. errorOdds ratioVIF
Interceptminus 3.400.21
Months since last orderminus 0.090.0120.911.6
Orders in the last 24 months0.280.041.322.1
Gift buyer (1 = yes)0.550.111.731.2
Corporate account (1 = yes)0.420.161.521.3
Clicked last spring's email (1 = yes)0.780.102.181.4
Average order value, per €100.060.021.061.9

Reading it line by line. Each month since the last order multiplies the odds by e−0.09=0.91e^{-0.09} = 0.91, so a customer who last bought 12 months ago has e−0.09×12=e−1.08=0.34e^{-0.09 \times 12} = e^{-1.08} = 0.34 times the odds of someone who bought last month. Each extra order in two years multiplies the odds by e0.28=1.32e^{0.28} = 1.32. Gift buyers have 73% higher odds than self buyers, and that is the segment the spring brochure is really for. Corporate accounts respond better than private ones even after controlling for order count, presumably because offices buy Easter hampers for staff. Clicking last year's spring email is the strongest single signal, an odds ratio above 2. Average order value matters, but modestly: €50 more is five units, so the odds rise by e0.06×5=e0.30=1.35e^{0.06 \times 5} = e^{0.30} = 1.35, about 35%.

The first version of this chapter used "opened last spring's email". I changed it to clicked: Apple's Mail Privacy Protection fetches the invisible image that records an open in the background, whether or not anyone reads the message,2 so an open is a weak signal.

A rule of thumb for the standard errors: a coefficient smaller than about twice its standard error could plausibly be zero. Everything here clears that bar. The corporate flag only just: 0.42/0.16=2.60.42 / 0.16 = 2.6.

The variance inflation factor is the collinearity alarm

The last column of the table is the variance inflation factor, VIF for short:

VIFj=11−Rj2\text{VIF}_j = \frac{1}{1 - R_j^2}

Here VIFj\text{VIF}_j is the variance inflation factor of predictor jj, and Rj2R_j^2 is the share of the variation in xjx_j explained when you regress xjx_j on all the other predictors. A VIF of 1 means xjx_j shares nothing with the rest. Foundations: correlation and Foundations: regression

Penn State's regression course gives the usual rule: VIFs above 4 deserve a closer look, and above 10 signal serious collinearity that needs correcting.3 I get nervous above 5 and remove or combine variables above 10.

Worked example: when two predictors are near twins

If the other predictors explain 80% of the variation in a variable, its VIF is 1/(1−0.80)=51 / (1 - 0.80) = 5, and its coefficient is being estimated from the 20% of information that is genuinely its own. At Northgate, order count and order value are mildly related (frequent buyers also buy bigger), hence 2.1 and 1.9. Nothing alarming.

The classic symptom of collinearity is a sign flip: a variable that should obviously raise response comes out negative, because it is fighting a near twin for the same information. If your model says frequent buyers respond less, do not publish a clever explanation. Check the VIFs, drop one of the twins and refit.

From probabilities to a ranked list

Scoring is arithmetic. Compute the linear score for every customer on the file, push it through the logistic function, sort the 20,000 people from most to least likely and cut them into 10 equal groups of 2,000, called deciles. Decile 1 is the best 10%, decile 10 the weakest.

The scoring pipelineA top to bottom flow: order history and email clicks build one feature row per customer, a logistic model is fitted on last spring, the checks either pass or send you back to fix the variables, all 20,000 customers are scored and ranked into deciles, the shop mails to the breakeven depth plus a random holdout, and the result is measured against the holdout and feeds next year's feature table.Order history and email clicksOne feature row per customer, as of themailing dateFit the logistic model on last springHoldout, VIF, sign and calibrationchecks pass?noFix the variables and refityesScore all 20,000 customers and rankinto decilesMail to the breakeven depth, plus arandom holdoutMeasure against the holdout, feed nextyear's model

Read it top to bottom. The decision step sends a failed check back to the variables, never forward to the mailing. And the last step is the point of the whole thing: every campaign feeds the next model.

Now the campaign economics. Each brochure costs cc, about €1.20 here. Each response is worth mm in contribution margin, the revenue minus product cost and delivery, roughly €38 on an average spring order of €68. For a decile with ndn_d customers and response rate rdr_d, the profit from mailing it is:

πd=nd(rd m−c)\pi_d = n_d \left( r_d \, m - c \right)

Here πd\pi_d is the profit from decile dd, ndn_d the number of customers in it, rdr_d its response rate, mm the margin per response and cc the cost per brochure. Foundations: expected value

It is positive whenever the decile's response rate is above the breakeven rate:

r∗=cmr^* = \frac{c}{m}

Here r∗r^* is the breakeven response rate, cc the cost per brochure and mm the margin per response. Any decile above it pays for itself, any decile below it loses money on every brochure. Foundations: marginal versus average

The lift of a decile is its response rate relative to the average:

Liftd=rdrˉ\text{Lift}_d = \frac{r_d}{\bar{r}}

Here Liftd\text{Lift}_d is the lift of decile dd, rdr_d its response rate and rˉ\bar{r} the response rate across the whole file. A lift of 3 means those customers respond three times as often as a random pick. Cumulative lift does the same for the top kk deciles together. Foundations: ratios and rates

Worked example: Priya's breakeven rate

r∗=1.20/38=0.0316r^* = 1.20 / 38 = 0.0316, about 3.2%. Any decile responding above 3.2% earns its brochures back. That is the whole "how deep" decision in one line.

The decile table shows where the next decile stops paying

Here is the scored file, with last year's response rates applied decile by decile. Revenue is responders times €68, profit is responders times €38 minus 2,000 brochures at €1.20, which is €2,400 per decile.

DecileCustomersResponse rateRespondersRevenue €Decile profit €Cumulative profit €Cumulative lift
12,00012.5%25017,0007,1007,1003.17
22,0008.0%16010,8803,68010,7802.60
32,0005.5%1107,4801,78012,5602.20
42,0004.0%805,44064013,2001.90
52,0003.0%604,080minus 12013,0801.68
62,0002.2%442,992minus 72812,3521.49
72,0001.6%322,176minus 1,18411,1681.33
82,0001.2%241,632minus 1,4889,6801.21
92,0000.8%161,088minus 1,7927,8881.09
102,0000.6%12816minus 1,9445,9441.00

Worked example: reading the table

Decile 1: 250×38−2,400=9,500−2,400=€7,100250 \times 38 - 2{,}400 = 9{,}500 - 2{,}400 = €7{,}100. Its lift is 12.5%/3.94%=3.1712.5\% / 3.94\% = 3.17. Decile 4 responds at 4.0%, just above the 3.2% breakeven, and adds 80×38−2,400=€64080 \times 38 - 2{,}400 = €640. Decile 5 responds at 3.0%, just below, and loses 60×38−2,400=−€12060 \times 38 - 2{,}400 = -€120. The cumulative lift after four deciles is (250+160+110+80)/8,000=7.5%(250 + 160 + 110 + 80) / 8{,}000 = 7.5\%, divided by 3.94%, which is 1.90.

So the answer is: mail 8,000 people, not 20,000. That saves 12,000×1.20=€14,40012{,}000 \times 1.20 = €14{,}400 in brochures, gives up 60+44+32+24+16+12=18860 + 44 + 32 + 24 + 16 + 12 = 188 orders from deciles 5 to 10, and lifts the campaign profit from €5,944 to €13,200, more than double. The revenue column is what a marketing team likes to look at. The cumulative profit column is what the owner should look at. They peak in different places, and that gap is where campaign budgets are quietly wasted.

One refinement matters. The breakeven depth is a point estimate. With 2,000 people in decile 4, its response rate has a standard error of 0.04×0.96/2,000=0.0044\sqrt{0.04 \times 0.96 / 2{,}000} = 0.0044, about 0.44 points, so a 95% interval runs from roughly 3.1% to 4.9%. Foundations: confidence intervals I tell clients that decile 4 is a coin flip and decile 5 is probably a no, which is a more honest sentence than "mail exactly 8,000". And because print gets less expensive per unit at volume, I recompute cc and mm per decile rather than assuming constants.

The gains chart shows what a good ranking looks like

The gains chart plots the share of all responders captured against the share of the file mailed. Random selection is the diagonal: mail 30% of the file, catch 30% of the responders. The model's curve should bow above it.

Cumulative gains, model against randomMailing the top 10% of the file by model score captures 32% of all responders, the top 20% captures 52%, the top 40% captures 76% and the top 60% captures 89%. Random selection captures the same share of responders as the share of the file mailed.Share of all responders captured0501001502000%20%40%60%80%100%ModelRandomResponders captured, %Share of the file mailed

The horizontal axis is the share of the file mailed, best scores first. The vertical axis is the share of all 788 responders those customers contain: the top decile holds 250/788=32%250 / 788 = 32\%, the top four 600/788=76%600 / 788 = 76\%. The height of the model's line above the random one in the first deciles is where the money is. Where it flattens, you are mailing people who will not respond either way.

The gap between the gains curve on the training data and on a holdout sample is your honesty check. If the holdout curve is noticeably lower, the model has memorised last year rather than learned something about customers.

The cumulative profit says the same thing in euro:

Cumulative profit by mailing depthCumulative campaign profit rises from €7,100 after decile 1 to a peak of €13,200 after decile 4, then falls to €5,944 when all ten deciles are mailed.Cumulative profit by mailing depth€0€5,000€10,000€15,000€20,000D1D2D3D4D5D6D7D8D9D10Cumulative profit in €Deepest decile mailed

Each bar is the total profit if Priya mails every decile up to and including that one. The peak at decile 4 is the answer to "how deep". Everything to the right of it is money spent to lose money.

Calibration, imbalance and when a tree does better

Ranking is one thing, the probabilities themselves another. A model is well calibrated when, among the customers it scores at about 10%, about 10% really respond.4 Logistic regression tends to be well calibrated on its own, because of the way its loss function and its link fit together,4 but it drifts: this year's spring is not last year's. I check by binning the predicted probabilities into 10 groups and comparing predicted with observed response in each. For the depth decision calibration matters more than for ranking, because the breakeven rule compares an absolute probability with c/mc / m.

Worked example: a model that is a fifth too optimistic

Suppose the holdout shows the model is 20% too optimistic across the board. The decile 4 customers it scored at 4.0% really respond at 4.0/1.2=3.3%4.0 / 1.2 = 3.3\%, barely above the 3.2% breakeven, and decile 3's 5.5% becomes 5.5/1.2=4.6%5.5 / 1.2 = 4.6\%. The ranking is unchanged, but decile 4 turns from a modest earner into a marginal call, and the safe depth is three deciles.

Class imbalance is the other worry, and it is usually overdone. With a 3.9% response rate, 96% of the rows are zeros. Logistic regression copes as long as there are enough ones in absolute terms, and 788 responders is plenty for six predictors. Do not rebalance the training data by throwing away zeros or duplicating ones and then read the predicted probabilities as real: that destroys calibration, and you end up mailing to a breakeven computed in a fictional world. If you must sample down for speed, correct the intercept afterwards. King and Zeng give the correction: subtract ln⁡[(1−ττ)(yˉ1−yˉ)]\ln\left[\left(\frac{1 - \tau}{\tau}\right)\left(\frac{\bar{y}}{1 - \bar{y}}\right)\right] from the intercept, where τ\tau is the true share of ones and yˉ\bar{y} the share in your sample.5

Tree based models, gradient boosted trees in particular, often beat logistic regression by a few points of lift, because they find interactions on their own. But logistic regression has three advantages that matter more often than the extra lift. You can read it: the odds ratio table above is a management conversation in itself. It is stable on small files, and 20,000 customers with 788 responders is small for boosting. And it degrades gracefully: when the world shifts, a logistic model gets a bit worse, while a tree can fall off a cliff. My rule: start logistic, add two or three interaction terms by hand where the business logic suggests them, and reach for trees only when the holdout gains chart says the complexity is being paid for.

Association rules count pairs, one at a time

Market basket analysis asks which products go together. The textbook approach is association rules.6 For a rule "wine hamper, so cheese hamper", three numbers:

conf(A→B)=nABnA,lift(A→B)=conf(A→B)P(B),interest(A→B)=conf(A→B)−P(B)\text{conf}(A \to B) = \frac{n_{AB}}{n_A}, \qquad \text{lift}(A \to B) = \frac{\text{conf}(A \to B)}{P(B)}, \qquad \text{interest}(A \to B) = \text{conf}(A \to B) - P(B)

Here nAn_A is the number of orders containing product AA, nABn_{AB} the number containing both AA and BB, conf(A→B)\text{conf}(A \to B) the confidence, the share of AA orders that also contain BB, and P(B)P(B) the share of all orders containing BB. The lift divides the confidence by that base rate, so a lift of 1 means BB turns up with AA no more often than anywhere.6 The interest subtracts it instead.7 Both answer the same question: is BB special to AA buyers, or simply popular? Foundations: probability and Foundations: base rates

This is the same co occurrence logic, and the same correction for popularity, as in building custom AI recommendations in Solidus.

Worked example: wine and cheese at Northgate

Of 14,000 orders, the cheese hamper is in 1,260, so P(cheese)=1,260/14,000=9%P(\text{cheese}) = 1{,}260 / 14{,}000 = 9\%. The wine hamper is in 2,000 orders, and 440 of those also contain cheese.

Confidence: 440/2,000=22%440 / 2{,}000 = 22\%. Lift: 0.22/0.09=2.440.22 / 0.09 = 2.44. Interest: 0.22−0.09=0.130.22 - 0.09 = 0.13. Wine buyers add cheese about two and a half times as often as the average order does.

The weakness is that a rule looks at one product at a time. The wine hamper might look as if it drives cheese when both are really bought by corporate accounts assembling gift sets, and the driver is the account type. Counting cannot tell the two apart.

Logistic regression does basket analysis with the other items held constant

Logistic regression handles this by making the presence of one product the outcome, and everything else in the basket, plus what you know about the buyer, the predictors:

P(cheese∣basket)=11+e−(β0+βwinexwine+βchocxchoc+βcorpxcorp+βnxn)P(\text{cheese} \mid \text{basket}) = \frac{1}{1 + e^{-(\beta_0 + \beta_{\text{wine}} x_{\text{wine}} + \beta_{\text{choc}} x_{\text{choc}} + \beta_{\text{corp}} x_{\text{corp}} + \beta_{n} x_{n})}}

Here P(cheese∣basket)P(\text{cheese} \mid \text{basket}) is the probability the cheese hamper is in the order, xwinex_{\text{wine}}, xchocx_{\text{choc}} and xcorpx_{\text{corp}} are 1 or 0 flags for the wine hamper, the chocolate hamper and a corporate buyer, xnx_n counts the other items in the basket, and each β\beta is the matching coefficient, with β0\beta_0 the intercept. Foundations: regression

Worked example: the basket model on 14,000 orders

PredictorCoefficientOdds ratioReading
Interceptminus 2.55base odds of about 0.078
Wine hamper in basket1.103.00triples the odds of cheese
Chocolate hamper in basketminus 0.450.64substitutes, not complements
Corporate buyer0.601.82offices build sets
Other items in basket (each)0.201.22bigger baskets get bigger

A private buyer with only the wine hamper: the score is −2.55+1.10=−1.45-2.55 + 1.10 = -1.45, so p=1/(1+e1.45)=1/(1+4.26)=0.19p = 1 / (1 + e^{1.45}) = 1 / (1 + 4.26) = 0.19. About 19%, against 1/(1+e2.55)=7.2%1 / (1 + e^{2.55}) = 7.2\% for a private buyer with neither. A corporate buyer with the wine hamper: −1.45+0.60=−0.85-1.45 + 0.60 = -0.85, so p=1/(1+e0.85)=1/(1+2.34)=0.30p = 1 / (1 + e^{0.85}) = 1 / (1 + 2.34) = 0.30.

Now compare with counting. The raw odds of cheese with wine are 440/1,560=0.282440 / 1{,}560 = 0.282, without wine 820/11,180=0.073820 / 11{,}180 = 0.073, a raw odds ratio of 0.282/0.073=3.850.282 / 0.073 = 3.85. The model, holding the buyer type and basket size constant, says 3.00. Part of "wine implies cheese" is a corporate effect in disguise. The first version of this chapter compared the model's odds ratio with the rule's lift. Those measure different things, so the like for like comparison is the raw odds ratio against the adjusted one.

The chocolate line is what association rules would have missed. Chocolate and cheese are substitutes in this shop: people pick one treat hamper. Recommending cheese to someone who has chosen chocolate is wasted screen space.

Fit one such model per product you might recommend, score the live basket at checkout, and show the two or three products with the highest predicted probability that are not already in it, with a floor, say 12%, below which you recommend nothing. The engineering, including how to wire it into Solidus, is in building custom AI recommendations in Solidus. With hundreds of products the one model per product approach gets heavy and you move to matrix factorisation. For 40 hampers it is exactly right.

Running it yourself

For the response model: one row per customer as of the campaign date, with the outcome (responded within the attribution window, usually four to six weeks, matched by code or email address) and predictors computed strictly from information available before the mailing. That last clause is where most first attempts leak. For the basket model: one row per order with a flag per product.

A minimal fit in Python with statsmodels,8 and the deciles with pandas:9

import numpy as np, pandas as pd
import statsmodels.api as sm

cols = ["months_since_last", "orders_24m", "gift_buyer",
        "corporate", "clicked_last_spring", "aov_per_10"]
X = sm.add_constant(df[cols])
model = sm.Logit(df["responded"], X).fit()
print(model.summary())
odds_ratios = np.exp(model.params)

df["p_hat"] = model.predict(X)
df["decile"] = pd.qcut(df["p_hat"].rank(method="first"), 10,
                       labels=range(10, 0, -1)).astype(int)  # decile 1 = best

An example of the timing: with clean order data, a first model takes about two days, and a validated one with a decile table and a documented depth recommendation about two weeks. The checks before you trust it are a holdout of 20% to 30% that never touches the fit, a holdout gains curve within a couple of points of the training curve, VIFs under 5, every sign explainable to a non statistician in one sentence, a calibration plot that hugs the diagonal, and a leakage hunt, where you list every variable with the exact date its value became known.

Pitfalls

Optimising response instead of profit. The top decile by response is not always the top decile by margin. Gift buyers respond well and buy the €49 Classic. Corporate accounts respond less often and buy twelve. If margin varies a lot by segment, model expected value, or at least compute mm per decile.

Leakage from the future. Any predictor measured after the mailing date, even innocently (a "customer status" field updated when they order), produces a beautiful gains curve in the office and a useless one in the field. "Clicked the spring email" is fine if it means last spring's, cheating if it means this spring's. If a model looks too good, it is.

Mailing the model into itself. Once you mail only the top four deciles, next year's data has responses only from those four, and a model fitted on it learns nothing about deciles 5 to 10. Keep a small random holdout across the whole file, a few hundred people who get the brochure regardless of score. It costs a little and keeps the model honest year after year. The same logic applies to lookalike audiences: a seed built only from people you already targeted teaches the platform to find more of the same.

Reading odds ratios as probability ratios. At a 3.9% base rate the difference is small. In the basket model, at 19% to 30%, it is not. An odds ratio of 3 is not "three times as likely": the private wine buyer's 19% is 2.6 times the 7.2% baseline, not three times.

Mistaking association for causation. The basket model says cheese and wine are bought together, not that showing cheese will sell more cheese. The recommendation is a hypothesis, and an A/B test on the checkout is the proof.

Too many variables. Forty predictors on 788 responders is overfitting waiting to happen. Ten is plenty, and a good few models do fine with fewer. If the team insists on everything, use a penalised fit (ridge or lasso) and keep the holdout sacred.

How I do this for clients

The deliverable is a scored list, a decile table with a marked depth, and a one page memo that says how many people to contact, what it will cost, what it should return, and how wrong that estimate could be. Not a slide deck about machine learning.

It starts with the free workshop, where we agree which outcome to model (respond, buy, churn, add to basket) and what a good decision looks like. Then come two weeks of real work on me: the feature table from your shop or CRM, a fitted and validated model, the decile table, the gains chart and the memo. If the model does not beat random by a margin worth acting on, I tell you so.

If it does, and you want it running for every campaign rather than once, that becomes a Data Science engagement: a scoring job that runs nightly and a random holdout that keeps it honest. For an eCommerce shop the basket model goes straight into the store as a recommendation block, which is Solidus work I do myself. Your raw data stays yours, and the terms for code and models are in the analytics and data science terms. Costs are on the pricing page.

Questions to ask before you approve the next mailing

  • What is the breakeven response rate for this campaign, and which deciles clear it?
  • Show me the gains chart on the holdout sample, not the training data.
  • Is there a random control group across the whole file, and how big is it?
  • Which variables are in the model, and on which date was each one measured?
  • Are we ranking by probability of response or by expected profit, and does it matter here?
  • For the recommendations: which product pairs are substitutes, and are we suppressing them?

The series is inspired by Mike Grigsby's Marketing Analytics.10 The explanations, examples and numbers here are my own.

Sources

Footnotes

  1. UCLA Office of Advanced Research Computing, FAQ: How do I interpret odds ratios in logistic regression?, OARC Stats, https://stats.oarc.ucla.edu/other/mult-pkg/faq/general/faq-how-do-i-interpret-odds-ratios-in-logistic-regression/

  2. Apple, Mail Privacy Protection and privacy, Apple Legal, https://www.apple.com/legal/privacy/data/en/mail-privacy-protection/

  3. Penn State Eberly College of Science, STAT 462 Applied Regression Analysis, 10.7 Detecting Multicollinearity Using Variance Inflation Factors, https://online.stat.psu.edu/stat462/node/180/

  4. scikit-learn developers, Probability calibration, scikit-learn user guide, 2026, https://scikit-learn.org/stable/modules/calibration.html

  5. Gary King and Langche Zeng, Logistic Regression in Rare Events Data, Political Analysis, volume 9, issue 2, 2001, https://gking.harvard.edu/files/0s.pdf

  6. Sebastian Raschka, association_rules: association rules generation from frequent itemsets, mlxtend user guide (citing Agrawal, Imielinski and Swami, SIGMOD 1993, for support and confidence, and Brin, Motwani, Ullman and Tsur for lift), https://rasbt.github.io/mlxtend/user_guide/frequent_patterns/association_rules/

  7. Jure Leskovec, Anand Rajaraman and Jeff Ullman, Mining of Massive Datasets, Frequent Itemset Mining and Association Rules (lecture slides), Stanford University, http://www.mmds.org/mmds/v2.1/ch06-assocrules.pdf

  8. statsmodels developers, statsmodels.discrete.discrete_model.Logit, statsmodels documentation, https://www.statsmodels.org/stable/generated/statsmodels.discrete.discrete_model.Logit.html

  9. pandas development team, pandas.qcut, pandas documentation, https://pandas.pydata.org/docs/reference/api/pandas.qcut.html

  10. Mike Grigsby, Marketing Analytics: A Practical Guide to Improving Consumer Insights Using Data Techniques, Kogan Page, 2022, ISBN 9781398608191, https://www.netgalley.co.uk/catalog/book/267990

If you have a customer file and a campaign coming up, or a shop full of baskets nobody has modelled, bring the data to the free workshop. We agree what to model, then I spend two weeks of real work on me building the decile table, the depth recommendation and the risk around it.