Home Marketing Predictions Article 4
Part 4 Generative AI

A/B Testing & Uplift Modelling

Image: AI-generated with Beuys


A/B Testing & Uplift Modelling

Statistical tests answer "Is the difference real?", uplift models answer "On whom does the campaign actually work?". Two related but fundamentally different questions – with two different method families and a from-scratch two-model example including a Qini curve.


1. Two Different Questions, Two Different Tools

Anyone who only predicts conversions optimises the wrong quantity: a conversion model sends coupons to people who would have bought anyway. The clean separation looks like this:

Question Tool Output
Is the average effect of the campaign significant? A/B test, bootstrap CI A single number: average treatment effect (ATE)
For which individuals does the campaign work? Uplift modelling Individual estimate of the treatment effect (CATE)

Both methods share the same data type: a randomised sample with treatment indicator T ∈ {0,1} and outcome Y ∈ {0,1}.

2. A/B Tests: What Is Actually Significant

❌ Pitfall: Peeking

Repeatedly evaluating during the runtime inflates the false-positive rate. With 10 interim checks at α=5 % the probability of a false hit quickly rises above 30 %.

✅ Clean practice

Quick check: What does 'peeking' mean in A/B tests and why is it problematic?

3. Uplift Modelling: The Four Segments

Every individual can conceptually be assigned to one of four segments:

Segment Y(T=0) Y(T=1) Campaign strategy
Persuadables 0 1 Show – this is where ROI lives
Sure Things 1 1 Don't show – wasted budget
Lost Causes 0 0 Don't show – no chance
Do-Not-Disturb 1 0 Don't show – harmful!

The core problem: for each individual we observe only one of the two worlds (Y(T=0) or Y(T=1)), never both. Uplift models estimate the individual treatment effect from randomised data.

Quick check: Which group of people do we primarily want to target with uplift models?

4. Two-Model Approach: From Scratch with Qini Curve

The simplest uplift approach trains two separate models (one on the treatment group, one on the control group) and takes the difference of the probabilities as the uplift estimate. For evaluation the Qini curve is used: it plots the cumulative incremental conversion against the share of the ranked population.

import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split

rng = np.random.default_rng(2)
n = 6000
X = rng.normal(size=(n, 4))
T = rng.integers(0, 2, n)                                      # randomised treatment

# Heterogeneous treatment effect: effective only when X[:,1] > 0
p_ctrl = 1 / (1 + np.exp(-(0.5 * X[:, 0] - 1.0)))             # base rate ~ 25 %
uplift_true = 0.25 * (X[:, 1] > 0)                            # true CATE
p = np.clip(p_ctrl + T * uplift_true, 0, 1)
y = (rng.random(n) < p).astype(int)

X_tr, X_te, T_tr, T_te, y_tr, y_te = train_test_split(
    X, T, y, test_size=0.4, random_state=0,
)

# Two-model approach: separate models for T=0 and T=1
m_t = LogisticRegression(max_iter=1000).fit(X_tr[T_tr == 1], y_tr[T_tr == 1])
m_c = LogisticRegression(max_iter=1000).fit(X_tr[T_tr == 0], y_tr[T_tr == 0])
uplift_hat = m_t.predict_proba(X_te)[:, 1] - m_c.predict_proba(X_te)[:, 1]

print(f"Estimated mean uplift: {uplift_hat.mean():+.3f}")
print(f"True mean uplift in test: {(0.25 * (X_te[:, 1] > 0)).mean():+.3f}")

# Qini curve: sort population by uplift_hat (descending),
# plot cumulative incremental conversions.
order = np.argsort(-uplift_hat)
y_sorted = y_te[order]
T_sorted = T_te[order]

# Cumulative conversions in treatment and control, scaled to the same size
cum_t = np.cumsum(y_sorted * (T_sorted == 1))
cum_c = np.cumsum(y_sorted * (T_sorted == 0))
n_t = np.cumsum(T_sorted == 1).clip(min=1)
n_c = np.cumsum(T_sorted == 0).clip(min=1)
# Increment = T rate − C rate, scaled to the population
incremental = cum_t - cum_c * (n_t / n_c)
share = np.arange(1, len(y_te) + 1) / len(y_te)

# Random baseline (straight line to the endpoint)
random_line = share * incremental[-1]

fig, ax = plt.subplots(figsize=(7, 5))
ax.plot(share, incremental, label="Two-model uplift")
ax.plot(share, random_line, "k--", label="Random targeting")
ax.set_xlabel("Share of addressed population (sorted by uplift score)")
ax.set_ylabel("Cumulative incremental conversions")
ax.set_title("Qini curve")
ax.legend()
ax.grid(alpha=0.3)
plt.tight_layout()
plt.show()

# Qini score: area between the model curve and the random line
qini_score = np.trapz(incremental - random_line, share)
print(f"Qini score (higher = better): {qini_score:.3f}")

For a useful uplift model the Qini curve lies above the random line. The larger the area between the curves (Qini score), the better the model separates Persuadables from the other segments. The bend at the end is normal: once you also address the bottom 30–40 % of the population, the incremental effect drops – that is where Sure Things, Lost Causes and Do-Not-Disturb live.

Quick check: What does the Qini curve show?

5. Discussion: When Two-Model and When Something Better?

Two-model is the simplest but not the most efficient method:

Practical heuristic: start your first uplift project with two-model, validate the Qini curve against random. Only invest in more complex estimators once ROI justifies it.

6. Wrap-up Quiz

How does uplift modelling differ from ordinary response prediction?

Which requirement must the training data satisfy for uplift models?

Which segment is *harmful* for the campaign if it is addressed?

7. Further Reading

Sources

📄 As PDF