Compare commits

...
4 Commits
Author SHA1 Message Date
ddadmin 515f386e5f docs(labs): добавлено наблюдение за выручкой до закрытия дня
Зачем: связать опоздания и отмены с изменением уже рассчитанного отчета.
Что: лабораторная с сохранением состояний заказов, ссылки в индексе и программе; убраны повторы в описании раздела.
Проверка: восемь однодневных world_next_day и etl_pipeline на Airflow 3.3.0; изменения сумм сошлись до копейки, закрытый день неизменен, DQ пройдены. Проверены 54 локальные ссылки, SQL-примеры и git diff --check.
2026-09-22 03:58:42 -04:00
ddadmin 2502277af1 docs(labs): добавлены три расследования расхождений выручки
Зачем: дать практику сверки событий покупки с заказами магазина.
Что: задания о потерях событий, повторах покупок и расхождении состава корзин.
Проверка: запросы под analyst на ClickHouse 26.3.17.56; счетчики, суммы и точный разбор JSON сошлись. make lint и независимая техническая проверка пройдены.
2026-09-22 03:46:45 -04:00
ddadmin ddb186f841 docs(labs): согласованы четыре расследования выручки
Зачем: превратить шесть идей факультативов в связный раздел из четырех работ.
Что: определены результаты работ и наблюдение за заказами через Airflow.
Проверка: git diff --check; порядок обновления данных сверен с кодом дагов.
2026-09-22 03:32:16 -04:00
ddadmin 090df8a715 docs(agents): убрано устаревшее распределение авторства
Зачем: оценка моделей изменилась, назначение Opus автором текстов устарело.
Что: автор задачи сам пишет и редактирует материалы, если владелец не назначил иное.
Проверка: git diff --check; проверены AGENTS.md, CLAUDE.md и docs/agents.
2026-09-22 03:32:12 -04:00
7 changed files with 391 additions and 28 deletions

No files matched your search

+3 -8
View File
@@ -46,15 +46,10 @@
## Кому что поручать
Читатель здесь не побочный потребитель, а тот, ради кого стенд существует:
код, комментарии и документы и есть продукт. Поэтому всё, что человек будет
читать и разбирать, пишет модель с чувством меры — сейчас это Opus. Codex
берёт техническую работу без вкусовых решений: миграции, разбор журналов и
объёмного вывода, обход репозитория, повторяющиеся правки. Там его запас
лимитов работает на нас, а экономить их на читаемом коде — экономия ложная:
переписывать выйдет дороже, чем сразу написать хорошо.
Автор задачи сам пишет и редактирует код, комментарии и документы, если
владелец не задал другое распределение работы.
Делить работу между ними стоит не целыми задачами, а подзадачами, и решать
Делить работу стоит не целыми задачами, а подзадачами, и решать
это при планировании. Вкусовая часть идёт первой: она задаёт форму, имена и
границы, под которые механическая потом подстраивается.
+13 -2
View File
@@ -124,8 +124,19 @@ DM](../architecture/dm.md).
### Почему выручка не сходится
Открывается после ступени 1. Расхождения между источниками, повторы
покупок и день, после которого отчет можно считать окончательным.
Открывается после ступени 1. Четыре расследования:
- [Куда пропали покупки](optional/missing-purchases.md): найти заказы без
события в трекере и объяснить разрыв воронки.
- [Почему повтор покупки завышает выручку](optional/duplicate-purchases.md):
посчитать деньги по номеру заказа и объяснить вклад повторов.
- [Какая позиция объясняет разницу в сумме](optional/order-amount-difference.md):
сопоставить корзины и найти причину расхождения до копейки.
- [Почему вчерашняя выручка меняется сегодня](optional/revenue-over-time.md):
проследить опоздания и отмены, затем определить момент закрытия дня.
Первые три работы используют готовые данные. Четвертая продвигает мир через
Airflow; для нее вы сохраняете состояния заказов после каждого модельного дня.
### Тонкий анализ данных
+65
View File
@@ -0,0 +1,65 @@
# Почему повтор покупки завышает выручку
**Чему учит:** отличать повторную доставку строки от повторного события об
одной покупке и выбирать ключ, по которому считать деньги.
Факультатив доступен после ступени 1. Работайте в DBeaver пользователем
`analyst` по [инструкции](../../../README.md#подключиться-из-dbeaver).
Дождитесь завершения `etl_pipeline`; оставьте `world_live` на паузе и
дождитесь уже начатых прогонов, чтобы данные не менялись между запросами.
## Посчитайте клиентскую сумму двумя способами
Возьмите один закрытый день: его дата должна быть не позднее
`max(snapshot_date) - 6` из `ods.order_v`. Во всех запросах к `dds.event_v`
отбирайте этот `event_date` и `event_type = 'purchase'`.
Номер заказа находится в `purchase_id[1]`, заявленная сумма — в
`purchase_revenue[1]`. По формату источника у события покупки один номер заказа.
Сначала посчитайте число событий и сумму всех заявленных сумм. Затем найдите
номера заказов, которые встречаются несколько раз. Если таких нет, выберите
другой закрытый день.
Для двух повторившихся покупок выпишите `event_id`, `client_id`, `event_time`,
номер заказа и сумму. Сопоставьте число событий и заказов в `dds.order_v`.
Теперь рассчитайте клиентскую сумму, учитывая каждый номер заказа один раз.
До выбора одной суммы на заказ проверьте, что во всех его событиях она
одинакова. Объясните, почему удалять одинаковые денежные значения вместо
повторных номеров заказов было бы ошибкой.
## Объясните разницу
Для каждого повторившегося заказа определите число лишних событий и их
вклад в завышение. Сложите эти вклады и сверьте с разностью двух расчетов.
Равенство должно выполняться до копейки.
Слой ODS убирает повторную доставку одного события по его идентификатору;
`dds.event_v` читает уже это состояние. Объясните по своим строкам, почему
повтор покупки сохранился. Какие поля совпадают и какие различаются?
Какой ключ отвечает на вопрос «это то же событие», а какой — «это тот же заказ»?
После собственного расчета найдите эти заказы в `dm.purchase_vs_orders_v`.
Если у повторившегося заказа класс отличается от `duplicate_event`, проверьте
его статус и [порядок присвоения классов](../../architecture/dm.md#dmpurchase_vs_orders_v).
Класс витрины не заменяет поиск всех повторов в событиях.
## Сравните с выручкой магазина
Сложите `revenue` из `dm.revenue_daily_v` за тот же `report_date` по всем
категориям. Эта витрина считает стоимость позиций оплаченных заказов,
без скидки и доставки.
Сравните результат с исправленной клиентской суммой. Совпадение не требуется:
удаление повторов устраняет одну причину расхождения. Найдите хотя бы один
заказ, который объясняет, почему деньги трекера и магазина все еще могут
различаться. Для этого сопоставьте события, заказ и готовую сверку.
## Что должно получиться
- [ ] сохранены две клиентские суммы и строки повторившихся покупок;
- [ ] разность сумм полностью объяснена вкладом лишних событий;
- [ ] выбор ключа для устранения повторов подтвержден данными;
- [ ] вы можете объяснить, почему устранение повторов не завершает сверку
выручки с магазином.
+79
View File
@@ -0,0 +1,79 @@
# Куда пропали покупки
**Чему учит:** обнаружить потерю событий при сверке двух источников и
обосновать, каким данным можно доверять при подсчете покупок.
Факультатив доступен после ступени 1. В DBeaver подключитесь пользователем
`analyst` по [инструкции](../../../README.md#подключиться-из-dbeaver).
Дождитесь завершения `etl_pipeline`; на время расследования оставьте
`world_live` на паузе и дождитесь уже начатых прогонов.
## Выберите день
Магазин зарегистрировал заказы, но часть событий покупки не дошла до трекера.
Найдите след этой потери сначала в событиях сайта, затем в заказах.
Возьмите день, для которого уже получен последний слепок семидневного окна.
Запрос показывает самую позднюю подходящую дату:
```sql
SELECT max(snapshot_date) - 6 AS last_closed_day
FROM ods.order_v;
```
Используйте выбранный день во всех запросах. События отбирайте по
`dds.event_v.event_date`, заказы — по `dds.order_v.order_date`.
Сравнивайте все заказы дня, включая отмененные: сейчас вопрос в том,
зарегистрировал ли магазин заказ, а не в том, был ли он оплачен.
## Найдите разрыв в событиях
Посчитайте просмотры страницы подтверждения и события покупки:
- у просмотра `event_type = 'pageview'`, а `url` заканчивается на `/confirmation`;
- у покупки `event_type = 'purchase'`.
Запишите оба числа и разность. Какую потерю она позволяет заподозрить?
Достаточно ли этих двух чисел, чтобы назвать пропавшие покупки?
## Сверьте с магазином
В событии номер заказа находится в `purchase_id[1]`, в заказах — в `order_id`.
Соберите список заказов выбранного дня, для которых нет события покупки.
Несколько событий с одним номером заказа должны оставаться одной покупкой.
Проверьте два равенства:
- число заказов магазина равно числу разных заказов из событий плюс число
заказов без события;
- разность просмотров подтверждения и событий покупки равна числу заказов
без события.
Первое равенство проверяет полноту сопоставления источников. Второе относится
к устройству этого стенда. Найдите покупку с повторным событием и посмотрите
соседние события того же `client_id` по времени: что произошло со страницей
подтверждения? Объясните, почему повтор не скрыл разрыв воронки.
Если в выбранном дне повторов нет, возьмите другой закрытый день.
Для оценки доли потерь используйте число заказов магазина и объясните,
почему число просмотров подтверждения может дать другую долю.
## Сопоставьте вывод с готовой сверкой
Найдите свои заказы без события в `dm.purchase_vs_orders_v` по `order_id`.
Сравните их `status` и `mismatch_class`. Витрина присваивает заказу один класс,
хотя причин расхождения может быть несколько. По
[правилу классификации](../../architecture/dm.md#dmpurchase_vs_orders_v)
объясните, почему потеря события не всегда попадает в `lost_event`.
Возьмите один заказ из своего списка и сохраните два запроса: первый находит
его в магазине, второй показывает отсутствие покупки в трекере. Сформулируйте,
какой источник нужен для отчета о поведении посетителей, а какой — для денег.
## Что должно получиться
- [ ] сохранены день, счетчики воронки и список заказов без события;
- [ ] оба равенства сходятся, влияние повторного просмотра объяснено;
- [ ] доля потерь посчитана с обоснованным знаменателем;
- [ ] один случай подтвержден запросами к обоим источникам;
- [ ] вы различаете отсутствие события и класс строки в готовой сверке.
@@ -0,0 +1,70 @@
# Какая позиция объясняет разницу в сумме
**Чему учит:** перейти от расхождения суммы заказа к конкретному товару и
проверить объяснение с точностью до копейки.
Факультатив доступен после ступени 1. В DBeaver используйте пользователя
`analyst` из [инструкции подключения](../../../README.md#подключиться-из-dbeaver).
Дождитесь завершения `etl_pipeline`; оставьте `world_live` на паузе и
дождитесь уже начатых прогонов.
## Найдите заказ
В `dm.purchase_vs_orders_v` выберите один заказ с
`mismatch_class = 'amount_delta'`. Возьмите закрытый день: `order_day`
не позднее `max(snapshot_date) - 6` из `ods.order_v`. Сохраните `order_id`,
`order_day`, `declared_revenue` и `items_total`.
Найдите этот заказ в `dds.order_v`. Сопоставьте `items_total`, `discount`,
`delivery` и `total`. Как связаны эти суммы? Объясните, почему клиентскую
`declared_revenue` нужно сравнивать с `items_total`, чтобы искать разницу
именно в составе корзины.
## Разберите позиции с двух сторон
Найдите событие покупки в `ods.event_v`: `purchaseID[1]` совпадает с номером
заказа, `EventDate` — с выбранным днем, `EventType = 'purchase'`.
Откройте поле `ecommerce`. Внутри JSON массив `purchase.products` содержит
позиции клиента с полями `id`, `price` и `quantity`.
Разверните этот массив в строки и получите артикул, точную цену, количество
и стоимость каждой позиции. Для разбора пригодятся `JSONExtractArrayRaw`
и `JSONExtract`, для разворачивания массива — `ARRAY JOIN`.
Деньги извлекайте как `Decimal` с двумя знаками после запятой.
С другой стороны используйте массивы `item_sku`, `item_price` и
`item_quantity` заказа в `dds.order_v`. Элементы с одним индексом относятся
к одной позиции; при разворачивании сохраняйте эту связь.
Сопоставьте позиции по артикулу так, чтобы остались видны товары,
присутствующие только с одной стороны. Покажите, какие позиции совпали,
а какая объясняет разницу. В результате нужны строки товаров, а не только
две итоговые суммы.
## Проверьте объяснение
Подтвердите три равенства:
- сумма стоимостей клиентских позиций равна `declared_revenue`;
- сумма стоимостей позиций магазина равна `items_total`;
- стоимость отсутствующей у магазина позиции равна
`declared_revenue - items_total`.
Затем сравните точные цены из JSON с готовым массивом `productPrice`
события. По [формату выгрузки](../../formats/clickstream-event.md) цены в нем
округлены до целых рублей. Покажите на своей корзине, что потеряется при
расчете по этому массиву. Если все цены выбранной корзины целые, возьмите
еще один заказ с расхождением и дробной ценой.
В коротком объяснении назовите заказ, товар, количество, цену и разницу.
Отделите то, что подтверждают данные, от причины, которую по ним установить
нельзя: отсутствие позиции в заказе видно, а причину ее удаления источник
отдельным полем не сообщает.
## Что должно получиться
- [ ] позиции клиента и магазина сопоставлены по артикулам;
- [ ] все три равенства сходятся до копейки;
- [ ] найден пример потери точности в готовом массиве цен;
- [ ] расхождение объяснено конкретной позицией, а скидка и доставка
отделены от сравнения корзин.
+131
View File
@@ -0,0 +1,131 @@
# Почему вчерашняя выручка меняется сегодня
**Чему учит:** объяснять изменения уже рассчитанного отчета поступлением
заказов и сменой их статусов, определять момент закрытия дня и границы
доступной истории.
Факультатив доступен после ступени 1. Понадобятся Airflow и DBeaver с
подключением `analyst` по [инструкции](../../../README.md#подключиться-из-dbeaver).
Вы будете наблюдать один день оформления заказов, продвигая мир по одному
модельному дню. Новые данные останутся в стенде.
## Подготовьте первое наблюдение
Поставьте `world_live` на паузу. Дождитесь завершения начатых прогонов
генерации и обработки, затем запустите `etl_pipeline` и дождитесь успеха.
Так исходное наблюдение будет снято после обновления витрин.
Узнайте последний день событий и дату последнего полученного слепка:
```sql
SELECT
(SELECT max(event_date) FROM dds.event_v) AS event_day,
(SELECT max(snapshot_date) FROM ods.order_v) AS snapshot_day;
```
День `event_day` обозначим **D**. Наблюдайте именно его до конца работы.
Дата слепка, по которому сделано очередное наблюдение, будет **S**.
Это модельные даты магазина. Календарная дата запуска Airflow день D не задает.
Слепок снимается к концу суток и доставляется следующим прогоном. Поэтому
покупки последнего сыгранного дня уже видны в событиях, а их заказов в
хранилище еще нет. Проверьте это для D в `dds.order_v` и
`dm.purchase_vs_orders_v`.
## Подготовьте запросы и сохраняйте результаты
Напишите запросы для следующих наблюдений:
| Что сохранить | Откуда взять |
|---|---|
| Дату полученного слепка S | `max(snapshot_date)` из `ods.order_v` |
| Номер, статус и `items_total` каждого заказа дня D | `dds.order_v`, отбор по `order_date` |
| Число заказов в каждом статусе и сумму `items_total` только у `paid` | те же заказы дня D |
| Выручку дня D | сумма `revenue` из `dm.revenue_daily_v`, отбор по `report_date` |
| Номера покупок, еще ожидающих заказ | строки `awaiting_order` из `dm.purchase_vs_orders_v`, отбор по `order_day` |
Выручку складывайте по всем категориям. В этой витрине она означает стоимость
позиций оплаченных заказов, без скидки и доставки, поэтому должна сходиться
с суммой `items_total` у заказов в статусе `paid`.
Сохраняйте результаты запросов отдельными файлами, например выгрузками CSV
из DBeaver. Подписывайте D и S. До поступления первого слепка список заказов
пуст, а выручка равна нулю; сохраните и это наблюдение.
Публичное представление заказа показывает текущее состояние, а сырые слепки
хранятся ограниченное время. Сохраненные выгрузки — история для расследования.
## Продвигайте мир по одному дню
Для каждого следующего наблюдения:
1. Запустите `world_next_day` с `days = 1` и дождитесь успеха.
2. Запустите `etl_pipeline` и дождитесь успеха: `world_next_day` сам витрины
не обновляет.
3. Повторите свои запросы и сохраните новый комплект результатов с датой S.
Во всех запросах остается прежний день D. За один запуск проигрывайте один
день, чтобы сохранить промежуточные состояния. Пакетный `world_next_day`
отправляет события без пауз; ждать 24 минуты живого дня здесь не нужно.
Продолжайте до слепка **D + 7** включительно. У заказов дня D окно состоит
из семи дней, от D до D + 6. Слепок D + 6 содержит окончательное состояние,
а еще один шаг позволит проверить, что оно сохранилось за пределами окна.
От исходного наблюдения потребуется восемь однодневных запусков.
## Объясните изменения отчета
После каждого шага сравнивайте заказы по `order_id`, а не по положению строк
в выгрузке. Найдите:
- заказы, отсутствовавшие в первом слепке D, но появившиеся в следующем;
- переходы из `created` в `paid`;
- отмены заказов, для которых раньше был виден статус `paid`.
Первый слепок, полученный на следующий день, — обычная доставка заказов.
Опоздавшими здесь считаются те, которых не было уже в этом слепке.
Покажите на нескольких номерах, как они исчезают из `awaiting_order`.
Для двух соседних наблюдений разложите изменение выручки по заказам.
Вклад заказа — его `items_total`, если статус `paid`, и ноль в остальных
случаях, включая отсутствие заказа. Разность вкладов объясняет, сколько
каждый заказ добавил к отчету или убрал из него. Сумма этих изменений должна
совпасть с изменением `revenue` до копейки.
Запишите даты, между которыми выручка выросла или уменьшилась. Отдельный
отмененный заказ мог убрать деньги, даже если итог дня вырос: покажите это
по его вкладу. Одна общая сумма скрывает встречные изменения.
## Что известно об отменах
Возьмите отмененный заказ, у которого в вашей истории был статус `paid`.
Укажите два наблюдения, доказывающих, что он был оплачен, а затем отменен.
Для отмененного заказа без наблюдаемой оплаты сформулируйте предел вывода.
Дневные слепки могли не застать промежуточное состояние: оплата и отмена
могли произойти между ними. Поэтому отсутствие `paid` в выгрузках не
доказывает, что оплаты не было. Не заменяйте недостающие данные догадкой.
## Когда день закрыт
Сравните сохраненные состояния при S = D + 6 и S = D + 7: состав заказов,
их статусы, суммы и выручку. Объясните, почему отчет за D теперь окончательный.
Основание — получен и обработан последний слепок окна изменяемости;
одно совпадение сумм в соседних наблюдениях этого не доказывает.
После собственных выводов сверьтесь с описанием
[слепка и его доставки](../../architecture/orders/snapshot.md#слепок-и-его-доставка)
и [границ истории отмен](../../architecture/orders/fate.md#судьба-заказа).
## Что должно получиться
- [ ] сохранена последовательность наблюдений одного дня D с датами слепков S;
- [ ] найдены опоздавшие заказы и изменения статусов;
- [ ] изменение выручки между двумя наблюдениями объяснено вкладами заказов;
- [ ] оплата одной из последующих отмен подтверждена двумя наблюдениями;
- [ ] вы различаете доказанную оплату и отсутствие сведений о ней;
- [ ] состояние дня за пределами окна совпало с окончательным, а момент
закрытия обоснован правилом доставки слепков.
После работы оставьте `world_live` на паузе либо верните автоматическое
продвижение, если пользовались им до наблюдения.
+30 -18
View File
@@ -149,28 +149,40 @@
## 4. Факультативы — пять разделов
Ниже перечислены только названия сюжетов и их уроки. Материал, формы и способы
проверки остаются в [инвентаризации](../research/2026-08-31-labs-inventory.md).
Ниже перечислены сюжеты, их уроки и принятые решения по форме заданий. Исходный
материал и способы проверки — в
[инвентаризации](../research/2026-08-31-labs-inventory.md).
### Почему выручка не сходится
Этот раздел рекомендован первым из факультативов. Он открывается после ступени
1 основной программы: материал живет на стенде с этапа 6.
Этот раздел рекомендован первым из факультативов и открывается после ступени
1 основной программы. Шесть сюжетов собраны в четыре лабораторные: три
расследования готовых данных и одно наблюдение во времени. Опоздания,
изменение выручки и история отмен используют одну историю заказов, поэтому
для них выбрана общая работа вместо трех отдельных.
- **Почему трекер недосчитывает покупки (#186).** Разрыв воронки подтверждений
показывает потерянные события и границу доверия к двум источникам.
- **Почему повтор покупки завышает выручку (#187).** Движок убирает технический
дубль строки, а повтор одной покупки приходится находить по деловому ключу.
- **Какая позиция объясняет разницу в сумме.** Расхождение суммы нужно
разложить до конкретной вычеркнутой позиции и объяснить до копейки.
- **Был ли отмененный заказ оплачен.** Текущее состояние не заменяет историю, а
некоторые различия после истечения срока хранения сырых данных уже не
восстановить.
- **Когда отчет за день можно считать окончательным (#188).** Отчет за день
становится окончательным лишь после того, как источник больше не может
изменить этот день.
- **Почему выручка за день еще меняется.** Внутри окна изменяемости выручка
движется, а после закрытия дня фиксируется.
- **[Куда пропали покупки](../labs/optional/missing-purchases.md) (#186).**
Учащийся объясняет разрыв воронки заказами, для которых нет события покупки.
- **[Почему повтор покупки завышает выручку](../labs/optional/duplicate-purchases.md) (#187).**
Разность клиентских сумм объясняется повторными событиями одной покупки.
- **[Какая позиция объясняет разницу в сумме](../labs/optional/order-amount-difference.md).**
Сравнение позиций из исходного JSON и заказа объясняет расхождение до копейки.
- **[Почему вчерашняя выручка меняется сегодня](../labs/optional/revenue-over-time.md) (#188).**
История одного дня связывает опоздания, оплату и отмены с изменением выручки,
позволяет определить момент закрытия дня и границу знания об оплате.
В четвертой лабораторной выбрано собственное наблюдение: учащийся продвигает
мир по одному дню через Airflow и сохраняет результаты после обновления витрин.
Готовые слепки и сочетание готового примера с собственным наблюдением
отклонены: нужную историю можно получить на стенде и самому увидеть,
как новые данные меняют уже рассчитанный отчет.
День считается закрытым после получения и обработки последнего слепка
семидневного окна изменяемости; этот слепок доставляется следующим модельным
днем. Одного совпадения сумм в соседних наблюдениях для такого вывода
недостаточно. История отмен тоже имеет границу: оплата и отмена между двумя
дневными слепками могут остаться невидимыми. Отсутствие наблюдаемой оплаты
не доказывает, что оплаты не было.
### Тонкий анализ данных