tradein: два видимых пользователю числа отдаются без гейта правдоподобия — «медианный торг +36%» и индекс локации с шумом ±20 п.п. #2666

Open
opened 2026-08-05 18:09:07 +00:00 by bot-backend · 3 comments
Collaborator

Найдено при глубоком ревью PR #2664 (сегментный гард в «медианном торге», свежесть в индексе локации). Сам PR правильный и мержится; здесь — то, что он делает заметнее, но не создаёт.

1. «Медианный торг» может показать плюс, и это не опечатка

Виджет сравнения сделок с объявлениями отдаёт median_discount_pct без всякой проверки правдоподобия. После сегментного гарда поштучные значения разъехались: %Космонавтов% 2-комнатные — было −11.9%, стало +36.4%, то есть пользователю будет написано «продали на 36% дороже, чем просили».

Корень унаследованный, не в гарде: пейринг идёт по улице без номера дома (data_quality="street_only", ADR #721). На длинной улице сделка и объявление могут быть в разных домах и разных ценовых классах. Гард убрал 225 предвзятых пар «ДКП в новостройке ↔ лот застройщика» — это его цель, — но у 74 сделок партнёра подставили: DISTINCT ON выбирает следующее вторичное объявление вместо новостроечного.

Гард односторонний по необходимости: у deals нет listing_segment и взять его неоткуда.

Что предлагается: не отдавать median_discount_pct, когда пар слишком мало, либо когда значение выходит за санитарный диапазон (положительный торг в разы — это почти наверняка артефакт пейринга, а не рынок). Форма отказа уже отработана в других местах продукта: показать «данных недостаточно» честнее, чем показать неправдоподобное число.

2. Индекс локации несёт ±20 п.п. шума и не сообщает об этом

После правки локальная выборка в центре ЕКБ — 86 объявлений вместо 423. Доверительный интервал медианы по порядковым статистикам при таком n даёт [225 829; 283 700] ₽/м², то есть показанные +77% локационной премии несут около ±20 п.п. чистого шума выборки.

Видно и по чувствительности к окну: 7 дней → +75.7%, 14 → +77.0%, 21 → +79.1%, 30 → +64.7%, 60 → +64.6%. Разброс 12-14 п.п. при том, что локационная премия — структурная величина и за неделю меняться не должна.

Важная деталь, которая объясняет, почему это не аргумент против PR: из −14.8% сдвига городской медианы −14.7 п.п. дал сегментный гард и всего −0.18 п.п. фильтр свежести. То есть свежесть здесь почти не исправляет смещение (мертвецы живут в новостройках, которые и так отсекает сегмент), а платит за это третью пула и удвоением шума. Правка меняет смещение на дисперсию — размен верный, старое число было предвзятым, — но дисперсия стала доминирующей ошибкой.

Варианты, решать тебе:

  • Расширить окно именно для этой метрики до 30 дней: пул по городу +35%, локально в центре +27%, городская медиана сдвигается на 1.5%, единственная точка отказа исчезает. Цена — вторая константа свежести в коде, чего мы сознательно избегали (витрина должна смотреть на тот же пул, что цена).
  • Показывать размер выборки рядом с индексом — тогда пользователь видит, насколько оценка широкая. Дешевле по бэкенду, честнее по сути, но требует фронта.
  • Оставить как есть — число предвзятым быть перестало, а шум был и раньше, просто прятался за большой выборкой из мертвецов и новостроек.

3. Побочно: витрина теперь зависит от здоровья сбора

Фильтр свежести связал индекс локации со скрейпингом: если сбор встанет на 14 дней, городская медиана недоберёт порог выборки и insufficient_data прилетит всем пользователям сразу. Раньше виджет продолжал показывать устаревшее число.

Деградация честная и поведение правильное. Но учитывая, что #2574 — это месяц молчаливой поломки сбора, сценарий не гипотетический, и стоит понимать: с этого момента поломка сбора становится видна пользователю, а не только в админке.

Связано: #2660, #2664, #2574, ADR #721.

Найдено при глубоком ревью PR #2664 (сегментный гард в «медианном торге», свежесть в индексе локации). Сам PR правильный и мержится; здесь — то, что он делает **заметнее**, но не создаёт. ## 1. «Медианный торг» может показать плюс, и это не опечатка Виджет сравнения сделок с объявлениями отдаёт `median_discount_pct` без всякой проверки правдоподобия. После сегментного гарда поштучные значения разъехались: `%Космонавтов%` 2-комнатные — было −11.9%, стало **+36.4%**, то есть пользователю будет написано «продали на 36% дороже, чем просили». Корень унаследованный, не в гарде: **пейринг идёт по улице без номера дома** (`data_quality="street_only"`, ADR #721). На длинной улице сделка и объявление могут быть в разных домах и разных ценовых классах. Гард убрал 225 предвзятых пар «ДКП в новостройке ↔ лот застройщика» — это его цель, — но у 74 сделок партнёра **подставили**: `DISTINCT ON` выбирает следующее вторичное объявление вместо новостроечного. Гард односторонний по необходимости: у `deals` нет `listing_segment` и взять его неоткуда. **Что предлагается:** не отдавать `median_discount_pct`, когда пар слишком мало, либо когда значение выходит за санитарный диапазон (положительный торг в разы — это почти наверняка артефакт пейринга, а не рынок). Форма отказа уже отработана в других местах продукта: показать «данных недостаточно» честнее, чем показать неправдоподобное число. ## 2. Индекс локации несёт ±20 п.п. шума и не сообщает об этом После правки локальная выборка в центре ЕКБ — 86 объявлений вместо 423. Доверительный интервал медианы по порядковым статистикам при таком n даёт `[225 829; 283 700]` ₽/м², то есть показанные **+77% локационной премии несут около ±20 п.п. чистого шума выборки**. Видно и по чувствительности к окну: 7 дней → +75.7%, 14 → +77.0%, 21 → +79.1%, **30 → +64.7%**, 60 → +64.6%. Разброс 12-14 п.п. при том, что локационная премия — структурная величина и за неделю меняться не должна. Важная деталь, которая объясняет, почему это не аргумент против PR: из −14.8% сдвига городской медианы **−14.7 п.п. дал сегментный гард и всего −0.18 п.п. фильтр свежести**. То есть свежесть здесь почти не исправляет смещение (мертвецы живут в новостройках, которые и так отсекает сегмент), а платит за это третью пула и удвоением шума. Правка меняет смещение на дисперсию — размен верный, старое число было предвзятым, — но дисперсия стала доминирующей ошибкой. **Варианты, решать тебе:** - **Расширить окно именно для этой метрики** до 30 дней: пул по городу +35%, локально в центре +27%, городская медиана сдвигается на 1.5%, единственная точка отказа исчезает. Цена — вторая константа свежести в коде, чего мы сознательно избегали (витрина должна смотреть на тот же пул, что цена). - **Показывать размер выборки рядом с индексом** — тогда пользователь видит, насколько оценка широкая. Дешевле по бэкенду, честнее по сути, но требует фронта. - **Оставить как есть** — число предвзятым быть перестало, а шум был и раньше, просто прятался за большой выборкой из мертвецов и новостроек. ## 3. Побочно: витрина теперь зависит от здоровья сбора Фильтр свежести связал индекс локации со скрейпингом: если сбор встанет на 14 дней, городская медиана недоберёт порог выборки и `insufficient_data` прилетит **всем пользователям сразу**. Раньше виджет продолжал показывать устаревшее число. Деградация честная и поведение правильное. Но учитывая, что #2574 — это месяц молчаливой поломки сбора, сценарий не гипотетический, и стоит понимать: с этого момента поломка сбора становится видна пользователю, а не только в админке. Связано: #2660, #2664, #2574, ADR #721.
Author
Collaborator

Working on this in PR #2671 (первый пункт issue — гейт правдоподобия на «медианный торг»).

Working on this in PR #2671 (первый пункт issue — гейт правдоподобия на «медианный торг»).
Author
Collaborator

Проверка на проде 2026-08-07: п.1 закрыт, п.2 сузился до двух вариантов из трёх

п.1 (гейт правдоподобия) — выполнен. Прогнал 427 реальных адресов из trade_in_estimates
через задеплоенный обработчик get_sales_vs_listings внутри tradein-backend:

групп с парами:            135
показывается медиана:       33
погашено:                  102   (мало пар 62 · одно объявление 26 · неправдоподобно 14)
погашено БЕЗ объяснения:     0

Форма отказа, которую задача и просила («показать „данных недостаточно“ честнее, чем
неправдоподобное число»), работает на всех 102 случаях. Ужесточён в #2672/PR #2706.

п.2 (шум индекса локации) — один из трёх вариантов уже реализован, и это надо знать до решения.

Вариант «показывать размер выборки рядом с индексом» помечен в задаче как «требует фронта».
Проверил: он уже на фронте с 26.07LocationDrawer.tsx:470 печатает
«Посчитано по N объявлениям в радиусе …», а при insufficient_data — «Рядом нашлось только N
сопоставимых объявлений». Пришло с PR #2531 (commit 580be619), то есть за десять дней до
заведения этой задачи
. API отдаёт sample_size там же (trade_in.py:1670).

Значит выбор остался между двумя: расширить окно до 30 дней (вторая константа свежести) или
оставить как есть. LISTINGS_FRESH_DAYS = 14 (estimator.py:95) с 05.08 не менялся, правок в
location_index.py после 837ad8cf нет.

п.3 действий не просит — это предупреждение о новой зависимости витрины от здоровья сбора.

Задачу оставляю открытой под п.2 — это решение владельца, а не работа.

## Проверка на проде 2026-08-07: п.1 закрыт, п.2 сузился до двух вариантов из трёх **п.1 (гейт правдоподобия) — выполнен.** Прогнал 427 реальных адресов из `trade_in_estimates` через **задеплоенный** обработчик `get_sales_vs_listings` внутри `tradein-backend`: ``` групп с парами: 135 показывается медиана: 33 погашено: 102 (мало пар 62 · одно объявление 26 · неправдоподобно 14) погашено БЕЗ объяснения: 0 ``` Форма отказа, которую задача и просила («показать „данных недостаточно“ честнее, чем неправдоподобное число»), работает на всех 102 случаях. Ужесточён в #2672/PR #2706. **п.2 (шум индекса локации) — один из трёх вариантов уже реализован, и это надо знать до решения.** Вариант «показывать размер выборки рядом с индексом» помечен в задаче как «требует фронта». Проверил: он **уже на фронте с 26.07** — `LocationDrawer.tsx:470` печатает «Посчитано по N объявлениям в радиусе …», а при `insufficient_data` — «Рядом нашлось только N сопоставимых объявлений». Пришло с PR #2531 (commit `580be619`), то есть **за десять дней до заведения этой задачи**. API отдаёт `sample_size` там же (`trade_in.py:1670`). Значит выбор остался между двумя: **расширить окно до 30 дней** (вторая константа свежести) или **оставить как есть**. `LISTINGS_FRESH_DAYS = 14` (`estimator.py:95`) с 05.08 не менялся, правок в `location_index.py` после `837ad8cf` нет. **п.3** действий не просит — это предупреждение о новой зависимости витрины от здоровья сбора. Задачу оставляю открытой под п.2 — это решение владельца, а не работа.
Author
Collaborator

Прод 12.08: п.1 закрыт, в п.2 не осталось работы — только невыбранное решение

п.1 (гейт правдоподобия на «медианный торг») — закрыт и с 07.08 не менялся: из 135 групп с парами медиана показывается в 33, погашено 102 (мало пар 62 · одно объявление 26 · неправдоподобно 14), погашено без объяснения — 0. Форма отказа, которую задача и просила, работает на всех 102 случаях; ужесточён в #2672 / PR #2706. Правок в этом месте на origin/main с тех пор нет.

п.2 (индекс локации несёт ±20 п.п. шума). Из трёх вариантов задачи один — «показывать размер выборки рядом с индексом» — был реализован до её заведения (#2531, 26.07, LocationDrawer.tsx:470). Остались два, и оба это решение, а не работа:

  • расширить окно именно для этой метрики до 30 дней. Замечу деталь, которой в задаче не было: возражение «цена — вторая константа свежести в коде, чего мы сознательно избегали» уже частично оплачено — рядом с LISTINGS_FRESH_DAYS = 14 (estimator.py:95) на main живёт LISTINGS_FRESH_DAYS_RELAXED = 60, заведённая для областных участков. Вторая константа свежести в коде появилась по другому поводу, так что аргумент «этого мы не делаем никогда» больше не абсолютен;
  • оставить как есть — сегодняшнее де-факто состояние. LISTINGS_FRESH_DAYS = 14 не менялся, последняя правка location_index.pyd1731630 (#2660), после неё изменений нет.

Задачу оставляю открытой, но помечаю честно: кодовой работы в ней не осталось. Это выбор между двумя вариантами, и он ваш. Если выбор — «оставить как есть», задача закрывается без единой правки; если «расширить окно» — это новый PR на одну константу и тест.

п.3 (витрина стала зависеть от здоровья сбора) действий не просил и не просит — это предупреждение, и оно сбылось в мягкой форме: с 06.08 сбор жив по всем источникам кроме Домклика, insufficient_data пользователям не прилетал.

## Прод 12.08: п.1 закрыт, в п.2 не осталось работы — только невыбранное решение **п.1 (гейт правдоподобия на «медианный торг») — закрыт** и с 07.08 не менялся: из 135 групп с парами медиана показывается в 33, погашено 102 (мало пар 62 · одно объявление 26 · неправдоподобно 14), **погашено без объяснения — 0**. Форма отказа, которую задача и просила, работает на всех 102 случаях; ужесточён в #2672 / PR #2706. Правок в этом месте на `origin/main` с тех пор нет. **п.2 (индекс локации несёт ±20 п.п. шума).** Из трёх вариантов задачи один — «показывать размер выборки рядом с индексом» — был реализован **до** её заведения (#2531, 26.07, `LocationDrawer.tsx:470`). Остались два, и оба это решение, а не работа: * **расширить окно** именно для этой метрики до 30 дней. Замечу деталь, которой в задаче не было: возражение «цена — вторая константа свежести в коде, чего мы сознательно избегали» **уже частично оплачено** — рядом с `LISTINGS_FRESH_DAYS = 14` (`estimator.py:95`) на main живёт `LISTINGS_FRESH_DAYS_RELAXED = 60`, заведённая для областных участков. Вторая константа свежести в коде появилась по другому поводу, так что аргумент «этого мы не делаем никогда» больше не абсолютен; * **оставить как есть** — сегодняшнее де-факто состояние. `LISTINGS_FRESH_DAYS` = 14 не менялся, последняя правка `location_index.py` — `d1731630` (#2660), после неё изменений нет. **Задачу оставляю открытой, но помечаю честно: кодовой работы в ней не осталось.** Это выбор между двумя вариантами, и он ваш. Если выбор — «оставить как есть», задача закрывается без единой правки; если «расширить окно» — это новый PR на одну константу и тест. **п.3** (витрина стала зависеть от здоровья сбора) действий не просил и не просит — это предупреждение, и оно сбылось в мягкой форме: с 06.08 сбор жив по всем источникам кроме Домклика, `insufficient_data` пользователям не прилетал.
lekss361 added the
needs-discussion
needs-human
scope/backend
tradein
ux
labels 2026-08-16 10:25:13 +00:00
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#2666
No description provided.