Programmatic Advertising AI: RTB, прогнозы и оптимизация бюджета

AI-система Programmatic Advertising: как выжать максимум из RTB-аукционов На одном из проектов DSP тратила 70% бюджета в первые 4 часа, после чего кампания проседала. Мы внедрили budget pacing и модели CTR, что позволило равномерно распределить расходы и увеличить конверсии на 25%. Главная боль к

Направления AI-разработки

Часто задаваемые вопросы

Последние работы

  • image_website-b2b-advance_0.webp
    Разработка сайта компании B2B ADVANCE
    1440
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1301
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    997
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1264
  • image_logo-advance_0.webp
    Разработка логотипа компании B2B Advance
    712
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    1002

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.

Процесс работы

  1. Аналитика: разбираем вашу текущую DSP/SSP, логи аукционов, метрики.
  2. Проектирование: выбираем архитектуру (количество моделей, фичи, budget pacing).
  3. Обучение: тренируем CTR/CVR модели на ваших исторических данных. A/B тестирование на live-трафике.
  4. Деплой: разворачиваем inference-сервис на Kubernetes с auto-scaling по QPS.
  5. Мониторинг: настраиваем дашборды (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,
        }