feat(tradein): ppm²-tier segmentation of asking→sold ratio (#928, flag OFF) #934
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
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#934
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "feat/tradein-ppm2-tier-ratio"
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?
Closes #928 (implementation; flag stays OFF until post-demo QA).
What
Segments the asking→sold correction ratio by ppm²-tier (terciles of active-listing ₽/m² per rooms bucket), so the cheap econom-panel submarket no longer shares a ratio with monolith new-builds. Targets the
expected_soldunder-valuation found in golden-сверка #794 vs Brusnika EKB-2485 (expected_sold 3.16M was −9% below the ~3.47M buyout). Headline median is NOT touched (same-building anchor + IQR/MAD unchanged).Behind a flag — ships dark
tier_aware_ratio_enabled: bool = False(config.py). With the flag OFF the estimator path is byte-identical to current behavior. Do NOT enable on prod until after the 2026-06-01 demo + manual QA + the held-out backtest (see "Gate before flag-flip" below).Design decisions
asking_to_sold_ratios_tiered+asking_to_sold_tier_bounds; the existingasking_to_sold_ratiosis untouched. Rationale: the flag-OFF estimator doesSELECT ... WHERE rooms_bucket=B AND district='' → fetchone(); adding tier rows to that table with a new PK would return multiple rows andfetchone()would pick arbitrarily → silent prod regression. Parallel table keeps the live path provably unchanged.data/sql/098_asking_to_sold_ratios_tiered.sql— strictly additive (CREATE TABLE IF NOT EXISTS + ON CONFLICT seed), no DROP/ALTER of existing objects, BEGIN/COMMIT, idempotent. Safe for the strict prod auto-apply.median(sold in tier)/median(asking in tier)with empirical-Bayes shrinkage toward the rooms-aggregate ratio:w = n_deals/(n_deals + tier_ratio_shrink_k),k=150. On current data all 15 cells are dense (n_deals ≥169) so shrinkage is near-no-op; it's insurance for future thin cells.(B,'',T)→ rooms-aggregate(B,'','all')→ global(-1,'','all')→(None,None). Graceful try/except + rollback on un-migrated DB.tasks/asking_to_sold_ratio.py) extended to re-derive the tiered tables in the same atomic transaction as the legacy table (byte-consistent derivation).Step-1 distribution gate (validated read-only on prod)
All 15 cells (5 rooms × 3 tiers) clear ≥30 deals AND ≥30 listings (min: 169 deals / 583 listings). Example: rb=2 t33=129167 t66=163870; low ratio 0.911, mid 0.978, high 0.991 vs current global 0.803. Brusnika EKB-2485 (anchor ppm² ≈92k → low tier) → ratio 0.911 → expected_sold ≈3.57M (error −9% → +3%).
Review fixes applied (code-reviewer ⚠️ MINOR, no blockers)
(bucket,"_legacy"), distinct from flag-ON(bucket,None)— prevents serving the old-table ratio for ~TTL after a flag flip.test_migration_098_band_matches_settings_defaultasserting the seed literals[30000,1200000]match_PPM2_MIN/asking_ratio_ppm2_maxdefaults.median_ppm2, not the same-building Tukey anchor #928 ideally specifies (anchor is computed post-call). Robust for the golden case; flagged to revisit before the flag-flip.Gate before flag-flip (post-demo, NOT in this PR)
Step 5 backtest: Brusnika expected_sold ≥3.35M (flag on) and all 53 golden cases' headline median byte-identical pre/post (regression guard, golden set =
research/TradeIn_Valuation_Golden_Dataset_May29§6); plus held-out ДКП check for mid/high tiers (those tiers' sold-side is untested by the asking-priced golden set) and the M3 anchor-placement revisit.Tests
1677 passed; 2 failures are pre-existing on main (
test_cian_valuation::…cache_hit,test_search_api::test_search_cache_hit) and unrelated. New: 8 tier-lookup unit tests + 2 guard tests. ruff clean.✅ APPROVE — корректный dark-ship (flag OFF), миграция safe для strict prod-auto-apply, demo-safe. Мержу.
Verified — два критичных риска (прод DB + valuation)
098— strictly additive + idempotent (проверил файл напрямую):BEGIN/COMMIT,CREATE TABLE IF NOT EXISTS×2 (новые параллельные таблицы),INSERT … ON CONFLICT DO UPDATE(idempotent seed).DROP/ALTER/DELETEвстречаются только в--комментах (описание refresh-task), реальных destructive DDL/DML нет. Существующаяasking_to_sold_ratiosНЕ тронута → safe для strict exit-1 auto-apply (см.project_tradein_migrations_auto_apply), повторный re-apply идемпотентен.use_tier_path = settings.tier_aware_ratio_enabled and anchor_ppm2 is not None; flag OFF → legacy-ветка тот жеSELECT ratio, basis FROM asking_to_sold_ratios(старая таблица) с отдельным cache-key(bucket,"_legacy")(M1 fix — нет collision с flag-ON 'all' после флипа). Tier-путь активен ТОЛЬКО flag-ON + anchor задан. Дефолтtier_aware_ratio_enabled=False→ прод/демо без изменений.Sound
fetchone()по старой таблице не получит multiple rows. ✓🟡 Follow-up (завожу issue — gate перед flag-flip)
НЕ включать
tier_aware_ratio_enabledна проде до Step-5 backtest: Brusnika expected_sold ≥3.35M (flag on) + все 53 golden headline median byte-identical pre/post + held-out ДКП для mid/high тиров + M3 anchor-placement ревизия. Завожу tracking-issue.Squash-merge + delete branch.
Closes #928.