tradein/pricing: у 4+ комнат коэффициент выкупа больше единицы — новостроечный гард стоит только на одной стороне, а deals.rooms обрезан на 4 #2620

Closed
opened 2026-08-02 09:21:29 +00:00 by lekss361 · 1 comment
Owner

Найдено при глубоком ревью PR #2617 (скоуп городов в коэффициенте asking→sold). Изначально выглядело как шум малой выборки — оказалось методической проблемой.

Симптом

Бакет «4 и более комнат» даёт ratio = sold_median / ask_median > 1, то есть сделки дороже объявлений. До фикса #2617 — 1.0500, после — 1.0272. Для вторичного рынка это неправдоподобно: продают обычно ниже запрошенного, все остальные бакеты дают 0.73-0.97.

Выборки при этом большие, на шум не спишешь: n_deals = 1476, n_listings = 782.

Две структурные причины

1. Новостроечный гард только на стороне объявлений. В ask_side/ask_global стоит AND (listing_segment IS NULL OR listing_segment = 'vtorichka') (гард #1186). На стороне сделок такого фильтра нет — и не может быть: в таблице deals маркера сегмента не существует вовсе (проверено по information_schema). То есть объявления очищены от новостроек, а сделки — нет.

2. Разная трактовка «4+». deals.rooms обрезан на четырёх — строк с rooms > 4 в таблице ноль. А в listings в бакет 4 сваливаются пяти-, шести-, семи- и даже десятикомнатные: 110 из 782. Сравниваются разные совокупности.

Почему это не горит

estimator.py:2911-2919 — настройка ESTIMATE_EXPECTED_SOLD_LE_ASKING (по умолчанию True, на проде не переопределена) клампит коэффициент выше единицы до 1.0. То есть на цену клиента это сейчас не влияет: и 1.0500, и 1.0272 превращаются в 1.0.

Но кламп — это защита от симптома, а не лечение. Он маскирует то, что коэффициент для этого бакета попросту неверен, и любое изменение настройки или методики немедленно вытащит проблему наружу. Плюс сама величина попадает в админку и может быть прочитана как «по четырёхкомнатным продают дороже запрошенного», что неправда.

Что делать

  • Уравнять совокупности по числу комнат: либо ограничить listings бакет ровно четырьмя комнатами, либо (лучше) понять, почему deals.rooms обрезан, и снять обрезку, если она искусственная.
  • Разобраться с новостройками на стороне сделок: если сегмент невосстановим, оценить долю новостроек в сделках по 4+ и решить, корректен ли бакет вообще, или его надо схлопывать в глобальный фолбэк.
  • Проверить остальные бакеты на тот же перекос — он может быть меньше, но присутствовать.

Замеры для отправной точки

Коэффициенты на 2026-08-02 (до применения #2617): global 0.888, студии 0.7482, однушки 0.8169, двушки 0.8776, трёшки 0.9712, четырёшки+ 1.0503.

Фактические коэффициенты по городам (посчитаны при ревью #2617): Екатеринбург 0.823, Нижний Тагил 0.822, Первоуральск 0.847, Верхняя Пышма 0.692, Каменск-Уральский 0.921, Серов 0.920 — отдельная тема, единый екатеринбургский коэффициент применяется ко всем городам (#647).

Связано: #2583, PR #2617, #1186, #647.

Найдено при глубоком ревью PR #2617 (скоуп городов в коэффициенте asking→sold). Изначально выглядело как шум малой выборки — оказалось методической проблемой. ## Симптом Бакет «4 и более комнат» даёт `ratio = sold_median / ask_median > 1`, то есть **сделки дороже объявлений**. До фикса #2617 — 1.0500, после — 1.0272. Для вторичного рынка это неправдоподобно: продают обычно ниже запрошенного, все остальные бакеты дают 0.73-0.97. Выборки при этом большие, на шум не спишешь: `n_deals = 1476`, `n_listings = 782`. ## Две структурные причины **1. Новостроечный гард только на стороне объявлений.** В `ask_side`/`ask_global` стоит `AND (listing_segment IS NULL OR listing_segment = 'vtorichka')` (гард #1186). На стороне сделок такого фильтра нет — и не может быть: в таблице `deals` маркера сегмента не существует вовсе (проверено по `information_schema`). То есть объявления очищены от новостроек, а сделки — нет. **2. Разная трактовка «4+».** `deals.rooms` обрезан на четырёх — строк с `rooms > 4` в таблице ноль. А в `listings` в бакет 4 сваливаются пяти-, шести-, семи- и даже десятикомнатные: **110 из 782**. Сравниваются разные совокупности. ## Почему это не горит `estimator.py:2911-2919` — настройка `ESTIMATE_EXPECTED_SOLD_LE_ASKING` (по умолчанию `True`, на проде не переопределена) клампит коэффициент выше единицы до `1.0`. То есть **на цену клиента это сейчас не влияет**: и 1.0500, и 1.0272 превращаются в 1.0. Но кламп — это защита от симптома, а не лечение. Он маскирует то, что коэффициент для этого бакета попросту неверен, и любое изменение настройки или методики немедленно вытащит проблему наружу. Плюс сама величина попадает в админку и может быть прочитана как «по четырёхкомнатным продают дороже запрошенного», что неправда. ## Что делать - Уравнять совокупности по числу комнат: либо ограничить `listings` бакет ровно четырьмя комнатами, либо (лучше) понять, почему `deals.rooms` обрезан, и снять обрезку, если она искусственная. - Разобраться с новостройками на стороне сделок: если сегмент невосстановим, оценить долю новостроек в сделках по 4+ и решить, корректен ли бакет вообще, или его надо схлопывать в глобальный фолбэк. - Проверить остальные бакеты на тот же перекос — он может быть меньше, но присутствовать. ## Замеры для отправной точки Коэффициенты на 2026-08-02 (до применения #2617): global 0.888, студии 0.7482, однушки 0.8169, двушки 0.8776, трёшки 0.9712, четырёшки+ **1.0503**. Фактические коэффициенты по городам (посчитаны при ревью #2617): Екатеринбург 0.823, Нижний Тагил 0.822, Первоуральск 0.847, Верхняя Пышма 0.692, Каменск-Уральский 0.921, Серов 0.920 — отдельная тема, единый екатеринбургский коэффициент применяется ко всем городам (#647). Связано: #2583, PR #2617, #1186, #647.
Collaborator

Закрыто в PR #2648 (merged + прод-verified принудительным refresh):

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

Фикс (обе стороны): (1) ask_side/ask_global бакетируются тем же area-CASE; (2) находка deep-review — эстиматор ПРИМЕНЯЛ коэффициент по реальным rooms клиента (29.9% исторических запросов легли бы в другой бакет) → _get_asking_sold_ratio ключуется по area_bucket(payload.area_m2). Границы — одна истина в трёх представлениях (shell/SQL/Python) с guard-тестом на дрейф.

Прод после refresh (08:17): 4+ 1.0266 → 0.8315, ряд 0.76–0.91 консистентен, n_listings 4+ 789→1405. Кламп остаётся страховкой, но для 4+ больше не маскирует ошибку.

Новостроечный гард на deals-стороне честно не добавлен (year_built-прокси сдвигал все бакеты равномерно; сделки 4+ содержат 44% year_build≥2020 vs 23% в 1–3к — ограничение задокументировано в докстринге). Остальные бакеты проверены — перекос был во всех, фикс уравнял все.

Закрыто в PR #2648 (merged + прод-verified принудительным refresh): **Корень (замеры)**: `deals.rooms` — 100% синтетика из площади при импорте (`import-rosreestr.sh` CASE 30/44/62/85; подтверждено: ноль исключений на границах). Сравнение area-синтетики сделок с реальными rooms объявлений мигрировало 23–55% объявлений между бакетами — не только 4+. **Фикс (обе стороны)**: (1) ask_side/ask_global бакетируются тем же area-CASE; (2) находка deep-review — эстиматор ПРИМЕНЯЛ коэффициент по реальным rooms клиента (29.9% исторических запросов легли бы в другой бакет) → `_get_asking_sold_ratio` ключуется по `area_bucket(payload.area_m2)`. Границы — одна истина в трёх представлениях (shell/SQL/Python) с guard-тестом на дрейф. **Прод после refresh (08:17)**: 4+ **1.0266 → 0.8315**, ряд 0.76–0.91 консистентен, n_listings 4+ 789→1405. Кламп остаётся страховкой, но для 4+ больше не маскирует ошибку. Новостроечный гард на deals-стороне честно не добавлен (year_built-прокси сдвигал все бакеты равномерно; сделки 4+ содержат 44% year_build≥2020 vs 23% в 1–3к — ограничение задокументировано в докстринге). Остальные бакеты проверены — перекос был во всех, фикс уравнял все.
Sign in to join this conversation.
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#2620
No description provided.