🔴 [tradein][вторичка][analytics] District/quarter price-index — локационная премия в estimator #647

Closed
opened 2026-05-29 12:34:40 +00:00 by Light1YT · 4 comments
Owner

Приоритет: СУПЕР (per Anton 2026-05-29). Единственный новый accuracy-буст вторички, НЕ зависящий от anti-bot (данные уже в БД).

Проблема

Estimator (tradein-mvp/backend/app/services/estimator.py) считает цену как median(аналоги в радиусе ST_DWithin) × cohort × repair/class-коэф. Локационной премии нет — учитывается только то, какие аналоги случайно попали в радиус. Квартира в престижном центре и на окраине с похожими соседними аналогами получают одинаковую базу, хотя рынок оценивает локацию по-разному. Это системная неточность по локациям.

(NB: #578 закрыт как site-finder-концепт; ЭТО — его tradein-эквивалент, отдельная фича в формуле оценки.)

Данные (уже есть!)

  • gendesign_rosreestr_deals (6.8M ДКП-сделок) доступна из tradein через FDW (gendesign_remote).
  • В основной БД gendesign уже строится mv_quarter_price_per_m2 (~9700 cad_quarters, из 6.83M ДКП) — см. vault Limit_Trend_Always_Capped (#33 D1).
  • → можно вывести median цена/м² на кадастровый квартал (или район) и нормализовать к городской/cohort-медиане → quarter_price_index.

Методология (черновик — нужен план)

  1. Построить quarter_price_index[cad_quarter] = median_ppm2(quarter) / median_ppm2(city) из ДКП (с порогом мин. числа сделок на квартал; sparse → fallback на район → город).
  2. Применить в estimator: скорректировать на разрыв между кварталом таргета и кварталами аналогов (НЕ double-count — если аналоги уже локальны, локация частично в медиане). Вариант: index как фактор, когда аналоги не из квартала таргета / разрежены.
  3. Хранить index в таблице/MV (refresh по cron), читать в estimator.

Риск (почему нужен твой «го» + аккуратность)

Меняет цифру, которую видит КАЖДЫЙ клиент. Обязательно:

  • backtesting: предсказать held-out ДКП-сделки с index vs без → MAE/MAPE улучшилось?
  • A/B-сверка: прогнать текущие canonical-оценки (напр. Малышева 125, Заводская 44а) до/после — разброс объясним?
  • confidence/disclosure: отразить в UI/PDF что учтена локация.

Acceptance

  • quarter_price_index выведен из ДКП + refresh-механизм
  • Применён в estimator без double-count с analog-median
  • Backtest: MAPE не хуже (лучше) на held-out ДКП
  • Нет регресса на canonical-оценках (сверка до/после)
  • Покрыто тестами + задокументировано в vault

Refs

  • estimator.py (analog-median + coeff chain), _fetch_analogs/_fetch_deals
  • mv_quarter_price_per_m2 (#33 D1), FDW gendesign_rosreestr_deals
  • vault: Limit_Trend_Always_Capped, Limit_POI_Factor_Tiny (локационный gap)
  • План реализации: tech-analyst → database-expert (index MV) → backend-engineer (estimator wiring) → backtest → code-review → A/B
**Приоритет: СУПЕР (per Anton 2026-05-29).** Единственный новый accuracy-буст вторички, НЕ зависящий от anti-bot (данные уже в БД). ## Проблема Estimator (`tradein-mvp/backend/app/services/estimator.py`) считает цену как median(аналоги в радиусе ST_DWithin) × cohort × repair/class-коэф. **Локационной премии нет** — учитывается только то, какие аналоги случайно попали в радиус. Квартира в престижном центре и на окраине с похожими соседними аналогами получают одинаковую базу, хотя рынок оценивает локацию по-разному. Это системная неточность по локациям. (NB: #578 закрыт как site-finder-концепт; ЭТО — его tradein-эквивалент, отдельная фича в формуле оценки.) ## Данные (уже есть!) - `gendesign_rosreestr_deals` (6.8M ДКП-сделок) доступна из tradein через FDW (`gendesign_remote`). - В основной БД gendesign уже строится `mv_quarter_price_per_m2` (~9700 cad_quarters, из 6.83M ДКП) — см. vault `Limit_Trend_Always_Capped` (#33 D1). - → можно вывести **median цена/м² на кадастровый квартал** (или район) и нормализовать к городской/cohort-медиане → `quarter_price_index`. ## Методология (черновик — нужен план) 1. Построить `quarter_price_index[cad_quarter] = median_ppm2(quarter) / median_ppm2(city)` из ДКП (с порогом мин. числа сделок на квартал; sparse → fallback на район → город). 2. Применить в estimator: скорректировать на **разрыв** между кварталом таргета и кварталами аналогов (НЕ double-count — если аналоги уже локальны, локация частично в медиане). Вариант: index как фактор, когда аналоги не из квартала таргета / разрежены. 3. Хранить index в таблице/MV (refresh по cron), читать в estimator. ## Риск (почему нужен твой «го» + аккуратность) **Меняет цифру, которую видит КАЖДЫЙ клиент.** Обязательно: - backtesting: предсказать held-out ДКП-сделки с index vs без → MAE/MAPE улучшилось? - A/B-сверка: прогнать текущие canonical-оценки (напр. Малышева 125, Заводская 44а) до/после — разброс объясним? - confidence/disclosure: отразить в UI/PDF что учтена локация. ## Acceptance - [ ] `quarter_price_index` выведен из ДКП + refresh-механизм - [ ] Применён в estimator без double-count с analog-median - [ ] Backtest: MAPE не хуже (лучше) на held-out ДКП - [ ] Нет регресса на canonical-оценках (сверка до/после) - [ ] Покрыто тестами + задокументировано в vault ## Refs - `estimator.py` (analog-median + coeff chain), `_fetch_analogs`/`_fetch_deals` - `mv_quarter_price_per_m2` (#33 D1), FDW `gendesign_rosreestr_deals` - vault: `Limit_Trend_Always_Capped`, `Limit_POI_Factor_Tiny` (локационный gap) - План реализации: tech-analyst → database-expert (index MV) → backend-engineer (estimator wiring) → backtest → code-review → A/B
Light1YT added the
enhancement
analytics
tradein
вторичка
labels 2026-05-29 12:34:40 +00:00
bot-analyst added the
needs-human
priority/p0
scope/backend
labels 2026-05-30 14:24:37 +00:00
Collaborator

Labels normalized (auto-analyst, milestone-6 sweep): +scope/backend +priority/p0 (priority/critical→p0). +needs-human — это multi-step feature (новая таблица district_price_index + estimator-интеграция + scheduled refresh + backtest, ≥1.5 дня), НЕ декомпозируется в single status/ready. Нужно решение: дробить на sub-issues (Foundation→Schema→Worker→Integration) и/или scope-cut под демо 2026-06-01. Без статуса pickup — worker не возьмёт epic целиком.

Labels normalized (auto-analyst, milestone-6 sweep): +`scope/backend` +`priority/p0` (`priority/critical`→p0). +`needs-human` — это multi-step feature (новая таблица district_price_index + estimator-интеграция + scheduled refresh + backtest, ≥1.5 дня), НЕ декомпозируется в single `status/ready`. Нужно решение: дробить на sub-issues (Foundation→Schema→Worker→Integration) и/или scope-cut под демо 2026-06-01. Без статуса pickup — worker не возьмёт epic целиком.
Owner

🧭 Resolver: решение принято (Anton, 2026-05-30) — FULL SEND к демо 2026-06-01. Эпик декомпозирован.

Категория B (client-visible pricing) → спросил пользователя, ответ: full send, но с обязательным backtest-гейтом перед клиентской интеграцией.

Декомпозиция (Foundation→Schema→Integration + backtest-gate):

  • #740 — #647-1 Foundation: quarter_price_index MV из ДКП (FDW). status/ready (неклиентский, стартует сейчас).
  • #741 — #647-2 Schema + refresh-cron. status/ready (неклиентский). Зависит от #740.
  • #742 — #647-4 Backtest GATE: MAPE held-out ДКП + canonical A/B (Малышева 125, Заводская 44а). status/ready. Зависит от #740.
  • #743 — #647-3 Estimator integration (CLIENT-VISIBLE). НАМЕРЕННО без status/ready — не мержить, пока #742 не зелёный и #740+#741 не смержены.

Guardrail (client-visible pricing): интеграция (#743) меняет цену у каждого клиента → reviewer НЕ мержит её без ссылки на зелёный verdict #742 (backtest не хуже + canonical A/B объясним + no double-count). Это зафиксировано в теле #743 и #742.

Снимаю needs-human с этого эпика (решение по нему принято); трекинг работы — в sub-issues. Эпик #647 остаётся открытым как зонтик.

🧭 **Resolver: решение принято (Anton, 2026-05-30) — FULL SEND к демо 2026-06-01. Эпик декомпозирован.** Категория B (client-visible pricing) → спросил пользователя, ответ: **full send**, но с обязательным backtest-гейтом перед клиентской интеграцией. **Декомпозиция (Foundation→Schema→Integration + backtest-gate):** - **#740** — #647-1 Foundation: `quarter_price_index` MV из ДКП (FDW). `status/ready` (неклиентский, стартует сейчас). - **#741** — #647-2 Schema + refresh-cron. `status/ready` (неклиентский). Зависит от #740. - **#742** — #647-4 Backtest GATE: MAPE held-out ДКП + canonical A/B (Малышева 125, Заводская 44а). `status/ready`. Зависит от #740. - **#743** — #647-3 Estimator integration (**CLIENT-VISIBLE**). НАМЕРЕННО без `status/ready` — не мержить, пока #742 не зелёный и #740+#741 не смержены. **Guardrail (client-visible pricing):** интеграция (#743) меняет цену у каждого клиента → reviewer НЕ мержит её без ссылки на зелёный verdict #742 (backtest не хуже + canonical A/B объясним + no double-count). Это зафиксировано в теле #743 и #742. Снимаю `needs-human` с этого эпика (решение по нему принято); трекинг работы — в sub-issues. Эпик #647 остаётся открытым как зонтик.
lekss361 removed the
needs-human
label 2026-05-30 16:07:30 +00:00
lekss361 added this to the Praktika demo — Mon 2026-06-01 milestone 2026-05-30 17:21:12 +00:00
bot-analyst added the
status/needs-analysis
label 2026-05-31 07:12:54 +00:00
Collaborator

analyst gate (needs-analysis → needs-human + blocked). Приоритет/«го» уже даны (Anton), но тело — methodology-черновик с развилкой, меняющей headline у КАЖДОГО клиента, которую archeology не снимает:

  1. Double-count: index применять всегда как фактор, или только когда аналоги НЕ из квартала таргета / разрежены? (если аналоги уже локальны — локация частично в медиане).
  2. Granularity/threshold: квартал vs район, мин. число ДКП на квартал для надёжности, fallback quarter→district→city.
  3. Обязателен backtest (held-out ДКП MAPE) + A/B на canonical (Малышева 125, Заводская 44а) ДО мержа — как #755/#774.

Это product-accuracy решение, не декомпозиция. После выбора пути 1 — расщеплю на: (A) data-layer quarter_price_index MV/таблица из ДКП через FDW (scope/db+backend, механически) + (B) estimator-wiring (scope/backend, по выбранной формуле, с backtest-gate). Сейчас — needs-human.

analyst gate (needs-analysis → needs-human + blocked). Приоритет/«го» уже даны (Anton), но тело — methodology-черновик с развилкой, меняющей headline у КАЖДОГО клиента, которую archeology не снимает: 1. **Double-count**: index применять всегда как фактор, или только когда аналоги НЕ из квартала таргета / разрежены? (если аналоги уже локальны — локация частично в медиане). 2. **Granularity/threshold**: квартал vs район, мин. число ДКП на квартал для надёжности, fallback quarter→district→city. 3. Обязателен backtest (held-out ДКП MAPE) + A/B на canonical (Малышева 125, Заводская 44а) ДО мержа — как #755/#774. Это product-accuracy решение, не декомпозиция. После выбора пути 1 — расщеплю на: (A) data-layer `quarter_price_index` MV/таблица из ДКП через FDW (scope/db+backend, механически) + (B) estimator-wiring (scope/backend, по выбранной формуле, с backtest-gate). Сейчас — `needs-human`.
bot-analyst added
needs-human
status/blocked
and removed
status/needs-analysis
labels 2026-05-31 07:16:15 +00:00
Collaborator

analyst: закрываю как superseded merged #858 + #863 — механизм этого issue уже в проде (verified чтением кода forgejo/main 2026-05-31).

quarter_price_index интегрирован в estimator: _apply_quarter_index(...) вызывается из estimate-пути (estimator.py:880), 28 usages quarter_index в файле. Это PR #858 (b16a45e «per-cadastral-quarter price index in estimator», Refs #764).

Покрывает ровно выбранную методологию (Anton 2026-05-31: «index фактор только для не-локальных аналогов»):

  • Gap-correction adjusted_ppm2 = base_median × (target_index / avg_analog_index), ppm²-взвешенный по аналогам с известным кварталом — НЕ double-count.
  • Guard-2: skip когда >0.6 аналогов из квартала таргета (локация уже в медиане) = ровно «только для не-локальных».
  • Sparse fallback (n_deals<10 → no-op), bimodal-guard (qi>2.0 & n<50 → no-op).
  • #863 (3f31d1f) добавил sanity-clamp factor ∈ [0.6, 1.8].
  • Backtest #763 зелёный: MAPE 23.0%→18.7%, human go (см. #858 body).

Все 5 acceptance-пунктов этого issue закрыты #858: index выведен из ДКП ✓, применён без double-count ✓ (Guard-2), backtest MAPE улучшился ✓, clamp против регрессии ✓ (#863). Дубль-декомпозиция не нужна.

Снимаю milestone-6. Если нужен district-уровень (issue упоминает quarter ИЛИ district) сверх quarter — это отдельный follow-up; quarter-гранулярность уже в проде.

✅ analyst: **закрываю как superseded merged #858 + #863** — механизм этого issue уже в проде (verified чтением кода forgejo/main 2026-05-31). `quarter_price_index` интегрирован в estimator: `_apply_quarter_index(...)` вызывается из estimate-пути (`estimator.py:880`), 28 usages `quarter_index` в файле. Это PR **#858** (`b16a45e` «per-cadastral-quarter price index in estimator», Refs #764). **Покрывает ровно выбранную методологию** (Anton 2026-05-31: «index фактор только для не-локальных аналогов»): - Gap-correction `adjusted_ppm2 = base_median × (target_index / avg_analog_index)`, ppm²-взвешенный по аналогам с известным кварталом — НЕ double-count. - **Guard-2:** skip когда >0.6 аналогов из квартала таргета (локация уже в медиане) = ровно «только для не-локальных». - Sparse fallback (n_deals<10 → no-op), bimodal-guard (qi>2.0 & n<50 → no-op). - **#863** (`3f31d1f`) добавил sanity-clamp factor ∈ [0.6, 1.8]. - **Backtest #763 зелёный**: MAPE 23.0%→18.7%, human go (см. #858 body). Все 5 acceptance-пунктов этого issue закрыты #858: index выведен из ДКП ✓, применён без double-count ✓ (Guard-2), backtest MAPE улучшился ✓, clamp против регрессии ✓ (#863). Дубль-декомпозиция не нужна. Снимаю milestone-6. Если нужен district-уровень (issue упоминает quarter ИЛИ district) сверх quarter — это отдельный follow-up; quarter-гранулярность уже в проде.
Sign in to join this conversation.
No project
No assignees
3 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#647
No description provided.