Передбачення CTR та CVR у рекламних системах
Ви запускаєте рекламну кампанію на мільйон показів на день. Передбачення CTR занижене на 15% — ви програєте топові місця в аукціоні. Завищене на 20% — зливаєте бюджет на нерелевантні кліки. Ми вирішуємо це завдання за допомогою кастомних ML-моделей під ключ. Наша AI-система передбачення CTR/CVR аналізує аудиторію, контекст та історичні дані, щоб оптимізувати ставки в реальному часі. Помилка в CTR-передбаченні на 20% може коштувати до 15-25% рекламного бюджету — це гроші, які йдуть на неефективні покази.
CTR та CVR — фундаментальні сигнали для ціноутворення в programmatic рекламі. Помилка в CTR-передбаченні на 20% безпосередньо транслюється в переплату або недовиграш аукціонів. У масштабі сотень мільйонів показів на день навіть покращення AUC з 0.76 до 0.78 означає мільйони зекономленого або заробленого бюджету. За нашими даними, LightGBM з калібруванням перевершує логістичну регресію за AUC на 0.03-0.05 — це пряме порівняння ефективності.
Як ми будуємо CTR-моделі під ключ
CTR-передбачення — бінарна класифікація з трьома ключовими складностями: крайній дисбаланс класів (CTR 0.1-2%), величезний об'єм (мільярди прикладів на день), приховані конверсії (CVR спостерігається тільки для тих, хто клікнув, що створює selection bias). Ми використовуємо LightGBM з налаштуванням scale_pos_weight та подальшим калібруванням. Калібрований LightGBM дає Log Loss у 1.3 рази нижче, ніж сирі ймовірності.
import numpy as np
import pandas as pd
import lightgbm as lgb
from sklearn.calibration import CalibratedClassifierCV
from sklearn.metrics import roc_auc_score, log_loss
class CTRFeatureEngineer:
"""Ðзнаки Ð´Ð»Ñ CTR-Ð¼Ð¾Ð´ÐµÐ»Ñ Ð² display advertising"""
def build_features(self, bid_logs: pd.DataFrame) -> pd.DataFrame:
"""
bid_logs: ÑÑÑоÑиÑÐ½Ñ Ð»Ð¾Ð³Ð¸ показÑв з пÑапоÑами clicked/converted
"""
df = bid_logs.copy()
# === СÑаÑиÑÑиÑÐ½Ñ Ð¾Ð·Ð½Ð°ÐºÐ¸ коÑиÑÑÑваÑа ===
user_stats = df.groupby('user_id').agg(
user_historical_ctr=('clicked', 'mean'),
user_impression_count=('clicked', 'count'),
user_conversion_rate=('converted', 'mean'),
).reset_index()
# === СÑаÑиÑÑиÑÐ½Ñ Ð¾Ð·Ð½Ð°ÐºÐ¸ плоÑадки ===
site_stats = df.groupby('site_domain').agg(
site_ctr=('clicked', 'mean'),
site_conversion_rate=('converted', 'mean'),
site_volume=('clicked', 'count'),
).reset_index()
# === Ðзнаки пеÑеÑÐ¸Ð½Ñ (user à ad) ===
df['hour'] = pd.to_datetime(df['timestamp']).dt.hour
df['is_weekend'] = pd.to_datetime(df['timestamp']).dt.dayofweek >= 5
df['is_prime_time'] = df['hour'].between(18, 22)
# Cross-ознаки: важливÑÑÑ Ð·Ð° одиноÑнÑ
df['ad_position_encoded'] = df['ad_position'].map({'atf': 1, 'btf': 0}).fillna(0.5)
df = df.merge(user_stats, on='user_id', how='left')
df = df.merge(site_stats, on='site_domain', how='left')
# Smoothed CTR Ð´Ð»Ñ Ð±Ð¾ÑоÑÑби з ÑозÑÑдженÑÑÑÑ (Wilson smoothing)
alpha = 100 # Prior strength
global_ctr = df['clicked'].mean()
df['user_smooth_ctr'] = (
df['user_historical_ctr'].fillna(global_ctr) * df['user_impression_count'].fillna(0) + global_ctr * alpha
) / (df['user_impression_count'].fillna(0) + alpha)
feature_cols = [
'user_smooth_ctr',
'user_impression_count',
'site_ctr',
'site_volume',
'hour',
'is_weekend',
'is_prime_time',
'ad_position_encoded',
'banner_width',
'banner_height',
'floor_price',
]
return df[feature_cols].fillna(0)
class CTRModel:
"""LightGBM Ð´Ð»Ñ CTR з пÑавилÑним калÑбÑÑваннÑм"""
def __init__(self):
self.model = lgb.LGBMClassifier(
n_estimators=1000,
learning_rate=0.03,
num_leaves=255,
min_child_samples=200,
subsample=0.8,
colsample_bytree=0.7,
scale_pos_weight=50, # ÐоÑекÑÑÑ Ð´Ð¸ÑбаланÑÑ: 1 клÑк на 50 показÑв
random_state=42,
n_jobs=-1,
)
self.calibrator = None
self._is_calibrated = False
def train(self, X_train: np.ndarray, y_train: np.ndarray, X_val: np.ndarray, y_val: np.ndarray):
"""ÐавÑÐ°Ð½Ð½Ñ Ð· ÑаннÑÐ¾Ñ Ð·ÑпинкоÑ"""
self.model.fit(
X_train,
y_train,
eval_set=[(X_val, y_val)],
eval_metric='auc',
callbacks=[
lgb.early_stopping(100, verbose=False),
lgb.log_evaluation(200)
]
)
# ÐалÑбÑÑÐ²Ð°Ð½Ð½Ñ â ÐÐÐÐ'ЯÐÐÐÐÐ Ð´Ð»Ñ Ð²Ð¸ÐºÐ¾ÑиÑÑÐ°Ð½Ð½Ñ Ð² bid price calculation
# СиÑий LightGBM Ð´Ð°Ñ Ñ
оÑоÑий ranking, але Ð¿Ð¾Ð³Ð°Ð½Ñ Ð¹Ð¼Ð¾Ð²ÑÑноÑÑÑ
self.calibrator = CalibratedClassifierCV(self.model, cv='prefit', method='isotonic')
self.calibrator.fit(X_val, y_val)
self._is_calibrated = True
def predict_ctr(self, X: np.ndarray) -> np.ndarray:
"""ÐалÑбÑÐ¾Ð²Ð°Ð½Ñ Ð¹Ð¼Ð¾Ð²ÑÑноÑÑÑ ÐºÐ»ÑкÑв"""
if self._is_calibrated:
return self.calibrator.predict_proba(X)[:, 1]
return self.model.predict_proba(X)[:, 1]
def evaluate(self, X_test: np.ndarray, y_test: np.ndarray) -> dict:
raw_probs = self.model.predict_proba(X_test)[:, 1]
cal_probs = self.predict_ctr(X_test) if self._is_calibrated else raw_probs
return {
'auc_raw': round(roc_auc_score(y_test, raw_probs), 4),
'auc_calibrated': round(roc_auc_score(y_test, cal_probs), 4),
'logloss_raw': round(log_loss(y_test, raw_probs), 4),
'logloss_calibrated': round(log_loss(y_test, cal_probs), 4),
'mean_predicted_ctr': round(float(cal_probs.mean()), 5),
'actual_ctr': round(float(y_test.mean()), 5),
}
class DelayedConversionCorrector:
"""
ÐоÑекÑÑÑ delayed conversions Ñ CVR-моделÑ.
ÐонвеÑÑÑÑ Ð¼Ð¾Ð¶ÑÑÑ Ð²ÑдбÑваÑиÑÑ ÑеÑез години/Ð´Ð½Ñ Ð¿ÑÑÐ»Ñ ÐºÐ»ÑкÑ.
ÐбÑÑзка навÑалÑÐ½Ð¾Ñ Ð²Ð¸Ð±ÑÑки за ÑаÑом ÑÑвоÑÑÑ Ð·Ð¼ÑÑеннÑ.
"""
def adjust_for_delayed_conversions(self, clicks: pd.DataFrame, observation_window_hours: int = 24) -> pd.DataFrame:
"""
ÐÑдкидаÑмо неÑÐ¾Ð´Ð°Ð²Ð½Ñ ÐºÐ»Ñки, Ñ ÑкиÑ
Ñе не минÑв window конвеÑÑÑÑ.
ÐнакÑе CVR бÑде занижений Ð´Ð»Ñ Ð¾ÑÑаннÑÑ
пÑикладÑв.
"""
cutoff = pd.Timestamp.now() - pd.Timedelta(hours=observation_window_hours)
return clicks[clicks['click_time'] < cutoff]
def estimate_conversion_delay_distribution(self, conversions: pd.DataFrame) -> dict:
"""РозподÑл заÑÑимок конвеÑÑÑй"""
delays = (conversions['conversion_time'] - conversions['click_time']).dt.total_seconds() / 3600
return {
'p50_hours': round(float(delays.quantile(0.50)), 1),
'p90_hours': round(float(delays.quantile(0.90)), 1),
'p99_hours': round(float(delays.quantile(0.99)), 1),
'recommended_window': f"{int(delays.quantile(0.95))} hours",
}"} Як оцінити якість моделі CTR?
Основні метрики: AUC-ROC (>0.75 хороше значення), Log Loss (<0.10) та Calibration Error (<0.005). У продакшені додатково дивляться на ΔAUC від нових фіч — приріст >0.001 виправдовує інжиніринг. Калібрований LightGBM дає Log Loss у 1.3 рази нижче, ніж сирі ймовірності.
Чому калібрування ймовірностей критичне?
Некалібрована модель може систематично переплачувати на аукціонах. Сирий LightGBM дає відмінні ранжування, але погані ймовірності. Ми використовуємо ізотонічну регресію, яка знижує Calibration Error до <0.005. Це критично для bid price calculation: якщо модель передбачає CTR 0.5%, а реальний — 0.8%, ви переплачуєте за кожен клік. Точне калібрування безпосередньо економить бюджет — економія від впровадження каліброваної LightGBM моделі становить до 30% витрат на рекламу.
Щотижневе оновлення моделі
Рекламний ландшафт змінюється швидко: нові креативи, сезонність, зміна поведінки аудиторії. Ми рекомендуємо щотижневе донавчання. Для цього налаштовуємо пайплайн автоматичного ретрейнінгу з моніторингом дрейфу ознак. У періоди високої волатильності (наприклад, чорна п'ятниця) може знадобитися daily-оновлення, але зазвичай це надмірно та вносить шум.
Delayed conversions: проблема та рішення
У CVR-моделях конверсії можуть відбуватися через години або дні після кліку. Якщо просто обрізати навчальну вибірку за часом, останні спостереження матимуть занижений CVR. Ми використовуємо корекцію, відкидаючи кліки з не минулим вікном конверсії (зазвичай 24-72 години залежно від розподілу затримок). Це покращує калібрування CVR на 10-20%.
Типовий розподіл затримок конверсій для e-commerce
| Перцентиль | Затримка (години) |
|---|---|
| p50 | 2.5 |
| p90 | 18.0 |
| p99 | 72.0 |
| Рекомендоване вікно | 95 годин |
Метрики якості CTR/CVR моделей
| Метрика | Хороше значення | Призначення |
|---|---|---|
| AUC-ROC | > 0.75 для CTR | Ранжувальна здатність |
| Log Loss | < 0.10 | Якість ймовірностей |
| Calibration Error | < 0.005 | Точність CTR-оцінок |
| NDCG@1000 | > 0.85 | Топ-аукціони |
| Delta AUC від нової фічі | > 0.001 | Окупність інжинірингу |
Для CTR-моделі AUC 0.76 vs 0.74 — значуща різниця в масштабі. Калібрування обов'язкове: некалібрована модель може систематично переплачувати.
Процес впровадження AI-передбачення CTR/CVR
Ми працюємо по етапах:
- Аудит даних — збираємо логи показів, перевіряємо якість та повноту. Оцінюємо приріст від ML.
- Feature Engineering — будуємо ознаки: smoothed CTR, site stats, часові патерни. Використовуємо Wilson smoothing для боротьби з розрідженістю.
- Навчання та калібрування — LightGBM + isotonic calibration. Перевіряємо на відкладеній тестовій вибірці.
- A/B тестування — у продакшені порівнюємо з baseline за revenue.
- Документація та деплой — експортуємо модель в ONNX або Triton, налаштовуємо моніторинг дрейфу.
Що входить у розробку моделі
Деліверабли:
- Звіт за аудитом даних та потенційним приростом
- Python-пакет з inference-пайплайном
- Документація API для bidder
- Навчання команди замовника
- Підтримка 3 місяці після деплою
Типові помилки в MVP CTR/CVR
- Ігнорування delayed conversions — CVR занижена на 10-20%.
- Відсутність валідації: одна крос-валідація, а не три часових відкладених.
- Занадто часті оновлення (daily) — шум від випадкових флуктуацій.
- Використання лише лінійних моделей — пропускають нелінійні перетини.
Замовте аудит ваших рекламних даних — ми визначимо потенційний приріст CTR. Отримайте консультацію: оцінимо ваші дані та скажемо, який приріст CTR можливий. Наш досвід: понад 20 проєктів для programmatic-платформ, 5+ років у ML-оптимізації реклами. Звертайтеся за консультацією щодо ваших даних.







