fix(tradein): area-бакеты в asking→sold — и расчёт, и применение (#2620) #2648

Merged
bot-backend merged 1 commit from fix/tradein-area-bucket-ratio into main 2026-08-05 08:11:05 +00:00
Collaborator

Summary

#2620: бакет «4+» коэффициента asking→sold давал ratio>1 («сделки дороже объявлений» — клампилось до 1.0, но величина методически неверна).

Корень (прод-замеры): deals.rooms — 100% синтетика из площади при импорте (Росреестр комнатность не отдаёт; import-rosreestr.sh CASE 30/44/62/85, подтверждено нулём исключений на границах). Сравнение area-синтетики сделок с реальными rooms объявлений мигрировало 23–55% объявлений между бакетами.

Фикс, половина 1 (расчёт): ask_side/ask_global бакетируются тем же area-CASE (_AREA_ROOMS_BUCKET_SQL, единая константа со ссылкой на источник истины). Прод-пересчёт: 4+ 1.0257 → 0.8315, все бакеты 0.76–0.91, консистентно; n_listings в 4+ 794→1412.

Половина 2 (применение; находка deep-review, 🟠 HIGH): эстиматор применял коэффициент по реальным payload.rooms — mismatch переезжал из расчёта в применение (29.9% исторических клиентских запросов легли бы в другой бакет; замер на trade_in_estimates). _get_asking_sold_ratio теперь ключуется по area_bucket(payload.area_m2) (rooms — только фолбэк без площади). Границы — одна истина в трёх представлениях (shell/SQL/Python) с guard-тестом на дрейф (парсит числа из SQL-строки).

Плюс: контракт-коммент 080 обновлён (rooms_bucket — легаси-имя, семантика area-based), AND area_m2 IS NOT NULL hardening (latent ELSE-4-trap), честно задокументированное ограничение «сделки содержат новостройки, объявления — нет» (year_built-гард не добавлен — прокси сдвигал все бакеты равномерно, замер в отчёте).

Кламп ESTIMATE_EXPECTED_SOLD_LE_ASKING не тронут (теперь он не понадобится для 4+, но остаётся страховкой).

Test plan

  • Red/green обе половины (area-classification ask-стороны; 4-комн 70м² → бакет 3; area отсутствует → rooms-фолбэк)
  • SQL↔Python boundary guard-тест
  • Прод-спот-чек ревьюером: старая формула 1.0266 / новая 0.8315 — сошлось
  • Полный сьют 3273 passed (1 known pre-existing 401), ruff чисто
  • Post-deploy: следующий refresh джобы перезапишет коэффициенты; спот-чек admin-выдачи бакетов

Review

deep-code-reviewer: 🟠 HIGH по consumer-side gap → закрыт в этом же PR его же рецептом (rekey по area, shared-границы, контракт 080, hardening). SQL-механика первой половины подтверждена (инъекций нет, инвариант 080-subset не выхолощен, blast radius = daily refresh, обратимо).

Refs #2620

## Summary **#2620**: бакет «4+» коэффициента asking→sold давал ratio>1 («сделки дороже объявлений» — клампилось до 1.0, но величина методически неверна). **Корень (прод-замеры)**: `deals.rooms` — 100% синтетика из площади при импорте (Росреестр комнатность не отдаёт; `import-rosreestr.sh` CASE 30/44/62/85, подтверждено нулём исключений на границах). Сравнение area-синтетики сделок с реальными rooms объявлений мигрировало **23–55%** объявлений между бакетами. **Фикс, половина 1 (расчёт)**: `ask_side`/`ask_global` бакетируются тем же area-CASE (`_AREA_ROOMS_BUCKET_SQL`, единая константа со ссылкой на источник истины). Прод-пересчёт: 4+ **1.0257 → 0.8315**, все бакеты 0.76–0.91, консистентно; n_listings в 4+ 794→1412. **Половина 2 (применение; находка deep-review, 🟠 HIGH)**: эстиматор применял коэффициент по реальным `payload.rooms` — mismatch переезжал из расчёта в применение (**29.9%** исторических клиентских запросов легли бы в другой бакет; замер на `trade_in_estimates`). `_get_asking_sold_ratio` теперь ключуется по `area_bucket(payload.area_m2)` (rooms — только фолбэк без площади). Границы — одна истина в трёх представлениях (shell/SQL/Python) с guard-тестом на дрейф (парсит числа из SQL-строки). Плюс: контракт-коммент `080` обновлён (rooms_bucket — легаси-имя, семантика area-based), `AND area_m2 IS NOT NULL` hardening (latent ELSE-4-trap), честно задокументированное ограничение «сделки содержат новостройки, объявления — нет» (year_built-гард не добавлен — прокси сдвигал все бакеты равномерно, замер в отчёте). Кламп `ESTIMATE_EXPECTED_SOLD_LE_ASKING` не тронут (теперь он не понадобится для 4+, но остаётся страховкой). ## Test plan - [x] Red/green обе половины (area-classification ask-стороны; 4-комн 70м² → бакет 3; area отсутствует → rooms-фолбэк) - [x] SQL↔Python boundary guard-тест - [x] Прод-спот-чек ревьюером: старая формула 1.0266 / новая 0.8315 — сошлось - [x] Полный сьют **3273 passed** (1 known pre-existing 401), ruff чисто - [ ] Post-deploy: следующий refresh джобы перезапишет коэффициенты; спот-чек admin-выдачи бакетов ## Review deep-code-reviewer: 🟠 HIGH по consumer-side gap → **закрыт в этом же PR его же рецептом** (rekey по area, shared-границы, контракт 080, hardening). SQL-механика первой половины подтверждена (инъекций нет, инвариант 080-subset не выхолощен, blast radius = daily refresh, обратимо). Refs #2620
bot-backend added 1 commit 2026-08-05 08:07:37 +00:00
fix(tradein): area-бакеты в asking→sold и в расчёте, и в применении (#2620)
All checks were successful
CI / changes (pull_request) Successful in 7s
CI Trade-In / changes (pull_request) Successful in 7s
CI Trade-In / frontend-checks (pull_request) Has been skipped
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) Successful in 2m41s
bcaf413e7c
deals.rooms — синтетика из площади при импорте (Росреестр комнатность не
отдаёт; границы 30/44/62/85 в import-rosreestr.sh). Сравнение
синтетик-бакета сделок с реальным rooms-бакетом объявлений мигрировало
23-55% объявлений между бакетами — отсюда аномалия «4+» ratio>1 (сделки
дороже объявлений). ask_side/ask_global теперь бакетируются тем же
area-CASE: 4+ падает 1.0257→0.8315, все бакеты в 0.76-0.91.

Вторая половина (deep-review): estimator применял коэффициент по реальным
rooms клиента — mismatch переезжал из расчёта в применение (29.9%
исторических запросов легли бы в другой бакет). _get_asking_sold_ratio
ключуется по area_bucket(payload.area_m2) (rooms — только фолбэк без
площади); границы — одна истина в трёх представлениях (shell/SQL/Python)
с guard-тестом на дрейф. Контракт-коммент 080 обновлён (rooms_bucket —
легаси-имя, семантика area-based). Новостроечный гард на deals-стороне
честно НЕ добавлен (year_built-прокси сдвигал все бакеты равномерно) —
задокументировано ограничением.

Refs #2620
bot-backend merged commit 90332e9827 into main 2026-08-05 08:11:05 +00:00
bot-backend deleted branch fix/tradein-area-bucket-ratio 2026-08-05 08:11:05 +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#2648
No description provided.