update-census · git:20260520.a548c30 · 2026-05-20 · sha256 5e48791ebb656665
update-census git:20260520.a548c30A
Immutable. This exact content never changes and is served at /api/v1/blob/5e48791ebb656665.
---
name: update-census
description: Rebuild the NSI Census 2021 demographics data (data/census_2021.json, data/census_2021_settlements.json, and the per-entity slices under data/census/) from the raw NSI XLSX publications. Use when the user asks to refresh or rebuild census data, when NSI re-releases a corrected Census 2021 publication, when adding a new census dimension (e.g. health status), after a fresh git clone if data/census_2021.json or data/census/ is missing, or when a future decennial census (2031) is published.
allowed-tools:
- Read
- Bash
- Edit
- Write
---
# Update Census skill
Rebuilds the NSI Census 2021 artifacts from the raw NSI XLSX publications:
| Output | Grain | Dimensions |
|---|---|---|
| `data/census_2021.json` | country + 28 oblasts + 265 municipalities | population, age, gender, ethnic, mother tongue, religion, education, employment |
| `data/census_2021_settlements.json` | 5,257 settlements (EKATTE) | population, age, gender only |
| `data/census/oblasts/*.json` | 28 per-oblast ~1 KB slices | same as oblast row above |
| `data/census/municipalities/*.json` | 265 per-municipality ~1 KB slices | same as municipality row above |
NSI publishes ethnocultural / education / employment breakdowns only down to the municipality level — the settlement sidecar therefore carries only population, age and sex (see `CensusSettlementEntity` in `src/data/census/censusTypes.ts`).
The per-entity slices exist so a region/municipality/settlement dashboard tile fetches a single ~1 KB file instead of the full payload.
## When to run
| Trigger | Action |
|---|---|
| Fresh git clone with no `data/census_2021.json` or empty `data/census/` | Steps 1–5 (full rebuild) |
| NSI re-releases a corrected Census 2021 XLSX | Replace the file in `raw_data/census_2021/`, then steps 2–5 |
| User asks to "refresh census" / "rebuild census data" | Steps 2–5 |
| Adding a new census dimension (health status, ...) | See "Adding a dimension" below |
| A future decennial census (2031) is published | See "A new census" below |
The census is **decennial** — NSI ran Census 2021 and the next is 2031. The daily watcher does **not** fingerprint a census source, so day-to-day there is nothing to do. This skill is event-driven, not scheduled.
## Step 1 — Place the raw files
The build reads four NSI Census 2021 English-language XLSX publications from `raw_data/census_2021/`:
| File | Required | Source |
|---|---|---|
| `Census2021_Population_EN.xlsx` | yes | population / age / sex, hierarchical down to settlement |
| `Census2021_Ethnocultural characteristics_EN.xlsx` | yes | ethnic group, mother tongue, religion |
| `Census2021_Economic characteristics_EN.xlsx` | yes | education, employment rates |
| `Census2021_Health status_EN.xlsx` | not yet parsed | present in `raw_data/` but no parser wired up — see "Adding a dimension" |
Download the EN publications from NSI's Census 2021 portal at <https://census2021.bg/> (the "Final results" / data tables section). Keep the file names exactly as above — the build looks them up by name. The three required files are already committed under `raw_data/census_2021/`; you only place files here when NSI issues a correction or for a new census.
## Step 2 — Build
```bash
npx tsx scripts/census/build_census.ts
```
Expected output:
```
Wrote /Users/.../data/census_2021.json: country + 28 oblasts + 265 municipalities
Wrote /Users/.../data/census_2021_settlements.json: 5257 settlements
Wrote 28 oblast slices to /Users/.../data/census/oblasts/ and 265 municipality slices to /Users/.../data/census/municipalities/
```
The build is deterministic — re-running with unchanged XLSX produces byte-identical output except the `generatedAt` timestamp in `census_2021.json`. If `git diff data/census_2021.json` shows only that one field changed, nothing material happened; you can `git checkout data/census_2021.json` to drop the churn.
The script throws (and writes nothing) if `raw_data/census_2021/` is missing, if a required XLSX is absent, or if it cannot locate the expected header rows inside a sheet.
## Step 3 — Verify
```bash
node -e "
const c = require('./data/census_2021.json');
const s = require('./data/census_2021_settlements.json');
console.log('country pop ', c.country.population.toLocaleString());
console.log('oblasts ', c.oblasts.length);
console.log('municipalities ', c.municipalities.length);
console.log('settlements ', s.length);
const fs = require('fs');
console.log('oblast slices ', fs.readdirSync('./data/census/oblasts').length);
console.log('muni slices ', fs.readdirSync('./data/census/municipalities').length);
"
```
Eyeball:
- Country population ≈ **6,519,789** (Census 2021 final figure).
- 28 oblasts, 265 municipalities, ~5,257 settlements.
- 28 oblast slices, 265 municipality slices (slice counts must match the array lengths).
- Spot-check a known oblast: Burgas (`BGS`) population ≈ 380,286. If a major oblast is missing or off by an order of magnitude, the Population sheet layout changed — inspect `parsePopulationSheet` in `scripts/census/build_census.ts`.
## Step 4 — Regenerate derived demographics
`scripts/parties/build_demographics.ts` reads `data/census_2021.json` to compute the per-party vote↔demographics Pearson correlations (the `/demographics` scatter, the party-dashboard fingerprint tile, and the home cleavages tile) across all 265 municipalities. It is the only downstream artifact whose content depends on the census. If the census **content** changed — an NSI correction, a new dimension, the 2031 census — regenerate it:
```bash
npx tsx scripts/parties/build_demographics.ts
```
This rewrites `data/<election>/parties/demographics/*.json`, `data/<election>/dashboard/demographic_cleavages.json` and `data/<election>/dashboard/demographic_scatter.json` for every election.
Skip this on a pure no-op rebuild (fresh clone where `git diff data/census_2021.json` shows only the `generatedAt` timestamp churned) — the correlations are unchanged. The census is not a daily-watcher source, so the `process-watch-report` orchestrator never triggers this; it is a manual follow-up to a census rebuild.
## Step 5 — Upload to bucket
```bash
npm run bucket:sync:dry # preview
npm run bucket:sync # rsync data/ to gs://data-electionsbg-com/
```
This pushes the changed census JSON (and any other changed `data/` files) to the bucket the SPA fetches from.
## Step 6 — Commit
```bash
git add data/census_2021.json data/census_2021_settlements.json data/census/
git commit -m "census: rebuild from NSI Census 2021 publications"
```
If you placed corrected raw files, add those too:
```bash
git add raw_data/census_2021/
```
## Adding a dimension (advanced)
To surface a new census dimension (e.g. health status / disability from `Census2021_Health status_EN.xlsx`):
1. Add the type to `src/data/census/censusTypes.ts` — a new `CensusHealth` type and an optional field on `CensusEntity`.
2. In `scripts/census/build_census.ts`, add a `FILE_HEALTH` constant and a `parseHealth()` function. Most dimensions follow the `parseSimpleSheet` pattern (find the `BG` data row, pick fixed columns). Inspect the sheet layout first:
```bash
node -e "
const X = require('xlsx');
const wb = X.read(require('fs').readFileSync('raw_data/census_2021/Census2021_Health status_EN.xlsx'));
console.log(wb.SheetNames);
"
```
3. Thread the new map through `buildEntity` and the `country` / `oblasts` / `munis` construction in `main()`.
4. If the dimension should appear on the choropleth, add a `CensusMetric` key in `censusTypes.ts` and wire `censusMetricValue` / `CENSUS_METRICS` in the frontend.
5. Run steps 2–5.
NSI publishes health status only down to the oblast/municipality level — do not expect a settlement breakdown.
## A new census (advanced)
When NSI publishes the 2031 census, this becomes a versioned rebuild rather than an in-place refresh: the artifacts are named `census_2021*` throughout (data files, query keys in `src/data/census/useCensus.tsx`, the `censusDate` field). Adding a second census year means parameterising the year across the script, the data hooks, and the UI — scope that as its own task, not a drop-in raw-file swap.
## What this skill does NOT do
- **Does not refresh the annual sub-national indicators.** Registered unemployment, DZI scores and yearly population change are `update-indicators`; NUTS 3 Eurostat series are `update-regional`. The census is a one-off decennial snapshot.
- **Does not scrape NSI.** The raw XLSX are downloaded by hand from census2021.bg and committed to `raw_data/census_2021/`.
- **Does not change the SPA UI.** Adding a dimension or a metric requires hand-editing the types and hooks as described above.
## File map
| Path | Purpose |
|---|---|
| `raw_data/census_2021/*.xlsx` | NSI source publications (committed) |
| `scripts/census/build_census.ts` | CLI entry — parse XLSX, write all census artifacts |
| `scripts/parties/build_demographics.ts` | downstream — recomputes party↔demographics correlations from `census_2021.json` (re-run after a census rebuild) |
| `data/census_2021.json` | country + oblast + municipality payload |
| `data/census_2021_settlements.json` | settlement sidecar (population/age/sex) |
| `data/census/oblasts/*.json`, `data/census/municipalities/*.json` | per-entity ~1 KB slices |
| `src/data/census/censusTypes.ts` | shared TypeScript types |
| `src/data/census/useCensus.tsx` | React Query hooks (full payload, slices, settlement sidecar) |
| `src/screens/DemographicsScreen.tsx` | `/demographics` screen consuming the data |
| `src/screens/dashboard/CensusDemographicsTile.tsx` | drilldown tile (region / municipality / settlement) |