Home Marketing Predictions Article 3
Part 3 Generative AI

Click-Through-Rate (CTR) Prediction

Image: AI-generated with Beuys


Click-Through-Rate (CTR) Prediction

CTR prediction is the textbook example of a heavily imbalanced classification problem. Class weights, calibration and feature hashing for high-cardinality ad data – and why accuracy is misleading here and a calibration plot reveals more than any AUC.


1. The Problem: When 1 % Performance Decides Over Millions

In display advertising real click-through rates often lie between 0.5 % and 3 %. A "model" that always predicts "no click" reaches over 97 % accuracy – and is still useless. Three consequences follow:

  1. Accuracy is unsuitable as a metric. PR-AUC, log loss and calibration are the more honest measures.
  2. Calibrated probabilities are mandatory, not optional. Bidding systems multiply P(click) × value(click). A probability off by a factor of 2 directly burns budget.
  3. High-cardinality features (user-ID, ad-ID, domain) require special handling – OneHot encoding would create millions of columns.

2. Class Imbalance: What Actually Helps

❌ Naive solution: more data, same loss

More training data alone does not solve the problem. With 99 % negatives the model keeps learning primarily the negative pattern.

✅ Three effective levers

Lever Effect Cost
class_weight="balanced" Re-weights the loss function No extra data, slight distortion of probabilities
Under-sampling negatives Faster training, stronger positive signal Probabilities must be recalibrated
CalibratedClassifierCV Brings probabilities back into the real range One extra CV pass

Quick check: Why is accuracy misleading in CTR prediction?

3. Feature Hashing for High-Cardinality Categories

User-ID, ad-ID, publisher domain – each of these fields has potentially millions of distinct values. OneHotEncoder is not practical. Two established alternatives:

Quick check: Which effect of feature hashing is unavoidable but usually acceptable?

4. End-to-End: Imbalanced CTR with Calibration

The block below trains a LogisticRegression with class weighting and compares it with the calibrated variant. For diagnosis a reliability diagram is drawn: it plots the predicted probability against the actually observed frequency. A perfectly calibrated model lies on the diagonal.

import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LogisticRegression
from sklearn.calibration import CalibratedClassifierCV, calibration_curve
from sklearn.metrics import average_precision_score, log_loss
from sklearn.model_selection import train_test_split

rng = np.random.default_rng(1)
n = 10000
X = rng.normal(size=(n, 6))
# CTR ~ 2 % – heavily imbalanced
logit = X[:, 0] + 0.5 * X[:, 1] - 4.0
p_true = 1 / (1 + np.exp(-logit))
y = (rng.random(n) < p_true).astype(int)
print(f"Positive rate: {y.mean():.3f}")

X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.3, random_state=0, stratify=y)

# Variant A: LogReg with class_weight="balanced" (distorted probabilities)
base = LogisticRegression(class_weight="balanced", max_iter=1000).fit(X_tr, y_tr)
p_base = base.predict_proba(X_te)[:, 1]

# Variant B: same LogReg, but calibrated
cal = CalibratedClassifierCV(
    LogisticRegression(class_weight="balanced", max_iter=1000),
    method="isotonic", cv=5,
).fit(X_tr, y_tr)
p_cal = cal.predict_proba(X_te)[:, 1]

print(f"PR-AUC  base={average_precision_score(y_te, p_base):.3f}  cal={average_precision_score(y_te, p_cal):.3f}")
print(f"LogLoss base={log_loss(y_te, p_base):.4f}  cal={log_loss(y_te, p_cal):.4f}")

# Reliability diagram
frac_base, mean_base = calibration_curve(y_te, p_base, n_bins=10, strategy="quantile")
frac_cal,  mean_cal  = calibration_curve(y_te, p_cal,  n_bins=10, strategy="quantile")

fig, ax = plt.subplots(figsize=(6, 6))
ax.plot([0, 1], [0, 1], "k--", label="Perfectly calibrated")
ax.plot(mean_base, frac_base, "o-", label="LogReg balanced (uncalibrated)")
ax.plot(mean_cal,  frac_cal,  "s-", label="+ Isotonic calibration")
ax.set_xlabel("Predicted probability")
ax.set_ylabel("Observed frequency")
ax.set_title("Reliability diagram (CTR model)")
ax.legend()
ax.grid(alpha=0.3)
plt.tight_layout()
plt.show()

The uncalibrated variant with class_weight="balanced" often delivers the same ranking quality (PR-AUC) as the calibrated one, but deviates clearly from the diagonal in the reliability diagram. For pure sorting tasks (top-N ranking) that is enough. As soon as probabilities enter a downstream decision as numbers – real-time bidding, expected conversion, budget allocation – they must be calibrated.

Quick check: What is `CalibratedClassifierCV` for?

5. Offline vs. Online Evaluation

Offline metrics (PR-AUC, log loss, reliability) are necessary but not sufficient. Three extra steps are standard in production CTR systems:

6. Wrap-up Quiz

Which metric is best suited for evaluating a CTR model with ~1 % positive rate?

A reliability diagram shows that the model at predicted p=0.1 actually has only 0.03 click rate. What does that mean?

Why is a chronological (time-based) train/test split for CTR data often more honest than a random split?

7. Further Reading

Sources

📄 As PDF