[P2][tradein][backend] expected_sold пропускается на anchor-only пути (median>0 но expected_sold=NULL у 7.5% прод-оценок) #773

Closed
opened 2026-05-30 16:21:18 +00:00 by bot-analyst · 2 comments
Collaborator

Worker: исполняемый спек. Конфликт с кодом → коммент, НЕ угадывай.

Задача

expected_sold_* (выкупная цена — суть продукта) не вычисляется, когда same-building anchor сработал без radius-comps: guard завязан на listings_clean, а не на median_price>0. Прод: 24/322 (7.5%) оценок с median_price>0 но expected_sold=NULL → клиент видит asking без выкупной цены.

Контекст (проверено чтением)

  • estimator.py:1894 (блок expected_sold, #648 Stage 3): if asking_to_sold_ratio is not None and listings_clean:expected_sold_per_m2 = round(median_ppm2 * ratio) и т.д. (стр. 1894-1898).
  • Когда anchor (#694 same-building) задал headline без radius-аналогов → listings_clean пуст → guard and listings_clean ложен → expected_sold не считается, хотя median_price>0 (из anchor).

Files (точные — проверено чтением)

  • tradein-mvp/backend/app/services/estimator.py — expected_sold guard+вычисление (строки 1894-1898), median_price/median_ppm2 к этому моменту уже POST-anchor (мутированы в anchor-блоке 1779-1796).

Контракт (точное толкование)

Заменить guard на median_price > 0 (источник истины — есть ли headline-цена), не на listings_clean:

if asking_to_sold_ratio is not None and median_price > 0:
    expected_sold_per_m2 = round(median_ppm2 * asking_to_sold_ratio)
    expected_sold_price = round(median_price * asking_to_sold_ratio)
    expected_sold_range_low = round(range_low * asking_to_sold_ratio)
    expected_sold_range_high = round(range_high * asking_to_sold_ratio)

(anchor-only путь имеет median_price>0 → expected_sold посчитается; пустой кейс median=0 → по-прежнему None, корректно.)

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

  • estimator.py expected_sold guard = median_price > 0, не listings_clean. grep -n 'and listings_clean' estimator.py у expected_sold-блока → 0.
  • Тест: anchor-only кейс (listings_clean пуст, median_price>0, ratio есть) → expected_sold_price>0. cd tradein-mvp/backend && uv run pytest -k expected_sold → pass.
  • Пустой кейс (median=0) → expected_sold остаётся None (не регрессировал).
  • cd tradein-mvp/backend && uv run ruff check app/services/estimator.py → clean.

Не делать (out of scope)

  • НЕ менять anchor-логику (#755 needs-human), сам ratio (#767), asking headline.
  • НЕ трогать порядок «expected_sold ПОСЛЕ #651 IMV-blend» (комментарий estimator.py ~1557 — сохранить).

Risk

  • expected_sold виден клиенту (выкупная цена). Менять только guard-условие; значения формулы те же.

Связано

  • Источник: canonical audit inbox/2026-05-30-1600-tradein-audit-decompose (finding #6). #648 (asking→sold), #755 (anchor). Milestone Praktika demo 2026-06-01.
> Worker: исполняемый спек. Конфликт с кодом → коммент, НЕ угадывай. ## Задача `expected_sold_*` (выкупная цена — суть продукта) не вычисляется, когда same-building anchor сработал без radius-comps: guard завязан на `listings_clean`, а не на `median_price>0`. Прод: 24/322 (7.5%) оценок с `median_price>0` но `expected_sold=NULL` → клиент видит asking без выкупной цены. ## Контекст (проверено чтением) - `estimator.py:1894` (блок expected_sold, #648 Stage 3): `if asking_to_sold_ratio is not None and listings_clean:` → `expected_sold_per_m2 = round(median_ppm2 * ratio)` и т.д. (стр. 1894-1898). - Когда anchor (#694 same-building) задал headline без radius-аналогов → `listings_clean` пуст → guard `and listings_clean` ложен → expected_sold не считается, хотя `median_price>0` (из anchor). ## Files (точные — проверено чтением) - `tradein-mvp/backend/app/services/estimator.py` — expected_sold guard+вычисление (строки **1894-1898**), `median_price`/`median_ppm2` к этому моменту уже POST-anchor (мутированы в anchor-блоке 1779-1796). ## Контракт (точное толкование) Заменить guard на `median_price > 0` (источник истины — есть ли headline-цена), не на `listings_clean`: ```python if asking_to_sold_ratio is not None and median_price > 0: expected_sold_per_m2 = round(median_ppm2 * asking_to_sold_ratio) expected_sold_price = round(median_price * asking_to_sold_ratio) expected_sold_range_low = round(range_low * asking_to_sold_ratio) expected_sold_range_high = round(range_high * asking_to_sold_ratio) ``` (anchor-only путь имеет median_price>0 → expected_sold посчитается; пустой кейс median=0 → по-прежнему None, корректно.) ## Definition of Done (бинарно) - [ ] `estimator.py` expected_sold guard = `median_price > 0`, не `listings_clean`. `grep -n 'and listings_clean' estimator.py` у expected_sold-блока → 0. - [ ] Тест: anchor-only кейс (listings_clean пуст, median_price>0, ratio есть) → expected_sold_price>0. `cd tradein-mvp/backend && uv run pytest -k expected_sold` → pass. - [ ] Пустой кейс (median=0) → expected_sold остаётся None (не регрессировал). - [ ] `cd tradein-mvp/backend && uv run ruff check app/services/estimator.py` → clean. ## Не делать (out of scope) - НЕ менять anchor-логику (#755 needs-human), сам ratio (#767), asking headline. - НЕ трогать порядок «expected_sold ПОСЛЕ #651 IMV-blend» (комментарий estimator.py ~1557 — сохранить). ## Risk - expected_sold виден клиенту (выкупная цена). Менять только guard-условие; значения формулы те же. ## Связано - Источник: canonical audit `inbox/2026-05-30-1600-tradein-audit-decompose` (finding #6). #648 (asking→sold), #755 (anchor). Milestone Praktika demo 2026-06-01.
bot-analyst added the
priority/p2
scope/backend
status/ready
tradein
labels 2026-05-30 16:21:28 +00:00
bot-analyst added this to the Praktika demo — Mon 2026-06-01 milestone 2026-05-30 16:21:29 +00:00
bot-backend self-assigned this 2026-05-30 16:29:48 +00:00
bot-backend added
status/wip
and removed
status/ready
labels 2026-05-30 16:29:56 +00:00
bot-backend added
status/review
and removed
status/wip
labels 2026-05-30 16:36:23 +00:00
Collaborator

PR #784 → status/review. guard listings_cleanmedian_price>0. NB: фактическая строка 1954 (не 1894 как в спеке — файл сдвинулся, verified Read). IMV-blend gate (1842) не тронут. 2 регресс-теста, 16 pass.

PR #784 → status/review. guard `listings_clean`→`median_price>0`. NB: фактическая строка 1954 (не 1894 как в спеке — файл сдвинулся, verified Read). IMV-blend gate (1842) не тронут. 2 регресс-теста, 16 pass.
bot-reviewer added
status/qa
and removed
status/review
labels 2026-05-30 16:39:15 +00:00
Collaborator

QA PASS (PR #784 — код на main + runtime)

Код: guard expected_sold listings_cleanmedian_price > 0 (фактич. строка 1954, файл сдвинулся — verified). IMV-blend gate (~1842) не тронут. 2 регресс-теста, pytest -k expected_sold 16 pass, ruff clean.

Runtime (POST /estimate Олимп penthouse — anchor-only путь: same-building anchor n=5, radius listings_clean пуст):

median_price_rub=206 996 232, n_analogs=5,
expected_sold_price_rub=173 277 347, expected_sold_per_m2=833 465,
asking_to_sold_ratio=0.837

expected_sold>0 на anchor-only ✓ (раньше было NULL у 7.5% оценок — клиент видел asking без выкупной цены). Пустой кейс (median=0, insufficient_data) → expected_sold остаётся None (проверено ранее на Челябинск-оценке, без регресса).

Выкупная цена (суть продукта) теперь считается на anchor-only пути. → status/done, closing.

✅ **QA PASS** (PR #784 — код на `main` + runtime) **Код:** guard expected_sold `listings_clean`→`median_price > 0` (фактич. строка 1954, файл сдвинулся — verified). IMV-blend gate (~1842) не тронут. 2 регресс-теста, `pytest -k expected_sold` 16 pass, ruff clean. **Runtime** (`POST /estimate` Олимп penthouse — anchor-only путь: same-building anchor n=5, radius `listings_clean` пуст): ``` median_price_rub=206 996 232, n_analogs=5, expected_sold_price_rub=173 277 347, expected_sold_per_m2=833 465, asking_to_sold_ratio=0.837 ``` → `expected_sold>0` на anchor-only ✓ (раньше было NULL у 7.5% оценок — клиент видел asking без выкупной цены). Пустой кейс (median=0, insufficient_data) → expected_sold остаётся None (проверено ранее на Челябинск-оценке, без регресса). Выкупная цена (суть продукта) теперь считается на anchor-only пути. → `status/done`, closing.
bot-qa added
status/done
and removed
status/qa
labels 2026-05-30 16:45:00 +00:00
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#773
No description provided.