# Как воспроизвести отчеты из raw data

Эта статья описывает, как построить в BI расчеты, близкие к отчетам Gravity Field, используя только CSV из [экспорта сырых данных](./export_raw_data.md).

!!!info Минимальный pipeline
1. Загрузите CSV стандартным CSV-парсером.
2. Приведите время, деньги и идентификаторы на staging-слое.
3. Разверните `eventCart` в отдельную таблицу строк заказа.
4. Считайте A/B и Strategies только по полям, которые есть в raw export.
!!!

## Базовый staging

Перед расчетом отчетов загрузите CSV в staging-таблицу событий и приведите типы.

Минимальный набор нормализованных полей:

| Поле в staging | Источник | Как использовать |
|---|---|---|
| `event_time` | `moscowTime` | Порядок событий и группировка по датам |
| `event_date` | `date(moscowTime)` | Дневные агрегаты |
| `event_type_id` | `type` | Фильтр типа события |
| `uid` | `uid` | Основная связка пользователя |
| `session_id` | `sessionId` | Связка внутри сессии |
| `campaign_id` | `campaignId` | Фильтр кампании |
| `experience_id` | `experienceId` | Фильтр experience |
| `version_id` | `versionId` | Фильтр версии теста |
| `variation_id` | `variationId` | Группировка A/B |
| `strategy_id` | `strategyId` | Группировка стратегий |
| `event_name` | `eventName` | Фильтр custom events |
| `event_type` | `eventType` | Фильтр `purchase-v1` |
| `event_value_minor` | `eventValue` | Деньги в minor units |
| `event_value` | `eventValue / 100` | Деньги в валюте события |
| `currency` | `eventCurrency` | Валюта |
| `transaction_id` | `uniqueTransactionId` | Дедупликация заказов |

!!!info Тип поля `type`
В примерах ниже фильтры по `type` указаны числовыми ID. Если после импорта ваш CSV хранит `type` строками, используйте соответствующие названия: `Variation Impression`, `Product Click`, `Event Hit` и т.д.
!!!

Отдельно разверните `eventCart` в таблицу строк заказа:

| Поле | Описание |
|---|---|
| `event_id` | `eventId` покупки |
| `transaction_id` | `uniqueTransactionId` покупки |
| `uid` | Пользователь покупки |
| `session_id` | Сессия покупки |
| `purchase_time` | `moscowTime` покупки |
| `product_id` | Первый элемент tuple из `eventCart` |
| `quantity` | Второй элемент tuple |
| `item_price_minor` | Третий элемент tuple |
| `line_revenue` | `quantity * item_price_minor / 100` |

Для покупок используйте только события:

- `type = 1`
- `eventType = 'purchase-v1'`
- непустой `uniqueTransactionId`

Если один и тот же `uniqueTransactionId` встречается несколько раз, оставьте первую покупку по времени.

## A/B report

### Настройки, которые нужно взять из платформы

Перед расчетом зафиксируйте:

- `campaignId`, `experienceId`, `versionId` или список `variationId`
- период отчета
- stickiness: по `uid` или по `sessionId`
- attribution window: session или N дней
- trigger атрибуции: `Variation Impression` или `Event is triggered`
- цель: `purchase-v1`, revenue или custom event
- включено ли исключение outliers в платформенном отчете

### Алгоритм для purchase/revenue

1. Отберите exposure-события:
   - `type = 2`
   - нужные `campaignId`, `experienceId`, `versionId` или `variationId`
   - непустой `uid`
   - период отчета
2. Если trigger = `Event is triggered`, найдите trigger-событие после exposure:
   - тот же `uid`
   - если attribution by session, тот же `sessionId`
   - `trigger_time >= exposure_time`
   - `eventName` соответствует настройке цели-триггера
3. Отберите покупки:
   - `type = 1`
   - `eventType = 'purchase-v1'`
   - дедупликация по `uniqueTransactionId`
4. Привяжите покупки к exposure или trigger:
   - тот же `uid`
   - если attribution window = session, тот же `sessionId`
   - `purchase_time >= attribution_start_time`
   - для day window: `purchase_time <= attribution_start_time + N days`
5. Для чистого сравнения исключите пользователей или сессии, где в рамках той же версии теста были разные `variationId`.
6. Сгруппируйте результат по `variationId`.

SQL-псевдокод:

```sql
with exposures as (
  select
    uid,
    sessionId as session_id,
    moscowTime as exposure_time,
    campaignId as campaign_id,
    experienceId as experience_id,
    versionId as version_id,
    variationId as variation_id
  from raw_events
  where type = 2
    and campaignId = :campaign_id
    and experienceId = :experience_id
    and versionId = :version_id
),
purchases as (
  select *
  from (
    select
      uid,
      sessionId as session_id,
      moscowTime as purchase_time,
      eventId as event_id,
      uniqueTransactionId as transaction_id,
      eventValue / 100.0 as revenue,
      row_number() over (
        partition by uniqueTransactionId
        order by moscowTime
      ) as rn
    from raw_events
    where type = 1
      and eventType = 'purchase-v1'
      and uniqueTransactionId <> ''
  )
  where rn = 1
),
attributed as (
  select
    e.variation_id,
    p.uid,
    p.transaction_id,
    p.revenue
  from exposures e
  join purchases p
    on p.uid = e.uid
   and p.purchase_time >= e.exposure_time
   and p.purchase_time <= e.exposure_time + interval ':window_days day'
   -- для session attribution добавьте:
   -- and p.session_id = e.session_id
)
select
  variation_id,
  count(distinct uid) as users_with_purchase,
  count(distinct transaction_id) as purchases,
  sum(revenue) as revenue,
  sum(revenue) / nullif(count(distinct transaction_id), 0) as aov
from attributed
group by variation_id;
```

Если в платформенном отчете включено исключение outliers, raw BI-расчет без такой же обработки будет отличаться. Минимально зафиксируйте это как причину расхождения. Для более близкого расчета примените тот же подход: значения ниже 5-го перцентиля и выше 95-го перцентиля заменяются на консервативное значение на основе среднего/медианы периода.

## Strategies report

### Что можно воспроизвести из raw data

Из raw export можно посчитать близкие к платформе:

- `Impressions`
- `Visible Impressions`
- `Clicks`
- `CTR`
- `Direct Revenue` по точному SKU
- `Direct Purchases` по точному SKU
- `Direct Revenue per 1000`
- дневную динамику и разрез по `strategyId`

### Настройки, которые нужно взять из платформы

- период отчета
- attribution window: session, 1, 7, 14 или 30 дней
- нужные `strategyId`, если считаете не все стратегии
- валюта и source-фильтры, если они применяются в платформенном отчете

### Базовые метрики вовлечения

Считайте события по `strategyId` и дате события:

| Метрика | Расчет из raw data |
|---|---|
| `Impressions` | `count(*) where type = 4` |
| `Visible Impressions` | `count(*) where type = 7` |
| `Clicks` | `count(*) where type = 6` |
| `CTR` | `Clicks / Visible Impressions` |
| `Visibility` | `Visible Impressions / Impressions` |

### Direct Revenue и Direct Purchases

Платформенная логика direct attribution для точного SKU строится от `Product Click` к последующей покупке того же SKU.

Алгоритм:

1. Отберите клики:
   - `type = 6`
   - непустой `uid`
   - непустой `strategyId`
   - разверните массив `sku` в отдельные строки `clicked_sku`
2. Отберите покупки:
   - `type = 1`
   - `eventType = 'purchase-v1'`
   - дедупликация по `uniqueTransactionId`
   - разверните `eventCart` в строки заказа
3. Привяжите purchase line к click:
   - тот же `uid`
   - `purchase_time >= click_time`
   - если attribution window = session, тот же `sessionId`
   - если attribution window = N days, `purchase_time <= click_time + N days`
   - `cart.product_id = clicked_sku`
4. Для таблицы по стратегиям группируйте по `strategyId` и дате покупки.
5. Для summary дедуплицируйте заказ так, чтобы один `uniqueTransactionId` не увеличивал итог несколько раз из-за нескольких стратегий.

SQL-псевдокод:

```sql
with product_clicks as (
  select
    uid,
    sessionId as session_id,
    moscowTime as click_time,
    strategyId as strategy_id,
    clicked_sku
  from raw_events
  cross join unnest(sku) as clicked_sku
  where type = 6
    and strategyId <> ''
),
purchase_lines as (
  select
    p.uid,
    p.sessionId as session_id,
    p.moscowTime as purchase_time,
    p.eventId as event_id,
    p.uniqueTransactionId as transaction_id,
    cart.product_id,
    cart.quantity,
    cart.item_price_minor,
    cart.quantity * cart.item_price_minor / 100.0 as line_revenue
  from deduplicated_purchases p
  cross join unnest_event_cart(p.eventCart) as cart
)
select
  c.strategy_id,
  date(l.purchase_time) as date,
  count(distinct l.transaction_id) as direct_purchases,
  sum(l.line_revenue) as direct_revenue
from product_clicks c
join purchase_lines l
  on l.uid = c.uid
 and l.product_id = c.clicked_sku
 and l.purchase_time >= c.click_time
 and l.purchase_time <= c.click_time + interval ':window_days day'
 -- для session attribution добавьте:
 -- and l.session_id = c.session_id
group by c.strategy_id, date(l.purchase_time);
```

`Direct Revenue per 1000`:

```text
Direct Revenue per 1000 = Direct Revenue / (Visible Impressions / 1000)
```

`CR click -> purchase`:

```text
CR click -> purchase = Direct Purchases / Clicks
```

### Почему сумма по стратегиям может быть больше summary

!!!info Summary и таблица
В таблице по стратегиям один заказ может быть атрибутирован нескольким стратегиям, если пользователь кликал товары в нескольких виджетах/стратегиях перед покупкой. Это ожидаемо.

В summary платформенный отчет дедуплицирует покупки. В BI повторите это правило: для итогового блока оставляйте один вклад на `uniqueTransactionId`, а для таблицы по стратегиям показывайте вклад каждой стратегии отдельно.
!!!

## FAQ по расхождениям

**Почему BI-выручка отличается от платформы?**  
Проверьте timezone, период отчета, attribution window, дедупликацию по `uniqueTransactionId`, outliers в A/B-отчете и late events. Также проверьте, не нужна ли метрика, которая использует неэкспортируемые поля.

**Что считать выручкой: gross, net, с НДС или без?**  
Gravity Field считает то значение, которое было отправлено в `eventValue` и `eventCart.itemPrice`. Платформа не выводит gross/net из каталога и не пересчитывает НДС или промо-скидки.

**Учитываются ли возвраты и отмены заказов?**  
Автоматически нет. Если возвраты или отмены не передаются отдельными событиями/данными и не настроены в проекте, purchase-событие остается в расчете.

**Почему `Direct Revenue (All Variants)` нельзя посчитать из raw CSV?**  
Для этой метрики нужен `group_id` кликнутого товара и купленных товаров. В текущем raw export этих полей нет.

**Можно ли использовать `eventId` для дедупликации покупок?**  
Нет. Для заказов используйте `uniqueTransactionId`. `eventId` идентифицирует конкретное событие и может отличаться у повторно отправленных событий одного заказа.

**Можно ли заменить `Product Click` событием add-to-cart?**  
Нет. `Product Click` (`type = 6`) — это engagement с рекомендательным товаром. Add-to-cart или другой сайт-клик является отдельным custom event и не равен клику по рекомендации.
