All checks were successful
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 8s
CI / backend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 50s
Глобальный DEAL_MIN_PPM2=50k резал 46.6% не-ЕКБ сделок (легит дешёвые города: Североуральск p50=21.7k, Новоуральск 36k, Асбест 41k). Band стал per-city. - migration 178: deal_city_price_bands (city PK, ppm2_min/max, n_deals), populate из deals (source=rosreestr, city<>Екатеринбург, count>=30), min=GREATEST(p1,8000) max=LEAST(p99,800000), idempotent upsert; HAVING-guard p99>=8000 (airtight против inverted band). - estimator.py оба enforcement-пойнта city-aware с fallback на глобаль: - _fetch_dkp_corridor SQL: LEFT JOIN deal_city_price_bands + COALESCE(band, :global). - _is_plausible_deal(city, bands) + _fetch_deals: _load_city_price_bands (TTL-cache), SELECT city. - ЕКБ исключён из таблицы → fallback на 50k/800k → EKB-сделки byte-identical (deals спеллит EKB только 'Екатеринбург', 49791). EKB deal-level инвариант airtight; interior-EKB оценки идентичны. Регресс-гейт байт-зелёный. Follow-ups: periodic refresh band-таблицы; city-scoping _fetch_dkp_corridor (→ C2).
87 lines
5.1 KiB
PL/PgSQL
87 lines
5.1 KiB
PL/PgSQL
-- 178_deal_city_price_bands.sql
|
||
-- Per-city ppm² plausible-deal guard-bands — снятие Екатеринбург-центричного
|
||
-- глобального DEAL_MIN_PPM2=50_000 (estimator.py) для остальной Свердловской обл.
|
||
--
|
||
-- WHY:
|
||
-- Миграция 177 открыла +47183 не-ЕКБ сделок (deals.city заполнен для всей
|
||
-- области). Но estimator.py всё ещё режет ДКП-сделки абсолютным guard-band
|
||
-- [DEAL_MIN_PPM2=50_000, DEAL_MAX_PPM2=800_000] ₽/м² — калиброванным
|
||
-- ИСКЛЮЧИТЕЛЬНО под ЕКБ-вторичку (см. комментарий у DEAL_MIN_PPM2 в
|
||
-- estimator.py). Для дешёвых городов области этот пол — не anti-outlier
|
||
-- guard, а cut-off легитимного рынка:
|
||
-- Североуральск median ≈ 21.7k ₽/м², Новоуральск ≈ 36k, Асбест ≈ 41k —
|
||
-- ВСЕ ниже 50k → 46.6% не-ЕКБ сделок молча дропаются как «доля/ошибка»,
|
||
-- хотя это нормальная цена для города.
|
||
--
|
||
-- WHAT:
|
||
-- Таблица deal_city_price_bands — per-city [ppm2_min, ppm2_max] band,
|
||
-- вычисленный из РЕАЛЬНЫХ rosreestr-сделок города (p1/p99 персентиль,
|
||
-- зажатый абсолютными hard-floor/ceiling 8000/800000 — доли и опечатки
|
||
-- режутся даже в самых дешёвых городах). Екатеринбург НАМЕРЕННО исключён
|
||
-- из популяции — estimator.py при отсутствии строки в этой таблице
|
||
-- fallback'ит на глобальные DEAL_MIN_PPM2/DEAL_MAX_PPM2 константы, т.е.
|
||
-- ЕКБ-поведение byte-identical сегодняшнему (см. invariant в PR).
|
||
--
|
||
-- Города с < 30 сделок исключены (HAVING count(*) >= 30) — слишком тонкая
|
||
-- выборка для устойчивого перцентиля, estimator fallback'ит на global band.
|
||
--
|
||
-- USAGE (estimator.py, тот же PR):
|
||
-- - SQL enforcement (_fetch_dkp_corridor, ~line 1263): LEFT JOIN на
|
||
-- deals.city = deal_city_price_bands.city, COALESCE(b.ppm2_min/max, global).
|
||
-- - Python enforcement (_is_plausible_deal via _fetch_deals): bands dict,
|
||
-- загружен один раз через _load_city_price_bands(db), .get(city, global).
|
||
--
|
||
-- IDEMPOTENCY / SAFETY:
|
||
-- CREATE TABLE IF NOT EXISTS — безопасный re-run. Populate — INSERT ...
|
||
-- ON CONFLICT (city) DO UPDATE, т.е. повторный прогон РЕФРЕШИТ бэнды под
|
||
-- свежие сделки (не no-op по дизайну — таблица предполагает периодический
|
||
-- refresh по мере роста deals; scheduler для авто-рефреша НЕ входит в этот
|
||
-- PR, см. follow-up в PR description).
|
||
--
|
||
-- Dependencies: 177_deals_city_region.sql (deals.city, backfill).
|
||
|
||
BEGIN;
|
||
|
||
CREATE TABLE IF NOT EXISTS deal_city_price_bands (
|
||
city text PRIMARY KEY,
|
||
ppm2_min int NOT NULL,
|
||
ppm2_max int NOT NULL,
|
||
n_deals int NOT NULL,
|
||
refreshed_at timestamptz NOT NULL DEFAULT now()
|
||
);
|
||
|
||
COMMENT ON TABLE deal_city_price_bands IS
|
||
'Per-city ppm² plausible-deal guard-bands (estimator.py _is_plausible_deal / '
|
||
'_fetch_dkp_corridor). Екатеринбург НЕ включён — estimator fallback на глобальные '
|
||
'DEAL_MIN_PPM2=50000/DEAL_MAX_PPM2=800000 сохраняет ЕКБ-поведение byte-identical. '
|
||
'Refresh: повторный прогон этого файла (ON CONFLICT DO UPDATE); авто-scheduler '
|
||
'пока не реализован (см. issue follow-up).';
|
||
|
||
INSERT INTO deal_city_price_bands (city, ppm2_min, ppm2_max, n_deals, refreshed_at)
|
||
SELECT
|
||
city,
|
||
GREATEST(round(percentile_cont(0.01) WITHIN GROUP (ORDER BY price_per_m2))::int, 8000)
|
||
AS ppm2_min,
|
||
LEAST(round(percentile_cont(0.99) WITHIN GROUP (ORDER BY price_per_m2))::int, 800000)
|
||
AS ppm2_max,
|
||
count(*) AS n_deals,
|
||
now() AS refreshed_at
|
||
FROM deals
|
||
WHERE source = 'rosreestr'
|
||
AND price_per_m2 IS NOT NULL
|
||
AND city IS NOT NULL
|
||
AND city <> 'Екатеринбург'
|
||
GROUP BY city
|
||
HAVING count(*) >= 30
|
||
-- Инвариант ppm2_max >= ppm2_min: если весь распределение города < 8000 ₽/м²
|
||
-- (p99 < 8000), LEAST(p99,800000) < GREATEST(p1,8000) → band [8000, <8000] не
|
||
-- матчит ничего → все сделки города молча дропались бы. Гейтим такие города
|
||
-- (для жилой ДКП недостижимо, но airtight против мусорного города).
|
||
AND round(percentile_cont(0.99) WITHIN GROUP (ORDER BY price_per_m2))::int >= 8000
|
||
ON CONFLICT (city) DO UPDATE
|
||
SET ppm2_min = EXCLUDED.ppm2_min,
|
||
ppm2_max = EXCLUDED.ppm2_max,
|
||
n_deals = EXCLUDED.n_deals,
|
||
refreshed_at = EXCLUDED.refreshed_at;
|
||
|
||
COMMIT;
|