Аналоги подбираются без учёта класса дома — total_floors как прокси серии #3234

Open
opened 2026-08-29 15:19:45 +00:00 by lekss361 · 1 comment
Owner

Радиусный подбор аналогов (tradein-mvp/backend/app/services/estimator.py:2365-2377) фильтрует только по rooms, area_m2, listing_segment='vtorichka', ST_DWithin(..., 500) и свежести. Класс дома не учитывается вообще: пятиэтажная хрущёвка и 25-этажный монолит через дорогу попадают в один пул и одинаково двигают медиану.

total_floors — дешёвый прокси серии/класса (5 — хрущёвка/панель, 9 — брежневка, 10-16 — поздняя панель/кирпич, 17+ — современный монолит). Он есть и в listings, и в houses, и заполнен лучше, чем house_type / series_name / year_built (см. services/domrf_kapremont_loader.py:6 — там прямо сказано, что эти поля дырявые).

Что уже есть и чем НЕ является:

  • services/buildings_query.py:170-172abs(l.total_floors - h.total_floors) <= 3 это санитар привязки объявления к дому, не подбор похожих.
  • core/config.py:402-408 (#680-WB) — гауссиана по относительной позиции floor/total_floors, sigma 0.25. Работает внутри одного дома (Tier A), между домами не применяется.

Предлагаемая форма — полосы этажности с мягкой деградацией, а не жёсткий AND: сначала аналоги в своей полосе, и только если их меньше estimate_sb_min_comps — расширение на соседнюю. Жёсткий фильтр уронит n на окраинах и включит fallback-ветки, которые дают хуже грязного пула.

Перед реализацией нужен замер на проде: доля оценок, где в пуле смешаны полосы, и как смещается медиана при их разделении. Без этого непонятно, стоит ли овчинка выделки.

Радиусный подбор аналогов (`tradein-mvp/backend/app/services/estimator.py:2365-2377`) фильтрует только по `rooms`, `area_m2`, `listing_segment='vtorichka'`, `ST_DWithin(..., 500)` и свежести. Класс дома не учитывается вообще: пятиэтажная хрущёвка и 25-этажный монолит через дорогу попадают в один пул и одинаково двигают медиану. `total_floors` — дешёвый прокси серии/класса (5 — хрущёвка/панель, 9 — брежневка, 10-16 — поздняя панель/кирпич, 17+ — современный монолит). Он есть и в `listings`, и в `houses`, и заполнен лучше, чем `house_type` / `series_name` / `year_built` (см. `services/domrf_kapremont_loader.py:6` — там прямо сказано, что эти поля дырявые). Что уже есть и чем НЕ является: - `services/buildings_query.py:170-172` — `abs(l.total_floors - h.total_floors) <= 3` это санитар привязки объявления к дому, не подбор похожих. - `core/config.py:402-408` (#680-WB) — гауссиана по относительной позиции `floor/total_floors`, sigma 0.25. Работает внутри одного дома (Tier A), между домами не применяется. Предлагаемая форма — полосы этажности с мягкой деградацией, а не жёсткий `AND`: сначала аналоги в своей полосе, и только если их меньше `estimate_sb_min_comps` — расширение на соседнюю. Жёсткий фильтр уронит n на окраинах и включит fallback-ветки, которые дают хуже грязного пула. Перед реализацией нужен замер на проде: доля оценок, где в пуле смешаны полосы, и как смещается медиана при их разделении. Без этого непонятно, стоит ли овчинка выделки.
Collaborator

Замер на проде + поправка к постановке

Поправка: фильтр по классу дома в коде ЕСТЬ, но почти всегда не доживает

В issue сказано «класс дома не учитывается вообще». По коду это верно только для последнего тира. _fetch_analogs трёхуровневый (estimator.py:6282):

  • Tier S — тот же дом (house_id_fk или нормализованный адрес);
  • Tier H — «тот же класс»: year_built ±15 и total_floors ±30 % (estimator.py:6626). Ровно то, что issue предлагает ввести, уже здесь;
  • Tier W — широкий радиус, класс только в relevance_score, жёсткого фильтра нет.

Tier H отдаётся наружу только при len(tier_h) >= 5 — порог захардкожен (estimator.py:6624). Иначе — молчаливый проход в Tier W.

Сколько раз механизм реально срабатывает (Loki, 30 суток, 79 оценок)

маркер за 30 суток
analogs tier=S(canonical) 14
analogs tier=H … (отдан наружу) 86
… only N (fallthrough to W) 61
analogs tier=W radius=… 67

То есть 61 из 86 попыток классового тира (71 %) не доживает до порога и уходит в пул без фильтра по классу.

Почему не доживает — распределение найденного

Строка фолбэка печатает, сколько комплов нашлось. За те же 30 суток:

Tier H нашёл случаев
0 комплов 31
1 11
2 7
3 5
4 7

Понижение порога 5 → 4 спасает 7 случаев из 61 (11 %), 5 → 3 — двенадцать (20 %). В половине случаев (31 из 61) в своей полосе нет НИ ОДНОГО компла — там порог не при чём, там нет данных. Частые окна этажности: 6–12, 3–7, 7–13, 11–21, 9–19.

Насколько смешан пул, который клиент видит

По 784 оценкам, где известна этажность дома клиента и показано ≥3 аналога (trade_in_estimates.analogs, полосы 1–5 / 6–9 / 10–16 / 17+):

  • пул смешан у 672 из 784 (85.7 %);
  • в среднем 51.8 % показанных аналогов — из чужой полосы этажности;
  • медианный сдвиг медианы ₽/м² при отсечении чужой полосы — 0.00 %, но p90 |сдвиг| = 21.7 %, и у 181 оценки (23 %) сдвиг больше 10 %.

Важная оговорка о честности этого замера: analogs в БД хранит не весь пул, а максимум 10 показанных позиций (max(jsonb_array_length(analogs)) = 10 при max(n_analogs) = 294). Поэтому «85.7 % смешанных» — это нижняя оценка (подмножество смешано ⇒ пул смешан), а числа про сдвиг медианы и про «меньше 4 комплов» относятся к показанной десятке, НЕ к полному пулу, и переносить их на полный пул нельзя.

Что из этого следует

  1. Вводить «полосы этажности» заново не нужно — они уже есть в Tier H. Работать надо с тем, почему он не доживает.
  2. Понижение порога >= 5 — дешёвая правка, но её потолок измерен: 11 % фолбэков при 5→4. Само по себе мало.
  3. Настоящее ограничение — разреженность: в половине фолбэков своя полоса пуста. Значит осмысленные рычаги — ширина окна (±15 лет / ±30 % этажей) и радиус, а не порог.
  4. Жёсткий фильтр в Tier W по-прежнему плохая идея — issue права.

Следующий шаг с датой. Прогнать backtest (scripts/backtest_estimator.py) на трёх вариантах: (а) порог 5→4; (б) окно этажей ±30 %→±50 % при сохранении года; (в) оба. Принимать только тот, что не ухудшает MAPE и снижает долю фолбэков. Без backtest не трогать — подбор аналогов влияет на каждую оценку.

Все числа получены только чтением: Loki query_range по суточным окнам и SELECT в боевой БД.

## Замер на проде + поправка к постановке ### Поправка: фильтр по классу дома в коде ЕСТЬ, но почти всегда не доживает В issue сказано «класс дома не учитывается вообще». По коду это верно только для последнего тира. `_fetch_analogs` трёхуровневый (`estimator.py:6282`): - **Tier S** — тот же дом (`house_id_fk` или нормализованный адрес); - **Tier H** — «тот же класс»: `year_built ±15` **и** `total_floors ±30 %` (`estimator.py:6626`). Ровно то, что issue предлагает ввести, уже здесь; - **Tier W** — широкий радиус, класс только в `relevance_score`, жёсткого фильтра нет. Tier H отдаётся наружу только при `len(tier_h) >= 5` — порог захардкожен (`estimator.py:6624`). Иначе — молчаливый проход в Tier W. ### Сколько раз механизм реально срабатывает (Loki, 30 суток, 79 оценок) | маркер | за 30 суток | |---|---| | `analogs tier=S(canonical)` | 14 | | `analogs tier=H …` (отдан наружу) | 86 | | `… only N (fallthrough to W)` | **61** | | `analogs tier=W radius=…` | 67 | То есть **61 из 86 попыток классового тира (71 %) не доживает до порога** и уходит в пул без фильтра по классу. ### Почему не доживает — распределение найденного Строка фолбэка печатает, сколько комплов нашлось. За те же 30 суток: | Tier H нашёл | случаев | |---|---| | 0 комплов | **31** | | 1 | 11 | | 2 | 7 | | 3 | 5 | | 4 | 7 | **Понижение порога 5 → 4 спасает 7 случаев из 61 (11 %), 5 → 3 — двенадцать (20 %).** В половине случаев (31 из 61) в своей полосе нет НИ ОДНОГО компла — там порог не при чём, там нет данных. Частые окна этажности: 6–12, 3–7, 7–13, 11–21, 9–19. ### Насколько смешан пул, который клиент видит По 784 оценкам, где известна этажность дома клиента и показано ≥3 аналога (`trade_in_estimates.analogs`, полосы 1–5 / 6–9 / 10–16 / 17+): - пул смешан у **672 из 784 (85.7 %)**; - в среднем **51.8 %** показанных аналогов — из чужой полосы этажности; - медианный сдвиг медианы ₽/м² при отсечении чужой полосы — **0.00 %**, но `p90 |сдвиг|` = **21.7 %**, и у 181 оценки (23 %) сдвиг больше 10 %. **Важная оговорка о честности этого замера:** `analogs` в БД хранит не весь пул, а максимум 10 показанных позиций (`max(jsonb_array_length(analogs)) = 10` при `max(n_analogs) = 294`). Поэтому «85.7 % смешанных» — это нижняя оценка (подмножество смешано ⇒ пул смешан), а числа про сдвиг медианы и про «меньше 4 комплов» относятся к показанной десятке, НЕ к полному пулу, и переносить их на полный пул нельзя. ## Что из этого следует 1. Вводить «полосы этажности» заново не нужно — они уже есть в Tier H. Работать надо с тем, почему он не доживает. 2. Понижение порога `>= 5` — дешёвая правка, но её потолок измерен: 11 % фолбэков при 5→4. Само по себе мало. 3. Настоящее ограничение — разреженность: в половине фолбэков своя полоса пуста. Значит осмысленные рычаги — ширина окна (`±15 лет` / `±30 % этажей`) и радиус, а не порог. 4. Жёсткий фильтр в Tier W по-прежнему плохая идея — issue права. **Следующий шаг с датой.** Прогнать backtest (`scripts/backtest_estimator.py`) на трёх вариантах: (а) порог 5→4; (б) окно этажей ±30 %→±50 % при сохранении года; (в) оба. Принимать только тот, что не ухудшает MAPE и снижает долю фолбэков. Без backtest не трогать — подбор аналогов влияет на каждую оценку. Все числа получены только чтением: Loki `query_range` по суточным окнам и `SELECT` в боевой БД.
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#3234
No description provided.