Собесов

Vizor Games — когортный отчёт по маркетинговым метрикам (CPI, ROAS, C2B)

SQLCohort analysis / MarketingСложнаяSenior

Условие

Даны два файла:

  • cost_data.csv — данные из рекламного кабинета: install_date, media_source, platform, ad_account_id, campaign_id, campaign_name, spend, impressions, clicks.
  • mmp_data.csv — данные MMP (атрибуция): install_time, event_time, event_name, media_source, campaign_name, campaign_id, country_code, user_id, platform, event_revenue_currency, event_revenue, event_revenue_usd, cohort_day (день жизни пользователя).

Построить когортный табличный отчёт для маркетинговых решений. Метрики:

  • Cost, Installs, Buyers, CPI, CPM, CTR, IPM, Total Revenue.
  • C2B_0,1,3,5 — конверсия в платящего на 0/1/3/5 день жизни когорты.
  • CPB_0,1,3,5 — cost per buyer на этих днях.
  • ROAS_0,1,3,5 — окупаемость затрат на эти дни.

В строках — когорты, в столбцах — метрики. Фильтры: platform, install_date, гранулярность когорт (день/неделя). Неделя начинается с пятницы.

Решение

Подход

Когорта = группа установок одного дня (или недели, начинающейся с пятницы).

Реализация (PostgreSQL / DWH-like)

WITH params AS (
  SELECT DATE '2024-01-01' AS d_from, DATE '2024-12-31' AS d_to,
         'week' AS gran  -- 'day' | 'week'
),
 
-- 1) Когортный ключ
cohort_key AS (
  SELECT install_date,
         CASE WHEN (SELECT gran FROM params) = 'day'
              THEN install_date
              -- неделя с пятницы: смещение PG: extract(dow) — 0=Sunday, 5=Friday
              ELSE install_date - ((EXTRACT(DOW FROM install_date)::int - 5 + 7) % 7)
         END AS cohort_dt
  FROM (SELECT DISTINCT install_date FROM cost_data
        UNION
        SELECT DISTINCT DATE(install_time) FROM mmp_data) d
),
 
-- 2) Cost-агрегаты по когорте
cost_agg AS (
  SELECT ck.cohort_dt,
         c.platform,
         SUM(c.spend)        AS cost,
         SUM(c.impressions)  AS impressions,
         SUM(c.clicks)        AS clicks
  FROM cost_data c
  JOIN cohort_key ck ON ck.install_date = c.install_date
  GROUP BY ck.cohort_dt, c.platform
),
 
-- 3) Installs из MMP (по install_time)
installs_agg AS (
  SELECT ck.cohort_dt, m.platform,
         COUNT(DISTINCT m.user_id) AS installs
  FROM mmp_data m
  JOIN cohort_key ck ON ck.install_date = DATE(m.install_time)
  WHERE m.event_name = 'install'        -- если такая семантика
  GROUP BY ck.cohort_dt, m.platform
),
 
-- 4) Платящие на день D (cohort_day <= D)
paying AS (
  SELECT ck.cohort_dt, m.platform, m.user_id,
         MIN(m.cohort_day) AS first_pay_day,
         SUM(m.event_revenue_usd) FILTER (WHERE m.cohort_day = 0) AS rev_d0,
         SUM(m.event_revenue_usd) FILTER (WHERE m.cohort_day <= 1) AS rev_d1,
         SUM(m.event_revenue_usd) FILTER (WHERE m.cohort_day <= 3) AS rev_d3,
         SUM(m.event_revenue_usd) FILTER (WHERE m.cohort_day <= 5) AS rev_d5
  FROM mmp_data m
  JOIN cohort_key ck ON ck.install_date = DATE(m.install_time)
  WHERE m.event_name = 'purchase'
  GROUP BY ck.cohort_dt, m.platform, m.user_id
),
 
cohort_pay AS (
  SELECT cohort_dt, platform,
         COUNT(*)                                                   AS buyers,
         SUM(CASE WHEN first_pay_day = 0 THEN 1 ELSE 0 END)          AS b0,
         SUM(CASE WHEN first_pay_day <= 1 THEN 1 ELSE 0 END)         AS b1,
         SUM(CASE WHEN first_pay_day <= 3 THEN 1 ELSE 0 END)         AS b3,
         SUM(CASE WHEN first_pay_day <= 5 THEN 1 ELSE 0 END)         AS b5,
         SUM(rev_d0) AS rev_d0,
         SUM(rev_d1) AS rev_d1,
         SUM(rev_d3) AS rev_d3,
         SUM(rev_d5) AS rev_d5,
         SUM(rev_d0 + COALESCE(rev_d1,0) + COALESCE(rev_d3,0) + COALESCE(rev_d5,0)) AS total_rev_proxy
  FROM paying
  GROUP BY cohort_dt, platform
)
 
-- 5) Финальный вывод
SELECT c.cohort_dt, c.platform,
       c.cost,
       i.installs,
       p.buyers,
       c.cost  / NULLIF(i.installs, 0)                  AS CPI,
       c.cost  * 1000.0 / NULLIF(c.impressions, 0)      AS CPM,
       c.clicks * 1.0   / NULLIF(c.impressions, 0)      AS CTR,
       i.installs * 1000.0 / NULLIF(c.impressions, 0)   AS IPM,
 
       p.b0   * 1.0 / NULLIF(i.installs, 0)             AS C2B_0,
       p.b1   * 1.0 / NULLIF(i.installs, 0)             AS C2B_1,
       p.b3   * 1.0 / NULLIF(i.installs, 0)             AS C2B_3,
       p.b5   * 1.0 / NULLIF(i.installs, 0)             AS C2B_5,
 
       c.cost / NULLIF(p.b0, 0)                         AS CPB_0,
       c.cost / NULLIF(p.b1, 0)                         AS CPB_1,
       c.cost / NULLIF(p.b3, 0)                         AS CPB_3,
       c.cost / NULLIF(p.b5, 0)                         AS CPB_5,
 
       p.rev_d0 / NULLIF(c.cost, 0)                     AS ROAS_0,
       p.rev_d1 / NULLIF(c.cost, 0)                     AS ROAS_1,
       p.rev_d3 / NULLIF(c.cost, 0)                     AS ROAS_3,
       p.rev_d5 / NULLIF(c.cost, 0)                     AS ROAS_5
FROM cost_agg c
LEFT JOIN installs_agg  i ON c.cohort_dt = i.cohort_dt AND c.platform = i.platform
LEFT JOIN cohort_pay    p ON c.cohort_dt = p.cohort_dt AND c.platform = p.platform
ORDER BY c.cohort_dt;

Дополнения

  • Можно добавить LTV_predicted (через расширение Power Curve / Weibull на основе ранних дней).
  • Метрика Avg time-to-buy для понимания, насколько быстро монетизируется.
  • Country-разрез: добавить country_code в груп.

BI

В Tableau/Power BI/Metabase:

  • Параметризировать гранулярность как dropdown.
  • Параметризировать period.
  • Сводная таблица.
  • Можно дать сравнение когорт по неделям (heat-map).

Подводные камни

  1. Атрибуционный лаг: MMP-installs приходят с лагом до 7 дней (особенно incremental). Сегодняшнее Installs = X может быть неполным.
  2. event_revenue_usd vs event_revenue в local: используйте USD для consistency. Floor — currency conversion может быть null.
  3. Cost source: рекламный кабинет даёт total spend, но без user-level. Cost per cohort = spend дня. Важно: для cross-channel campaign user может быть атрибутирован разным install_date.
  4. Неделя с пятницы: EXTRACT(DOW) в Postgres = 0..6 (0=Sun). Friday = 5. (dow - 5 + 7) % 7 = days since Friday.
  5. first_pay_day — день первой оплаты. b0 = «buyers на день 0». Накопительно.
  6. Filter NULLIF: deletion-by-zero защита везде.

Эталонный ответ

CTE: cohort key (день/неделя с пятницы) → cost_agg → installs_agg → paying (первый день оплаты на user) → cohort_pay → финальные расчёты CPI/CPM/CTR/IPM/C2B/CPB/ROAS. Всё через NULLIF. Гранулярность переключается параметром.

Хочешь увидеть разбор?

Зарегистрируйся бесплатно — откроется развёрнутое решение этой задачи и ещё 4 на выбор.

Зарегистрироваться и увидеть разбор
Уже есть аккаунт? Войти