fix(tradein/estimate): не строить цену по одному-двум аналогам, не отдавать 0 ₽ #2629
No reviewers
Labels
No labels
Fable 5 ревью
GG-форсайт
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
feedback/max
generative
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#2629
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/tradein-analog-sufficiency"
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?
Summary
Money-path fix: the estimator built a headline price from as few as 1-4 scraped listings with no sufficiency check, and could return a literal
median_price_rub=0without an honest refusal reaching the consumer in every code path.HEADLINE_LISTINGS_MIN_N = 5(estimator.py). Below the threshold the radius-listings aggregate (median/range/n_analogs/cv) is suppressed to the same "no usable listings" state — routing automatically through the already-existing, already-tested same-building anchor /#oblast-Ddeals-headline-fallback (ДКП Росреестра) /insufficient_datachain.listings_cleanitself is deliberately not cleared, so the same-building anchor's own ghost-anchor guard (#1871) still tells "genuinely zero nearby listings" from "some nearby, just too few to trust".deals-headline-fallbackgated onanchor_tier is None, butanchor_tierstays stale (not reset) when_compute_same_building_anchorrejects a candidate outright — blocking the fallback even with a large ДКП corridor available. Now gates onanchor is None(the actual computed anchor).insufficient_data(computed field,median_price_rub<=0) remains the single source of truth; verified the PDF exporter and API schema both already gate on it (no code change needed there — this was already correct, just re-verified end to end).Does not touch analog selection/tiers/radii, the asking→sold ratio, quarter-index, hedonic correction, scrapers, or geocoding.
Threshold — 5, data-driven
Live repro (Серов, 2-комн 45 м², coords 59.604/60.577): n=3 listings → 42 391 ₽/м² (−36% vs the city-wide ДКП corridor at the same moment, 54 126 ₽/м²). A neighbouring street (Фуфачева) on n=4 listings gave 56 351 ₽/м² — 33% apart from the first street's 42 391 for the same room/area combo, same city.
5was chosen to match the existingMIN_ANALOGS_TIER_0convention ("enough to trust a cohort tier") already used elsewhere in this file — not a new, unrelated magic number.Before/after (live prod, 2026-08-02, real listings + real ДКП corridor at capture time)
DKP_CORRIDOR_CITY_WIDE_MIN_N, existing#oblast-Dmechanism) — out of this PR's scope per the task boundaryNote: today's live re-run did not reproduce a literal
Каменск 3-комн → 0 ₽for the task's exact coordinates — ДКП coverage for that room/area/street combo has since improved (DB is continuously updated by the daily importer) and the existing fallback already covers it (0 listings, unaffected by this gate). Reproduced the same literal-0 ₽shape instead with an atypical room count (4) at the same coordinates (0 listings + insufficient ДКП match) — confirmedinsufficient_data=Trueis set correctly and neither the PDF exporter nor the API JSON leak0as a confident number.7 estimate rows were created on prod by this verification (
trade_in_estimates, all against the currently-deployed code — my local fix isn't live yet, so all "after" numbers above were computed by feeding the same real captured prod listings + ДКП corridor into the patched_price_from_inputslocally, not by re-hitting the endpoint).Tests
New
tests/test_estimator_headline_sufficiency.py(14 tests, 2 layers — direct_price_from_inputsunit tests +estimate_qualityintegration tests). Falsified: revertedHEADLINE_LISTINGS_MIN_Nto0(gate no-ops, same signature) — 11/14 fail on wrong VALUES (median/n_analogs/explanation content), notAttributeError, confirming the tests exercise the gate's behavior and not just its existence.13 pre-existing test files needed fixture bumps (n=1..3 analogs → n≥5, mostly identical-price/symmetric so asserted medians stay byte-identical) — those files test other mechanics (repair coefficient, expected-sold ratio, quarter-index, range floor, segment multiplier, IMV blend, anchor-vs-radius gating) that used a thin sample purely as a convenience fixture, now incompatible with the new floor.
tests/fixtures/backtest_baseline.jsonregenerated from the frozen 277-deal prod fixture (replay_fixture, dedup pinned OFF to match whattest_backtest_regression_gateactually computes — the CLI--update-baselinescript itself doesn't pin this and crashes on this fixture; pre-existing gap since#2173, unrelated to this PR, left as-is):expected_sold.overall.n277→269 (8 deals now correctly route through the deals-fallback / stay insufficient instead of a noisy thin median), MAPE 13.23%→13.18%, headlinespread_pct17.98%→16.65%.Full
pytest: 3113 passed, 9 skipped, 1 pre-existing failure (test_search_api.py::test_search_cache_hit, 401 RBAC — unrelated, known, not touched).Test plan
pytest tradein-mvp/backendfull suite green (except the known pre-existing failure)ruff check/ruff formatcleanRefs: live money-path audit 2026-08-02 (Серов/Каменск-Уральский/Первоуральск/Екатеринбург)