docs(labs): переписана проза пяти ТЗ ступени 1 и раздел «Как принято»
- Зачем:
- владелец при чтении забраковал прозу ТЗ ступени 1: образы вместо
утверждений, заголовки-метафоры, договоренности стенда поданы как
законы. Тикет #228.
- Что:
- пять ТЗ (задания 1–5) написаны заново по форме ТЗ ступени 2:
содержание, колонки, формулы, самопроверка и приемка сохранены.
- раздел README лаб «Общие правила ступени 1» переименован в «Как принято
на ступени 1» и переписан как выбор с доводом; точечно поправлены
«Самопроверка» и таблица ступени 1.
- Проверка:
- grep по «ё», «менти», формам «ты» и словам-законам по docs/labs/*.md
молчит (единственное «правило» — в нетронутом разделе «Уборка»).
- скрипт ссылок и якорей из #226 печатает пусто; git diff --check чист.
- сверка имен в обратных кавычках и чекбоксов старых и новых файлов —
без расхождений.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
dc2cbc99dc
commit
29a01f0016
6 files changed
+472
-522
No files matched your search
+101
-113
@@ -1,150 +1,138 @@
|
||||
# Задание 5: `dm.utm_effectiveness`
|
||||
|
||||
**Чему учит:** приставка `declared_` в имени колонки — предупреждение, а не
|
||||
украшение. Она говорит: это заявка клиента, а не деньги отчетности. Урок
|
||||
здесь дает конфликт: суммы не сойдутся, и раскапывать почему предстоит
|
||||
самостоятельно.
|
||||
**Чему учит:** понимать предупреждение в приставке `declared_`: это сумма со
|
||||
слов клиента, а не деньги отчетности. Самостоятельно находить причины
|
||||
расхождения двух сумм выручки.
|
||||
|
||||
Правило стенда: **поведение и атрибуцию считаем по трекеру, деньги — по
|
||||
бэкенду**. Эта витрина — единственное разрешенное исключение: атрибуции по
|
||||
UTM у бэкенда нет, метки живут только в кликстриме. Раз считать иначе
|
||||
нечем, клиентская сумма берется, но называется своим именем.
|
||||
## Сначала прочитать
|
||||
|
||||
- [«Хранилище и его слои»](../handbook/storage-layers.md);
|
||||
- [«Потоки Airflow и даги модельного времени»](../handbook/airflow-dags.md).
|
||||
|
||||
## Роль витрины
|
||||
|
||||
Эффективность рекламных каналов по дням: сколько трафика привел канал,
|
||||
сколько корзин и покупок в нем случилось и на какую сумму эти покупки, по
|
||||
словам клиента.
|
||||
Аналитик сравнивает рекламные каналы по дням: трафик, добавления в корзину,
|
||||
покупки и заявленные клиентом суммы.
|
||||
|
||||
## Атрибуция: по событию, а не по визиту
|
||||
На стенде поведение и атрибуцию считают по трекеру, деньги — по бэкенду.
|
||||
Здесь сделано исключение: UTM-метки есть только в кликстриме, поэтому для
|
||||
оценки покупок по меткам используют клиентскую сумму.
|
||||
|
||||
Метки едут на каждом событии, и витрина группирует события по их
|
||||
собственным меткам. Модели «первое касание» или «последнее касание» здесь
|
||||
нет: их построение — отдельная тема, а это витрина трафика по меткам.
|
||||
## Что сделать
|
||||
|
||||
`dds.session_v` при этом несет UTM первого события визита, и ответ «какой
|
||||
канал привел» из сессии и из этой витрины будет разным. Это не
|
||||
ошибка ни одной из сторон — это два разных вопроса, и в отчете их надо
|
||||
различать.
|
||||
Читайте `dds.event_v` напрямую, без соседних витрин DM. Зерно — **день
|
||||
события × `utm_source` × `utm_medium` × `utm_campaign`**. Зерно новое,
|
||||
поэтому витрина хранится готовым агрегатом — см. [«Как принято на ступени
|
||||
1»](README.md#как-принято-на-ступени-1). Создайте пять объектов:
|
||||
`dm.utm_effectiveness_rep` / `_dist`, донор `_stage_rep` / `_stage_dist` и
|
||||
публичное представление `dm.utm_effectiveness_v`. Ключ партиции — день.
|
||||
|
||||
## Прямой трафик из витрины не выбрасывается
|
||||
### Состав колонок
|
||||
|
||||
Соблазн отфильтровать строки без меток велик: витрина же про рекламу.
|
||||
Отфильтровать нельзя по двум причинам. Первая деловая: прямой трафик —
|
||||
обычно самый крупный «канал» магазина, и отчет, где его нет, врет о доле
|
||||
рекламы. Вторая техническая: как только часть событий выпала, итог витрины
|
||||
перестал сходиться с итогом источника, и проверить сборку стало нечем.
|
||||
|
||||
Пустые метки остаются пустыми строками, как в источнике. Своего слова для
|
||||
«прямого захода» витрина не изобретает: чем меньше выдуманных значений, тем
|
||||
меньше расшифровывать читателю.
|
||||
|
||||
## Зерно и форма
|
||||
|
||||
Зерно — **день события × `utm_source` × `utm_medium` × `utm_campaign`**.
|
||||
Зерно источника — событие, значит витрина материализуется:
|
||||
`dm.utm_effectiveness_rep` / `_dist`, донор `_stage_rep` / `_stage_dist`,
|
||||
публичное `dm.utm_effectiveness_v`. Ключ партиции — день.
|
||||
|
||||
Источник — `dds.event_v`, публичный договор события: витрина слоя DM читает
|
||||
договор DDS, а не соседнюю витрину.
|
||||
|
||||
## Состав колонок
|
||||
|
||||
| Колонка | Тип | Что это |
|
||||
| Колонка | Тип | Значение |
|
||||
|---|---|---|
|
||||
| `report_date` | `Date` | день события |
|
||||
| `utm_source` | `LowCardinality(String)` | источник кампании |
|
||||
| `utm_medium` | `LowCardinality(String)` | канал кампании |
|
||||
| `utm_campaign` | `String` | имя кампании |
|
||||
| `visitors` | `UInt64` | число разных `client_id` |
|
||||
| `pageviews` | `UInt64` | просмотров страниц |
|
||||
| `add_to_carts` | `UInt64` | добавлений в корзину |
|
||||
| `purchases` | `UInt64` | покупок |
|
||||
| `declared_revenue` | `Decimal(18, 2)` | выручка покупок глазами клиента |
|
||||
| `_load_id` | `String` | запуск, собравший день |
|
||||
| `_load_ts` | `DateTime64(3, 'UTC')` | когда собран |
|
||||
| `report_date` | `Date` | День события |
|
||||
| `utm_source` | `LowCardinality(String)` | Источник кампании |
|
||||
| `utm_medium` | `LowCardinality(String)` | Канал кампании |
|
||||
| `utm_campaign` | `String` | Имя кампании |
|
||||
| `visitors` | `UInt64` | Число разных `client_id` |
|
||||
| `pageviews` | `UInt64` | Число просмотров страниц |
|
||||
| `add_to_carts` | `UInt64` | Число добавлений в корзину |
|
||||
| `purchases` | `UInt64` | Число покупок |
|
||||
| `declared_revenue` | `Decimal(18, 2)` | Сумма покупок, заявленная клиентом |
|
||||
| `_load_id` | `String` | Идентификатор запуска, собравшего день |
|
||||
| `_load_ts` | `DateTime64(3, 'UTC')` | Время сборки |
|
||||
|
||||
`declared_revenue` складывает `purchase_revenue[1]` событий `purchase`.
|
||||
Индекс не описка: блок `purchase*` едет параллельными массивами, и у события
|
||||
покупки в них ровно один элемент — контракт источника обещает один номер
|
||||
заказа и одну объявленную сумму на событие. Тем же индексом берет свою
|
||||
сторону стендовая сверка `purchase_vs_orders`.
|
||||
Для `declared_revenue` сложите `purchase_revenue[1]` событий `purchase`. В
|
||||
параллельных массивах `purchase*` по контракту хранятся один номер заказа и
|
||||
одна сумма на событие. В сверке `purchase_vs_orders` используют тот же
|
||||
индекс.
|
||||
|
||||
Приводить тип не нужно: в договоре `purchase_revenue` уже
|
||||
`Array(Decimal(18, 2))`. `Float64` эта сумма была только в выгрузке
|
||||
источника, и ODS перевел ее в точные деньги на приеме — сложение Decimal
|
||||
точное, и где стоит `CAST`, до или после `sum`, на результат не влияет.
|
||||
Витрина просто хранит то, что сложила, в `Decimal(18, 2)`.
|
||||
Тип `purchase_revenue` — `Array(Decimal(18, 2))`: исходный `Float64`
|
||||
преобразован в ODS. Сложение Decimal точное; `CAST` до или после `sum` не
|
||||
меняет результат и не нужен. Тип итога — `Decimal(18, 2)`.
|
||||
|
||||
`utm_campaign` оставлена обычной строкой: имен кампаний со временем
|
||||
становится много, и словарь `LowCardinality` на растущем множестве
|
||||
перестает окупаться.
|
||||
Для `utm_campaign` выбран тип `String`: имен кампаний со временем становится
|
||||
много, и затраты на словарь `LowCardinality` перестают окупаться.
|
||||
|
||||
## Ритм пересборки
|
||||
### Ритм пересборки
|
||||
|
||||
Источник — события: день замерзает сразу. Объем прогона и механика те же,
|
||||
что в задании 2: хвост несобранных дней, донор, замена дневных партиций.
|
||||
События прожитого дня больше не меняются. Как в задании 2, соберите в донор
|
||||
дни от первого несобранного до последнего прожитого и замените дневные
|
||||
партиции. Добавьте задачу в `dm_transform`.
|
||||
|
||||
## Ключ шардирования
|
||||
### Ключ шардирования
|
||||
|
||||
Ваше решение по [правилу донора](../architecture/storage.md), «Раскладка по
|
||||
шардам». Зерно здесь составное, из трех колонок, и решение «взять хеш от
|
||||
одной из них» надо защищать отдельно: назовите, какая доля строк придется на
|
||||
самое частое значение выбранной колонки.
|
||||
Выберите ключ самостоятельно. Если берете хеш одной из трех UTM-колонок,
|
||||
посчитайте долю строк с ее самым частым значением и обоснуйте выбор.
|
||||
|
||||
## Конфликт, ради которого витрина и написана
|
||||
Ключ донора — тот же, что у цели (см. [«Как принято на ступени
|
||||
1»](README.md#как-принято-на-ступени-1) и раздел «Раскладка по шардам»
|
||||
[справочника хранилища](../architecture/storage.md)).
|
||||
|
||||
Соберите за один и тот же день две суммы: `sum(declared_revenue)` из своей
|
||||
витрины и `sum(revenue)` из `dm.revenue_daily_v`. Они не сойдутся, и это
|
||||
верное поведение обеих витрин.
|
||||
## Атрибуция по событию, а не по визиту
|
||||
|
||||
Базы у двух сумм при этом сравнимые: `revenue_daily` складывает цену позиций
|
||||
на количество и ни скидки, ни доставки в себя не берет — ровно то, что видит
|
||||
клиент. Значит все расхождение до копейки объясняется тем, что случилось с
|
||||
заказом, а не тем, как считали деньги. Ищите ответы среди таких причин:
|
||||
Группируйте события по их собственным меткам. Модели «первое касание» и
|
||||
«последнее касание» здесь не рассчитываются.
|
||||
|
||||
- отмененный заказ из кликстрима никуда не девается: событие покупки
|
||||
случилось, а `revenue_daily` считает только оплаченные;
|
||||
- событие покупки может потеряться по дороге, а заказ при этом доехать —
|
||||
и наоборот, событие приехать дважды;
|
||||
- клиент назвал сумму, отличную от серверной суммы позиций: генератор
|
||||
делает это намеренно у одного-двух заказов из ста.
|
||||
В `dds.session_v` записаны UTM первого события визита. Распределение по
|
||||
каналам может отличаться от этой витрины, и это не означает ошибку в
|
||||
расчете. В отчете различайте атрибуцию визитов и событий.
|
||||
|
||||
Как эти же расхождения выглядят на зерне заказа, показывает стендовая
|
||||
витрина `dm.purchase_vs_orders_v` с ее классами расхождений: это готовый
|
||||
ответ на вопрос «а сколько именно и из-за чего». Запишите свой вывод —
|
||||
в двух-трех фразах, своими словами.
|
||||
## Прямой трафик остается в витрине
|
||||
|
||||
Проверять сходимость этих двух сумм не нужно и нельзя: они и не должны
|
||||
сходиться.
|
||||
Сохраните события без меток: прямой трафик обычно крупнейший канал, и без
|
||||
него доля рекламы будет завышена. Если отфильтровать такие строки, дневные
|
||||
итоги перестанут сходиться с источником — а по этому совпадению вы
|
||||
проверяете сборку. Разница составит вклад исключенных событий.
|
||||
|
||||
Оставьте пустые метки пустыми строками: новое обозначение прямого трафика
|
||||
потребовало бы объяснения.
|
||||
|
||||
## Расхождение с `dm.revenue_daily_v`
|
||||
|
||||
Сравните за один день `sum(declared_revenue)` своей витрины и `sum(revenue)`
|
||||
из `dm.revenue_daily_v`. Суммы не сходятся; проверять их равенство не нужно.
|
||||
|
||||
В `revenue_daily` складываются цены позиций, умноженные на количество, без
|
||||
скидок и доставки — та же основа, что у клиентской суммы. Расхождение
|
||||
объясняется историей заказов, а не способом расчета:
|
||||
|
||||
- событие покупки остается после отмены заказа, а `revenue_daily` учитывает
|
||||
только оплаченные заказы;
|
||||
- событие покупки может потеряться при доставке, хотя заказ поступил, или
|
||||
поступить дважды;
|
||||
- клиентская сумма может отличаться от серверной суммы позиций: генератор
|
||||
намеренно создает это расхождение у одного-двух заказов из ста.
|
||||
|
||||
Запишите свой вывод в двух-трех фразах. Число расхождений по заказам и их
|
||||
причины можно посмотреть в `dm.purchase_vs_orders_v`, где выделены классы
|
||||
расхождений.
|
||||
|
||||
## Как проверить себя
|
||||
|
||||
Деловой ключ — тройка `utm_source`, `utm_medium`, `utm_campaign` внутри
|
||||
дня. Соберите прямой агрегат из `dds.event_v` за день, соедините с
|
||||
`dm.utm_effectiveness_v` полным внешним соединением и посчитайте строки, где
|
||||
ключа нет с одной из сторон или где расходится любая из пяти величин. Ноль —
|
||||
зеленый.
|
||||
Деловой ключ — `utm_source`, `utm_medium`, `utm_campaign` внутри дня.
|
||||
Получите прямой агрегат из `dds.event_v` за день и соедините его с
|
||||
`dm.utm_effectiveness_v` полным внешним соединением по ключу. Посчитайте
|
||||
строки, где ключ отсутствует с одной из сторон или различается любая из пяти
|
||||
величин. Расхождений должно быть ноль.
|
||||
|
||||
Две проверки покрупнее ловят самые частые ошибки:
|
||||
Дополнительно сверьте дневные итоги:
|
||||
|
||||
- сумма `pageviews`, `add_to_carts` и `purchases` за день равна числу
|
||||
событий соответствующего типа в `dds.event_v` за тот же день;
|
||||
- сумма `declared_revenue` за день равна сумме `purchase_revenue[1]` по всем
|
||||
событиям `purchase` этого дня.
|
||||
- суммы `pageviews`, `add_to_carts` и `purchases` равны числу событий
|
||||
соответствующих типов в `dds.event_v`;
|
||||
- сумма `declared_revenue` равна сумме `purchase_revenue[1]` по всем
|
||||
событиям `purchase` того же дня.
|
||||
|
||||
Обе не сойдутся, если прямой трафик отфильтрован, и разойдутся ровно на его
|
||||
долю — по этому следу отфильтрованные строки и находят. `visitors`, как и в
|
||||
задании 3, по строкам не складывается: один посетитель приходит по разным
|
||||
меткам.
|
||||
Сумма `visitors` по строкам повторно учитывает одного посетителя,
|
||||
приходящего по разным меткам. Для числа посетителей дня она не подходит.
|
||||
|
||||
## Приемка
|
||||
|
||||
- [ ] пять объектов витрины созданы, донор объявлен через `AS` и шардируется
|
||||
ключом цели;
|
||||
- [ ] строки без UTM-меток в витрине остались;
|
||||
- [ ] созданы пять объектов, донор объявлен через `AS` и шардируется ключом
|
||||
цели;
|
||||
- [ ] сохранены строки без UTM-меток;
|
||||
- [ ] `declared_revenue` хранится в `Decimal(18, 2)`;
|
||||
- [ ] расхождение с `dm.revenue_daily_v` объяснено письменно;
|
||||
- [ ] задача `utm_effectiveness` в `homework_check` зеленая;
|
||||
- [ ] задача `utm_effectiveness` в `homework_check` завершилась успешно;
|
||||
- [ ] задача витрины добавлена в `dm_transform`.
|
||||
Reference in new issue
Block a user