[tradein][analytics] Accuracy-loop → CI-гейт: point-in-time backtest на ПОЛНОМ движке (per-rooms MAPE + range-coverage SLA) #1966

Closed
opened 2026-06-27 08:51:48 +00:00 by lekss361 · 2 comments
Owner

Из стратегического аудита 2026-06-27 (audits/Mera_Strategic_Audit_0627). Фундаментальный разблокировщик — №1.

Проблема

Полный движок estimator никогда честно не измерялся на ground-truth. Существующий scripts/backtest_estimator.py (#648) — mechanism-proof, не accuracy-proof: (а) НЕ point-in-time (текущие active listings vs прошлые ДКП — time-mismatch), (б) тестит только asking-median ядро, НЕ anchor/IMV/quarter/corridor слои (которые реально двигают цену), (в) in-sample by default (3 оговорки самого скрипта :43-58). → фичи в движок добавляются вслепую; каждый новый estimate_* флаг = +локальная точность, +глобальный риск регрессии. Live golden: бизнес 40%/элит 44%/премиум 33% в диапазоне — но мы не знаем, насколько вообще правы.

DoD

  • Point-in-time backtest на listing_source_snapshots (#570, harness уже ждёт таблицу): для каждой ДКП-сделки брать asking-медиану на дату сделки из снапшотов.
  • Гонять полный пайплайн (anchor/IMV/quarter/corridor), не только asking-median ядро.
  • --holdout-split (out-of-sample).
  • Метрики: per-rooms MAPE И range-coverage (% сделок, попавших в диапазон) по сегментам (эконом/комфорт/бизнес/элит/премиум).
  • Вынести как merge-gate в deploy-tradein.yml / CI: любое изменение estimator проходит порог (напр. range-coverage ≥80%, MAPE не вырос).
  • Калибровать range (R2): диапазон обязан покрывать ≥80% backtest-сделок, иначе confidence вниз.

Это разблокировщик всего: без него правки движка слепы, схлопывание 40 флагов опасно, МСК-экспансия = тираж неизмеренной ошибки. Refs #648, #570, #667, аудит 0627.

Из стратегического аудита 2026-06-27 (`audits/Mera_Strategic_Audit_0627`). **Фундаментальный разблокировщик — №1.** ## Проблема Полный движок estimator **никогда честно не измерялся на ground-truth**. Существующий `scripts/backtest_estimator.py` (#648) — mechanism-proof, не accuracy-proof: (а) НЕ point-in-time (текущие active listings vs прошлые ДКП — time-mismatch), (б) тестит только asking-median ядро, НЕ anchor/IMV/quarter/corridor слои (которые реально двигают цену), (в) in-sample by default (3 оговорки самого скрипта :43-58). → фичи в движок добавляются вслепую; каждый новый `estimate_*` флаг = +локальная точность, +глобальный риск регрессии. Live golden: бизнес 40%/элит 44%/премиум 33% в диапазоне — но мы не знаем, насколько вообще правы. ## DoD - Point-in-time backtest на `listing_source_snapshots` (#570, harness уже ждёт таблицу): для каждой ДКП-сделки брать asking-медиану **на дату сделки** из снапшотов. - Гонять **полный пайплайн** (anchor/IMV/quarter/corridor), не только asking-median ядро. - `--holdout-split` (out-of-sample). - Метрики: **per-rooms MAPE И range-coverage** (% сделок, попавших в диапазон) по сегментам (эконом/комфорт/бизнес/элит/премиум). - Вынести как **merge-gate** в `deploy-tradein.yml` / CI: любое изменение estimator проходит порог (напр. range-coverage ≥80%, MAPE не вырос). - Калибровать range (R2): диапазон обязан покрывать ≥80% backtest-сделок, иначе confidence вниз. Это разблокировщик всего: без него правки движка слепы, схлопывание 40 флагов опасно, МСК-экспансия = тираж неизмеренной ошибки. Refs #648, #570, #667, аудит 0627.
lekss361 added the
analytics
ci
priority/p1
scope/backend
tradein
labels 2026-06-27 08:53:16 +00:00
Author
Owner

Re-scope after live prod verification (2026-06-27).

Point-in-time backtest as originally specified is data-blocked right now — verified on prod:

  • listing_source_snapshots (#570) accumulates only from 2026-05-30 (~1 mo; useful is_active from ~06-18).
  • deals ground truth (49 791 rosreestr ДКП) is quarter-batched; latest deal_date = 2026-01; deals with deal_date >= 2026-05-30 (snapshot window) = 0. No overlap of snapshot window × ground-truth, and none expected until a rosreestr batch covering post-05-30 sales lands (~Q4 2026+).
  • No fresher sold source exists (asking_to_sold_ratios* are the prod correction tables, not ground truth; offer_price_history is sparse asking history).

→ A true point-in-time absolute accuracy SLA is deferred until data catches up. We ship the achievable ~80% now as a regression gate (relative — "this PR must not worsen MAPE/coverage vs a frozen baseline" — not an absolute SLA), staged:

  • PR 1/3 — #1976 (this): extract the deterministic pricing spine into a pure _price_from_inputs() (single source of truth, unit-testable). Behavior-preserving; full suite green, no existing test modified.
  • PR 2/3: backtest harness on _price_from_inputs over a committed JSON fixture (frozen held-out deals + their analog inputs) → per-rooms and price-segment MAPE + range-coverage + confidence-calibration; frozen backtest_baseline.json.
  • PR 3/3: wire the fixture backtest as a CI regression gate (hermetic — the CI test job has no DB, so the gate runs on the JSON fixture, not prod).

Empirical grounding (freshest deals Q4'25–Q1'26, asking-median core, n=548 matched): expected_sold honest held-out MAPE ~19.6%, bias −2.6%; raw asking overpredicts sold +26% (studios +55% → 4+ ~0%). Caveat: still time-mismatched → this is a regression baseline, not absolute truth.

Forward-logging of prod estimates (the substrate that makes the true point-in-time gate possible once data lands) tracked as a separate small issue (п.6).

**Re-scope after live prod verification (2026-06-27).** Point-in-time backtest as originally specified is **data-blocked right now** — verified on prod: - `listing_source_snapshots` (#570) accumulates only from **2026-05-30** (~1 mo; useful `is_active` from ~06-18). - `deals` ground truth (49 791 rosreestr ДКП) is quarter-batched; latest `deal_date` = **2026-01**; deals with `deal_date >= 2026-05-30` (snapshot window) = **0**. No overlap of snapshot window × ground-truth, and none expected until a rosreestr batch covering post-05-30 sales lands (~Q4 2026+). - No fresher sold source exists (`asking_to_sold_ratios*` are the prod correction tables, not ground truth; `offer_price_history` is sparse asking history). → A true point-in-time **absolute** accuracy SLA is **deferred until data catches up**. We ship the achievable ~80% now as a **regression gate** (relative — "this PR must not worsen MAPE/coverage vs a frozen baseline" — not an absolute SLA), staged: - **PR 1/3 — #1976** (this): extract the deterministic pricing spine into a pure `_price_from_inputs()` (single source of truth, unit-testable). Behavior-preserving; full suite green, no existing test modified. - **PR 2/3**: backtest harness on `_price_from_inputs` over a committed JSON fixture (frozen held-out deals + their analog inputs) → per-rooms **and price-segment** MAPE + **range-coverage** + **confidence-calibration**; frozen `backtest_baseline.json`. - **PR 3/3**: wire the fixture backtest as a **CI regression gate** (hermetic — the CI `test` job has no DB, so the gate runs on the JSON fixture, not prod). Empirical grounding (freshest deals Q4'25–Q1'26, asking-median core, n=548 matched): expected_sold honest held-out **MAPE ~19.6%, bias −2.6%**; raw asking overpredicts sold **+26%** (studios +55% → 4+ ~0%). Caveat: still time-mismatched → this is a regression baseline, not absolute truth. Forward-logging of prod estimates (the substrate that makes the *true* point-in-time gate possible once data lands) tracked as a separate small issue (п.6).
Author
Owner

Верификация: issue фактически закрыт серией PR за последние 6 дней, vault не успел это зафиксировать

Проверил git log + прочитал код и committed baseline — все ссылки ниже перепроверены (git show), не взяты на веру:

  • #1976 (b60df61f) — детерминированный ценовой спайн вынесен в чистую _price_from_inputs().
  • PR 2/3 (57bf8ffb) — full-spine backtest: полный пайплайн (anchor/IMV/quarter/corridor), не только asking-median ядро; метрики per-rooms MAPE + range-coverage по сегментам.
  • PR 3/3 (5a73bbeb + 3 других коммита) — hermetic regression gate: frozen fixture + committed baseline + test_backtest_regression_gate.py, вживлено в existing pre-merge CI (backend pytest блокирует merge при регрессии).
  • 9448a945 — калибровка 80% prediction interval для expected_sold range (DoD-пункт R2).
  • d15ff3d9 (#2255/#2293) — сегментная поправка бизнес/элит по ценовому бэнду.

Committed baseline (tests/fixtures/backtest_baseline.json, проверено напрямую): range_coverage.overall = 81.95% (n=277, covered=227) — выше порога 80% из DoD.

Оговорки (не забытый скоуп, осознанные):

  1. CI-гейт — relative regression («не хуже baseline»), не абсолютный accuracy-SLA. Если нужен hard floor (assert coverage >= 80% в самом тесте, а не только diff-от-baseline) — отдельное решение, не техническая доработка.
  2. --holdout-split существует, но применяется к legacy asking→sold ratio, не к full-spine expected_sold предсказаниям, которые заморожены в CI-baseline.
  3. Честный point-in-time backtest на listing_source_snapshots — единственный реально нерешённый DoD-пункт, заблокирован ДАННЫМИ (таблица копится с 2026-05-30, свежие ДКП имеют квартальный лаг регистрации Росреестра — пересечение окон практически нулевое, ETA не раньше Q4 2026). Завёл отдельный low-priority tracking issue, см. ниже.
  4. Элит (n=6-9) / премиум (n=1) сегменты — статистически шумная выборка, calibration там ограничена данными, не движком. Продуктовое решение (принять лимит vs расширять сбор), не инженерное — не блокирует closeout.

Issue закрываю. Vault обновлю отдельной записью (текущий audits/Mera_Fullspine_Accuracy_0627.md устарел на 6 дней — фиксирует coverage 56.6%/бизнес-bias −22.7%, хотя реальный текущий baseline 81.95%/−10.19%).

## Верификация: issue фактически закрыт серией PR за последние 6 дней, vault не успел это зафиксировать Проверил git log + прочитал код и committed baseline — все ссылки ниже перепроверены (`git show`), не взяты на веру: - **#1976** (`b60df61f`) — детерминированный ценовой спайн вынесен в чистую `_price_from_inputs()`. - **PR 2/3** (`57bf8ffb`) — full-spine backtest: полный пайплайн (anchor/IMV/quarter/corridor), не только asking-median ядро; метрики per-rooms MAPE + range-coverage по сегментам. - **PR 3/3** (`5a73bbeb` + 3 других коммита) — hermetic regression gate: frozen fixture + committed baseline + `test_backtest_regression_gate.py`, вживлено в existing pre-merge CI (backend pytest блокирует merge при регрессии). - **`9448a945`** — калибровка 80% prediction interval для `expected_sold` range (DoD-пункт R2). - **`d15ff3d9`** (#2255/#2293) — сегментная поправка бизнес/элит по ценовому бэнду. **Committed baseline** (`tests/fixtures/backtest_baseline.json`, проверено напрямую): `range_coverage.overall = 81.95% (n=277, covered=227)` — выше порога 80% из DoD. **Оговорки (не забытый скоуп, осознанные):** 1. CI-гейт — **relative regression** («не хуже baseline»), не абсолютный accuracy-SLA. Если нужен hard floor (`assert coverage >= 80%` в самом тесте, а не только diff-от-baseline) — отдельное решение, не техническая доработка. 2. `--holdout-split` существует, но применяется к legacy asking→sold ratio, не к full-spine `expected_sold` предсказаниям, которые заморожены в CI-baseline. 3. **Честный point-in-time backtest на `listing_source_snapshots`** — единственный реально нерешённый DoD-пункт, заблокирован ДАННЫМИ (таблица копится с 2026-05-30, свежие ДКП имеют квартальный лаг регистрации Росреестра — пересечение окон практически нулевое, ETA не раньше Q4 2026). Завёл отдельный low-priority tracking issue, см. ниже. 4. Элит (n=6-9) / премиум (n=1) сегменты — статистически шумная выборка, calibration там ограничена данными, не движком. Продуктовое решение (принять лимит vs расширять сбор), не инженерное — не блокирует closeout. **Issue закрываю.** Vault обновлю отдельной записью (текущий `audits/Mera_Fullspine_Accuracy_0627.md` устарел на 6 дней — фиксирует coverage 56.6%/бизнес-bias −22.7%, хотя реальный текущий baseline 81.95%/−10.19%).
Sign in to join this conversation.
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#1966
No description provided.