fix(estimator): deterministic comp tie-breaker + n_analogs counts priced analogs #1939
No reviewers
Labels
No labels
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
Fable 5 ревью
feedback/max
generative
GG-форсайт
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
вторичка
ИРД
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#1939
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/estimator-tiebreaker-nanalogs"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Два подтверждённых 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вbaseCTE обоих тиров; внешняя проекция не изменена (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', без двойной обработки), downstreamlistings_clean(analogs_lots/sources/repair_coef) не тронут.Проверки
ruff✅ · estimator-subset 423 passed · full suite 2450 passed (1 pre-existing unrelatedtest_search_cache_hitпадает и на чистом main).test_estimator_n_analogs_priced.py) — code-reviewer эмпирически подтвердил, что они FAIL на старом коде.code-reviewer: ✅ APPROVE, 0 critical (SQL-валидность, total-order PK, #698/#691 контракты — все трассированы).Намеренно НЕ тронуто (аудит флагнул, но отклонено)
objective_complex_mappingимеет 0 дублейdomrf_obj_id(303/303 distinct, max 1 row), fan-out физически невозможен.>vs>=fallthrough (2103/2125) — косметика (equal count = идентичный superset, меняет лишь disclosure-флаг).Refs #1871