[needs-human][P1][tradein][backend] Same-building anchor фабрикует завышенный headline при n=2 (upward-only, заменяет median/confidence) #755

Closed
opened 2026-05-30 16:03:20 +00:00 by bot-analyst · 6 comments
Collaborator

Worker: исполняемый спек. Параметры выбраны человеком (Anton 2026-05-31). file:line verified чтением forgejo/main 2026-05-31 — НО estimator.py = hot-file (дрейфанул после #858/#863 quarter-index merges), grep-подтверди символы перед правкой, не доверяй абсолютным строкам. Конфликт с кодом → коммент, НЕ угадывай.

Контекст (проблема)

Same-building anchor (_compute_same_building_anchor, estimator.py:1131) при 2 комплах того же дома полностью заменяет headline median/range/confidence на «премиум»-значение (взвешенный p70 верхнего квантиля, только вверх), рапортуя «high» достоверность на 2 asking-ценах. Anchor-комплы не фильтруются Tukey (_filter_outliers estimator.py:3115 только для n≥5 radius-аналогов) → одна опечатка в цене раздувает anchor. На демо ГК «Практика» (2026-06-01, killer-фактор — точность) это «уверенно завышенная цифра на 2 лотах».

Решение человека (3 параметра — РЕАЛИЗОВАТЬ ВСЕ)

1. min_comps 2 → 4. estimate_sb_min_comps: int = 2 (config.py:78) → 4. Anchor срабатывает только при ≥4 комплах того же дома (читается в _compute_same_building_anchor через settings.estimate_sb_min_comps).

2. confidence cap «medium» при n<5. В anchor-блоке: когда число anchor-комплов < 5, итоговый confidence НЕ может быть «high» — ограничить до «medium» (даже если anchor сработал при n=4). Найти где anchor проставляет confidence внутри _compute_same_building_anchor / его caller; добавить cap. (min_comps=4 и cap n<5 совместимы: при n=4 anchor работает, но confidence≤medium.)

3. MAD-clip anchor-комплов перед агрегацией. Перед вычислением anchor median/p70 — отфильтровать ценовые выбросы среди anchor-комплов через MAD (median absolute deviation), threshold = новый settings.estimate_sb_mad_k (default 3.5, в config.py рядом с estimate_sb_*). Защита от опечаток в объявлении. Применять только при наличии ≥ min_comps после клипа (если клип уронил ниже порога — anchor не срабатывает, fallback на radius-median).

Files (verified forgejo/main 2026-05-31, grep-confirm — строки дрейфуют)

  • tradein-mvp/backend/app/services/estimator.py: _compute_same_building_anchor (:1131, def), p70-uplift + guardrail + confidence-assignment — ВНУТРИ этой функции (старые строки 1253-1266/1765-1796 дрейфанули, читать тело функции); _filter_outliers (:3115) — reference для паттерна MAD/Tukey, можно переиспользовать стиль.
  • tradein-mvp/backend/app/core/config.py: estimate_sb_min_comps: int = 2 (:78) → 4; добавить estimate_sb_mad_k: float = 3.5.

Definition of Done (бинарно)

  • grep -n "estimate_sb_min_comps" tradein-mvp/backend/app/core/config.py → значение = 4.
  • grep -n "estimate_sb_mad_k" tradein-mvp/backend/app/core/config.py → ≥1 (новый setting, default 3.5).
  • В _compute_same_building_anchor: confidence cap — unit-тест: anchor при n=4 → confidence != "high" (medium/low); anchor при n≥5 высокой согласованности → может быть high.
  • MAD-clip — unit-тест: anchor-набор [10M, 10.2M, 9.9M, 25M(опечатка)] → 25M отфильтрован, anchor median не раздут; набор без выбросов → не меняется.
  • min_comps — unit-тест: 3 компла того же дома → anchor НЕ срабатывает (fallback radius-median); 4 компла → срабатывает.
  • Backtest re-run (#763 harness, merge-gate): held-out ДКП MAPE НЕ хуже текущего 18.7% + A/B на canonical-адресах (Малышева 125, Заводская 44а) — диапазон не схлопнут/не вывернут. Приложить до/после в PR. (Как #858 — backtest зелёный = merge разрешён обычным review.)
  • cd tradein-mvp/backend && uv run ruff check app/services/estimator.py app/core/config.py clean; uv run pytest -k "anchor or same_building or estimator" pass.

Не делать

  • НЕ применять p70-uplift «не заменяет median» (вариант 2 опции — человек его НЕ выбрал; anchor-замена остаётся, меняем только порог+confidence+MAD-clip).
  • НЕ трогать quarter-index (#858, отдельный путь), radius-tier, asking→sold (#648).
  • НЕ менять anchor-формулу сверх трёх пунктов выше.

Risk

Меняет headline у клиентов где anchor срабатывал на 2-3 комплах (теперь не сработает или с medium confidence) — это и есть цель (убрать ложно-уверенное завышение). Backtest+A/B обязательны как anti-regression на killer-факторе.

Связано

Источник: аудит inbox/2026-05-30-1545 (finding #1). Решение параметров: Anton 2026-05-31 (analyst AskUserQuestion). #694 (anchor origin), #755-history. Milestone Praktika demo 2026-06-01.

> Worker: исполняемый спек. Параметры выбраны человеком (Anton 2026-05-31). file:line verified чтением forgejo/main 2026-05-31 — НО estimator.py = hot-file (дрейфанул после #858/#863 quarter-index merges), **grep-подтверди символы перед правкой**, не доверяй абсолютным строкам. Конфликт с кодом → коммент, НЕ угадывай. ## Контекст (проблема) Same-building anchor (`_compute_same_building_anchor`, `estimator.py:1131`) при **2 комплах** того же дома полностью заменяет headline median/range/confidence на «премиум»-значение (взвешенный p70 верхнего квантиля, только вверх), рапортуя «high» достоверность на 2 asking-ценах. Anchor-комплы не фильтруются Tukey (`_filter_outliers` `estimator.py:3115` только для n≥5 radius-аналогов) → одна опечатка в цене раздувает anchor. На демо ГК «Практика» (2026-06-01, killer-фактор — точность) это «уверенно завышенная цифра на 2 лотах». ## Решение человека (3 параметра — РЕАЛИЗОВАТЬ ВСЕ) **1. min_comps 2 → 4.** `estimate_sb_min_comps: int = 2` (`config.py:78`) → **4**. Anchor срабатывает только при ≥4 комплах того же дома (читается в `_compute_same_building_anchor` через `settings.estimate_sb_min_comps`). **2. confidence cap «medium» при n<5.** В anchor-блоке: когда число anchor-комплов < 5, итоговый `confidence` НЕ может быть «high» — ограничить до «medium» (даже если anchor сработал при n=4). Найти где anchor проставляет confidence внутри `_compute_same_building_anchor` / его caller; добавить cap. (min_comps=4 и cap n<5 совместимы: при n=4 anchor работает, но confidence≤medium.) **3. MAD-clip anchor-комплов перед агрегацией.** Перед вычислением anchor median/p70 — отфильтровать ценовые выбросы среди anchor-комплов через MAD (median absolute deviation), threshold = новый `settings.estimate_sb_mad_k` (default **3.5**, в `config.py` рядом с `estimate_sb_*`). Защита от опечаток в объявлении. Применять только при наличии ≥ min_comps после клипа (если клип уронил ниже порога — anchor не срабатывает, fallback на radius-median). ## Files (verified forgejo/main 2026-05-31, grep-confirm — строки дрейфуют) - `tradein-mvp/backend/app/services/estimator.py`: `_compute_same_building_anchor` (**:1131**, def), p70-uplift + guardrail + confidence-assignment — ВНУТРИ этой функции (старые строки 1253-1266/1765-1796 дрейфанули, читать тело функции); `_filter_outliers` (**:3115**) — reference для паттерна MAD/Tukey, можно переиспользовать стиль. - `tradein-mvp/backend/app/core/config.py`: `estimate_sb_min_comps: int = 2` (**:78**) → 4; добавить `estimate_sb_mad_k: float = 3.5`. ## Definition of Done (бинарно) - [ ] `grep -n "estimate_sb_min_comps" tradein-mvp/backend/app/core/config.py` → значение `= 4`. - [ ] `grep -n "estimate_sb_mad_k" tradein-mvp/backend/app/core/config.py` → ≥1 (новый setting, default 3.5). - [ ] В `_compute_same_building_anchor`: confidence cap — unit-тест: anchor при n=4 → `confidence != "high"` (medium/low); anchor при n≥5 высокой согласованности → может быть high. - [ ] MAD-clip — unit-тест: anchor-набор `[10M, 10.2M, 9.9M, 25M(опечатка)]` → 25M отфильтрован, anchor median не раздут; набор без выбросов → не меняется. - [ ] min_comps — unit-тест: 3 компла того же дома → anchor НЕ срабатывает (fallback radius-median); 4 компла → срабатывает. - [ ] **Backtest re-run (#763 harness, merge-gate):** held-out ДКП MAPE НЕ хуже текущего 18.7% + A/B на canonical-адресах (Малышева 125, Заводская 44а) — диапазон не схлопнут/не вывернут. Приложить до/после в PR. (Как #858 — backtest зелёный = merge разрешён обычным review.) - [ ] `cd tradein-mvp/backend && uv run ruff check app/services/estimator.py app/core/config.py` clean; `uv run pytest -k "anchor or same_building or estimator"` pass. ## Не делать - НЕ применять p70-uplift «не заменяет median» (вариант 2 опции — человек его НЕ выбрал; anchor-замена остаётся, меняем только порог+confidence+MAD-clip). - НЕ трогать quarter-index (#858, отдельный путь), radius-tier, asking→sold (#648). - НЕ менять anchor-формулу сверх трёх пунктов выше. ## Risk Меняет headline у клиентов где anchor срабатывал на 2-3 комплах (теперь не сработает или с medium confidence) — это и есть цель (убрать ложно-уверенное завышение). Backtest+A/B обязательны как anti-regression на killer-факторе. ## Связано Источник: аудит `inbox/2026-05-30-1545` (finding #1). Решение параметров: Anton 2026-05-31 (analyst AskUserQuestion). #694 (anchor origin), #755-history. Milestone Praktika demo 2026-06-01.
bot-analyst added the
needs-human
priority/p1
scope/backend
tradein
labels 2026-05-30 16:03:33 +00:00
bot-analyst added this to the Praktika demo — Mon 2026-06-01 milestone 2026-05-30 16:03:34 +00:00
Owner

🧭 Resolver: решение (Anton, 2026-05-30) — Safe-guard к демо. Перевожу в status/ready со спеком + backtest-гейтом.

Категория B (меняет цену у каждого клиента) → спросил пользователя. Выбрано направление safe-guard (снизить фабрикацию завышения на малом n).

Спек для backend (выбранные параметры)

  1. min_comps 2 → 4 (config.py:78 estimate_sb_min_comps): anchor срабатывает только при ≥4 комплах того же дома.
  2. Confidence cap: при n < 5 anchor-комплов — НЕ рапортовать «high», максимум «medium».
  3. MAD-clip anchor-комплов перед агрегацией (защита от опечаток в цене) — отбросить выбросы по медианному абсолютному отклонению (порог на усмотрение, старт ~3·MAD); это закрывает gap «anchor-комплы не Tukey-фильтруются» (estimator.py:3046 _filter_outliers только для n≥5 radius).
  4. Guardrail вверх: p70-uplift (estimator.py:1253-1266, only-up) не применять как ЗАМЕНУ headline при n < min_comps_new; при срабатывании — anchor проходит через тот же guardrail, что и вниз.

⚠️ Backtest-гейт (client-visible — ОБЯЗАТЕЛЬНО перед merge)

Каждый параметр сдвигает цифру у всех клиентов. Reviewer НЕ мержит PR без приложенного в комменте результата:

  • Backtest held-out ДКП: MAPE с safe-guard ≤ MAPE без (не хуже).
  • Canonical A/B до/после: Малышева 125, Заводская 44а (+ golden-set) — изменения объяснимы, без диких скачков.
  • Подтвердить: фабрикация на n=2 устранена (anchor не заменяет headline «high» на 2 asking).

Files (из issue)

estimator.py: _compute_same_building_anchor (1180), p70-uplift (1253-1266), guardrail (1765-1779), anchor-замена headline (1779-1796), _fetch_anchor_comps (~1043); config.py: estimate_sb_min_comps (78).

Снимаю needs-human, ставлю status/ready. Backend берёт; мерж — только с зелёным backtest (гейт выше).

🧭 **Resolver: решение (Anton, 2026-05-30) — Safe-guard к демо. Перевожу в `status/ready` со спеком + backtest-гейтом.** Категория B (меняет цену у каждого клиента) → спросил пользователя. Выбрано направление **safe-guard** (снизить фабрикацию завышения на малом n). ## Спек для backend (выбранные параметры) 1. **min_comps 2 → 4** (`config.py:78` `estimate_sb_min_comps`): anchor срабатывает только при ≥4 комплах того же дома. 2. **Confidence cap:** при `n < 5` anchor-комплов — НЕ рапортовать «high», максимум «medium». 3. **MAD-clip anchor-комплов** перед агрегацией (защита от опечаток в цене) — отбросить выбросы по медианному абсолютному отклонению (порог на усмотрение, старт ~3·MAD); это закрывает gap «anchor-комплы не Tukey-фильтруются» (`estimator.py:3046` `_filter_outliers` только для n≥5 radius). 4. **Guardrail вверх:** p70-uplift (`estimator.py:1253-1266`, only-up) не применять как ЗАМЕНУ headline при `n < min_comps_new`; при срабатывании — anchor проходит через тот же guardrail, что и вниз. ## ⚠️ Backtest-гейт (client-visible — ОБЯЗАТЕЛЬНО перед merge) Каждый параметр сдвигает цифру у всех клиентов. Reviewer НЕ мержит PR без приложенного в комменте результата: - [ ] Backtest held-out ДКП: MAPE с safe-guard ≤ MAPE без (не хуже). - [ ] Canonical A/B до/после: Малышева 125, Заводская 44а (+ golden-set) — изменения объяснимы, без диких скачков. - [ ] Подтвердить: фабрикация на n=2 устранена (anchor не заменяет headline «high» на 2 asking). ## Files (из issue) `estimator.py`: `_compute_same_building_anchor` (1180), p70-uplift (1253-1266), guardrail (1765-1779), anchor-замена headline (1779-1796), `_fetch_anchor_comps` (~1043); `config.py`: `estimate_sb_min_comps` (78). Снимаю `needs-human`, ставлю `status/ready`. Backend берёт; мерж — только с зелёным backtest (гейт выше).
lekss361 added
status/ready
and removed
needs-human
labels 2026-05-30 16:19:53 +00:00
Author
Collaborator

analyst gate: снимаю status/ready, ставлю needs-human. Тело issue прямо запрещает автодекомпозицию в ready («Требует решения человека по параметрам + backtest перед мержем, НЕ автодекомпозируется в status/ready») — это изменение ценовой логики, влияющее на headline у каждого клиента. С status/ready его мог claim'нуть backend-worker и начать править estimator.py без обязательного человеческого выбора параметров (min_comps / uplift / confidence cap / MAD-clip) и backtest+A/B на canonical-адресах. Возврат в ready — только после решения человека по 4 пунктам и с верифицированным телом.

analyst gate: снимаю `status/ready`, ставлю `needs-human`. Тело issue прямо запрещает автодекомпозицию в ready («Требует решения человека по параметрам + backtest перед мержем, НЕ автодекомпозируется в status/ready») — это изменение ценовой логики, влияющее на headline у каждого клиента. С `status/ready` его мог claim'нуть backend-worker и начать править `estimator.py` без обязательного человеческого выбора параметров (min_comps / uplift / confidence cap / MAD-clip) и backtest+A/B на canonical-адресах. Возврат в ready — только после решения человека по 4 пунктам и с верифицированным телом.
bot-analyst added the
needs-human
label 2026-05-31 06:58:43 +00:00
bot-analyst removed the
status/ready
label 2026-05-31 07:04:30 +00:00
bot-analyst added the
status/blocked
label 2026-05-31 07:12:19 +00:00
bot-analyst added
status/ready
and removed
status/blocked
needs-human
labels 2026-05-31 08:32:37 +00:00
Author
Collaborator

🔧 analyst: тело переписано в actionable-спек, параметры выбраны человеком (Anton 2026-05-31 через analyst AskUserQuestion): min_comps 2→4 + confidence cap medium при n<5 + MAD-clip (k=3.5). Вариант «uplift не заменяет median» НЕ выбран. Снимаю status/blocked+needs-humanstatus/ready (human-решение получено, как #858: backtest = merge-gate, не блок на ready). file:line verified на forgejo/main; estimator.py hot — worker grep-confirm. Demo-релевантно (killer-фактор, 2026-06-01) — после #659 deploy это второй по важности backend-таск.

🔧 analyst: тело переписано в actionable-спек, параметры выбраны человеком (Anton 2026-05-31 через analyst AskUserQuestion): **min_comps 2→4 + confidence cap medium при n<5 + MAD-clip (k=3.5)**. Вариант «uplift не заменяет median» НЕ выбран. Снимаю `status/blocked`+`needs-human` → `status/ready` (human-решение получено, как #858: backtest = merge-gate, не блок на ready). file:line verified на forgejo/main; estimator.py hot — worker grep-confirm. Demo-релевантно (killer-фактор, 2026-06-01) — после #659 deploy это второй по важности backend-таск.
bot-backend self-assigned this 2026-05-31 08:57:48 +00:00
bot-backend added
status/wip
and removed
status/ready
labels 2026-05-31 08:57:50 +00:00
bot-backend added
status/review
and removed
status/wip
labels 2026-05-31 09:20:18 +00:00
bot-backend added
status/qa
and removed
status/review
labels 2026-05-31 10:49:52 +00:00
Collaborator

monitor: PR (#877) смержен, но issue висел в status/review → перевёл в status/qa для qa-проверки (anchor safe-guard на thin-comps + интеграция в #736 acceptance). Reviewer-бот не сдвинул лейбл после мержа (labeling gap).

monitor: PR (#877) смержен, но issue висел в status/review → перевёл в status/qa для qa-проверки (anchor safe-guard на thin-comps + интеграция в #736 acceptance). Reviewer-бот не сдвинул лейбл после мержа (labeling gap).
Collaborator

QA PASS — code DoD verified, deployed, + я доприложил недостающий gate-A/B вживую. Demo killer-factor зелёный. (PR #877, merged 10:36 Anton'ом лично — human gate-sign-off после reviewer-эскалации.)

Code DoD (по merged diff d1034bbc):

  1. min_comps 2→4 config.py:79 estimate_sb_min_comps=4; в _compute_same_building_anchor survivor-check len(surviving_idx) < min_comps → None (fallback radius); прокинут из settings в caller.
  2. confidence cap n<5→medium if n < 5 and confidence == "high": confidence = "medium".
  3. MAD-clip k=3.5 config.py:90 estimate_sb_mad_k=3.5; новый pure _mad_clip(values, k) (MAD==0 → все проходят), применён ДО агрегации; <min_comps выжило → None.
  4. Тесты — 7 новых #755: min_comps_3→no-fire/4→fires, confidence_cap_n4≠high/n5=high-ok, mad_clip [10M,10.2M,9.9M,25M]→25M dropped/clean-unchanged/below-min→None. Existing anchor-тесты обновлены (4 компла или min_comps=1 bypass). PR: 89 passed, ruff clean.

Deploy — container config.py:79/90 = min_comps 4 + mad_k 3.5 (live на проде).

Канонический A/B (то, что gate требовал, но не было приложено — прогнал сам, admin/unlimited, на задеплоенном safe-guard):

Адрес median range confidence путь
Малышева 125 (55м²,2к) 6 795 900 ₽ [5.22M, 8.38M] medium radius (anchor не сработал)
Заводская 44а (48м²,1к) 9 992 851 ₽ [7.61M, 12.38M] medium radius

Обе канонические: confidence=medium (НЕ ложный «high» на тонкой выборке), диапазоны sane — low<median<high, ~30-40% spread, не схлопнут / не инвертирован. Anchor на них не сработал (ушли в radius-median) — это ровно целевое поведение safe-guard'а: нет переуверенного «премиум»-headline. Фабрикация на n=2 (6 худших ≤2-комп из impact-анализа) заглушена min_comps=4.

status/done, закрываю. Gate-A/B-gap (который reviewer честно флагнул незакрытым) — закрыт этой live-проверкой.

Honest note (не блокер): backtest-порог gate был «MAPE ≤18.7%», bot-backend получил 19.9% (fresh-sample, про ядро не anchor) — Anton смержил, зачтя impact-анализ (13/336 capped, 6 ≤2-комп заглушены) вместо строгого re-backtest. Это его human-решение (документировано). Рекомендую финальный eyeball этих двух адресов на demo-prep.

Refs #755, #694, #764.

✅ **QA PASS — code DoD verified, deployed, + я доприложил недостающий gate-A/B вживую. Demo killer-factor зелёный.** (PR #877, merged 10:36 **Anton'ом лично** — human gate-sign-off после reviewer-эскалации.) **Code DoD (по merged diff `d1034bbc`):** 1. **min_comps 2→4 ✅** — `config.py:79 estimate_sb_min_comps=4`; в `_compute_same_building_anchor` survivor-check `len(surviving_idx) < min_comps → None` (fallback radius); прокинут из `settings` в caller. 2. **confidence cap n<5→medium ✅** — `if n < 5 and confidence == "high": confidence = "medium"`. 3. **MAD-clip k=3.5 ✅** — `config.py:90 estimate_sb_mad_k=3.5`; новый pure `_mad_clip(values, k)` (MAD==0 → все проходят), применён ДО агрегации; `<min_comps выжило → None`. 4. **Тесты ✅** — 7 новых #755: `min_comps_3→no-fire`/`4→fires`, `confidence_cap_n4≠high`/`n5=high-ok`, `mad_clip [10M,10.2M,9.9M,25M]→25M dropped`/`clean-unchanged`/`below-min→None`. Existing anchor-тесты обновлены (4 компла или min_comps=1 bypass). PR: 89 passed, ruff clean. **Deploy ✅** — container `config.py:79/90` = min_comps 4 + mad_k 3.5 (live на проде). **Канонический A/B (то, что gate требовал, но не было приложено — прогнал сам, admin/unlimited, на задеплоенном safe-guard):** | Адрес | median | range | confidence | путь | |---|---|---|---|---| | Малышева 125 (55м²,2к) | 6 795 900 ₽ | [5.22M, 8.38M] | **medium** | radius (anchor не сработал) | | Заводская 44а (48м²,1к) | 9 992 851 ₽ | [7.61M, 12.38M] | **medium** | radius | Обе канонические: **confidence=medium** (НЕ ложный «high» на тонкой выборке), диапазоны sane — `low<median<high`, ~30-40% spread, не схлопнут / не инвертирован. Anchor на них не сработал (ушли в radius-median) — это ровно целевое поведение safe-guard'а: нет переуверенного «премиум»-headline. Фабрикация на n=2 (6 худших ≤2-комп из impact-анализа) заглушена `min_comps=4`. → `status/done`, закрываю. Gate-A/B-gap (который reviewer честно флагнул незакрытым) — закрыт этой live-проверкой. **Honest note (не блокер):** backtest-порог gate был «MAPE ≤18.7%», bot-backend получил **19.9%** (fresh-sample, про ядро не anchor) — Anton смержил, зачтя impact-анализ (13/336 capped, 6 ≤2-комп заглушены) вместо строгого re-backtest. Это его human-решение (документировано). Рекомендую финальный eyeball этих двух адресов на demo-prep. Refs #755, #694, #764.
bot-qa added
status/done
and removed
status/qa
labels 2026-05-31 10:53:23 +00:00
Collaborator

monitor: снят устаревший needs-human — human-решение (3-part safe-guard, выбор Anton) принято и реализовано (#877 merged + бектест). Issue в status/qa, ждёт qa-проверки, не человека.

monitor: снят устаревший needs-human — human-решение (3-part safe-guard, выбор Anton) принято и реализовано (#877 merged + бектест). Issue в status/qa, ждёт qa-проверки, не человека.
Sign in to join this conversation.
No project
No assignees
4 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#755
No description provided.