gendesign/data/sql/123_macro_indicator.sql
Light1YT dbae4b0bda
All checks were successful
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Successful in 2m20s
Deploy / build-backend (push) Successful in 2m32s
Deploy / deploy (push) Successful in 1m14s
Deploy / changes (push) Successful in 6s
feat(site-finder): macro_indicator table + backfill + reader (#945 PR A) (#963)
2026-06-02 18:41:28 +00:00

194 lines
15 KiB
PL/PgSQL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- 123_macro_indicator.sql
-- #945 (Site Finder v2 / «GG-форсайт» ТЗ §8.8) — EPIC «Унифицированная сущность Макропоказатель».
-- PR A: создать единую типизированную таблицу временных рядов макропоказателей
-- и сделать backfill из уже существующих разрозненных macro-таблиц.
--
-- Архитектурный принцип (фиксирован): ДЕТЕРМИНИРОВАННО, БЕЗ LLM — это чистый SQL
-- + тонкий reader (backend/app/services/site_finder/macro.py). Никаких внешних вызовов.
--
-- Это фундамент для (последующие эпики, НЕ этот PR):
-- • Mortgage Affordability Index, макро-коэффициент, key-rate sensitivity.
--
-- Зависимости (источники backfill, НЕ дропаются — продолжают писать своими импортёрами):
-- • cbr_mortgage_series (43_anton_import.sql) — period — РУССКАЯ строка
-- 'Месяц YYYY' (напр. 'Август 2019'), ключ ряда —
-- устойчивый series_id (region в prod = 'sverdl').
-- • domrf_mortgage_rates (20_schema_domrf_extras.sql) — per-bank, агрегируем в median.
--
-- indicator_type 'key_rate' НАМЕРЕННО остаётся НЕзаполненным в этом PR —
-- LIVE-скрапер ключевой ставки ЦБ это отдельный follow-up (PR B), он и заполнит 'key_rate'.
--
-- Deploy: auto-applied by deploy.yml через _schema_migrations tracking (ровно один раз).
-- Idempotent: CREATE TABLE/INDEX IF NOT EXISTS + INSERT ... ON CONFLICT DO UPDATE —
-- повторный прогон (или re-apply после refresh источников) безопасен и обновит value.
-- Migration order: эта SQL-миграция применяется ПЕРВОЙ, backend macro.py — следом.
BEGIN;
-- ── 1. Единая таблица типизированных рядов макропоказателей (ТЗ §8.8) ───────────
CREATE TABLE IF NOT EXISTS macro_indicator (
indicator_type TEXT NOT NULL, -- 'key_rate','mortgage_rate_primary','mortgage_rate_secondary',
-- 'mortgage_rate_weighted','mortgage_issued_count','mortgage_issued_volume',
-- 'mortgage_debt','mortgage_overdue_share','income_per_capita','cpi',
-- 'housing_input','construction_price_index', ...
region TEXT NOT NULL DEFAULT 'rf', -- 'rf' | 'sverdl' | ...
obs_date DATE NOT NULL, -- нормализовано к 1-му числу месяца для monthly-рядов
value NUMERIC,
source TEXT NOT NULL, -- 'cbr' | 'domrf' | 'rosstat' | ...
frequency TEXT NOT NULL DEFAULT 'monthly', -- 'daily'|'monthly'|'quarterly'|'yearly'
unit TEXT, -- '%','шт','млрд руб','руб', ...
comment TEXT,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (indicator_type, region, obs_date)
);
CREATE INDEX IF NOT EXISTS idx_macro_type_region_date
ON macro_indicator (indicator_type, region, obs_date DESC);
COMMENT ON TABLE macro_indicator IS
'Унифицированный типизированный временной ряд макропоказателей (ТЗ §8.8, #945). '
'Long-формат: (indicator_type, region, obs_date) → value. Backfill из cbr_mortgage_series '
'+ domrf_mortgage_rates (источники не дропаются). indicator_type=key_rate заполняет PR B '
'(LIVE-скрапер ставки ЦБ). Детерминированно, без LLM. Потребители: affordability/'
'sensitivity-эпики через backend/app/services/site_finder/macro.py.';
-- ── 2. Backfill из cbr_mortgage_series ─────────────────────────────────────────
-- period — РУССКАЯ строка 'Месяц YYYY' (напр. 'Август 2019', 'Апрель 2020').
-- to_date(period,'Mon YYYY') здесь НЕ работает: на русском месяце Postgres падает
-- с «invalid value "Август" for "Mon"». Поэтому парсим руками:
-- make_date(YYYY, <номер месяца>, 1), где номер месяца — CASE по русскому названию
-- (split_part(period,' ',1)). 1-е число месяца зашито в make_date — date_trunc не нужен.
-- Маппинг по УСТОЙЧИВОМУ КЛЮЧУ series_id (НЕ по title!). В prod строка «в рублях»
-- живёт в series_id, а title — общий («Средневзвешенная ставка по ипотечным жилищным
-- кредитам, предоставленным физическим лицам-резидентам») и НЕ содержит «в рублях»,
-- поэтому прежний title-ILIKE матчил 0 строк. series_id-значения чистые/стабильные,
-- поэтому сопоставляем ТОЧНЫМ равенством series_id = '...'. Всё прочее НЕ матчится
-- (детерминированно, без побочных эффектов). Скипнутые ряды задокументированы ниже.
--
-- Маппинг (series_id ТОЧНО → indicator_type, unit) — семейство _mortgage_ (ИЖК), форма 02.
-- У каждого показателя есть варианты _в_рублях и _итого; берём ТОЛЬКО _в_рублях
-- маппинг обоих на один indicator_type коллиднул бы по PK (type,region,obs_date):
-- '02_13_Rates_mortgage_ставка_в_рублях' → mortgage_rate_weighted (%)
-- [льготно-средневзвешенная ИЖК-ставка ~7-8% в рублях; та же, что читает
-- analytics_queries._current_mortgage_rate; РЫНОЧНОЙ ставки без льгот в БД нет]
-- '02_13_Rates_mortgage_срок_в_руб' → mortgage_term (мес)
-- '02_10_Quantity_mortgage_в_рублях' → mortgage_issued_count (шт)
-- '02_11_New_loans_mortgage_в_рублях' → mortgage_issued_volume (млн руб)
-- '02_14_Debt_mortgage_в_рублях' → mortgage_debt (млн руб)
-- '02_14_Debt_mortgage_в_т.ч._просроч._в_рублях' → mortgage_overdue (млн руб)
-- НЕ берём: _housing_ (02_06/07/08 — все жилищные, шире ИЖК), _scpa_mortgage_
-- (02_15-18), _Share_ (02_44 — доли), и все _итого-варианты.
--
-- Guard: пропускаем строки с NULL value; строки с непарсящимся period (в prod в
-- period встречается мусор вроде '10054588.0') — отсекаем регуляркой
-- '^[А-Яа-яЁё]+ [0-9]{4}$' (русское слово + 4-значный год) ДО парсинга, плюс
-- требуем, чтобы CASE по названию месяца дал не-NULL номер (неизвестное слово →
-- month_num IS NULL → строка скипается, make_date никогда не получает NULL);
-- и строки, чей series_id не входит в маппинг (indicator_type IS NULL).
-- region берётся из источника КАК ЕСТЬ (в prod это 'sverdl' — единственный регион в
-- cbr_mortgage_series; 'rf'-рядов там нет).
--
-- Замечание про единицы: фактические единицы (млн/млрд руб, шт/единиц) зависят от
-- конкретного XLSX-листа ЦБ; ставим наиболее вероятную единицу формы 02. Это поле
-- справочное (не участвует в PK) и легко правится отдельным UPDATE при необходимости.
INSERT INTO macro_indicator
(indicator_type, region, obs_date, value, source, frequency, unit, comment)
SELECT
m.indicator_type,
s.region,
make_date(split_part(s.period, ' ', 2)::int, m.month_num, 1) AS obs_date,
s.value,
'cbr' AS source,
'monthly' AS frequency,
m.unit,
'backfill #945 from cbr_mortgage_series; series_id=' || s.series_id AS comment
FROM cbr_mortgage_series AS s
CROSS JOIN LATERAL (
SELECT
CASE s.series_id
WHEN '02_13_Rates_mortgage_ставка_в_рублях' THEN 'mortgage_rate_weighted'
WHEN '02_13_Rates_mortgage_срок_в_руб' THEN 'mortgage_term'
WHEN '02_10_Quantity_mortgage_в_рублях' THEN 'mortgage_issued_count'
WHEN '02_11_New_loans_mortgage_в_рублях' THEN 'mortgage_issued_volume'
WHEN '02_14_Debt_mortgage_в_рублях' THEN 'mortgage_debt'
WHEN '02_14_Debt_mortgage_в_т.ч._просроч._в_рублях' THEN 'mortgage_overdue'
ELSE NULL
END AS indicator_type,
CASE s.series_id
WHEN '02_13_Rates_mortgage_ставка_в_рублях' THEN '%'
WHEN '02_13_Rates_mortgage_срок_в_руб' THEN 'мес'
WHEN '02_10_Quantity_mortgage_в_рублях' THEN 'шт'
WHEN '02_11_New_loans_mortgage_в_рублях' THEN 'млн руб'
WHEN '02_14_Debt_mortgage_в_рублях' THEN 'млн руб'
WHEN '02_14_Debt_mortgage_в_т.ч._просроч._в_рублях' THEN 'млн руб'
ELSE NULL
END AS unit,
-- русское название месяца → номер (1..12); неизвестное слово → NULL (скип ниже)
CASE split_part(s.period, ' ', 1)
WHEN 'Январь' THEN 1 WHEN 'Февраль' THEN 2 WHEN 'Март' THEN 3
WHEN 'Апрель' THEN 4 WHEN 'Май' THEN 5 WHEN 'Июнь' THEN 6
WHEN 'Июль' THEN 7 WHEN 'Август' THEN 8 WHEN 'Сентябрь' THEN 9
WHEN 'Октябрь' THEN 10 WHEN 'Ноябрь' THEN 11 WHEN 'Декабрь' THEN 12
ELSE NULL
END AS month_num
) AS m
WHERE s.value IS NOT NULL
AND m.indicator_type IS NOT NULL
-- period — русская строка 'Месяц YYYY'; отсекаем мусор (напр. '10054588.0')
AND s.period ~ '^[А-Яа-яЁё]+ [0-9]{4}$'
-- неизвестное название месяца → month_num NULL → скип (make_date не получит NULL)
AND m.month_num IS NOT NULL
ON CONFLICT (indicator_type, region, obs_date) DO UPDATE
SET value = EXCLUDED.value,
source = EXCLUDED.source,
frequency = EXCLUDED.frequency,
unit = EXCLUDED.unit,
comment = EXCLUDED.comment,
updated_at = now();
-- ── 3. Backfill из domrf_mortgage_rates (per-bank → market median) ─────────────
-- Источник — ставки топ-банков по дате снапшота. Агрегируем к рыночному ряду:
-- median(primary_rate) по snapshot_date (set-based, NULL игнорируются percentile_cont).
-- ДОМ.РФ-ставка по первичке хранится под ОТДЕЛЬНЫМ indicator_type, чтобы не
-- коллидировать по PK с возможной cbr-ставкой на ту же дату (PK = type+region+obs_date).
-- ЦБ-ряд выше = mortgage_rate_weighted (льготная, ~7-8%); это иное измерение, поэтому
-- их сосуществование корректно и без disambiguation, но рыночную первичку ДОМ.РФ всё
-- равно держим отдельным типом 'mortgage_rate_primary_domrf' для прозрачности источника.
-- snapshot_date берём как obs_date КАК ЕСТЬ (это не строго monthly-ряд — это снапшоты);
-- frequency помечаем 'daily' (нерегулярные снапшоты), НЕ нормализуем к 1-му числу,
-- чтобы не схлопнуть два снапшота одного месяца в одну строку.
INSERT INTO macro_indicator
(indicator_type, region, obs_date, value, source, frequency, unit, comment)
SELECT
'mortgage_rate_primary_domrf' AS indicator_type,
'rf' AS region,
r.snapshot_date AS obs_date,
percentile_cont(0.5) WITHIN GROUP (ORDER BY r.primary_rate) AS value,
'domrf' AS source,
'daily' AS frequency,
'%' AS unit,
'backfill #945 median(primary_rate) per snapshot from domrf_mortgage_rates' AS comment
FROM domrf_mortgage_rates AS r
WHERE r.primary_rate IS NOT NULL
GROUP BY r.snapshot_date
HAVING percentile_cont(0.5) WITHIN GROUP (ORDER BY r.primary_rate) IS NOT NULL
ON CONFLICT (indicator_type, region, obs_date) DO UPDATE
SET value = EXCLUDED.value,
source = EXCLUDED.source,
frequency = EXCLUDED.frequency,
unit = EXCLUDED.unit,
comment = EXCLUDED.comment,
updated_at = now();
-- ── Skipped / не сопоставлено (детерминированно, документировано) ──────────────
-- • sber_ddf_metrics — пропущено намеренно (формат «2 точки value_2019/value_2025»
-- без месячной шкалы; не временной ряд, ТЗ допускает пропуск).
-- • Любые cbr_mortgage_series, чей series_id не входит в маппинг выше (все _итого-
-- варианты, _housing_ 02_06/07/08, _scpa_mortgage_ 02_15-18, _Share_ 02_44 и др.) —
-- НЕ вставлены. Это видно по COUNT источника vs вставленных строк; маппинг
-- расширяется отдельной миграцией добавлением series_id в оба CASE.
-- • indicator_type='key_rate' — пусто (заполнит PR B, LIVE-скрапер ставки ЦБ).
COMMIT;