gendesign/data/sql/154_ekb_district_demographics.sql
Light1YT 1b77f479e7
All checks were successful
CI / changes (push) Successful in 6s
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 7s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (push) Successful in 1m57s
CI / openapi-codegen-check (pull_request) Successful in 1m51s
CI / backend-tests (push) Successful in 8m52s
CI / backend-tests (pull_request) Successful in 8m50s
feat(site-finder): POI saturation per capita по районам ЕКБ (#42)
Демография 8 районов ЕКБ (Росстат 01.01.2025) + обеспеченность
социнфраструктурой на 1000 чел. целевой когорты vs норматив СП 42.13330.

- migration 154: ekb_district_demographics (население/площадь/ОКАТО факт,
  возрастные когорты — региональная оценка, помечены) + backfill
  ekb_districts.population
- saturation.py: provision_ratio (школа/детсад/поликлиника), severity,
  score_multiplier (x1.2 дефицит / x0.5 профицит)
- analyze: infra.saturation блок (best-effort, SAVEPOINT-изолирован)
- tests: 18 (вкл. acceptance — дефицитный район -> school x1.2)

Refs #42
2026-06-14 19:51:42 +05:00

107 lines
7.8 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.

-- Контекст: Issue #42 (Infra I1) — POI saturation per capita. Чтобы считать
-- обеспеченность инфраструктурой района (школа/детсад/поликлиника на
-- 1000 чел. целевой когорты vs норматив СП 42.13330), нужна демография
-- по 8 внутригородским районам ЕКБ.
--
-- Что делает:
-- 1. Заполняет ekb_districts.population (была NULL) точными числами Росстата.
-- 2. Создаёт таблицу-справочник ekb_district_demographics — насел./площадь/ОКАТО
-- + ДОКУМЕНТИРОВАННЫЙ источник/дата + ОЦЕНОЧНЫЕ возрастные когорты (помечены).
--
-- ИСТОЧНИК населения (источник истины):
-- Wikipedia «Административно-территориальное деление Екатеринбурга» →
-- первоисточник «Численность постоянного населения РФ по муниципальным
-- образованиям на 1 января 2025», Росстат. as_of_date = 2025-01-01.
--
-- ВОЗРАСТНЫЕ КОГОРТЫ — ОЦЕНКА, НЕ ФАКТ. На уровне внутригородского района Росстат
-- публичных половозрастных срезов НЕ даёт. Применены РЕГИОНАЛЬНЫЕ доли (РФ/Свердл.
-- обл., перепись-2020 / EMISS, порядок величин):
-- • дошкольники 0-7 ≈ 8.0 % (age_share_preschool)
-- • школьники 7-18 ≈ 11.5 % (age_share_school)
-- • пожилые 65+ ≈ 16.0 % (age_share_elderly)
-- Это ГРУБАЯ оценка для saturation-расчёта (estimate-флаг в API). Реальные
-- район-уровневые доли потребуют отдельного источника (см. issue #42 risks).
--
-- Deploy order: после 56_schema_ekb_districts_geom (FK на ekb_districts) — стоит.
-- Idempotent: CREATE TABLE IF NOT EXISTS + INSERT … ON CONFLICT DO UPDATE
-- (re-apply перезаписывает данными из этой миграции — single source).
-- Deploy: auto-applied by deploy.yml через _schema_migrations tracking (ровно один раз по NN).
BEGIN;
-- ── 1. Справочник демографии района ──────────────────────────────────────────
CREATE TABLE IF NOT EXISTS ekb_district_demographics (
district_name TEXT PRIMARY KEY
REFERENCES ekb_districts(district_name),
population INTEGER NOT NULL, -- чел., Росстат 2025-01-01
area_km2 NUMERIC(8, 2) NOT NULL, -- км²
okato TEXT, -- код ОКАТО района
-- Возрастные доли — ОЦЕНКА (региональный fallback), а не район-факт.
age_share_preschool NUMERIC(5, 4), -- доля 0-7 лет (детсад)
age_share_school NUMERIC(5, 4), -- доля 7-18 лет (школа)
age_share_elderly NUMERIC(5, 4), -- доля 65+ лет (поликлиника)
age_cohorts_estimated BOOLEAN NOT NULL DEFAULT TRUE, -- TRUE = когорты оценочные
source TEXT NOT NULL, -- человекочитаемый источник
as_of_date DATE NOT NULL, -- дата актуальности населения
loaded_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
COMMENT ON TABLE ekb_district_demographics IS
'Демография 8 внутригородских районов ЕКБ для POI saturation per capita (#42). '
'population/area — Росстат 2025-01-01 (факт). Возрастные когорты — ОЦЕНКА '
'(региональные доли РФ/Свердл.), см. age_cohorts_estimated.';
COMMENT ON COLUMN ekb_district_demographics.age_cohorts_estimated IS
'TRUE: age_share_* — оценка региональными долями, НЕ район-факт Росстата (#42).';
-- ── 2. Seed: 8 районов (Росстат на 1 января 2025) ────────────────────────────
-- population/area/okato — ФАКТ. age_share_* — единая региональная оценка (см. шапку).
INSERT INTO ekb_district_demographics
(district_name, population, area_km2, okato,
age_share_preschool, age_share_school, age_share_elderly,
age_cohorts_estimated, source, as_of_date)
VALUES
('Академический', 125000, 46.01, '65401362', 0.0800, 0.1150, 0.1600, TRUE,
'Росстат, численность населения МО на 01.01.2025 (via Wikipedia АТД Екатеринбурга)',
DATE '2025-01-01'),
('Верх-Исетский', 240822, 219.79, '65401364', 0.0800, 0.1150, 0.1600, TRUE,
'Росстат, численность населения МО на 01.01.2025 (via Wikipedia АТД Екатеринбурга)',
DATE '2025-01-01'),
('Железнодорожный', 158675, 125.65, '65401368', 0.0800, 0.1150, 0.1600, TRUE,
'Росстат, численность населения МО на 01.01.2025 (via Wikipedia АТД Екатеринбурга)',
DATE '2025-01-01'),
('Кировский', 220749, 86.25, '65401373', 0.0800, 0.1150, 0.1600, TRUE,
'Росстат, численность населения МО на 01.01.2025 (via Wikipedia АТД Екатеринбурга)',
DATE '2025-01-01'),
('Ленинский', 222258, 22.19, '65401377', 0.0800, 0.1150, 0.1600, TRUE,
'Росстат, численность населения МО на 01.01.2025 (via Wikipedia АТД Екатеринбурга)',
DATE '2025-01-01'),
('Октябрьский', 151775, 158.60, '65401380', 0.0800, 0.1150, 0.1600, TRUE,
'Росстат, численность населения МО на 01.01.2025 (via Wikipedia АТД Екатеринбурга)',
DATE '2025-01-01'),
('Орджоникидзевский', 263820, 99.30, '65401385', 0.0800, 0.1150, 0.1600, TRUE,
'Росстат, численность населения МО на 01.01.2025 (via Wikipedia АТД Екатеринбурга)',
DATE '2025-01-01'),
('Чкаловский', 286277, 389.81, '65401390', 0.0800, 0.1150, 0.1600, TRUE,
'Росстат, численность населения МО на 01.01.2025 (via Wikipedia АТД Екатеринбурга)',
DATE '2025-01-01')
ON CONFLICT (district_name) DO UPDATE SET
population = EXCLUDED.population,
area_km2 = EXCLUDED.area_km2,
okato = EXCLUDED.okato,
age_share_preschool = EXCLUDED.age_share_preschool,
age_share_school = EXCLUDED.age_share_school,
age_share_elderly = EXCLUDED.age_share_elderly,
age_cohorts_estimated = EXCLUDED.age_cohorts_estimated,
source = EXCLUDED.source,
as_of_date = EXCLUDED.as_of_date,
loaded_at = now();
-- ── 3. Backfill ekb_districts.population (была NULL) — по name-match ──────────
-- ekb_districts.area_m2 уже есть в схеме (NUMERIC) — конвертируем км² → м².
UPDATE ekb_districts d
SET population = s.population,
area_m2 = COALESCE(d.area_m2, s.area_km2 * 1000000.0)
FROM ekb_district_demographics s
WHERE d.district_name = s.district_name;
COMMIT;