Не выдавать цену, когда рядом нет ни одного аналога #3537

Merged
lekss361 merged 3 commits from fix/estimator-zero-analogs-no-price into main 2026-09-16 17:16:27 +00:00
Owner

Зачем

Прод-репро: «Красногорск, Янтарная» возвращал 236 766 ₽/м² при n_analogs = 0. Тестировщик открывает список аналогов, видит пустоту и справедливо перестаёт верить числу. Доля таких ответов — 3.8% по ЕКБ и 4.8% по 77/50, то есть дефект продукта, а не регионального покрытия.

Откуда бралась цена

Источник ровно один — deals-headline-fallback #oblast-D (estimator.py:4184-4225): когда листинговых аналогов нет совсем, headline берётся из коридора ДКП Росреестра, а n_analogs остаётся 0.

Остальные кандидаты проверены и исключены:

  • _apply_imv_blend — no-op при median_price <= 0, сам блок гейтится median_price > 0;
  • _compute_same_building_anchor — всегда проставляет n_analogs = anchor["n"], по построению не оставляет ноль;
  • deal_city_price_bands — только sanity-clamp правдоподобия сделок, не источник headline;
  • квартальный индекс / segment-multiplier / min-width floor — мультипликативны, при базе 0 дают 0.

Почему гейт не голый n_analogs == 0

Случай #oblast-E (1-4 объявления плюс ≥3 реальные сделки) тоже зануляет n_analogs, но там reliability намеренно закреплена на 'low', а не 'very_low' — коридор из полусотни сделок Росреестра это настоящий сигнал. Гасить его значило бы откатить задокументированное решение от 10.08.2026.

Поэтому условие: n_analogs == 0 AND reliability == "very_low" — уже существующий в коде сигнал «движок сам считает, что данных почти нет».

Что видит фронт

Отказ переиспользует паттерн _empty_estimate (цена/диапазон/ppm² обнулены), но, в отличие от него, сохраняет уже найденные адрес/координаты/дом — рядом с причиной есть что показать. Текст: «Недостаточно данных для точной оценки по этому адресу…».

Флаг estimate_require_analogs (default True) позволяет откатить без релиза.

Test plan

  • Новый tests/test_estimator_requires_analogs.py: полный ноль данных → цена подавлена; репро Красногорска → подавлена, адрес сохранён; флаг False → старое поведение (236 766 ₽/м²) не сломано.
  • pytest по 6 смежным файлам эстиматора → 118 passed (проверено в main-сессии).
  • ruff format + ruff check — чисто.
  • После деплоя: замерить бэктестом дельту no_pred при ESTIMATE_REQUIRE_ANALOGS=false против true — это точная доля ответов, которые станут честным отказом.
  • Фронт: карточка «недостаточно данных» — отдельной задачей, сейчас придёт пустая цена с текстом в confidence_explanation.
## Зачем Прод-репро: «Красногорск, Янтарная» возвращал 236 766 ₽/м² при `n_analogs = 0`. Тестировщик открывает список аналогов, видит пустоту и справедливо перестаёт верить числу. Доля таких ответов — 3.8% по ЕКБ и 4.8% по 77/50, то есть дефект продукта, а не регионального покрытия. ## Откуда бралась цена Источник ровно один — deals-headline-fallback `#oblast-D` (`estimator.py:4184-4225`): когда листинговых аналогов нет совсем, headline берётся из коридора ДКП Росреестра, а `n_analogs` остаётся 0. Остальные кандидаты проверены и исключены: - `_apply_imv_blend` — no-op при `median_price <= 0`, сам блок гейтится `median_price > 0`; - `_compute_same_building_anchor` — всегда проставляет `n_analogs = anchor["n"]`, по построению не оставляет ноль; - `deal_city_price_bands` — только sanity-clamp правдоподобия сделок, не источник headline; - квартальный индекс / segment-multiplier / min-width floor — мультипликативны, при базе 0 дают 0. ## Почему гейт не голый `n_analogs == 0` Случай `#oblast-E` (1-4 объявления **плюс** ≥3 реальные сделки) тоже зануляет `n_analogs`, но там `reliability` намеренно закреплена на `'low'`, а не `'very_low'` — коридор из полусотни сделок Росреестра это настоящий сигнал. Гасить его значило бы откатить задокументированное решение от 10.08.2026. Поэтому условие: `n_analogs == 0 AND reliability == "very_low"` — уже существующий в коде сигнал «движок сам считает, что данных почти нет». ## Что видит фронт Отказ переиспользует паттерн `_empty_estimate` (цена/диапазон/ppm² обнулены), но, в отличие от него, сохраняет уже найденные адрес/координаты/дом — рядом с причиной есть что показать. Текст: «Недостаточно данных для точной оценки по этому адресу…». Флаг `estimate_require_analogs` (default `True`) позволяет откатить без релиза. ## Test plan - [x] Новый `tests/test_estimator_requires_analogs.py`: полный ноль данных → цена подавлена; репро Красногорска → подавлена, адрес сохранён; флаг `False` → старое поведение (236 766 ₽/м²) не сломано. - [x] `pytest` по 6 смежным файлам эстиматора → 118 passed (проверено в main-сессии). - [x] `ruff format` + `ruff check` — чисто. - [ ] После деплоя: замерить бэктестом дельту `no_pred` при `ESTIMATE_REQUIRE_ANALOGS=false` против `true` — это точная доля ответов, которые станут честным отказом. - [ ] Фронт: карточка «недостаточно данных» — отдельной задачей, сейчас придёт пустая цена с текстом в `confidence_explanation`.
lekss361 added 1 commit 2026-09-16 16:22:34 +00:00
fix(estimator): не выдавать цену, когда рядом нет ни одного аналога
Some checks failed
CI / changes (pull_request) Successful in 12s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 9s
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Failing after 4m58s
4f4d8ee929
Прод-репро: «Красногорск, Янтарная» возвращал 236 766 ₽/м² при n_analogs=0.
Тестировщик открывает список аналогов, видит пустоту и справедливо
перестаёт верить числу. Доля таких ответов 3.8% по ЕКБ и 4.8% по 77/50 —
дефект продукта, не регионального покрытия.

Источник цены нашёлся ровно один: deals-headline-fallback #oblast-D
(estimator.py:4184-4225). Когда листинговых аналогов нет совсем, headline
берётся из коридора ДКП Росреестра, а n_analogs остаётся 0. Остальные
кандидаты проверены и исключены: IMV-blend и same-building anchor цену из
воздуха не создают (первый no-op при median_price<=0, второй всегда
проставляет n_analogs=anchor['n']), deal_city_price_bands — только
sanity-clamp правдоподобия сделок.

Гейт сужен до n_analogs==0 AND reliability=='very_low', а не голого
n_analogs==0. Причина: случай #oblast-E (1-4 объявления плюс >=3 реальные
сделки) тоже зануляет n_analogs, но там reliability намеренно закреплена
на 'low' — коридор из полусотни сделок Росреестра это настоящий сигнал, и
гасить его было бы регрессией задокументированного решения от 10.08.2026.

Отказ переиспользует существующий паттерн _empty_estimate, но сохраняет
уже найденные адрес/координаты/дом — фронту есть что показать рядом с
причиной. Флаг estimate_require_analogs позволяет откатить без релиза.
bot-backend added 1 commit 2026-09-16 16:55:12 +00:00
fix(estimator): резать общегородской коридор, а не любую цену без аналогов
Some checks failed
CI / changes (pull_request) Successful in 16s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Failing after 1m23s
CI Trade-In / changes (pull_request) Successful in 13s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
182f1d2802
Первая версия гейта гасила цену всегда при n_analogs==0 — и уронила
контракт #oblast-D: тест test_non_ekb_empty_listings_uses_deals_headline
(Нижний Тагил, 12 сделок по улице) падал. Это не случайный тест, а фича,
без которой в область протекал екатеринбургский asking вшестеро выше.

Проверка на проде показала, где настоящая граница. У Янтарной в
Красногорске ОДНА сделка — меньше DKP_CORRIDOR_CITY_WIDE_MIN_N=3, поэтому
_fetch_dkp_corridor расширился до всего города: 4770 сделок, медиана
200 676 ₽/м². Пользователю показали среднюю цену Красногорска как оценку
его квартиры. А 12 сделок по улице в Тагиле — настоящий сигнал.

Коридор уже сам помечает себя scope='street' | 'city_wide', так что
правило формулируется точно: общегородской коридор не может стать
headline, уличный может. Подавление ответа осталось только оформлением
пустого результата — условие median_price <= 0 намеренно совпадает с
computed-полем AggregatedEstimate.insufficient_data, по которому фронт уже
рисует карточку, чтобы признак был один, а не два расходящихся.

Флаг переименован в estimate_require_local_evidence: он не про «требовать
аналоги», а про «цена обязана опираться на данные этого адреса». Обе части
правила под одним флагом — False возвращает прежнее поведение целиком.
bot-backend added 1 commit 2026-09-16 16:56:44 +00:00
style: ruff format после правки гейта
All checks were successful
CI Trade-In / changes (pull_request) Successful in 11s
CI / changes (pull_request) Successful in 13s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 5m10s
39af474f44
lekss361 merged commit c63c48eea2 into main 2026-09-16 17:16:27 +00:00
lekss361 deleted branch fix/estimator-zero-analogs-no-price 2026-09-16 17:16:27 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
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#3537
No description provided.