# INF-15 «Полотно»: словарь данных

Пакет `grid` v1.0.0 (2026-07-26). Этот документ пишется специально под
**переиспользование следующим исследованием (INF-14, школы и медпункты)** —
раздел 2 и Приложение А ТЗ прямо это требуют: «правильно считать, сколько
людей живёт далеко от ближайшей школы, нужны люди по клеткам, а не
"население района, сваленное в центр района"». Формулы метрик —
`METHODS.md`; допущения — `params/assumptions.yaml`.

---

## 0. Самое важное — система координат (читайте это первым)

Приложение А ТЗ приводит ПРИМЕР контракта данных с полем
`"grid": {"crs": "EPSG:3857", ...}`. **Этот пакет считает и публикует НЕ в
EPSG:3857, а в ESRI:54009 (Молльвейде)** — это осознанное отступление от
буквы примера, зафиксированное `docs/decisions/INF-15.md`, запись **D-001**.

Причина: EPSG:3857 (Web Mercator) — конформная, НЕ равноплощадная проекция.
На широте Беларуси (51,3°–56,2° с.ш.) клетка «1000×1000 м» в координатах
EPSG:3857 покрывает на местности реальную площадь примерно в **2,5–3,2 раза
меньше** одного квадратного километра, причём искажение РАЗНОЕ на юге и на
севере страны. Если бы следующее исследование (INF-14) взяло значения
`grid.json` этого пакета и посчитало площадь ячейки по формуле
«`cell_m² = cell_m × cell_m`» в предположении EPSG:3857 (как подсказывает
пример Приложения А), результат был бы систематически и неравномерно по
широте занижен. **Практическое следствие для INF-14:** поле `grid.crs` в
данных этого пакета равно `"ESRI:54009"`; при любых собственных
геопространственных расчётах (например, привязка школы к клетке) нужно либо
работать в той же проекции, либо явно репроецировать, а НЕ считать площадь
ячейки по номинальным метрам, взятым из системы координат EPSG:3857.

---

## 1. `web/public/data/grid.json` — контракт данных сайта

Формат JSON, `ensure_ascii=false`, без отступов (компактная сериализация).
Размер на момент написания — **≈202 КБ** (206 694 байт), бюджет — 1536 КБ.
Файл собирается модулем `etl.grid_project::write_grid_json()` из
промежуточных агрегатов `data/raw/grid/*.json` — в архиве пакета он
материализуется сборкой манифеста (`python -m etl.artifacts.build grid`) как
`data/final/grid.json`.

### 1.1. Верхний уровень

| Ключ | Тип | Содержание |
|---|---|---|
| `version` | строка | `"1.0.0"` — версия пакета |
| `forecast_version` | строка | `"v2026.4"` — версия использованного прогноза районов |
| `grid` | объект | геопривязка единой сетки (раздел 1.2) |
| `years` | объект | `{observed: int[10], forecast: int[6]}` |
| `frames` | объект | ключ → путь к webp-кадру карты (раздел 1.3) |
| `national` | объект | национальные показатели (раздел 1.4) |
| `territories` | объект | 119 записей `territory_id → показатели района` (раздел 1.5) |
| `validation` | объект | сводки всех восьми проверок (раздел 1.6) |
| `meta` | объект | утверждения, их статус, плашка честности (раздел 1.7) |

### 1.2. `grid` — геопривязка единой сетки

| Поле | Тип | Значение / единица | Примечание |
|---|---|---|---|
| `crs` | строка | `"ESRI:54009"` | **НЕ** `"EPSG:3857"` из примера Приложения А — см. раздел 0 |
| `cell_m` | int | `1000` | сторона клетки, метров, В ПРОЕКЦИИ `crs` |
| `width` | int | `857` | клеток по оси X |
| `height` | int | `514` | клеток по оси Y |
| `transform` | float[6] | `[1000.0, 0.0, 1602000.0, 0.0, -1000.0, 6512000.0]` | аффинное преобразование пиксель→метр (см. раздел 3, стабильность ячеек) |
| `note` | строка | текст | явная ссылка на решение D-001 |

Полная (более подробная, с `bounds_moll`) версия этого объекта —
`sources/raw/grid_meta.json` пакета.

### 1.3. `years` и `frames`

`years.observed` — 10 наблюдаемых эпох `[1975, 1980, …, 2020]` (шаг 5 лет).
`years.forecast` — 6 прогнозных узлов `[2026, 2031, 2036, 2041, 2046, 2050]`
(последний — интерполяция, не «родной» узел прогноза района, см. раздел 4).

`frames` — плоский словарь, **46** записей (10 наблюдаемых + 6 лет × 3
сценария × 2 варианта = 36 прогнозных):

| Ключ | Формат | Пример |
|---|---|---|
| наблюдаемый год | `"<year>"` | `"2020"` → `/data/grid_frames/pop_2020.webp` |
| прогнозный | `"<year>:<scenario>:<variant>"` | `"2050:base:A"` → `/data/grid_frames/pop_2050_base_A.webp` |

`scenario ∈ {base, optimistic, negative}`, `variant ∈ {A, B}`. Кадры —
картинки (webp, ≤220 КБ каждая), не числовой массив: цвет клетки кодирует
`log1p`-нормированную (потолок 5000 чел/км², гамма 0,92) плотность с
добавленным дизерингом (±0,5 к каналу яркости для «зажжённых» пикселей,
seed 42/43) — **из значения пикселя картинки нельзя точно восстановить
исходное число жителей клетки**, только визуально сравнимую яркость (см.
раздел 5 — как на самом деле читать значение ячейки).

### 1.4. `national` — национальные ряды

| Ключ | Тип | Единица | Диапазон ключей |
|---|---|---|---|
| `area_share_below["5"\|"1"\|"25"]` | объект `{ключ_года → float}` | доля площади страны, 0–1 | 10 набл. + 36 прогнозных ключей |
| `arithmetic_density` | объект | чел/км² | то же |
| `population_weighted_density` | объект | чел/км² | то же |
| `settlement_components[год]` | объект | `{n_components:int, largest_area_km2:float, largest_pop:float, largest_share_of_pop:float (0-1), largest_share_of_area:float (0-1)}` | 10 набл. + прогнозные ключи |
| `centroid_track` | массив объектов | `{year:int, lon:float, lat:float, dtype:"c"\|"e"\|"f", err_km:int}` | 1897…2050 (16 точек) |
| `raion_reconciliation_national` | массив объектов | `{epoch:int, grid_sum:float(чел), official:float(чел), pct_diff:float(%), pass:bool}` | 10 набл. эпох — сверка НАЦИОНАЛЬНОЙ суммы клеток с офиц. рядом (G-1.2, сырой продукт, до calibровки; после calibровки расхождение отражает только точность интерполяции официального ряда, не сам факт калибровки) |

`centroid_track[i].dtype`: `c` — точное совпадение с годом переписи/сеткой
наблюдаемой эпохи; `e` — оценка (не перепись, но наблюдаемая точка ряда);
`f` — модельная интерполяция (прогнозные годы и 2050). `err_km`: 25 для
1897–1970 (нет клеточной сетки, веса по офиц. численности района на его
географическом центроиде), 10 для 1975–2050.

### 1.5. `territories[territory_id]` — показатели по 119 территориям

`territory_id` — тот же реестр, что и в остальном проекте: `r-*` (118
районов) + `BY-HM` (город Минск). **Важно:** это агрегаты НА УРОВНЕ РАЙОНА,
а не отдельных клеток — «клетки района, свёрнутые в один ряд по времени»,
не координатный список.

| Поле | Тип | Единица | Примечание |
|---|---|---|---|
| `pop_grid_sum` | объект `{ключ_года → float}` | человек | сумма клеток района на эту эпоху/прогнозный ключ; для наблюдаемых эпох — ТОЖДЕСТВЕННО официальному ряду (calibровка, раздел 3 METHODS.md), для прогнозных — тождественно прогнозу района (G-2) |
| `area_share_below["5"]` | объект `{год(только наблюдаемые) → float}` | доля площади района, 0–1 | только 10 наблюдаемых эпох, только порог 5 |
| `network_per_capita` | объект \| отсутствует | `{road_km_per_1000:float, road_km_per_km2:float, road_km_total:float(км)}` | снимок OSM на 2026 год (не история); присутствует у территорий, для которых есть данные сети |

### 1.6. `validation` — сводки восьми проверок

Каждый ключ — копия (или ссылка на) соответствующего промежуточного отчёта
из `sources/raw/`: `g1_reconciliation_summary` (← `metrics_observed.json →
reconciliation_summary`), `g2_summary` (← `metrics_forecast.json →
g2_report_summary`), `g3_nightlights_crosscheck` (← `g3_nightlights_
crosscheck.json` целиком), `g4_g5` (← `metrics_observed.json → validation`),
`g6_network` (← `network_metrics.json → g6`), `c3_correlation` (←
`network_metrics.json → correlation_c3`). Точные числа и пороги — `VALIDATION.md`.

### 1.7. `meta`

| Поле | Значение |
|---|---|
| `claims` | `["C-001", "C-002", "C-003"]` |
| `claims_status` | `{"C-001": "open_question", "C-002": "open_question", "C-003": "verified"}` |
| `artifact` | `"by-maps-grid-v1.0.0.zip"` |
| `honesty` | текст плашки честности (дословно на странице, без прокрутки, раздел 3 ТЗ) |

---

## 2. Единицы измерения — сводная таблица

| Величина | Единица |
|---|---|
| Население клетки/района (`pop_grid_sum`, `pop`) | человек (может быть дробным — артефакт дазиметрической интерполяции и calibровки, не подделка точности) |
| Плотность (`arithmetic_density`, `population_weighted_density`) | человек/км² |
| Доля площади (`area_share_below`, `largest_share_of_area`) | безразмерная доля, 0–1 (НЕ проценты) |
| Доля населения (`largest_share_of_pop`) | безразмерная доля, 0–1 |
| Координаты (`centroid_track[].lat/lon`, `frames`-привязка) | градусы WGS84 (EPSG:4326) — единственное место контракта, где координаты в градусах, а не в метрах Молльвейде |
| Погрешность центра масс (`err_km`) | километры |
| Длина сети (`road_km_*`) | километры |
| Площадь клетки/района | км² (1 клетка = 1 км² РОВНО, т.к. `crs` равноплощадная) |
| Отклонения сверки (`pct_diff`, `delta_pct`) | проценты (не доли) |
| Корреляция (`c3_correlation.spearman_rho`) | безразмерный коэффициент Спирмена, [-1, 1] |

---

## 3. Стабильность идентификаторов ячеек

Единая сетка (`grid.width=857`, `grid.height=514`, `grid.transform`) —
**одна и та же для ВСЕХ эпох**, наблюдаемых и прогнозных: это гарантируется
на уровне кода — `etl/grid_extract.py` собирает мозаику и клип с одинаковым
`bounds`/`res` для каждой из 10 эпох (докстринг модуля: «Решётка одинакова
для всех эпох (общий transform/width/height) — обязательное условие
сравнимости кадров и проверки G-2»), а прогнозный модуль
(`etl/grid_project.py::load_observed_stack()`) переиспользует ТОТ ЖЕ
`transform`/`shape`, что и наблюдаемый стек. Поэтому **клетка одного и того
же места на местности находится по одному и тому же индексу (номер строки,
номер столбца) в любую эпоху 1975–2050** — не нужно искать соответствие
между растрами разных лет, оно тождественно по построению.

**Как вычислить координаты клетки по индексу.** Аффинный `transform =
[a, b, c, d, e, f]` (порядок GDAL/rasterio) даёт координаты ВЕРХНЕГО ЛЕВОГО
угла клетки `(col, row)`:

```
x_moll = c + a * col          # метры, ESRI:54009
y_moll = f + e * row          # e отрицательный (сетка растёт вниз)
```

Координаты ЦЕНТРА клетки (так считается центр масс населения в
`etl/grid.py::centroid_grid`) — `col + 0.5`, `row + 0.5` в той же формуле.
Полные параметры зафиксированы в `sources/raw/grid_meta.json` (и
эквивалентно — в `grid.json → grid`).

**Стабильный идентификатор клетки** для внешнего использования (например,
следующим исследованием INF-14) — пара `(row, col)` относительно этого
фиксированного `transform`, либо эквивалентный линейный индекс
`row * width + col` (0-индексация, `width = 857`). Оба варианта стабильны
между версиями `grid.json`, пока `grid.transform`/`width`/`height` не
меняются (любое изменение сетки — новая MAJOR-версия пакета, раздел 8
`ARTIFACT_STANDARD.md`).

---

## 4. Правила интерполяции и метки достоверности (dtype)

| Годы | Что это | Метка |
|---|---|---|
| 1975, 1980, …, 2020 (10 эпох) | наблюдаемые эпохи GHS-POP, calibровано под официальный ряд района | наблюдение (клеточная сетка); `centroid_track.dtype = "c"` |
| 1897, 1959, 1970 | центр масс до появления клеточной сетки, посчитан по офиц. численности района на его геогр. центроиде | `dtype = "c"` (перепись) при точном годе переписи, `"e"` (оценка) для 1897 |
| 2026, 2031, 2036, 2041, 2046 | «родные» узлы `forecast.json` (шаг 5 лет), прогноз по клеткам, варианты A/Б | модель `[MODEL]`; в `centroid_track` — `dtype = "f"` |
| 2050 | НЕ «родной» узел `forecast.json` (шаг 5 лет от 2026 не попадает в 2050) — линейная интерполяция между узлами 2046 и 2051 | модель `[MODEL]`, `dtype = "f"` — обязательно наравне с остальными прогнозными точками, а не как отдельная, более достоверная категория |

Официальный ряд района к узлам НАБЛЮДАЕМЫХ эпох (1975–2020) приводится
линейной интерполяцией между ближайшими точками `data.json`
(`etl/grid.py::interp_official`) — точно совпадают с годом ряда только
эпохи **2005 и 2015**; остальные 8 эпох — интерполированные значения
официального ряда (не самих клеток — калибровочный коэффициент вычисляется
именно из интерполированного официального числа). Экстраполяция за пределы
диапазона официального ряда не выполняется нигде в конвейере.

---

## 5. Как читать значение отдельной ячейки

Раздел «Контракт данных» Приложения А ТЗ требует зафиксировать выбор между
двумя способами клиентского чтения значения клетки — сделано, запись
**D-003** в `docs/decisions/INF-15.md` (не пустая заготовка).

**Выбор: типизированный бинарный формат с ленивой подгрузкой.**
`web/public/data/grid_cells/pop_<epoch>.bin` — сырой `Uint16` (little-endian),
row-major, `width×height` из `grid.json.grid` (857×514 = 880 996 байт на
файл; население округлено до целого человека). 10 файлов на наблюдаемые
эпохи (860 КБ каждый) + 2 файла на прогноз `2050:base:A`/`2050:base:B`
(самая вероятная точка клика) — не все 36 комбинаций сценарий×вариант×год,
осознанное и раскрытое ограничение охвата (см. D-003 за обоснованием).
Индексация: `row = floor((transform[3] − y_moll) / |transform[4]|)`,
`col = floor((x_moll − transform[2]) / transform[0])`; значение —
`view[row*width + col]`. Клиентская реализация — `web/lib/grid.ts`
(`loadCellGrid`, `lonLatToCellIndex`, `cellValue`), покрыта тестами
`web/lib/grid.test.ts` (сверка проекции Молльвейде с `rasterio.warp.
transform` до <1 мм — важно: `ESRI:54009` в реализации PROJ/GDAL здесь
псевдосферическая, `R = 6378137` (большая полуось WGS84), БЕЗ перевода в
авторическую широту — типовая формула сферической Молльвейде с
авторической широтой или радиусом 6371007 даёт ошибку 7–20 км).

**Что можно использовать уже сейчас (для полного, не облегчённого
воспроизведения или для офлайн-анализа следующим исследованием):**
точные значения численности каждой клетки на каждую наблюдаемую эпоху
лежат в откалиброванных растрах `sources/raw/rasters/pop_<epoch>.tif`
(GeoTIFF, `float32`, округление до 3 знаков, `nodata=0.0`, та же сетка
857×514 в ESRI:54009 — раздел 3 выше). Прогнозные значения по клеткам НЕ
персистятся отдельным файлом (см. `etl/grid_project.py`, докстринг:
«прогнозные растры не персистятся на диск — считаются на лету... чтобы не
раздувать репозиторий») — их нужно пересчитать модулем `etl.grid_project`
(функция `project_epoch()`) заново, если нужны точные прогнозные значения
клеток, а не только агрегаты по району из `territories[].pop_grid_sum`.

**Чего делать НЕ следует:** восстанавливать численность клетки из цвета
пикселя webp-кадра (`frames`) — кодирование через `log1p`-нормировку с
фиксированным потолком 5000 чел/км², гамма-коррекцией и дизерингом
(раздел 1.3 выше) необратимо теряет точность и специально проектировалось
только для визуального восприятия карты, не для численного чтения.
