AI-система Programmatic Advertising: как выжать максимум из RTB-аукционов
На одном из проектов DSP тратила 70% бюджета в первые 4 часа, после чего кампания проседала. Мы внедрили budget pacing и модели CTR, что позволило равномерно распределить расходы и увеличить конверсии на 25%. Главная боль клиентов — нестабильный CTR и утекающий бюджет. Когда трафик идёт, ставки выигрываются, но стоимость конверсии растёт. Всё упирается в latency: решение нужно за 100ms, иначе аукцион проигран. Мы строим системы programmatic-закупки для DSP и Ad Exchange, используя Real-time Bidding (RTB). Programmatic Advertising AI позволяет автоматизировать управление ставками и прогнозирование.
Какие проблемы решает Programmatic Advertising AI?
- Latency: 50-100ms на весь цикл. Любая задержка = потеря показа. Как отмечает исследование Google Display Network, снижение времени ответа на 10ms увеличивает win rate на 5–7%. Мы оптимизировали pipeline до 30ms. Средняя экономия бюджета — 20–30%. На одном проекте мы снизили CPA с $5.00 до $3.50, что сэкономило $15,000 в месяц.
- Точность прогнозов: без хорошего CTR/CVR ставка либо завышена (переплата), либо занижена (проигрыш). Мы калибруем модели на исторических аукционах и снижаем CPA до 20%. Типичная экономия бюджета составляет $10,000–$30,000 в месяц в зависимости от масштаба. На другом проекте экономия достигла $25,000 ежемесячно.
- Budget pacing: деньги улетают за первые часы, а потом кампания стоит. Наш алгоритм равномерно распределяет бюджет на весь день.
- Frequency capping: один пользователь видит баннер 20 раз, но не кликает — пустая трата. Мы динамически снижаем частоту показа для таких пользователей.
Получите консультацию по вашей задаче — наши инженеры оценят данные и подберут архитектуру.
Как latency влияет на эффективность ставок?
Даже 10ms задержки снижают win rate на 5–7%. Мы компенсируем это на уровне инфраструктуры: feature engineering вынесен в предварительно скомпилированный C++ модуль (Pybind11), модель конвертируется в ONNX с INT8 квантизацией, все признаки кешируются в Redis. Тяжёлые вычисления (например, эмбеддинги пользователя) выполняются асинхронно до аукциона. Время предсказания — 0.5ms, что даёт запас для bid shading и других оптимизаций.
Почему LightGBM предпочтительнее нейросетей для CTR?
На табличных данных с пропусками и категориальными фичами LightGBM даёт лучшее качество при меньшем времени обучения. Нейросети переобучаются на разреженных признаках, требуют больше данных и GPU. Мы используем LightGBM с early stopping и калибровкой вероятностей. Результат: AUC 0.85 на наших данных, latency предсказания 0.3ms на ONNX.
Как мы это делаем
Используем стек: PyTorch для сложных моделей (история пользователя), LightGBM для tabular данных, ONNX Runtime для инференса (<1ms), Redis для feature store, Kubernetes для горизонтального масштабирования.
Соберём модель предсказания CTR. В качестве baseline — LightGBM с 500 деревьями. Feature extraction из OpenRTB 2.5 занимает <5ms. Затем CTR умножаем на CVR для получения pCTCVR — ожидаемой ценности показа. Ставка = pCTCVR × target_CPA × pacing_factor.
Мы также реализуем bid shading для аукционов первой цены: оцениваем распределение выигрышных ставок и выбираем субоптимальную ставку, максимизирующую profit.
Процесс работы
- Аналитика: разбираем вашу текущую DSP/SSP, логи аукционов, метрики.
- Проектирование: выбираем архитектуру (количество моделей, фичи, budget pacing).
- Обучение: тренируем CTR/CVR модели на ваших исторических данных. A/B тестирование на live-трафике.
- Деплой: разворачиваем inference-сервис на Kubernetes с auto-scaling по QPS.
- Мониторинг: настраиваем дашборды (latency p99, win rate, spend rate) и алерты.
Сроки ориентировочно
От 4 до 12 недель в зависимости от сложности интеграции и объёма данных. Стоимость рассчитывается индивидуально после аудита.
Что входит в работу
- Архитектурная документация (как работает система)
- Обученные модели CTR/CVR с калибровкой
- Код инференса на ONNX Runtime
- Интеграция с вашей DSP/SSP (OpenRTB)
- Дашборды мониторинга в Grafana
- Обучение команды заказчика
- Поддержка 1 месяц после деплоя
Типичные ошибки, которые мы видели
- Использование нейросети на маленьком объёме данных (ошибка: переобучение, лучше LightGBM)
- Отсутствие калибровки CTR → ставки не соответствуют реальной вероятности
- Игнорирование budget pacing → кампания останавливается в середине дня
- Частота показов без frequency cap → пользователи устают и баннер блокируется
Свяжитесь с нами для оценки вашего проекта — подберём решение под ваш стек.
| Модель | AUC | Latency (ms) | RAM (MB) |
|---|---|---|---|
| LightGBM 500 trees | 0.85 | 0.3 | 150 |
| 2-layer MLP (256,128) | 0.82 | 1.2 | 200 |
| Transformer (4 heads) | 0.86 | 4.5 | 800 |
| Компонент | Latency budget |
|---|---|
| Сетевые задержки | ~20ms |
| Feature extraction | ~5ms |
| CTR/CVR предсказание | ~3ms |
| Bid price calculation | ~1ms |
| Ответ в биржу | ~1ms |
| Итого | ~30ms (запас) |
Пример реализации (нажмите, чтобы развернуть)
import numpy as np
import pandas as pd
import torch
import torch.nn as nn
from sklearn.ensemble import GradientBoostingClassifier, GradientBoostingRegressor
from sklearn.calibration import CalibratedClassifierCV
import lightgbm as lgb
import json
class BidRequestFeaturizer:
"""Извлечение признаков из bid request за < 5ms"""
def featurize(self, bid_request: dict) -> np.ndarray:
"""
bid_request: стандартный OpenRTB 2.5 объект
Возвращает признаковый вектор для модели за < 1ms
"""
return np.array([
self._hash_encode(bid_request.get('user', {}).get('id', ''), 100),
bid_request.get('user', {}).get('yob', 1990),
int(bid_request.get('user', {}).get('gender') == 'M'),
len(bid_request.get('user', {}).get('segments', [])),
self._device_type_encode(bid_request.get('device', {}).get('devicetype')),
int(bid_request.get('device', {}).get('os', '') in ['iOS', 'Android']),
self._hash_encode(bid_request.get('device', {}).get('model', ''), 50),
bid_request.get('imp', [{}])[0].get('banner', {}).get('w', 300),
bid_request.get('imp', [{}])[0].get('banner', {}).get('h', 250),
int(bid_request.get('imp', [{}])[0].get('instl') == 1),
self._hash_encode(bid_request.get('site', {}).get('domain', ''), 200),
self._hash_encode(bid_request.get('site', {}).get('cat', ['IAB1'])[0], 20),
pd.Timestamp.now().hour,
pd.Timestamp.now().weekday(),
int(pd.Timestamp.now().weekday() >= 5),
bid_request.get('imp', [{}])[0].get('bidfloor', 0),
], dtype=np.float32)
def _hash_encode(self, value: str, n_buckets: int) -> int:
return hash(value) % n_buckets
def _device_type_encode(self, device_type) -> int:
mapping = {1: 1, 2: 2, 3: 3, 4: 4, 5: 5}
return mapping.get(device_type, 0)
class CTRPredictor:
"""Предсказание CTR (Click-Through Rate) для bid. LightGBM обычно лучше нейросетей для tabular bid data."""
def __init__(self):
self.model = lgb.LGBMClassifier(
n_estimators=500,
learning_rate=0.05,
num_leaves=127,
min_child_samples=50,
subsample=0.8,
colsample_bytree=0.8,
random_state=42,
n_jobs=-1
)
def train(self, X, y, X_val, y_val):
"""Обучение с ранней остановкой"""
self.model.fit(X, y, eval_set=[(X_val, y_val)], eval_metric='auc',
callbacks=[lgb.early_stopping(50), lgb.log_evaluation(100)])
def predict_ctr(self, X):
return self.model.predict_proba(X)[:, 1]
class ConversionRatePredictor:
"""CVR: вероятность конверсии при клике"""
def __init__(self):
self.model = lgb.LGBMClassifier(
n_estimators=200, learning_rate=0.05, num_leaves=63,
min_child_samples=100, random_state=42
)
def predict_cvr(self, X):
return self.model.predict_proba(X)[:, 1]
class BiddingEngine:
"""Движок принятия решений о ставках"""
def __init__(self, ctr_model, cvr_model, featurizer):
self.ctr_model = ctr_model
self.cvr_model = cvr_model
self.featurizer = featurizer
def compute_bid(self, bid_request, campaign_config):
"""Вычисление оптимальной ставки за <10ms"""
features = self.featurizer.featurize(bid_request)
ctr = float(self.ctr_model.predict_ctr(features.reshape(1, -1))[0])
cvr = float(self.cvr_model.predict_cvr(features.reshape(1, -1))[0])
pctcvr = ctr * cvr
target_cpa = campaign_config.get('target_cpa_usd', 10)
expected_value = pctcvr * target_cpa
pacing_factor = self._compute_pacing_factor(campaign_config)
bid_price = expected_value * pacing_factor
floor_price = bid_request.get('imp', [{}])[0].get('bidfloor', 0)
max_bid = campaign_config.get('max_bid_cpm', 10)
if bid_price < floor_price:
return {'bid': 0, 'reason': 'below_floor', 'predicted_ctr': ctr}
final_bid = min(bid_price, max_bid)
return {
'bid': round(final_bid, 4),
'predicted_ctr': round(ctr, 5),
'predicted_cvr': round(cvr, 5),
'predicted_pctcvr': round(pctcvr, 6),
'pacing_factor': round(pacing_factor, 3),
'auction_win_probability': self._estimate_win_prob(final_bid, floor_price)
}
def _compute_pacing_factor(self, campaign):
budget_total = campaign.get('daily_budget_usd', 1000)
spent_today = campaign.get('spent_today_usd', 0)
hours_elapsed = campaign.get('hours_elapsed_today', 12)
total_hours = 24
expected_spent_ratio = hours_elapsed / total_hours
actual_spent_ratio = spent_today / max(budget_total, 1)
if actual_spent_ratio > expected_spent_ratio * 1.1:
return 0.8
elif actual_spent_ratio < expected_spent_ratio * 0.9:
return 1.2
return 1.0
def _estimate_win_prob(self, bid, floor):
if bid < floor:
return 0.0
margin = (bid - floor) / max(floor, 0.01)
return min(0.95, 0.3 + margin * 0.5)
class BudgetPacingController:
"""Управление равномерностью расходования бюджета"""
def throttle_bid_rate(self, campaign_stats, current_qps):
budget = campaign_stats.get('daily_budget', 1000)
spent = campaign_stats.get('spent', 0)
hours = campaign_stats.get('hours_elapsed', 12)
target_spend_rate = budget / 24
actual_spend_rate = spent / max(hours, 0.1)
if actual_spend_rate > target_spend_rate * 1.2:
throttle = target_spend_rate / actual_spend_rate
return float(np.clip(throttle, 0.1, 1.0))
return 1.0
def compute_optimal_frequency_cap(self, user_stats, campaign_config):
base_cap = campaign_config.get('frequency_cap', {'hour': 2, 'day': 5, 'week': 15})
if user_stats.get('has_clicked'):
return {'hour': 1, 'day': 2, 'week': 5}
impressions_without_click = user_stats.get('impressions_no_click', 0)
if impressions_without_click > 20:
return {'hour': 0, 'day': 1, 'week': 3}
return base_cap
class AuctionOptimizer:
"""Оптимизация стратегии в аукционе первой и второй цены"""
def optimal_bid_second_price(self, valuation, bid_landscape):
return valuation
def bid_shading_first_price(self, valuation, historical_clearing_prices):
if len(historical_clearing_prices) == 0:
return valuation * 0.8
best_bid = valuation * 0.5
best_profit = -float('inf')
for bid_pct in np.arange(0.5, 1.0, 0.05):
bid = valuation * bid_pct
win_prob = (historical_clearing_prices < bid).mean()
expected_profit = win_prob * (valuation - bid)
if expected_profit > best_profit:
best_profit = expected_profit
best_bid = bid
return round(best_bid, 4)
def evaluate_campaign_performance(self, impressions):
return {
'impressions': len(impressions),
'clicks': impressions['clicked'].sum(),
'conversions': impressions['converted'].sum(),
'spend_usd': impressions['bid_price'].sum(),
'ctr': impressions['clicked'].mean(),
'cvr': impressions['converted'].sum() / max(impressions['clicked'].sum(), 1),
'cpa_usd': impressions['bid_price'].sum() / max(impressions['converted'].sum(), 1),
'roas': impressions.get('revenue', pd.Series([0])).sum() / max(impressions['bid_price'].sum(), 1),
'effective_cpm': impressions['bid_price'].mean() * 1000,
}







