[HIGH] tradein/estimate: якорь дома и знаменатель коэффициента выкупа читают is_active без фильтра свежести — протухшие объявления напрямую двигают цену #2656

Closed
opened 2026-08-05 15:16:32 +00:00 by bot-backend · 4 comments
Collaborator

Найдено при разборе пункта 3 #2574 («14 314 объявлений числятся активными, не показавшись 30+ дней»). Пять независимых разведок, код читан на origin/main, числа сняты с tradein-postgres 2026-08-05.

Гипотеза была «фантомные строки попадают в выборку аналогов». Она опровергнута: радиусный пул (Tier S/H/W) их не пускает. Но деньги двигаются через две другие двери, и обе шире.

Что защищено, и чем именно

_COMMON_WHERE (estimator.py:4770-4802, побайтовая inline-копия в Tier W :5221) несёт is_active = true AND scraped_at > NOW() - 14 days плюс сегментный гард #1186. На проде «свежий scraped_at при протухшем last_seen_at» не существует ни в одной из 10 групп source×segment — 0 строк. Типовой запрос «2к 44 м² ЕКБ»: 1487 аналогов, фантомных 0.

Важно, чем именно защищено: фильтром свежести, а не сегментом. Из 14 314 фантомов 13 571 — новостройки, их отсекает сегмент; но NULL-сегмент проходит все гарды и протух на 96.4% (743 из 771). Если кто-то ослабит fresh_days или заменит scraped_at на last_seen_at, весь пул войдёт в аналоги. Это стоит закрепить тестом.

Дыра 1 — якорь дома (прямые деньги)

_fetch_anchor_comps, Tier A estimator.py:1885 и Tier C :1977: в обоих SQL только WHERE is_active = true AND price_per_m2 > 0. Ни scraped_at, ни last_seen_at — grep по last_seen_at во всём estimator.py даёт ноль совпадений.

При срабатывании якорь замещает headline: median_ppm2 = new_ppm2, median_price = point (:2728), n_analogs = anchor["n"] (:2739), а с ними confidence и объяснение. Порог — estimate_sb_min_comps=4.

Прод:

  • 21 132 из 37 497 активных строк протухли по 14-дневной мерке самого эстиматора, но полностью годны для якоря.
  • 268 из 553 адресных групп набирают порог 4 только за счёт протухших; 25 групп не имеют ни одного свежего аналога.
  • Tier A: 31 дом из 952 получает якорь исключительно благодаря протухшим, 7 якорей построены на них на 100%, сдвиг медианы дома до +19.6%.
  • Tier C на реальных запросах (100 последних оценок 2к 40-50 м²): якорь срабатывает в 76 случаях из 100, фантомы в пуле у 7, доля в пуле до 58%, сдвиг до +3.0%.

Направление ошибки системное: протухшие строки в среднем на 39% дороже живых (111 702 против 155 022 ₽/м²) — дорогое висит дольше и первым выпадает из выдачи. То есть фантом в тонком пуле тянет якорь вверх.

Дыра 2 — знаменатель коэффициента выкупа (прямые деньги)

asking_to_sold_ratio.py:157 (ask_side) и :217 (ask_global) — фильтра свежести нет ни в одной из двух CTE. Множитель применяется прямо к деньгам: expected_sold_price = round(median_price * effective_ratio) (estimator.py:3053-3054).

Контрфакт на проде: 648 фантомов из 13 158 завышают ask_median 144 392 → 142 571 (+1.28% глобально), в бакете 44-62 м² 128 151 → 125 000 (+2.52%). Завышенный знаменатель занижает коэффициент, а значит и выкупную цену — ровно на столько же.

Отдельная асимметрия: числитель оценки строится на 14-дневной популяции, а знаменатель коэффициента — на бессрочной. Калибровка и применение считаются по разным рынкам.

Чем чинить

Один и тот же предикат, который уже стоит в _COMMON_WHERE, в четыре места: два SQL якоря и две CTE коэффициента.

Но мерить надо ДО мержа. 268 из 553 адресных групп перестанут набирать порог 4 и уедут в ДКП-фолбэк или в insufficient_data. Это честнее, чем якорь на мёртвых объявлениях, но это заметное изменение поведения: нужно посчитать, сколько оценок сменит путь и насколько сдвинется итоговая цена, и решить, не пора ли одновременно пересмотреть min_comps=4.

Корень проблемы шире фикса: TTL деактивации (30 дней у cian/yandex-вторички, NULL-сегмент — никогда) разошёлся с окном свежести эстиматора (14 дней). Чинить со стороны потребителя дешевле и лечит сразу всех читателей: is_active сейчас означает разное для разных источников, а scraped_at — одно и то же.

Связано: #2574 (пункт 3), #2583 (H2), #2629, #1186, #1774, #2206.

Найдено при разборе пункта 3 #2574 («14 314 объявлений числятся активными, не показавшись 30+ дней»). Пять независимых разведок, код читан на `origin/main`, числа сняты с `tradein-postgres` 2026-08-05. Гипотеза была «фантомные строки попадают в выборку аналогов». Она **опровергнута**: радиусный пул (Tier S/H/W) их не пускает. Но деньги двигаются через две другие двери, и обе шире. ## Что защищено, и чем именно `_COMMON_WHERE` (`estimator.py:4770-4802`, побайтовая inline-копия в Tier W `:5221`) несёт `is_active = true AND scraped_at > NOW() - 14 days` плюс сегментный гард #1186. На проде «свежий `scraped_at` при протухшем `last_seen_at`» не существует ни в одной из 10 групп source×segment — 0 строк. Типовой запрос «2к 44 м² ЕКБ»: 1487 аналогов, фантомных **0**. Важно, чем именно защищено: **фильтром свежести, а не сегментом**. Из 14 314 фантомов 13 571 — новостройки, их отсекает сегмент; но NULL-сегмент проходит все гарды и протух на 96.4% (743 из 771). Если кто-то ослабит `fresh_days` или заменит `scraped_at` на `last_seen_at`, весь пул войдёт в аналоги. Это стоит закрепить тестом. ## Дыра 1 — якорь дома (прямые деньги) `_fetch_anchor_comps`, Tier A `estimator.py:1885` и Tier C `:1977`: в обоих SQL только `WHERE is_active = true AND price_per_m2 > 0`. Ни `scraped_at`, ни `last_seen_at` — grep по `last_seen_at` во всём `estimator.py` даёт **ноль совпадений**. При срабатывании якорь **замещает** headline: `median_ppm2 = new_ppm2`, `median_price = point` (`:2728`), `n_analogs = anchor["n"]` (`:2739`), а с ними confidence и объяснение. Порог — `estimate_sb_min_comps=4`. Прод: - 21 132 из 37 497 активных строк протухли по 14-дневной мерке **самого эстиматора**, но полностью годны для якоря. - 268 из 553 адресных групп набирают порог 4 **только** за счёт протухших; 25 групп не имеют ни одного свежего аналога. - Tier A: 31 дом из 952 получает якорь исключительно благодаря протухшим, 7 якорей построены на них на 100%, сдвиг медианы дома до **+19.6%**. - Tier C на реальных запросах (100 последних оценок 2к 40-50 м²): якорь срабатывает в 76 случаях из 100, фантомы в пуле у 7, доля в пуле до 58%, сдвиг до +3.0%. Направление ошибки системное: протухшие строки в среднем **на 39% дороже** живых (111 702 против 155 022 ₽/м²) — дорогое висит дольше и первым выпадает из выдачи. То есть фантом в тонком пуле тянет якорь **вверх**. ## Дыра 2 — знаменатель коэффициента выкупа (прямые деньги) `asking_to_sold_ratio.py:157` (`ask_side`) и `:217` (`ask_global`) — фильтра свежести нет ни в одной из двух CTE. Множитель применяется прямо к деньгам: `expected_sold_price = round(median_price * effective_ratio)` (`estimator.py:3053-3054`). Контрфакт на проде: 648 фантомов из 13 158 завышают `ask_median` 144 392 → 142 571 (**+1.28%** глобально), в бакете 44-62 м² 128 151 → 125 000 (**+2.52%**). Завышенный знаменатель занижает коэффициент, а значит и выкупную цену — ровно на столько же. Отдельная асимметрия: **числитель оценки строится на 14-дневной популяции, а знаменатель коэффициента — на бессрочной**. Калибровка и применение считаются по разным рынкам. ## Чем чинить Один и тот же предикат, который уже стоит в `_COMMON_WHERE`, в четыре места: два SQL якоря и две CTE коэффициента. **Но мерить надо ДО мержа.** 268 из 553 адресных групп перестанут набирать порог 4 и уедут в ДКП-фолбэк или в `insufficient_data`. Это честнее, чем якорь на мёртвых объявлениях, но это заметное изменение поведения: нужно посчитать, сколько оценок сменит путь и насколько сдвинется итоговая цена, и решить, не пора ли одновременно пересмотреть `min_comps=4`. Корень проблемы шире фикса: TTL деактивации (30 дней у cian/yandex-вторички, NULL-сегмент — никогда) разошёлся с окном свежести эстиматора (14 дней). Чинить со стороны потребителя дешевле и лечит сразу всех читателей: `is_active` сейчас означает разное для разных источников, а `scraped_at` — одно и то же. Связано: #2574 (пункт 3), #2583 (H2), #2629, #1186, #1774, #2206.
Author
Collaborator

Замер перед правкой. Прогнаны все 1040 записей trade_in_estimates через настоящую _price_from_inputs в контейнере (деплойнутый код побайтово равен origin/main, сверено по md5), по четыре прогона каждая: как есть / только фильтр якоря / только фильтр знаменателя / обе правки. Само-сверка построенного пула с настоящей _fetch_anchor_comps — 0 расхождений на 1014×4 прогонах.

Три поправки к тому, что я написал выше

1. «268 из 553 адресных групп» — верно, но это не про запросы. По адресным группам подтвердилось (269 из 554). Но по РЕАЛЬНЫМ запросам якорь теряют 139 из 678 якорных = 20.5%, то есть 13.7% всех оценок. Адресные группы сильно переоценивают эффект: тонкие протухшие группы редко попадают под живой запрос.

2. «Протухшие на 39% дороже живых» — верно только для всей популяции is_active, которую тянут новостройки (их якорь почти не пускает). В сегменте, из которого якорь реально строится (вторичка + NULL): +7.7%, а не +39%. Направление то же, величина впятеро меньше.

3. «Знаменатель завышен на 1.28% глобально» — не воспроизводится для 14-дневного фильтра по scraped_at. Тот счёт был по фантомам last_seen_at. По scraped_at > 14д глобальный коэффициент уходит вниз на 0.49%, вверх двигаются только бакеты 44-62 (+2.43%) и 62-85 (+4.58%). Причина: в знаменателе живёт только вторичка и NULL, а в этом сегменте протухшее дешевле свежего.

Что будет, если смержить

значение
теряют якорь 139 из 1014 (13.7%), все Tier C, Tier A не теряет ни одной
куда уезжают радиусный путь 127, ДКП-фолбэк 6, insufficient_data 6
рост отказов 79 → 85, то есть +0.59% оценок
сдвиг цены, фильтр якоря сумма выкупа −3.63%
сдвиг цены, фильтр знаменателя +1.25%
вместе −2.42%
двигаются больше чем на 10% 111 оценок (10.9%), худший случай −48.6%

У сохранивших якорь сдвиг симметричный, а не «всё вверх»: 241 вверх, 227 вниз, медиана 0.00%. Медианный размер пула 19 → 13.

Существенно: потеря якоря шире потери порога. У 106 из 139 пул после фильтра всё ещё ≥4 — их валят гейты, потому что тоньше пул → выше штраф в FSD → confidence low → подавление.

Порог трогать не надо

сценарий якорь insufficient Δ суммы выкупа confidence low
сейчас 678 79 221
фильтр + min=3 676 83 −1.45% 179
фильтр + min=4 546 85 −2.42% 236
фильтр + min=5 490 89 −3.34% 261
фильтр + min=6 468 91 −3.45% 267

Повышать нельзя — хуже по всем осям. Понижение до 3 — единственная реальная компенсация (покрытие восстанавливается почти байт-в-байт), но оно обнуляет гейт тонкого пула estimate_sb_gate_min_n=3, который условен на n < 3. Это отдельное решение под бэктест, не в этот PR.

Попутная находка

anchor_tier остаётся равным "C", когда _compute_same_building_anchor вернула None, и этот залипший флаг молча глушит IMV-blend (estimator.py:2770). Из-за него 24 оценки, у которых якоря нет ни до, ни после, всё равно меняют цену. Поведение с фильтром становится корректным случайно — это надо починить явно, тем же PR.

NULL-сегмент закрывается сам

97.4% NULL-строк протухли. В знаменателе их 674 из 13 097, после фильтра остаётся 20 (0.22%). Причём они дорогие (медиана 162 981 против 142 857 у вторички), то есть именно они и завышали ask. Отдельный разговор про NULL этой правке не нужен — но у источника проблема остаётся: у yandex NULL 537 из 544 протухших, у cian 214 из 224, TTL-деактивация их не трогает (#2659).

Ловушка деплоя

asking_to_sold_ratios пересчитывается ночным заданием (06:00-07:00 UTC). То есть после деплоя одну ночь будет действовать только якорная половина, то есть полные −3.63% без компенсирующего +1.25%. Либо дёргать пересчёт вручную сразу после деплоя, либо знать об этом.

Именно поэтому обе половины надо мержить одним PR: разнесённые по релизам они дадут два заметных скачка цены вместо одного меньшего.


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

Замер перед правкой. Прогнаны **все 1040 записей `trade_in_estimates`** через настоящую `_price_from_inputs` в контейнере (деплойнутый код побайтово равен `origin/main`, сверено по md5), по четыре прогона каждая: как есть / только фильтр якоря / только фильтр знаменателя / обе правки. Само-сверка построенного пула с настоящей `_fetch_anchor_comps` — 0 расхождений на 1014×4 прогонах. ## Три поправки к тому, что я написал выше **1. «268 из 553 адресных групп» — верно, но это не про запросы.** По адресным группам подтвердилось (269 из 554). Но по РЕАЛЬНЫМ запросам якорь теряют **139 из 678 якорных = 20.5%, то есть 13.7% всех оценок**. Адресные группы сильно переоценивают эффект: тонкие протухшие группы редко попадают под живой запрос. **2. «Протухшие на 39% дороже живых» — верно только для всей популяции `is_active`**, которую тянут новостройки (их якорь почти не пускает). В сегменте, из которого якорь реально строится (вторичка + NULL): **+7.7%**, а не +39%. Направление то же, величина впятеро меньше. **3. «Знаменатель завышен на 1.28% глобально» — не воспроизводится** для 14-дневного фильтра по `scraped_at`. Тот счёт был по фантомам `last_seen_at`. По `scraped_at > 14д` глобальный коэффициент уходит **вниз** на 0.49%, вверх двигаются только бакеты 44-62 (+2.43%) и 62-85 (+4.58%). Причина: в знаменателе живёт только вторичка и NULL, а в этом сегменте протухшее **дешевле** свежего. ## Что будет, если смержить | | значение | |---|---| | теряют якорь | 139 из 1014 (13.7%), **все Tier C**, Tier A не теряет ни одной | | куда уезжают | радиусный путь 127, ДКП-фолбэк 6, **`insufficient_data` 6** | | рост отказов | 79 → 85, то есть **+0.59% оценок** | | сдвиг цены, фильтр якоря | сумма выкупа **−3.63%** | | сдвиг цены, фильтр знаменателя | **+1.25%** | | **вместе** | **−2.42%** | | двигаются больше чем на 10% | 111 оценок (10.9%), худший случай −48.6% | У сохранивших якорь сдвиг **симметричный**, а не «всё вверх»: 241 вверх, 227 вниз, медиана 0.00%. Медианный размер пула 19 → 13. Существенно: потеря якоря шире потери порога. У 106 из 139 пул после фильтра всё ещё ≥4 — их валят гейты, потому что тоньше пул → выше штраф в FSD → confidence `low` → подавление. ## Порог трогать не надо | сценарий | якорь | insufficient | Δ суммы выкупа | confidence low | |---|---|---|---|---| | сейчас | 678 | 79 | — | 221 | | фильтр + min=3 | 676 | 83 | −1.45% | 179 | | **фильтр + min=4** | 546 | 85 | −2.42% | 236 | | фильтр + min=5 | 490 | 89 | −3.34% | 261 | | фильтр + min=6 | 468 | 91 | −3.45% | 267 | Повышать нельзя — хуже по всем осям. Понижение до 3 — единственная реальная компенсация (покрытие восстанавливается почти байт-в-байт), но оно **обнуляет гейт тонкого пула** `estimate_sb_gate_min_n=3`, который условен на `n < 3`. Это отдельное решение под бэктест, не в этот PR. ## Попутная находка `anchor_tier` остаётся равным `"C"`, когда `_compute_same_building_anchor` вернула `None`, и этот залипший флаг молча глушит IMV-blend (`estimator.py:2770`). Из-за него 24 оценки, у которых якоря нет ни до, ни после, всё равно меняют цену. Поведение с фильтром становится корректным случайно — это надо починить явно, тем же PR. ## NULL-сегмент закрывается сам 97.4% NULL-строк протухли. В знаменателе их 674 из 13 097, после фильтра остаётся **20 (0.22%)**. Причём они дорогие (медиана 162 981 против 142 857 у вторички), то есть именно они и завышали ask. Отдельный разговор про NULL этой правке не нужен — но у источника проблема остаётся: у `yandex` NULL 537 из 544 протухших, у `cian` 214 из 224, TTL-деактивация их не трогает (#2659). ## Ловушка деплоя `asking_to_sold_ratios` пересчитывается ночным заданием (06:00-07:00 UTC). То есть после деплоя одну ночь будет действовать **только якорная половина, то есть полные −3.63% без компенсирующего +1.25%**. Либо дёргать пересчёт вручную сразу после деплоя, либо знать об этом. Именно поэтому обе половины надо мержить **одним PR**: разнесённые по релизам они дадут два заметных скачка цены вместо одного меньшего. --- **Решение о мерже — твоё.** Правка технически бесспорна: сегодня цена местами строится на объявлениях, которых никто не видел месяц. Но она снижает предлагаемую клиенту сумму в среднем на 2.42%, а в отдельных случаях сильно больше, и это уже вопрос политики, а не кода. Реализацию подготовлю и отдам на ревью; мержить не буду, пока не скажешь.
Author
Collaborator

Реализация в PR #2661 (не мержу — решение о мерже за владельцем, правка снижает предлагаемую клиенту сумму на 2.42% в среднем).

Реализация в PR #2661 (не мержу — решение о мерже за владельцем, правка снижает предлагаемую клиенту сумму на 2.42% в среднем).
Author
Collaborator

Перезамер 12.08 (детали и таблицы — в PR #2661).

Дефект в силе, проверено по git show origin/main: оба SQL якоря и обе CTE коэффициента по-прежнему читают is_active без фильтра свежести. Соседние правки (#2797 TTL, #2771 координаты, #2818 адреса) чинили поставщика данных, а не потребителей.

Но цена дефекта упала почти до нуля. Той же оснасткой, на тех же 1052 живых оценках:

05.08 12.08
сумма выкупа, полная ветка −2.25% +0.09%
теряют якорь 139 из 1014 (13.7%) 57 из 1052 (5.4%) (и 11 приобретают)
рост отказов 79 → 85 нет (#2823 снял гейт обнуления headline)
премия протухших в якорном сегменте +7.7% +3.3%

Причина — состав данных, не код:

  1. Вторичка перестала протухать. avito 7629 активных вторичных строк — 0% протухших, domklik 752 — 0%, cian 20.6%, yandex 21.6%. Протухшая масса (19 989 из 45 030) на 84% новостройки, которых гард #1186 в якорь почти не пускает. Утверждение шапки «21 132 из 37 497 активных строк протухли и были годны для якоря» сегодня надо читать как «19 989 из 45 030, и годны для якоря из них немногие».
  2. Знаменатель частично перевернулся. Протухшая часть теперь — cian/yandex вторичка (2428 строк, медианы 128 906 / 137 179 ₽/м²), она ДЕШЕВЛЕ свежей популяции (147 137). Дорогой NULL-сегмент (654 строки) её больше не перевешивает. Контрфакт шапки «648 фантомов завышают ask_median на +1.28%» не воспроизводится ни в ту, ни в другую сторону: фильтр теперь ask_median поднимает (147 137 → 148 648) и глобальный коэффициент опускает на 1.06%.

Сверка направления на известных объектах (914 оценок против медианы ДКП по тому же дому, ≤150 м, 12 мес.): там, где правка шевелит цену на ≥5%, новое число ближе к сделкам — медиана ошибки 22.90% → 19.56%, ближе 91 из 147.

Вывод для #2656: формулировка «протухшие объявления напрямую двигают цену» верна и сейчас (57 оценок теряют якорь, у 765 из 923 пул якоря режется в медиане на 20%, 66 оценок двигаются больше чем на 10%), но «двигают ВВЕРХ и это стоит клиенту 2.25%» — уже нет. Число зависит от состава собранных данных и за неделю уехало на 2.3 п.п.; любое решение по нему стоит принимать со свежим замером.

Перезамер 12.08 (детали и таблицы — в [PR #2661](https://git.gendsgn.ru/lekss361/gendesign/pulls/2661#issuecomment-25282)). **Дефект в силе**, проверено по `git show origin/main`: оба SQL якоря и обе CTE коэффициента по-прежнему читают `is_active` без фильтра свежести. Соседние правки (#2797 TTL, #2771 координаты, #2818 адреса) чинили поставщика данных, а не потребителей. **Но цена дефекта упала почти до нуля.** Той же оснасткой, на тех же 1052 живых оценках: | | 05.08 | 12.08 | |---|---|---| | сумма выкупа, полная ветка | −2.25% | **+0.09%** | | теряют якорь | 139 из 1014 (13.7%) | **57 из 1052 (5.4%)** (и 11 приобретают) | | рост отказов | 79 → 85 | **нет** (#2823 снял гейт обнуления headline) | | премия протухших в якорном сегменте | +7.7% | **+3.3%** | Причина — состав данных, не код: 1. **Вторичка перестала протухать.** avito 7629 активных вторичных строк — 0% протухших, domklik 752 — 0%, cian 20.6%, yandex 21.6%. Протухшая масса (19 989 из 45 030) на 84% новостройки, которых гард #1186 в якорь почти не пускает. Утверждение шапки «21 132 из 37 497 активных строк протухли и были годны для якоря» сегодня надо читать как «19 989 из 45 030, и годны для якоря из них немногие». 2. **Знаменатель частично перевернулся.** Протухшая часть теперь — cian/yandex вторичка (2428 строк, медианы 128 906 / 137 179 ₽/м²), она ДЕШЕВЛЕ свежей популяции (147 137). Дорогой NULL-сегмент (654 строки) её больше не перевешивает. Контрфакт шапки «648 фантомов завышают ask_median на +1.28%» не воспроизводится ни в ту, ни в другую сторону: фильтр теперь `ask_median` **поднимает** (147 137 → 148 648) и глобальный коэффициент опускает на 1.06%. Сверка направления на известных объектах (914 оценок против медианы ДКП по тому же дому, ≤150 м, 12 мес.): там, где правка шевелит цену на ≥5%, новое число ближе к сделкам — медиана ошибки 22.90% → 19.56%, ближе 91 из 147. **Вывод для #2656:** формулировка «протухшие объявления напрямую двигают цену» верна и сейчас (57 оценок теряют якорь, у 765 из 923 пул якоря режется в медиане на 20%, 66 оценок двигаются больше чем на 10%), но «двигают ВВЕРХ и это стоит клиенту 2.25%» — уже нет. Число зависит от состава собранных данных и за неделю уехало на 2.3 п.п.; любое решение по нему стоит принимать со свежим замером.
lekss361 added the
bug
priority/p1
scope/backend
tradein
labels 2026-08-16 10:25:11 +00:00
Author
Collaborator

Проверено на проде: фикс не только смержен, но и виден в данных — закрываю

PR #2661 смержен 26.08 13:31 UTC. Проверял не по диффу, а по трём независимым срезам: код в main, код на машине, числа в таблице коэффициентов.

1. Все четыре места несут фильтр

Место Файл
Якорь Tier A estimator.py:2270scraped_at > NOW() - (:fresh_days || ' days')::interval
Якорь Tier C estimator.py:2366
ask_side tasks/asking_to_sold_ratio.py:173
ask_global tasks/asking_to_sold_ratio.py:236

LISTINGS_FRESH_DAYS = 14 — тот же источник, что у _COMMON_WHERE, а не отдельная константа.

2. На проде ровно этот код

md5 обоих файлов в контейнере tradein-backend совпал с блобом main побайтово:

990284eaf45b07564605c5da0baade97  app/services/estimator.py
da212ef6e50d7413171e672a97fee31f  app/tasks/asking_to_sold_ratio.py

3. Главное — фильтр виден в самих числах

Таблица asking_to_sold_ratios пересчитана 27.08 06:12 UTC. Сравнил хранящиеся n_listings с двумя вариантами пересчёта — с фильтром свежести и без:

бакет в таблице пересчёт с фильтром пересчёт без фильтра
0 1731 1731 1964
1 4307 4305 5033
2 4907 4907 5470
3 4114 4113 4449
4 2566 2565 2710

Совпадение с фильтрованным вариантом — точное (расхождение в 1-2 строки набежало за часы после пересчёта), с нефильтрованным — на 8-14%. То есть знаменатель коэффициента выкупа сегодня считается по той же 14-дневной популяции, что и числитель оценки. Асимметрия «калибровка и применение по разным рынкам», ради которой задача заводилась, устранена в данных, а не только в коде.

ask_median при этом сдвинулся мало (бакет 1: 145 780 → 145 328, бакет 4: 162 091 → 162 545) — согласуется с перезамером 12.08, где цена дефекта упала почти до нуля из-за состава данных.

Что НЕ закрыто этой задачей

Корень, названный в шапке, остаётся: TTL деактивации (30 дней у cian/yandex, NULL-сегмент — никогда) разошёлся с 14-дневным окном свежести, то есть is_active по-прежнему означает разное у разных источников. Фикс закрыл потребителей (якорь и знаменатель), поставщика — нет. Это отдельно: #2994 (26 203 мёртвых объявления числятся активными).

Закрываю: предмет задачи — четыре места без фильтра свежести — на проде отсутствует.

## Проверено на проде: фикс не только смержен, но и виден в данных — закрываю PR #2661 смержен 26.08 13:31 UTC. Проверял не по диффу, а по трём независимым срезам: код в main, код на машине, **числа в таблице коэффициентов**. ### 1. Все четыре места несут фильтр | Место | Файл | |---|---| | Якорь Tier A | `estimator.py:2270` — `scraped_at > NOW() - (:fresh_days \|\| ' days')::interval` | | Якорь Tier C | `estimator.py:2366` | | `ask_side` | `tasks/asking_to_sold_ratio.py:173` | | `ask_global` | `tasks/asking_to_sold_ratio.py:236` | `LISTINGS_FRESH_DAYS = 14` — тот же источник, что у `_COMMON_WHERE`, а не отдельная константа. ### 2. На проде ровно этот код md5 обоих файлов в контейнере `tradein-backend` совпал с блобом `main` побайтово: ``` 990284eaf45b07564605c5da0baade97 app/services/estimator.py da212ef6e50d7413171e672a97fee31f app/tasks/asking_to_sold_ratio.py ``` ### 3. Главное — фильтр виден в самих числах Таблица `asking_to_sold_ratios` пересчитана 27.08 06:12 UTC. Сравнил хранящиеся `n_listings` с двумя вариантами пересчёта — с фильтром свежести и без: | бакет | в таблице | пересчёт **с** фильтром | пересчёт **без** фильтра | |---:|---:|---:|---:| | 0 | 1731 | 1731 | 1964 | | 1 | 4307 | 4305 | 5033 | | 2 | 4907 | 4907 | 5470 | | 3 | 4114 | 4113 | 4449 | | 4 | 2566 | 2565 | 2710 | Совпадение с фильтрованным вариантом — точное (расхождение в 1-2 строки набежало за часы после пересчёта), с нефильтрованным — на 8-14%. То есть знаменатель коэффициента выкупа сегодня считается по той же 14-дневной популяции, что и числитель оценки. Асимметрия «калибровка и применение по разным рынкам», ради которой задача заводилась, устранена в данных, а не только в коде. `ask_median` при этом сдвинулся мало (бакет 1: 145 780 → 145 328, бакет 4: 162 091 → 162 545) — согласуется с перезамером 12.08, где цена дефекта упала почти до нуля из-за состава данных. ### Что НЕ закрыто этой задачей Корень, названный в шапке, остаётся: TTL деактивации (30 дней у cian/yandex, NULL-сегмент — никогда) разошёлся с 14-дневным окном свежести, то есть `is_active` по-прежнему означает разное у разных источников. Фикс закрыл потребителей (якорь и знаменатель), поставщика — нет. Это отдельно: #2994 (26 203 мёртвых объявления числятся активными). Закрываю: предмет задачи — четыре места без фильтра свежести — на проде отсутствует.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#2656
No description provided.