fix(tradein/estimator): честный asking_to_sold_ratio — бейдж «−N% к рынку» больше не врёт #2152

Merged
lekss361 merged 1 commit from fix/tradein-honest-asking-to-sold-ratio into main 2026-07-02 15:37:01 +00:00
Collaborator

Проблема

Оценщик клиента: «большой интервал между рекомендованной ценой и оценкой». Бейдж «−23% к рынку» (web HeroSummary + PDF, формула round((1−ratio)×100)) систематически завышал скидку.

Root cause

Сохранённый asking_to_sold_ratio — СЫРОЙ per-rooms/tier дисконт из ratio_resolver, но фактический expected_sold сдвинут относительно median×ratio последующими корректировками: hedonic year+area (#2002, factor ∈ [0.75, 1.30], ON by default), le_asking-clamp, corridor-clamp. Стор ratio НЕ пересчитывался.

Прод 451de30b: median 7.75M × raw 0.771 = 5.97M, hedonic ×1.226expected_sold 7.32M, но stored ratio остался 0.771, тогда как факт expected_sold/median = 0.945. Бейдж «−23%» вместо честных «−5%». Аналогично 4605bd18 (факт 0.871), 76539fcd (факт 0.949).

Fix (модельный выбор — recompute ratio, НЕ трогаем expected_sold)

После финализации expected_sold пересчитываем сохранённый asking_to_sold_ratio = expected_sold_price / median_price (честный дескриптор). expected_sold (выкуп) НЕ трогаем — hedonic-uplift легитимен и остаётся прибит к sale-модели; наивный expected = clamped_median × raw_ratio уронил бы выкуп до ~5.97M (то, чего просили избежать). Порог _RATIO_DESCRIPTOR_EPS=1e-4 отсекает шум округления → без сдвига (hedonic OFF, нет клампа) табличный ratio сохраняется байт-в-байт (регрессия отсутствует).

Почему recompute-ratio, а не follow-clamp: stored asking_to_sold_ratio — чисто ДЕСКРИПТОР (web/PDF/history badge), НЕ калибровочный вход. Калибровочный ratio живёт в таблице asking_to_sold_ratios (refresh-task → resolver) — не тронута. Backtest #1966 скорит expected_sold_per_m2 (не stored ratio) → не затронут (expected_sold без изменений). Recompute делает бейдж честным без риска для accuracy.

Читатели stored ratio (аудит)

  • web HeroSummary + PDF _discount_pct — badge round((1−ratio)×100) → теперь честный автоматически (FE не трогаю).
  • /history + GET /estimate — pass-through дескриптора.
  • refresh-task asking_to_sold_ratio.py — пишет ТАБЛИЦУ (source), не читает estimate-колонку → не затронут.
  • backtest #1966 — re-run _price_from_inputs, скорит expected_sold_per_m2 → не затронут.

ДО/ПОСЛЕ (451de30b)

median expected_sold stored ratio badge
ДО 7.75M 7.32M 0.771 −23% (врёт)
ПОСЛЕ 7.75M (=) 7.32M (=) 0.945 −5% (честно)

Follow-up (вне scope)

Corridor-clamp double-discount на expected_sold (accuracy, не badge) — отдельная тема; vault отмечает clamp как «inert на harness», трогать под backtest-контролем.

Test plan

  • ruff check — clean; pytest tests/2749 passed (кроме pre-existing test_search_cache_hit).
  • 3 новых теста: инвариант при hedonic-uplift + corridor-clamp (abs(ratio − expected/median) < 1e-3), byte-identical регрессия без сдвига.

Refs #2141

## Проблема Оценщик клиента: «большой интервал между рекомендованной ценой и оценкой». Бейдж **«−23% к рынку»** (web `HeroSummary` + PDF, формула `round((1−ratio)×100)`) систематически завышал скидку. ## Root cause Сохранённый `asking_to_sold_ratio` — СЫРОЙ per-rooms/tier дисконт из `ratio_resolver`, но фактический `expected_sold` сдвинут относительно `median×ratio` последующими корректировками: **hedonic year+area** (#2002, factor ∈ [0.75, 1.30], ON by default), `le_asking`-clamp, corridor-clamp. Стор ratio НЕ пересчитывался. Прод 451de30b: `median 7.75M × raw 0.771 = 5.97M`, hedonic `×1.226` → `expected_sold 7.32M`, но stored ratio остался `0.771`, тогда как факт `expected_sold/median = 0.945`. Бейдж «−23%» вместо честных «−5%». Аналогично 4605bd18 (факт 0.871), 76539fcd (факт 0.949). ## Fix (модельный выбор — recompute ratio, НЕ трогаем expected_sold) После финализации `expected_sold` пересчитываем сохранённый `asking_to_sold_ratio = expected_sold_price / median_price` (честный дескриптор). **`expected_sold` (выкуп) НЕ трогаем** — hedonic-uplift легитимен и остаётся прибит к sale-модели; наивный `expected = clamped_median × raw_ratio` уронил бы выкуп до ~5.97M (то, чего просили избежать). Порог `_RATIO_DESCRIPTOR_EPS=1e-4` отсекает шум округления → без сдвига (hedonic OFF, нет клампа) табличный ratio сохраняется **байт-в-байт** (регрессия отсутствует). **Почему recompute-ratio, а не follow-clamp:** stored `asking_to_sold_ratio` — чисто ДЕСКРИПТОР (web/PDF/history badge), НЕ калибровочный вход. Калибровочный ratio живёт в таблице `asking_to_sold_ratios` (refresh-task → resolver) — не тронута. Backtest #1966 скорит `expected_sold_per_m2` (не stored ratio) → **не затронут** (expected_sold без изменений). Recompute делает бейдж честным без риска для accuracy. ## Читатели stored ratio (аудит) - web `HeroSummary` + PDF `_discount_pct` — badge `round((1−ratio)×100)` → теперь честный автоматически (FE не трогаю). - `/history` + GET `/estimate` — pass-through дескриптора. - refresh-task `asking_to_sold_ratio.py` — пишет ТАБЛИЦУ (source), не читает estimate-колонку → не затронут. - backtest #1966 — re-run `_price_from_inputs`, скорит `expected_sold_per_m2` → не затронут. ## ДО/ПОСЛЕ (451de30b) | | median | expected_sold | stored ratio | badge | |---|---|---|---|---| | ДО | 7.75M | 7.32M | 0.771 | −23% (врёт) | | ПОСЛЕ | 7.75M (=) | 7.32M (=) | **0.945** | **−5%** (честно) | ## Follow-up (вне scope) Corridor-clamp double-discount на `expected_sold` (accuracy, не badge) — отдельная тема; vault отмечает clamp как «inert на harness», трогать под backtest-контролем. ## Test plan - `ruff check` — clean; `pytest tests/` — **2749 passed** (кроме pre-existing `test_search_cache_hit`). - 3 новых теста: инвариант при hedonic-uplift + corridor-clamp (`abs(ratio − expected/median) < 1e-3`), byte-identical регрессия без сдвига. Refs #2141
bot-backend added 1 commit 2026-07-02 15:26:56 +00:00
fix(tradein/estimator): честный asking_to_sold_ratio — бейдж «−N% к рынку» больше не врёт
All checks were successful
CI / changes (pull_request) Successful in 8s
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
ecc3ab5aab
Оценщик клиента жаловался на «большой интервал между рекомендованной ценой
и оценкой». Разбор: бейдж «−23% к рынку» (web HeroSummary + PDF, формула
round((1−ratio)×100)) систематически завышал скидку.

Root cause: сохранённый asking_to_sold_ratio — это СЫРОЙ per-rooms/tier дисконт
из ratio_resolver, но фактический expected_sold сдвинут относительно median×ratio
последующими корректировками: hedonic year+area (#2002, factor ∈ [0.75, 1.30], ON
by default), le_asking-clamp и corridor-clamp. Пример с прода (451de30b): median
7.75M × raw 0.771 = 5.97M, hedonic ×1.226 → expected_sold 7.32M — но stored ratio
остался 0.771, тогда как фактическое expected_sold/median = 0.945. Бейдж показывал
«−23%» вместо честных «−5%».

Fix: после финализации expected_sold пересчитываем сохранённый asking_to_sold_ratio
как реальное expected_sold_price/median_price (честный дескриптор). Сам expected_sold
(выкуп) НЕ трогаем — hedonic-uplift остаётся прибит к sale-модели, buyout не падает
до наивного median×raw. Порог _RATIO_DESCRIPTOR_EPS=1e-4 отсекает шум округления:
без сдвига (hedonic OFF, нет клампа) табличный ratio сохраняется байт-в-байт →
регрессия на не-зажатых оценках отсутствует.

Стор asking_to_sold_ratio — чисто ДЕСКРИПТОР (web/PDF/history badge), НЕ калибровочный
вход: калибровочный ratio живёт в таблице asking_to_sold_ratios (refresh-task, читает
resolver) — не тронута. Backtest #1966 скорит expected_sold_per_m2 (не stored ratio) —
не затронут (expected_sold без изменений).

Tests: 3 новых в test_estimator_price_spine.py (инвариант при hedonic-uplift +
corridor-clamp; byte-identical регрессия без сдвига); поправлен
test_global_fallback_basis_carried_through (hedonic OFF для сырого ratio).
Full suite: 2749 passed (кроме pre-existing test_search_cache_hit).

Refs #2141
lekss361 merged commit 9d71d94586 into main 2026-07-02 15:37:01 +00:00
lekss361 deleted branch fix/tradein-honest-asking-to-sold-ratio 2026-07-02 15:37:01 +00:00
Sign in to join this conversation.
No reviewers
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#2152
No description provided.