fix(estimator): deterministic comp tie-breaker + n_analogs counts priced analogs #1939

Merged
bot-backend merged 1 commit from fix/estimator-tiebreaker-nanalogs into main 2026-06-27 00:58:15 +00:00
Collaborator

Два подтверждённых correctness-бага, найденных adversarial-аудитом valuation-пути (multi-agent: 5 dimensions × find→refute-by-default verify; 8 confirmed, отфильтровано до 2 bot-safe + резолв 1 false-positive на проде).

FIX A (#5) — недетерминированный выбор компов

Оба radius-запроса компов (Tier H ~3990, Tier W ~4135) заканчивались голым ORDER BY relevance_score. При равном relevance_score (одинаковый distance-bucket + year-delta + house_type) Postgres возвращает строки в неопределённом порядке → один и тот же /analyze мог выбирать РАЗНЫЕ компы между прогонами (Python-sort стабилен, лишь фиксирует уже-неопределённый SQL-порядок).

Фикс: детерминированный tiebreaker ORDER BY relevance_score ASC, distance_m ASC, scraped_at DESC NULLS LAST, id ASC (id = PK listings → полный порядок, нулевые остаточные ties). Добавлен id в base CTE обоих тиров; внешняя проекция не изменена (id не утекает downstream).

FIX B (#2) — n_analogs считал беспрайсовые аналоги

_filter_outliers сохраняет строки с price_per_m2 IS NULL, но медиана строится из prices_ppm2 (которая их отбрасывает), а n_analogs = len(listings_clean) считал ВСЕ — завышая число «вкладывающихся» компов («Найдено N аналогов» вводит в заблуждение; при всех беспрайсовых median=0, но n_analogs>0). Фикс: n_analogs = len(prices_ppm2) в radius-пути.

Контракты сохранены (проверено code-reviewer'ом построчно): #698 anchor (n_analogs = anchor["n"] перетирает позже, в anchor-ветке), #691 / _enforce_zero_analog_low (all-price-less → n_analogs=0 + confidence='low', без двойной обработки), downstream listings_clean (analogs_lots/sources/repair_coef) не тронут.

Проверки

  • ruff · estimator-subset 423 passed · full suite 2450 passed (1 pre-existing unrelated test_search_cache_hit падает и на чистом main).
  • 3 новых теста (test_estimator_n_analogs_priced.py) — code-reviewer эмпирически подтвердил, что они FAIL на старом коде.
  • code-reviewer: APPROVE, 0 critical (SQL-валидность, total-order PK, #698/#691 контракты — все трассированы).

Намеренно НЕ тронуто (аудит флагнул, но отклонено)

  • velocity.py fan-out — false-positive: на проде objective_complex_mapping имеет 0 дублей domrf_obj_id (303/303 distinct, max 1 row), fan-out физически невозможен.
  • > vs >= fallthrough (2103/2125) — косметика (equal count = идентичный superset, меняет лишь disclosure-флаг).
  • Tier-S fallthrough signal / cohort era-mix / yandex cache-miss — продуктовые решения, не баги.

Refs #1871

Два подтверждённых correctness-бага, найденных adversarial-аудитом valuation-пути (multi-agent: 5 dimensions × find→refute-by-default verify; 8 confirmed, отфильтровано до 2 bot-safe + резолв 1 false-positive на проде). ## FIX A (#5) — недетерминированный выбор компов Оба radius-запроса компов (Tier H ~3990, Tier W ~4135) заканчивались голым `ORDER BY relevance_score`. При равном `relevance_score` (одинаковый distance-bucket + year-delta + house_type) Postgres возвращает строки в неопределённом порядке → **один и тот же /analyze мог выбирать РАЗНЫЕ компы между прогонами** (Python-sort стабилен, лишь фиксирует уже-неопределённый SQL-порядок). Фикс: детерминированный tiebreaker `ORDER BY relevance_score ASC, distance_m ASC, scraped_at DESC NULLS LAST, id ASC` (`id` = PK listings → полный порядок, нулевые остаточные ties). Добавлен `id` в `base` CTE обоих тиров; внешняя проекция не изменена (id не утекает downstream). ## FIX B (#2) — n_analogs считал беспрайсовые аналоги `_filter_outliers` сохраняет строки с `price_per_m2 IS NULL`, но медиана строится из `prices_ppm2` (которая их отбрасывает), а `n_analogs = len(listings_clean)` считал ВСЕ — завышая число «вкладывающихся» компов («Найдено N аналогов» вводит в заблуждение; при всех беспрайсовых median=0, но n_analogs>0). Фикс: `n_analogs = len(prices_ppm2)` в radius-пути. **Контракты сохранены** (проверено code-reviewer'ом построчно): #698 anchor (`n_analogs = anchor["n"]` перетирает позже, в anchor-ветке), #691 / `_enforce_zero_analog_low` (all-price-less → n_analogs=0 + confidence='low', без двойной обработки), downstream `listings_clean` (analogs_lots/sources/repair_coef) не тронут. ## Проверки - `ruff` ✅ · estimator-subset **423 passed** · full suite 2450 passed (1 pre-existing unrelated `test_search_cache_hit` падает и на чистом main). - **3 новых теста** (`test_estimator_n_analogs_priced.py`) — code-reviewer эмпирически подтвердил, что они FAIL на старом коде. - `code-reviewer`: **✅ APPROVE**, 0 critical (SQL-валидность, total-order PK, #698/#691 контракты — все трассированы). ## Намеренно НЕ тронуто (аудит флагнул, но отклонено) - velocity.py fan-out — **false-positive**: на проде `objective_complex_mapping` имеет 0 дублей `domrf_obj_id` (303/303 distinct, max 1 row), fan-out физически невозможен. - `>` vs `>=` fallthrough (2103/2125) — косметика (equal count = идентичный superset, меняет лишь disclosure-флаг). - Tier-S fallthrough signal / cohort era-mix / yandex cache-miss — продуктовые решения, не баги. Refs #1871
bot-backend added 1 commit 2026-06-27 00:57:32 +00:00
fix(estimator): deterministic comp tie-breaker + n_analogs counts priced analogs
All checks were successful
CI / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
bcdc7ccb04
Found by adversarial valuation audit (2 confirmed, bot-safe).

FIX A (#5): both radius comp queries (Tier H ~3990, Tier W ~4135) ended with
a bare ORDER BY relevance_score; on score ties Postgres returned rows in
undefined order, so the same /analyze could pick different comps across runs.
Append deterministic tiebreaker: relevance_score ASC, distance_m ASC,
scraped_at DESC NULLS LAST, id ASC (id = listings PK → total order). Added id
to each base CTE; outer projection unchanged (no leak downstream).

FIX B (#2): _filter_outliers keeps rows with price_per_m2 IS NULL, but the
median is built from prices_ppm2 (drops them) while n_analogs counted all of
listings_clean — overstating contributing comps ("Найдено N аналогов"
misleading; all-price-less -> median=0 but n_analogs>0). Count n_analogs from
prices_ppm2 in the radius path. #698 anchor overwrite + #691 zero-analog->low
guard unaffected; listings_clean itself not filtered.

Adds tests/test_estimator_n_analogs_priced.py (verified to fail on old code).
Audit also flagged velocity fan-out (false-positive: 0 duplicate domrf_obj_id
on prod) and >/>= disclosure tweaks (cosmetic) — deliberately not changed.

Refs #1871
bot-backend merged commit 264701a4e4 into main 2026-06-27 00:58:15 +00:00
Sign in to join this conversation.
No reviewers
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#1939
No description provided.