Перейти к основному содержимому

Правило выбора наблюдений: пустое не затирает непустое

РЕШЕНО владельцем 13.08.2026. Оглавление — ../LOGIKA-VYBORA-NABLYUDENIY.md.

Зачем это вообще

Владелец 13.08: «данных мы копим тысячи, а потом мы не можем логически взять что-то более адекватное. Если даже данные какие-то потерялись, почему они пропадают, я не понимаю».

Ниже — ответ на «почему пропадают» и предлагаемое правило выбора.

Сначала развести три разные беды

Их постоянно смешивают, и от этого решения получаются неверные.

БедаПрирода
1Склад сырья пухнет: прогоны идут по нескольку раз в день, эндпоинтов свыше 125, s2 собирает своёхранение
2При нескольких наблюдениях одного и того же за день мы не выбираем — последний пишет поверхвыбор
3Данные пропадаютследствие пункта 2, не отдельная беда

Этот документ — только про пункт 2. Пункт 1 требует отдельного решения и намеренно не смешивается с ним (см. хвост документа).

Почему данные пропадают — установлено на данных

Замер 13.08 по трём прогонам одного эндпоинта за одну дату:

прогонвремясырых ответовиз них с обогащением
572704:24369226
5807 (сбой)16:46369165
5824 (перезапуск)17:21369226

61 артикул «потерял» 28-дневные показатели вечером. При этом они лежали в базе — в сырых ответах утреннего прогона, под тем же ключом, в той же таблице.

Причина одна и она в писателе. Разбор идёт тем же проходом, что и сбор, видит только свои ответы и пишет:

INSERT ... ON DUPLICATE KEY UPDATE `столбец` = VALUES(`столбец`)   -- для ВСЕХ столбцов

Ключ [offer_id, snapshot_date], индекс uq_offer_date. Ничего не удаляется — строка обновляется на месте, и пустое значение переписывает хорошее.

Основание правила: что означает строка

Строка за дату — это то, что мы знаем про артикул за 13 августа, а не отпечаток ответа в 16:46.

Как только это принято, правило выводится само: строка за дату есть накопитель наблюдений этой даты, а не снимок последнего ответа.

Чем рискуем, удержав значение (две породы чисел)

Правило одно, но последствия у него разные — это важно понимать, а не заводить из-за этого два правила.

породапримерменяется ли в течение дняриск удержания
«сколько лежит»current_stock, наличие на складахда, весь деньнебольшой: цифра суточной давности вместо пустоты
«как продавалось»ads_28d, idc_28d, liquidity_grade, days_without_salesнет: площадка считает ночью один раз и весь день отдаёт одно и то женикакого

По остаткам риск почти умозрительный: список приходит одним запросом целиком, наполовину пустым он не бывает — либо пришёл, либо прогон прерывается (if not turnover: return).

Отложенный вопрос: перенос ЧЕРЕЗ сутки

Владелец 13.08: «даже если в течение следующих суток данные не прилетают… средняя продажа в день не меняется 5 дней. То есть сутки для нас это даже не предел».

Замечание по делу, но это отдельное решение, и вот почему. Внутри суток мы значение наблюдали — удерживая его, мы не сочиняем. Перенеся его на следующую дату, мы запишем в строку за 14 августа число, которого 14 августа не видели. Снаружи наблюдённое и перенесённое станут неотличимы.

Честный способ это сделать существует: переносить вместе с отметкой, когда значение замерено на самом деле (отдельная колонка происхождения). Тогда строка говорит «0,7, замерено 11 августа», и ничего не выдумано.

Это большее изменение, чем правило внутри суток, и в один заход с ним не идёт.

Ядро правила (первая редакция, ОТМЕНЕНО — оставлено как разбор)

Ниже — исходное деление на три класса. Оно не применяется: проверка показала, что правило для классов Б и В получается одно и то же, а разница лежит в риске, а не в правиле. Оставлено, чтобы не проделывать этот путь заново.

Разные поля означают разное, поэтому одного правила на всех быть не может.

Класс А — ключ

offer_id, snapshot_date. По нему опознаётся строка, не меняется.

Класс Б — мгновенное состояние

current_stock и прочие счётчики наличия. Меняется в течение дня, позднее наблюдение вернее раннего.

Правило: побеждает последнее непустое.

Класс В — суточная сводка

ads_28d, idc_28d, days_without_sales, liquidity_grade, turnover_days, idc_grade, turnover_grade. Это агрегаты за 28 и 60 дней, площадка пересчитывает их раз в сутки. Внутри дня они не меняются, значит любое наблюдение этого дня равноценно.

Правило: побеждает любое непустое; пустым не затирать.

Здесь и есть корень потерь: сейчас класс В обрабатывается как класс Б — суточную сводку переписывает мгновенный ответ, которому нечего сказать.

Почему «пустым не затирать» здесь безопасно

Обычное возражение верное: если запретить затирать пустым, замрёт последнее известное значение и будет выдавать себя за свежее. Ровно та беда, с которой мы воюем.

Но возражение верно между днями и неверно внутри дня. Ключ содержит snapshot_date: завтра заводится новая строка, пустая с нуля. Значит устаревание физически ограничено сутками — больше одного дня стухшее значение прожить не может.

Это надо принять как сознательное решение, а не получить побочным эффектом.

Отдельный случай, который надо назвать вслух: если площадка отдавала ликвидность в 04:24, а к 16:46 артикул выбыл — правило удержит утреннее значение до конца суток. Это верно: мы действительно наблюдали его в этот день. Завтра строка будет пустой с самого начала.

Где считать — и почему НЕ в сырье

Соблазн: разбирать заново из всех сырых ответов за дату, выбирая лучшее. Не нужно и дорого — при нынешнем объёме склада это скан.

Строка за дату уже является накопителем. Значит сравнивать надо входящее наблюдение с тем, что уже лежит в строке: одна операция на строку, без скана.

ON DUPLICATE KEY UPDATE
current_stock = VALUES(current_stock), -- класс Б
liquidity_grade = COALESCE(VALUES(liquidity_grade), liquidity_grade), -- класс В
ads_28d = COALESCE(VALUES(ads_28d), ads_28d), -- класс В
...

Сырьё остаётся тем, чем должно быть: следом для разбирательства и источником для восстановления, а не участником ежедневного расчёта.

Место правки одно — spec_runtime/next_mapper.py, write_intake_row: сейчас там безусловное \c`=VALUES(`c`)для всего спискаon_duplicate_update`. Класс поля объявляется в спецификации эндпоинта рядом с самим полем.

Восстановление — отдельный инструмент, не горячий путь

Переразбор из сырья за дату нужен, но как починка по требованию, а не часть ежедневного прогона. Колонка raw_payload.processed_at для этого и заведена.

Что правило чинит и чего не чинит

Чинит: класс потерь целиком и сразу для всех эндпоинтов. Случай 13.08 не возник бы, ручной перезапуск не понадобился бы.

Не чинит: разрастание склада сырья. Это беда №1, и её решение — отдельное.