-- Контекст: 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;