Расхождения A–D и опоздания: механика, доли и что обещано #72
Notifications
Due Date
No due date set.
Blocks
Depends on
#74 Место заказов в стартовом мире
ddmitry/clickstream-data-platform
#71 Генератор слепков: где живёт и чем связан с событиями
ddmitry/clickstream-data-platform
Reference: ddmitry/clickstream-data-platform#72
Reference in New Issue
Block a user
Part of #69.
Вопрос
Чем именно генератор порождает четыре класса расхождений и опоздания, что из этого обещано воспроизводимо и что хранит опись мира?
Мастер-спека (раздел 4) даёт механику и ориентиры долей: отмены ~5%, потерянные события ~3%, дельта суммы ~1–2%, дубли ~2%, опоздания ~10%, — и говорит, что точные доли фиксируются при пересборке эталонного мира.
Отдельный вопрос порядка: по страховочному срезу 2 расхождения B и D входят в этап 6 (#8). Но если генератор научится им позже, мир придётся переигрывать — значит механику для них решать здесь, даже если сверка по ним пишется потом.
Хвост от закрытого #71 — три вещи, которые там решены и сюда приходят готовыми.
Опоздание считается от дня снятия слепка, а не отправки. #71 решил, что даг,
играющий день D, отправляет слепок дня D−1 — ночная выгрузка за вчера. Сдвиг
транспортный, содержимое слепка от него не зависит. Если считать опоздание от дня
отправки, сдвиг удвоится и обещанные «~10% приезжают в слепке D+1/D+2» станут
D+2/D+3.
Вся судьба заказа обязана уложиться в окно K либо не случиться вовсе. Правило
шире момента оплаты: отмена подчиняется тому же пределу. Заказ, отменённый на
восьмой день, в слепок уже не попадёт и навсегда останется в прежнем статусе — а
тогда обещание «за окном заказ неизменяем» окажется ложью, и
revenue_daily_vразойдётся с миром без объяснения.
Появился исход, которого нет в таблице раздела 4 мастер-спеки. Момент оплаты —
бросок по таблице весов, и вес за краем окна означает «не оплачен никогда»: заказ
навсегда в
createdпри доехавшем событииpurchase. Суммы сходятся, событие наместе, выручки за ним нет — по нынешним классам это
match. Получает ли этотслучай своё имя в
mismatch_class— решать здесь.Целиком резолюция — в #71.
Резолюция
Ревизия 3: два холодных ревью, второе — узкое, по переписанному. Что изменилось
против первой редакции — в конце, разделом «Что поправило ревью».
Рамка. Расхождение — не грязь в данных и не шум, а часть мира: судьба
заказа, решённая при его рождении и уложенная в окно K целиком.
1. Где живут броски
Два подпотока дня:
Оба берутся у того дня, которому их предмет принадлежит по рождению. Событиям
это ничего не добавляет — они и так живут в своём дне; заказу добавляет, потому
что слепок несёт семь дней рождения сразу и судьбу каждого заказа обязан читать
из его собственного дня (#71).
Имена
DISCREPANCIESиLATECOMERSвseeds.pyрешения не переживают: ониназваны по классам витрины, а компонент называет часть мира. Занимают новые
стороны те же позиции — из мёртвых имён никто не бросает, так что переименование
ничего не сдвигает. Как их звать — при реализации.
Довод за разделение — различимость по описи: правка заказной механики не двигает
хеши событий, правка событийной не двигает байты слепка, и по покрасневшим хешам
видно, какую сторону трогали. Приём в репозитории не новый: хеш каталога лежит в
описи ровно затем, чтобы отличить «правили CSV» от «правили код»
(
inventory.py, шапка модуля).Правило формы, без которого разделение не работает: броски заказной стороны
делаются на полную длину дня, а не на отобранных заказах. Соблазн бросать
«какую позицию вычеркнуть» длиной в число заказов с дельтой велик и обходится
дорого: длина броска становится функцией доли, и правка доли перебрасывает весь
подпоток после себя. В генераторе такое место уже есть —
_at_least_oneбросаетдлиной в число опустевших групп, поэтому смена одного процента в
ABANDONED_POSITION_PERCENTперекладывает торговый подпоток целиком. Изоляциюдаёт форма броска, а не число подпотоков.
Отклонено: один компонент на всю судьбу — правка событийной механики молча
меняла бы байты слепка; компонент на класс — пять имён под ручки калибровки,
которые всё равно крутятся разом при пересборке.
2. Путь заказа по статусам
Три исхода, все внутри окна: оплачен; оплачен и отменён; не оплачен и
отменён.
Инвариант на выходе из окна: заказ либо
paid, либоcancelled.created—только промежуточное состояние, живое внутри окна; в последнем слепке заказа его
не бывает. Довод: за окном будущего у заказа нет —
paidон уже не станет, — азаказ, навсегда застрявший в
created, это не «висит, отменят позже», амодельная небрежность, которой в выгрузке живого магазина соответствия нет.
Отсюда снимается вопрос, переданный сюда из #71: исход «навсегда
created» невозникает, и нового значения
mismatch_classне нужно —cancelledпокрывает обе дороги. (Шестое значение там уже занято:
awaiting_order,мастер-спека §7.)
Форма броска
долю неоплаченных явной строкой.
0…143, плюс равномерная секунда внутри часа.
второй выбрасывается. Где их два по существу, ранний считается оплатой —
условной точки отсчёта и правила «отмена после оплаты» не нужно, порядок выходит
сортировкой. Бросать «два там, где их два» нельзя: длина броска стала бы
функцией долей исходов, и правка доли переложила бы весь подпоток после себя
(правило формы, §1).
143 часа — это самый узкий край окна: слепок снимается на границе суток, поэтому
у заказа, рождённого в 23:50, до последнего его слепка 144 часа, а у рождённого
в 00:05 — почти 168. Таблица кончается там, где кончается окно у самого
невезучего, и дальше в ней просто ничего нет: вылезти нечему, сторожа не нужно.
Цена — заказ, рождённый в начале суток, не использует почти сутки своего окна
(167:55 против 143:59:59): его судьба всегда решается раньше края. В хвосте
таблицы веса мизерные, поэтому в данных это не видно.
Это отменяет форму, записанную в #71 («вес за краем окна означает „не оплачен
никогда“»). Причины две. Исход, который тот вес кодировал, в #72 убран: такой
заказ теперь отменяется, и ему нужен второй момент. И край окна не выражается
числом часов — одна и та же строка почасовой таблицы для одного заказа внутри
окна, для другого за краем, отчего доля неоплаченных стала бы функцией часа
покупки: купил вечером — реже платишь. Наклона этого никто не выбирал, а менти
нашёл бы его первым же разрезом по часу.
Из #71 остаётся нетронутым: момент — бросок, а не правило; целые веса; хвост
меряется в часах, но тянется на дни; секунда внутри часа, чтобы
updated_at − created_atне давал точных равенств (урок правки #50); и «правилаусечения не нужно ни в каком виде» — теперь оно выполняется строже прежнего.
Доли и что из этого видно
Ориентир мастер-спеки ~5% читается как доля отменённых вообще; как она делится
между двумя дорогами — строки таблицы исходов. Точные числа — калибровка этапа 7;
проверок вида «отмен должно быть от 4 до 6 процентов» не заводим.
Побочный урок, ради которого стоит держать обе дороги: в
dds.orderонинеразличимы — там последняя версия, — а различает их история слепков в
ods.order_snapshot. «Почему для одних отмен выручка убыла, а для других нет» —вопрос, ответ на который оправдывает слой сырья ровно тем, ради чего тот заведён.
Различает не всегда, и обещать лабе полное различение нельзя. Слепок — состояние
на границе суток, поэтому
paidвиден только у заказов, между оплатой и отменойкоторых лежит полночь; оплатили и отменили в один день — обе дороги в сырье
выглядят одинаково. То же у сильно опоздавших: приехав уже терминальным, заказ не
покажет, был ли он оплачен (§5). Насколько велика эта доля, решает калибровка
хвоста моментов.
Отклонено: отмена только после оплаты (строгий префикс цепочки мастер-спеки) —
неоплаченному заказу тогда некуда деться, кроме как остаться брошенным;
мгновенная отмена при рождении — «дыхание» окна на отменах исчезает; чтение
«мир живёт дальше, а окно просто не показывает поздние изменения» — мастер-спека
держит заказ за окном неизменяемым, а не «изменяемым, но не вывозимым»;
отмен нет вовсе — это страховочный срез 1, он в резерве.
3. Дельта суммы (класс C)
Механика — вычеркнутая позиция: товара не оказалось в наличии, позицию сняли.
У заказа на одну позицию меньше, чем в клиентских массивах, а
items_totalменьше на её стоимость.
Момента у неё нет: заказ приезжает урезанным во всех своих слепках. Легенда
держится сама — первый слепок снимается на границе суток, когда склад заказ уже
собрал. Дышать внутри окна умеют статусы, и второй механики дыхания урок класса C
не покупает.
Заказ из одной позиции дельты не получает — пустых заказов не бывает. Позиция
выбирается равновероятно: корреляция со спросом (в жизни вылетает ходовое)
отклонена, потому что на доле 1–2% за восемь дней она статистически
ненаблюдаема — менти платит за неё таблицей чисел мира, а увидеть не может ничем.
Доводы за вычёркивание: остаток видно без микроскопа — это сотни рублей, они
торчат в витрине сверки сами; расхождение объясняется сравнением позиций, то есть
разбором вложенного JSON и
ARRAY JOIN, ровно тем навыком, ради которого позициина стенде разбираются; история рассказывается словами без легенды про генератор.
Отклонено: переоценка позиции — дельта в десятки рублей против сотен, её надо
захотеть заметить; другое количество — то же самое, слабее; чистая дельта без
истории — тупик, объяснить нечем; врёт клиент, а не бэкенд — заказ у нас
проекция той же корзины (#71), и чтобы соврал клиент, править пришлось бы событие
после того, как заказ от него отпочковался.
4. Событийная сторона: потеря (B) и дубль (D)
Глубина порчи у них разная, и разводит их не вкус, а счёт.
purchase, просмотр/confirmationостаётся: события уезжают разными запросами, потерять один и сохранить другой —
обычное дело. И это единственный вариант, при котором потеря видна со стороны
трекера: до страницы подтверждения дошли сто человек, покупок девяносто семь.
в связности легенды, а в том, что точечный дубль ломает урок соседнего класса:
просмотров осталось бы сто, покупок 100 − 3 + 2 = 99, и разрыв воронки, на
котором держится потеря, сжался бы с трёх процентов до одного. При сюжетном
просмотров 102 против 99 покупок — разрыв снова равен доле потерь.
Назначенные планом покупки не теряются.
dds.identity_mapстроится из моста«событие
purchase↔ заказ», и мастер-спека §5 держит на этом лабу склейки:каждый двухкуковый покупатель обязан сделать по заказу с каждой куки, иначе вторая
кука в карту не попадает. Выброшенное событие уносит куку из карты, а опись хранит
число пар — счётчик разошёлся бы с картой по построению, и это отказ лабы, а не
расхождение в данных. Гарантия плана старше расхождения; менти различить не может.
И назначенные планом заказы не опаздывают — та же дыра с другого конца:
опоздавший заказ у края горизонта не попадает ни в один снятый слепок, и кука
выпадает из карты так же надёжно, как от потерянного события.
Дубль случается там, где для него есть место. Соседние визиты куки разведены
ровно на таймаут плюс торговый хвост (
day.py,_starts), и запас над таймаутому прижатых пар — секунды. Дубль через полминуты увёл бы паузу под тридцать минут,
и сборка сессий у менти разошлась бы с
VisitID— сломался бы эталон, радикоторого
VisitIDв потоке и лежит. Правило честно по легенде: кто открыл новыйвизит, старую вкладку не обновляет. Наблюдаемая доля дублей от этого чуть ниже
брошенной, как и у прочих классов.
Прочее об этих двух:
(
TRADE_DELAY_SECONDS): секунды-минуты, короче таймаута визита, поэтомуVisitIDу дубля тот же.purchaseID, суммы и позиции у дубля один в один — в этом и весь бизнес-дубль:посчитал наивно, удвоил выручку.
держит это правило для подтверждения (
day.py: «полночь забирает подтверждениевместе с торговым хвостом или не забирает ни того, ни другого»), и по той же
причине: просмотр без покупки — подделка под класс B.
комментарием в
_seal; дублю то же нужно по обратной причине — ему требуетсякопия уже присвоенного номера.
Отклонено: обе точечные — дубль затирает урок потери; обе сюжетные — потеря
перестаёт быть видна со стороны трекера.
5. Опоздание
Заказ прячется от ранних слепков:
created_atчестный — момент покупки, — нов слепках дней d…d+δ−1 строки нет, а с d+δ она появляется в том состоянии, до
которого заказ дожил.
0(не опаздывает)на подавляющем весе,
1и2. Это и есть «D+1/D+2» мастер-спеки, буквав букву.
приехал бы ровно в одном слепке, и обещание «пропущенный день ничего не ломает,
следующий слепок самовосстанавливает» (мастер-спека §2, ADR 0008) на нём
перестало бы быть верным — потеряли эту пачку, потеряли заказ насовсем. При
δ ≤ 2 у всякого заказа слепков не меньше пяти.
третий приедет в первом же своём слепке как
cancelled—createdего хранилищене увидит никогда. Так и выглядит выгрузка, догоняющая жизнь.
Одно следствие для переливки, но нового в нём нет: партиции всех дней окна
перестраиваются, а не только последнего. Это уже так из-за «дыхания» статусов;
опоздание лишь добавляет, что у дня меняется ещё и состав, а не только суммы.
Отклонено: сдвиг
created_at—dds.orderпартиционируется поtoDate(created_at), заказ уехал бы в чужой день, и «выручка дня D» перестала быотвечать покупкам дня D, то есть сломалась бы та самая сверка, ради которой всё
строится.
6. Пересечения классов и что хранит опись
Броски независимы, пересечения выходят арифметикой сами, и приоритет
мастер-спеки работает по-настоящему. Исключений два, и оба названы выше: дубль
решается только у выживших покупок (дублировать потерянное нечего) и назначенные
планом покупки не теряются. Зависимости «оплата — отмена» среди них нет: обе
дороги перечислены строками таблицы исходов, а не выведены одна из другой.
Вариант «один класс на заказ» отклонён: приоритет в SQL стал бы мёртвой веткой,
которую менти читает как живую.
Следствие для калибровки: брошенная доля и наблюдаемая в сверке — разные
числа. Часть заказов забирают победители по приоритету, часть у класса C
недоступна с самого начала (заказы из одной позиции — их больше половины).
Что кладётся в опись:
судьбе заказов дня. Мастер-спека просит их как самопроверку лабы сверки; сойтись
с запросом менти они могут только на днях, у которых окно закрылось, а таких при
N сыгранных днях ровно N − 7: на стартовом мире (восемь дней) один,
на эталонном (четырнадцать) семь. Считается это из сдвига #71 — прогон дня D
отправляет слепок дня D−1, поэтому последний отправленный слепок несёт день N−2,
а день d закрывается слепком d+6.
складывает строки — дело этапа 4; опись на её форму не опирается.
хешами дней ни при каком раскладе подпотоков — это второй артефакт мира, и до сих
пор опись о нём не знала ничего. Побайтовое обещание на слепок дано в #71
(«переснимается и даёт те же байты»), опись начинает его сторожить.
Разделение труда: хеши сторожат мир, счётчики служат лабе. Порогов нет: ни
«отмен от 4 до 6 процентов», ни сторожа
assert момент <= окно— правило окнавыражено длиной таблицы.
Расхождения с принятым
Вносятся тем же коммитом, что и спека второго источника (заметки карты #69).
Мастер-спека:
created → paid → cancelledдопускает прямой переходcreated → cancelled— неоплаченный заказ отменяют. Статусов по-прежнему три.~5%» читается как доля отменённых вообще, обе дороги вместе.
amount_delta— необъяснённый остаток» читать как «необъяснённый приведением к сравнимой базе»; сравнением позиций он как раз
объясняется, и на этом стоит урок класса C.
наблюдаемые, после приоритета, и считаются заказами.
Спека генератора, §2: перечисление компонентов дня («трафик, торговые события,
расхождения, опоздания») меняется — расхождения и опоздания уступают место
заказной и событийной сторонам.
Резолюция #71: форма броска момента оплаты («вес за краем окна означает „не
оплачен никогда“») отменена, причины — в §2.
CONTEXT.md: у термина «Слепок» строка «один заказ приезжает в столькихслепках, сколько дней окна он прожил» перестаёт быть верной для опоздавших; у
«Описи мира» — «по строке на день» дополняется строкой на слепок. Плюс новый
термин «судьба заказа», несущий уже вторую резолюцию подряд.
Швы соседям
проводе; там же нужен канонический порядок строк внутри слепка — иначе
побайтовое обещание не на чем держать.
мир, — это решается там. И там же ждёт находка холодного ревью, которую #72
закрыть не может: при сдвиге #71 слепок последнего сыгранного дня не
отправляется, а у 18 из 73 пар стартового мира поздний назначенный заказ
падает как раз на этот день (замерено на каноническом зерне). Карта склейки их
не увидит, хотя счётчик пар описи считает — он смотрит только на горизонт
(
plan.py,np.all(pair_order_days < days)), про слепки не зная ничего. Либостартовый мир дошлёт последний слепок, либо счётчик пар научится спрашивать про
слепки.
валидной строкой, потерянное событие не приезжает вовсе. Разговор про контур
проверок качества для расхождений (туман карты #69) естественнее вести там.
по ним пишется там, мир переигрывать не придётся — ради чего тикет и заводился.
Форму строк витрины сверки резолюция за них не решает.
доли исходов, таблица моментов, доли классов, таблица задержки опоздания и
следом счётчики описи. Устройство не потребует.
Что поправило ревью
Холодное ревью в двух линзах (дефекты и связность; уместность и вычитание) против
первой ревизии:
с условной точкой отсчёта у отмены) не выражала инвариант окна: край окна
меряется сутками, а таблица часами.
следующий бросок переменного размера. На его месте — правило формы в §1.
визиты, у вычёркивания позиции не был назван момент.
про пропущенный день.
_sealобещает про потерю, не про дубль), числозначений
mismatch_class(шестое занятоawaiting_order) и границы: форму строквитрины сверки решает этап 4, а не эта резолюция.
Второе ревью, узкое, по переписанному (граница окна выведена независимо и сошлась,
оба новых правила в коде осуществимы —
assigned_orderуже лежит вplan.DayAudience, а покупка всегда последнее событие своего визита):дверь из двух: заказ у края горизонта рвёт карту склейки не только потерянным
событием, но и опозданием.
суток, поэтому оплата и отмена в один день в сырье неразличимы.
шести» — верно ни для стартового мира, ни для эталонного).
а не «длиной 143 часа»: единица там решает.
Поправка после решения «Форма записи слепка на проводе»
Горячее ревью решения о формате слепка
нашло ошибку нейминга в этой резолюции. Агенту, который будет собирать решения
карты в единую спецификацию, нельзя переносить фразы, где
created_atназван«моментом покупки»,
updated_at - created_atчитается как длительностьбизнес-перехода, а
toDate(created_at)— как бизнес-день заказа.Каноническое чтение после решения о формате слепка:
created_atиupdated_at— время создания и последнего изменения строки почасам базы источника;
purchase.UTCEventTime;от этого не меняется;
toDate(created_at)— день создания строки источника, не доказательство днябизнес-события.
Поправка меняет только смысл временных полей. Механика судеб заказов, статусов,
опозданий и расхождений из резолюции остаётся в силе.