diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..95ede81 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +# Preserve hash-pinned publisher bytes, including original line endings/spacing. +db/data/stats_nz/subnational_population_estimates_2025/subnational-population-estimates-at-30-june-2025-provisional.xlsx binary +db/data/stats_nz/regional_council_2025_codes/regional-council-2025-codes.json binary +db/data/stats_nz/national_population_estimates_2025/national-population-estimates-at-30-june-2025-landing-page.html binary diff --git a/changelog.d/nz-partial-population-2025.added.md b/changelog.d/nz-partial-population-2025.added.md new file mode 100644 index 0000000..34fbf58 --- /dev/null +++ b/changelog.d/nz-partial-population-2025.added.md @@ -0,0 +1,5 @@ +Add 85 source-backed, provisional Stats NZ population counts for 30 June 2025, +covering the 16 regions and the separately published national total across all +ages and four broad age groups. Pin publisher artifacts and REGC 2025 codes. +Keep five-year-age-by-sex regional coverage and national single-year-age coverage +explicitly incomplete; no calibrated population or completed target set is added. diff --git a/chronicle/source_package.py b/chronicle/source_package.py index 993b4d9..1e5bdfb 100644 --- a/chronicle/source_package.py +++ b/chronicle/source_package.py @@ -74,6 +74,9 @@ SOURCE_PACKAGE_RESOURCE_PACKAGE = "packages" SOURCE_PACKAGE_ALIASES = { + "stats-nz-subnational-population-estimates-2025": Path( + "stats_nz/subnational_population_estimates_2025" + ), "bea-nipa-personal-income-components": Path("bea/nipa_personal_income_components"), "bea-nipa-personal-income-disposition": Path( "bea/nipa_personal_income_disposition" diff --git a/db/data/stats_nz/national_population_estimates_2025/manifest.yaml b/db/data/stats_nz/national_population_estimates_2025/manifest.yaml new file mode 100644 index 0000000..bb85b90 --- /dev/null +++ b/db/data/stats_nz/national_population_estimates_2025/manifest.yaml @@ -0,0 +1,19 @@ +source_id: stats_nz +package_id: national_population_estimates_2025 +dataset: stats_nz_national_population_estimates_2025 +source_page: https://www.stats.govt.nz/information-releases/national-population-estimates-at-30-june-2025/ +table: 'National population estimates: At 30 June 2025 (landing-page evidence only; + no single-year-age artifact)' +files: + 2025: + filename: national-population-estimates-at-30-june-2025-landing-page.html + source_url: https://www.stats.govt.nz/information-releases/national-population-estimates-at-30-june-2025/ + sha256: b2d2751800857895b1498d87df6f03ac3511ec8c7a9b17cc3150c8ab56533053 + size_bytes: 61286 + fetched_at: '2026-08-30T01:43:59+00:00' + storage: + r2: + provider: r2 + bucket: ledger-raw + key: raw/nz/stats_nz/national_population_estimates_2025/2025/b2d2751800857895b1498d87df6f03ac3511ec8c7a9b17cc3150c8ab56533053/national-population-estimates-at-30-june-2025-landing-page.html + uri: r2://ledger-raw/raw/nz/stats_nz/national_population_estimates_2025/2025/b2d2751800857895b1498d87df6f03ac3511ec8c7a9b17cc3150c8ab56533053/national-population-estimates-at-30-june-2025-landing-page.html diff --git a/db/data/stats_nz/national_population_estimates_2025/national-population-estimates-at-30-june-2025-landing-page.html b/db/data/stats_nz/national_population_estimates_2025/national-population-estimates-at-30-june-2025-landing-page.html new file mode 100644 index 0000000..c6b5be5 --- /dev/null +++ b/db/data/stats_nz/national_population_estimates_2025/national-population-estimates-at-30-june-2025-landing-page.html @@ -0,0 +1,111 @@ + + + + + + + + + + + +National population estimates: At 30 June 2025 | Stats NZ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+
+
+
+
+ + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/db/data/stats_nz/regional_council_2025_codes/manifest.yaml b/db/data/stats_nz/regional_council_2025_codes/manifest.yaml new file mode 100644 index 0000000..ef09675 --- /dev/null +++ b/db/data/stats_nz/regional_council_2025_codes/manifest.yaml @@ -0,0 +1,18 @@ +source_id: stats_nz +package_id: regional_council_2025_codes +dataset: stats_nz_regional_council_2025_codes +source_page: https://services2.arcgis.com/vKb0s8tBIA3bdocZ/arcgis/rest/services/Regional_Council_2025/FeatureServer +table: Regional council 2025 code and name attributes (1 January 2025 boundaries) +files: + 2025: + filename: regional-council-2025-codes.json + source_url: https://services2.arcgis.com/vKb0s8tBIA3bdocZ/arcgis/rest/services/Regional_Council_2025/FeatureServer/0/query?where=1%3D1&outFields=REGC2025_V1_00%2CREGC2025_V1_00_NAME&returnGeometry=false&orderByFields=REGC2025_V1_00&f=pjson + sha256: e5d53fa6abf742121d123bff970c77658a16e8c9b405b83d1a978cc22920d660 + size_bytes: 3381 + fetched_at: '2026-08-30T01:43:16+00:00' + storage: + r2: + provider: r2 + bucket: ledger-raw + key: raw/nz/stats_nz/regional_council_2025_codes/2025/e5d53fa6abf742121d123bff970c77658a16e8c9b405b83d1a978cc22920d660/regional-council-2025-codes.json + uri: r2://ledger-raw/raw/nz/stats_nz/regional_council_2025_codes/2025/e5d53fa6abf742121d123bff970c77658a16e8c9b405b83d1a978cc22920d660/regional-council-2025-codes.json diff --git a/db/data/stats_nz/regional_council_2025_codes/regional-council-2025-codes.json b/db/data/stats_nz/regional_council_2025_codes/regional-council-2025-codes.json new file mode 100644 index 0000000..b7b0f48 --- /dev/null +++ b/db/data/stats_nz/regional_council_2025_codes/regional-council-2025-codes.json @@ -0,0 +1,140 @@ +{ + "objectIdFieldName" : "OBJECTID", + "uniqueIdField" : + { + "name" : "OBJECTID", + "isSystemMaintained" : true + }, + "globalIdFieldName" : "", + "geometryType" : "esriGeometryPolygon", + "spatialReference" : { + "wkid" : 2193, + "latestWkid" : 2193 + }, + "fields" : [ + { + "name" : "REGC2025_V1_00", + "type" : "esriFieldTypeString", + "alias" : "Regional Council Code", + "sqlType" : "sqlTypeOther", + "length" : 2, + "domain" : null, + "defaultValue" : null, + "description" : "{\"value\":\"Regional Council unique identifier\",\"fieldValueType\":\"uniqueIdentifier\"}" + }, + { + "name" : "REGC2025_V1_00_NAME", + "type" : "esriFieldTypeString", + "alias" : "Regional Council Name", + "sqlType" : "sqlTypeOther", + "length" : 254, + "domain" : null, + "defaultValue" : null, + "description" : "{\"value\":\"Regional Council name (supports macrons)\",\"fieldValueType\":\"nameOrTitle\"}" + } + ], + "features" : [ + { + "attributes" : { + "REGC2025_V1_00" : "01", + "REGC2025_V1_00_NAME" : "Northland Region" + } + }, + { + "attributes" : { + "REGC2025_V1_00" : "02", + "REGC2025_V1_00_NAME" : "Auckland" + } + }, + { + "attributes" : { + "REGC2025_V1_00" : "03", + "REGC2025_V1_00_NAME" : "Waikato Region" + } + }, + { + "attributes" : { + "REGC2025_V1_00" : "04", + "REGC2025_V1_00_NAME" : "Bay of Plenty Region" + } + }, + { + "attributes" : { + "REGC2025_V1_00" : "05", + "REGC2025_V1_00_NAME" : "Gisborne Region" + } + }, + { + "attributes" : { + "REGC2025_V1_00" : "06", + "REGC2025_V1_00_NAME" : "Hawke's Bay Region" + } + }, + { + "attributes" : { + "REGC2025_V1_00" : "07", + "REGC2025_V1_00_NAME" : "Taranaki Region" + } + }, + { + "attributes" : { + "REGC2025_V1_00" : "08", + "REGC2025_V1_00_NAME" : "Manawatū-Whanganui Region" + } + }, + { + "attributes" : { + "REGC2025_V1_00" : "09", + "REGC2025_V1_00_NAME" : "Wellington Region" + } + }, + { + "attributes" : { + "REGC2025_V1_00" : "12", + "REGC2025_V1_00_NAME" : "West Coast Region" + } + }, + { + "attributes" : { + "REGC2025_V1_00" : "13", + "REGC2025_V1_00_NAME" : "Canterbury Region" + } + }, + { + "attributes" : { + "REGC2025_V1_00" : "14", + "REGC2025_V1_00_NAME" : "Otago Region" + } + }, + { + "attributes" : { + "REGC2025_V1_00" : "15", + "REGC2025_V1_00_NAME" : "Southland Region" + } + }, + { + "attributes" : { + "REGC2025_V1_00" : "16", + "REGC2025_V1_00_NAME" : "Tasman Region" + } + }, + { + "attributes" : { + "REGC2025_V1_00" : "17", + "REGC2025_V1_00_NAME" : "Nelson Region" + } + }, + { + "attributes" : { + "REGC2025_V1_00" : "18", + "REGC2025_V1_00_NAME" : "Marlborough Region" + } + }, + { + "attributes" : { + "REGC2025_V1_00" : "99", + "REGC2025_V1_00_NAME" : "Area Outside Region" + } + } + ] +} \ No newline at end of file diff --git a/db/data/stats_nz/subnational_population_estimates_2025/manifest.yaml b/db/data/stats_nz/subnational_population_estimates_2025/manifest.yaml new file mode 100644 index 0000000..efa8567 --- /dev/null +++ b/db/data/stats_nz/subnational_population_estimates_2025/manifest.yaml @@ -0,0 +1,18 @@ +source_id: stats_nz +package_id: subnational_population_estimates_2025 +dataset: stats_nz_subnational_population_estimates_2025 +source_page: https://www.stats.govt.nz/information-releases/subnational-population-estimates-at-30-june-2025/ +table: 'Subnational population estimates: At 30 June 2025 (provisional)' +files: + 2025: + filename: subnational-population-estimates-at-30-june-2025-provisional.xlsx + source_url: https://www.stats.govt.nz/assets/Uploads/Subnational-population-estimates/Subnational-population-estimates-At-30-June-2025/Download-data/subnational-population-estimates-at-30-june-2025-provisional.xlsx + sha256: 001e8a896cfb50f5ed17836dc815b235e3bcca55ee91c9869a2afaeb054b50a6 + size_bytes: 97990 + fetched_at: '2026-08-30T01:38:43+00:00' + storage: + r2: + provider: r2 + bucket: ledger-raw + key: raw/nz/stats_nz/subnational_population_estimates_2025/2025/001e8a896cfb50f5ed17836dc815b235e3bcca55ee91c9869a2afaeb054b50a6/subnational-population-estimates-at-30-june-2025-provisional.xlsx + uri: r2://ledger-raw/raw/nz/stats_nz/subnational_population_estimates_2025/2025/001e8a896cfb50f5ed17836dc815b235e3bcca55ee91c9869a2afaeb054b50a6/subnational-population-estimates-at-30-june-2025-provisional.xlsx diff --git a/db/data/stats_nz/subnational_population_estimates_2025/subnational-population-estimates-at-30-june-2025-provisional.xlsx b/db/data/stats_nz/subnational_population_estimates_2025/subnational-population-estimates-at-30-june-2025-provisional.xlsx new file mode 100644 index 0000000..8b551d3 Binary files /dev/null and b/db/data/stats_nz/subnational_population_estimates_2025/subnational-population-estimates-at-30-june-2025-provisional.xlsx differ diff --git a/docs/pe-nz-population-source-notes.md b/docs/pe-nz-population-source-notes.md new file mode 100644 index 0000000..a51df0b --- /dev/null +++ b/docs/pe-nz-population-source-notes.md @@ -0,0 +1,98 @@ +# NZ population source notes + +This records the bounded source-ingestion work for demographic packages 1 and 2 +of [issue #176](https://github.com/PolicyEngine/chronicle/issues/176), checked on +30 August 2026 UTC. It does not mark the demographic target surface complete. + +## Pinned publisher evidence + +| Artifact | Publisher reference | SHA-256 | Bytes | +|---|---|---|---:| +| Subnational population workbook | At 30 June 2025, published 29 October 2025 | `001e8a896cfb50f5ed17836dc815b235e3bcca55ee91c9869a2afaeb054b50a6` | 97,990 | +| Regional council code/name attributes | Boundaries at 1 January 2025 | `e5d53fa6abf742121d123bff970c77658a16e8c9b405b83d1a978cc22920d660` | 3,381 | +| National population release landing page only | At 30 June 2025, published 19 August 2025 | `b2d2751800857895b1498d87df6f03ac3511ec8c7a9b17cc3150c8ab56533053` | 61,286 | + +All artifacts came directly from Stats NZ's website or Stats NZ's official +geographic service. Their manifests record exact publisher URLs, hashes, +retrieval timestamps, sizes, and immutable country-scoped R2 keys: + +- [Subnational workbook manifest](../db/data/stats_nz/subnational_population_estimates_2025/manifest.yaml). +- [REGC 2025 classification manifest](../db/data/stats_nz/regional_council_2025_codes/manifest.yaml). +- [National landing-page evidence manifest](../db/data/stats_nz/national_population_estimates_2025/manifest.yaml). + +The objects use +`r2://ledger-raw/raw/nz/stats_nz//2025//`. +After upload, all three remote objects were downloaded through the authorized +Chronicle/Wrangler path and their byte counts and SHA-256 hashes matched the +local publisher artifacts. +The two population references share a 30 June 2025 reference date, not a +publication date. The national landing page is evidence of the missing download +surface, **not** a single-year-age data artifact or a national source package. +The classification is geography evidence only and emits no population facts. + +## S7: partial workbook package + +The [source package](../packages/stats_nz/subnational_population_estimates_2025/README.md) +admits 85 cells from Tables 1 and 3: 17 geographic rows × all ages/four broad +age bands. Its precise scope, semantics, and guards are documented there. + +The workbook does not contain the requested five-year-age × sex × region cut. +Stats NZ's public metadata catalog lists `POPES_SUB_004`: + +> Subnational population estimates (RC, SA2), by age and sex, at 30 June 1996-2025 (2025 boundaries) + +This identifies the needed population/age/sex/boundary family, not a claim that +the query's original publication vintage has been verified. The documented API +request for its structure returned **HTTP 401** without a subscription: + +```text +https://api.data.stats.govt.nz/rest/dataflow/STATSNZ/POPES_SUB_004/1.0?references=all&detail=referencepartial +``` + +The [official API guide](https://www.stats.govt.nz/tools/aotearoa-data-explorer/ade-api-user-guide/) +describes API subscription access. No existing subscription was available to +this ingestion run. No account was created and no authentication bypass was +attempted. An authorized export still needs verification of the exact five-year +age categories, sex categories, 2025 REGC boundaries, reference date, and source +revision vintage before it can extend this package. In particular, +`POPES_SUB_003` advertises **2026 boundaries** and is not a substitute. + +## S8: national single-year ages remain missing + +The pinned [19 August 2025 national release](https://www.stats.govt.nz/information-releases/national-population-estimates-at-30-june-2025/) +contains headline counts and directs users to Infoshare's Population / Population +Estimates – DPE tables for annual and quarterly single-year-age data. Its page +payload contains no downloadable XLSX/CSV document block. This corrects the +inventory's assumption that the release itself provided a single-year-age XLSX. + +No reproducible publisher single-year-age artifact for this original reference +date/publication vintage was acquired. The public ADE catalog query did not +identify a corresponding national 2025 age table. The missing artifact remains +an access/discovery gap, not a zero population count. A subscribed API request +may assist discovery, but access alone does not establish that the required +original-vintage table is available. + +The release explicitly says that Stats NZ revises the preceding six quarters. +A current 2025 observation may therefore differ from the 19 August 2025 +publication. An eventual publisher export must carry its actual revision +vintage; it must not be relabeled as the original release. No latest-2026 +population, projection, mirrored third-party bytes, or inferred age allocation +has been substituted. No national single-year-age source-package alias is +registered, and the national facts count added by S8 is **zero**. + +## Validation and boundary + +The partial S7 package passes source-package validation, exact source-cell +comparisons, geography mapping checks, source-semantic drift tests, consumer +contract validation, and an explicit one-package build. The bounded source +build records 85/85 lineaged facts and 2,265 parsed cells. + +The source build warns that Axiom CLI canonical-concept validation did not run. +All mappings are source-label relations; this work claims neither Axiom +canonical certification nor calibration quality. + +Microcosm retains responsibility for choosing constraints, reconciling source +universes, aging, imputing, constructing solver targets, and activating a +population. The 85 counts are available source facts, not proof that these +downstream steps have run. Independent source-fidelity and boundary review is +still required before landing. diff --git a/docs/pe-nz-source-checklist.md b/docs/pe-nz-source-checklist.md index c0b362a..41ef2a2 100644 --- a/docs/pe-nz-source-checklist.md +++ b/docs/pe-nz-source-checklist.md @@ -53,8 +53,8 @@ tests, and record a verified `raw/nz/...` R2 URI. | Package from #176 | Artifact pinned | Package valid | `raw/nz` verified | Notes | |---|---:|---:|---:|---| -| `stats_nz/subnational_population_estimates_2025` | [ ] | [ ] | [ ] | | -| `stats_nz/national_population_estimates_2025` | [ ] | [ ] | [ ] | | +| `stats_nz/subnational_population_estimates_2025` | [x] | [ ] | [x] | **Partial:** 85 validated workbook counts (16 regions + published NZ total; all ages/four broad age bands). Five-year-age × sex coverage is missing; its ADE structure request returned HTTP 401. Package-completion box stays unchecked. [Evidence and access notes](pe-nz-population-source-notes.md). | +| `stats_nz/national_population_estimates_2025` | [ ] | [ ] | [ ] | **Blocked:** no original-vintage single-year-age artifact acquired. The 19 August 2025 release points to Infoshare, not an XLSX. Landing-page evidence only is hash-pinned in `raw/nz`; it emits zero facts. [Evidence and access notes](pe-nz-population-source-notes.md). | | `stats_nz/census_2023_households_by_region` | [ ] | [ ] | [ ] | | | `stats_nz/census_2023_family_type` | [ ] | [ ] | [ ] | | | `stats_nz/census_2023_ethnicity_age_region` | [ ] | [ ] | [ ] | | diff --git a/packages/stats_nz/subnational_population_estimates_2025/README.md b/packages/stats_nz/subnational_population_estimates_2025/README.md new file mode 100644 index 0000000..07e1f76 --- /dev/null +++ b/packages/stats_nz/subnational_population_estimates_2025/README.md @@ -0,0 +1,77 @@ +# Stats NZ subnational population, 30 June 2025 — partial + +This package emits 85 publisher-backed population counts: the 16 regional +council areas and the separately published New Zealand total, each for all ages +and the four source age bands. It does **not** complete the five-year-age × sex × +region surface requested by [issue #176](https://github.com/PolicyEngine/chronicle/issues/176). + +The [29 October 2025 release](https://www.stats.govt.nz/information-releases/subnational-population-estimates-at-30-june-2025/) +and its unchanged XLSX provide these cells: + +| Source sheet | Exact selection | Facts | +|---|---|---:| +| Table 1 | D7:D22 and D25, headed `2025 P` | 17 | +| Table 3 | Columns D:G, rows 10, 13, 16, 19, 22, 25, 28, 31, 34, 37, 40, 43, 46, 49, 52, 55, 64 | 68 | + +Table 3's columns are 0–14, 15–39, 40–64, and 65+. All selected counts include +all sexes; no sex-specific counts appear in these worksheets. Both source +worksheets are parsed over their complete used ranges (2,265 cells), including +unselected years and explanatory footnotes. The package does not ingest +territorial authority/local board tables, island subtotals, percentages, median +ages, or population-change components. + +## Reference period and population definition + +- Reference date: **30 June 2025**, expressed as `calendar_year: 2025` plus + equal start/end coverage dates `2025-06-30`. These are point-in-time counts, + not annual average populations. +- Publication vintage: **29 October 2025**; all selected figures are + **provisional**. The source artifact remains pinned even if Stats NZ later + revises its historical estimates. +- Population: estimated resident population (ERP), based on the 2023 census + usually resident population and adjusted for net census under/overcount, + residents temporarily overseas on census night, and subsequent births, + deaths, and net migration. This is not the unadjusted census-night count. +- Geography: regional boundaries at **1 January 2025**, with REGC codes stored + as strings and `geography_vintage: regc_2025`. The separately pinned official + [REGC 2025 attributes](https://services2.arcgis.com/vKb0s8tBIA3bdocZ/arcgis/rest/services/Regional_Council_2025/FeatureServer) + supply the codes and names. The classification names code `02` “Auckland”; + the population workbook labels the same region “Auckland region”. +- National scope: Table 1!D25 publishes **5,324,700**, including areas outside + regions, such as Chatham Islands territory. The package reads the published + national cells directly. It neither sums the 16 regions nor constructs a + code-`99` outside-region residual. Published rounding also prevents exact + reconciliation from being assumed. +- Provenance: `census` for a census-controlled ERP estimate, and + `assertion: observation` for the historical reference date. “Observation” + does not mean final or error-free; the source's provisional status remains + explicit in the period and concept evidence. +- Concept relation: `source_label`, with Stats NZ evidence. No Axiom canonical + equivalence, population calibration, uprating, interpolation, or target + activation is claimed. + +Every numeric value is a single selected source cell with scale 1. Row labels, +age headers, reference years, the boundary footnote, and the provisional-status +footnote have guards. The tests compare every admitted value against the +publisher workbook and every regional code/name against the independent +publisher classification. + +## Artifacts and remaining access gaps + +See [NZ population source notes](../../../docs/pe-nz-population-source-notes.md) +for the three pinned checksums, raw storage, the detailed S7 API access failure, +and S8's missing single-year-age artifact. The wave-1 checklist deliberately +keeps the package-completion checkbox unchecked. + +## Reproduce the bounded checks + +```sh +uv run chronicle validate-package stats-nz-subnational-population-estimates-2025 --year 2025 +uv run chronicle build-suite stats-nz-subnational-population-estimates-2025 --year 2025 --out /tmp/chronicle-nz-population-suite +uv run pytest -q tests/test_chronicle_nz_population.py tests/test_chronicle_artifacts.py tests/test_source_package_alias_drift.py +``` + +Source and consumer checks pass for this partial package. The concept-alignment +warning records that no Axiom CLI metadata validation ran; the source-label +mapping is not a certified canonical alignment. Independent source-fidelity +and boundary judges must review this candidate before landing. diff --git a/packages/stats_nz/subnational_population_estimates_2025/source_package.yaml b/packages/stats_nz/subnational_population_estimates_2025/source_package.yaml new file mode 100644 index 0000000..004e381 --- /dev/null +++ b/packages/stats_nz/subnational_population_estimates_2025/source_package.yaml @@ -0,0 +1,1066 @@ +# Partial #176 source package: 85 directly published counts, not the missing +# five-year-age x sex x region table. See README.md for scope and source access. +# The all-age national total is Table 1!D25, never the sum of the 16 regions. +# REGC codes/names come from the separately pinned Stats NZ REGC2025 attributes. +schema_version: ledger.source_package.v1 +package_id: stats-nz-subnational-population-estimates-2025 +label: 'Stats NZ 30 June 2025 provisional regional population: all ages and broad age groups (partial)' +artifact: + source_name: stats_nz + source_table: 'Subnational population estimates: At 30 June 2025 (provisional), Tables 1 and 3' + resource_package: db + resource_directory: data/stats_nz/subnational_population_estimates_2025 + manifest: manifest.yaml + vintage: stats_nz_subnational_population_2025_10_29 + extracted_at: '2026-08-30' + extraction_method: xlsx used-range cell parse restricted to the two source worksheets + parser: xlsx_used_range + sheets: + - Table 1 + - Table 3 + artifact_year: 2025 +record_sets: +- &population_set + record_set_id: stats_nz.cy2025.subnational_population.all_ages + record_set_spec_id: stats_nz.subnational_population.all_ages.v1 + source_record_id_prefix: stats_nz.cy2025.subnational_population.all_ages + provenance_class: census + assertion: observation + sheet_name: Table 1 + period_type: calendar_year + period: 2025 + period_coverage: + start_date: '2025-06-30' + end_date: '2025-06-30' + basis: calendar + source_period_label: At 30 June 2025 (provisional) + notes: Point-in-time estimated resident population, not an annual average; published 29 October 2025 using the 2023 census base. + geography_id: NZ + geography_level: country + geography_name: New Zealand + geography_vintage: current + entity: person + entity_role: resident_population + domain: resident_population + groupby_dimension: geography + measures: + - &population_measure + measure_id: population + label: Estimated resident population + ordinal: 0 + column: D + source_column_id: estimated_resident_population + concept: stats_nz.estimated_resident_population + source_concept: stats_nz.estimated_resident_population + concept_relation: source_label + concept_authority: stats_nz + concept_evidence_url: https://www.stats.govt.nz/information-releases/subnational-population-estimates-at-30-june-2025/ + concept_evidence_notes: >- + Tables 1 and 3 publish provisional population counts at 30 June 2025, + using 1 January 2025 boundaries and the 2023 census base. ERP includes + adjustments for net census under/overcount, residents temporarily overseas + on census night, and subsequent births, deaths, and net migration. + These counts include all sexes; there is no sex-specific split in these + worksheets. Only all ages and the source bands 0–14, 15–39, 40–64, and 65+ + are admitted, not five-year or single-year ages. The New Zealand row + includes areas outside regions (for example Chatham Islands territory); + it is read directly, never constructed from regional counts. Published + rounding means component counts need not sum to totals. Source-only + facts: no uprating, interpolation, target activation, or calibration. + unit: count + aggregation: sum + expected_cell_type: number + expected_column_header_row: 6 + expected_column_header: 2025 P + rows: + - value_id: regc_01 + label: "Northland region" + ordinal: 0 + row_number: 7 + expected_row_header_column: A + expected_row_header: "Northland region" + geography_id: "01" + geography_level: region + geography_name: "Northland Region" + geography_vintage: regc_2025 + table_record_kind: total + guard_cells: + - column: A + row: 4 + expected_value: 'At 30 June 2023–2025' + label: point-in-time release period + - column: A + row: 29 + expected_value: '2. Boundaries at 1 January 2025.' + label: regional boundary vintage + - column: A + row: 34 + expected_value: P provisional + label: publication status + - value_id: regc_02 + label: "Auckland region" + ordinal: 1 + row_number: 8 + expected_row_header_column: A + expected_row_header: "Auckland region" + geography_id: "02" + geography_level: region + geography_name: "Auckland" + geography_vintage: regc_2025 + table_record_kind: total + guard_cells: + - column: A + row: 4 + expected_value: 'At 30 June 2023–2025' + label: point-in-time release period + - column: A + row: 29 + expected_value: '2. Boundaries at 1 January 2025.' + label: regional boundary vintage + - column: A + row: 34 + expected_value: P provisional + label: publication status + - value_id: regc_03 + label: "Waikato region" + ordinal: 2 + row_number: 9 + expected_row_header_column: A + expected_row_header: "Waikato region" + geography_id: "03" + geography_level: region + geography_name: "Waikato Region" + geography_vintage: regc_2025 + table_record_kind: total + guard_cells: + - column: A + row: 4 + expected_value: 'At 30 June 2023–2025' + label: point-in-time release period + - column: A + row: 29 + expected_value: '2. Boundaries at 1 January 2025.' + label: regional boundary vintage + - column: A + row: 34 + expected_value: P provisional + label: publication status + - value_id: regc_04 + label: "Bay of Plenty region" + ordinal: 3 + row_number: 10 + expected_row_header_column: A + expected_row_header: "Bay of Plenty region" + geography_id: "04" + geography_level: region + geography_name: "Bay of Plenty Region" + geography_vintage: regc_2025 + table_record_kind: total + guard_cells: + - column: A + row: 4 + expected_value: 'At 30 June 2023–2025' + label: point-in-time release period + - column: A + row: 29 + expected_value: '2. Boundaries at 1 January 2025.' + label: regional boundary vintage + - column: A + row: 34 + expected_value: P provisional + label: publication status + - value_id: regc_05 + label: "Gisborne region" + ordinal: 4 + row_number: 11 + expected_row_header_column: A + expected_row_header: "Gisborne region" + geography_id: "05" + geography_level: region + geography_name: "Gisborne Region" + geography_vintage: regc_2025 + table_record_kind: total + guard_cells: + - column: A + row: 4 + expected_value: 'At 30 June 2023–2025' + label: point-in-time release period + - column: A + row: 29 + expected_value: '2. Boundaries at 1 January 2025.' + label: regional boundary vintage + - column: A + row: 34 + expected_value: P provisional + label: publication status + - value_id: regc_06 + label: "Hawke's Bay region" + ordinal: 5 + row_number: 12 + expected_row_header_column: A + expected_row_header: "Hawke's Bay region" + geography_id: "06" + geography_level: region + geography_name: "Hawke's Bay Region" + geography_vintage: regc_2025 + table_record_kind: total + guard_cells: + - column: A + row: 4 + expected_value: 'At 30 June 2023–2025' + label: point-in-time release period + - column: A + row: 29 + expected_value: '2. Boundaries at 1 January 2025.' + label: regional boundary vintage + - column: A + row: 34 + expected_value: P provisional + label: publication status + - value_id: regc_07 + label: "Taranaki region" + ordinal: 6 + row_number: 13 + expected_row_header_column: A + expected_row_header: "Taranaki region" + geography_id: "07" + geography_level: region + geography_name: "Taranaki Region" + geography_vintage: regc_2025 + table_record_kind: total + guard_cells: + - column: A + row: 4 + expected_value: 'At 30 June 2023–2025' + label: point-in-time release period + - column: A + row: 29 + expected_value: '2. Boundaries at 1 January 2025.' + label: regional boundary vintage + - column: A + row: 34 + expected_value: P provisional + label: publication status + - value_id: regc_08 + label: "Manawatū-Whanganui region" + ordinal: 7 + row_number: 14 + expected_row_header_column: A + expected_row_header: "Manawatū-Whanganui region" + geography_id: "08" + geography_level: region + geography_name: "Manawatū-Whanganui Region" + geography_vintage: regc_2025 + table_record_kind: total + guard_cells: + - column: A + row: 4 + expected_value: 'At 30 June 2023–2025' + label: point-in-time release period + - column: A + row: 29 + expected_value: '2. Boundaries at 1 January 2025.' + label: regional boundary vintage + - column: A + row: 34 + expected_value: P provisional + label: publication status + - value_id: regc_09 + label: "Wellington region" + ordinal: 8 + row_number: 15 + expected_row_header_column: A + expected_row_header: "Wellington region" + geography_id: "09" + geography_level: region + geography_name: "Wellington Region" + geography_vintage: regc_2025 + table_record_kind: total + guard_cells: + - column: A + row: 4 + expected_value: 'At 30 June 2023–2025' + label: point-in-time release period + - column: A + row: 29 + expected_value: '2. Boundaries at 1 January 2025.' + label: regional boundary vintage + - column: A + row: 34 + expected_value: P provisional + label: publication status + - value_id: regc_16 + label: "Tasman region" + ordinal: 9 + row_number: 16 + expected_row_header_column: A + expected_row_header: "Tasman region" + geography_id: "16" + geography_level: region + geography_name: "Tasman Region" + geography_vintage: regc_2025 + table_record_kind: total + guard_cells: + - column: A + row: 4 + expected_value: 'At 30 June 2023–2025' + label: point-in-time release period + - column: A + row: 29 + expected_value: '2. Boundaries at 1 January 2025.' + label: regional boundary vintage + - column: A + row: 34 + expected_value: P provisional + label: publication status + - value_id: regc_17 + label: "Nelson region" + ordinal: 10 + row_number: 17 + expected_row_header_column: A + expected_row_header: "Nelson region" + geography_id: "17" + geography_level: region + geography_name: "Nelson Region" + geography_vintage: regc_2025 + table_record_kind: total + guard_cells: + - column: A + row: 4 + expected_value: 'At 30 June 2023–2025' + label: point-in-time release period + - column: A + row: 29 + expected_value: '2. Boundaries at 1 January 2025.' + label: regional boundary vintage + - column: A + row: 34 + expected_value: P provisional + label: publication status + - value_id: regc_18 + label: "Marlborough region" + ordinal: 11 + row_number: 18 + expected_row_header_column: A + expected_row_header: "Marlborough region" + geography_id: "18" + geography_level: region + geography_name: "Marlborough Region" + geography_vintage: regc_2025 + table_record_kind: total + guard_cells: + - column: A + row: 4 + expected_value: 'At 30 June 2023–2025' + label: point-in-time release period + - column: A + row: 29 + expected_value: '2. Boundaries at 1 January 2025.' + label: regional boundary vintage + - column: A + row: 34 + expected_value: P provisional + label: publication status + - value_id: regc_12 + label: "West Coast region" + ordinal: 12 + row_number: 19 + expected_row_header_column: A + expected_row_header: "West Coast region" + geography_id: "12" + geography_level: region + geography_name: "West Coast Region" + geography_vintage: regc_2025 + table_record_kind: total + guard_cells: + - column: A + row: 4 + expected_value: 'At 30 June 2023–2025' + label: point-in-time release period + - column: A + row: 29 + expected_value: '2. Boundaries at 1 January 2025.' + label: regional boundary vintage + - column: A + row: 34 + expected_value: P provisional + label: publication status + - value_id: regc_13 + label: "Canterbury region" + ordinal: 13 + row_number: 20 + expected_row_header_column: A + expected_row_header: "Canterbury region" + geography_id: "13" + geography_level: region + geography_name: "Canterbury Region" + geography_vintage: regc_2025 + table_record_kind: total + guard_cells: + - column: A + row: 4 + expected_value: 'At 30 June 2023–2025' + label: point-in-time release period + - column: A + row: 29 + expected_value: '2. Boundaries at 1 January 2025.' + label: regional boundary vintage + - column: A + row: 34 + expected_value: P provisional + label: publication status + - value_id: regc_14 + label: "Otago region" + ordinal: 14 + row_number: 21 + expected_row_header_column: A + expected_row_header: "Otago region" + geography_id: "14" + geography_level: region + geography_name: "Otago Region" + geography_vintage: regc_2025 + table_record_kind: total + guard_cells: + - column: A + row: 4 + expected_value: 'At 30 June 2023–2025' + label: point-in-time release period + - column: A + row: 29 + expected_value: '2. Boundaries at 1 January 2025.' + label: regional boundary vintage + - column: A + row: 34 + expected_value: P provisional + label: publication status + - value_id: regc_15 + label: "Southland region" + ordinal: 15 + row_number: 22 + expected_row_header_column: A + expected_row_header: "Southland region" + geography_id: "15" + geography_level: region + geography_name: "Southland Region" + geography_vintage: regc_2025 + table_record_kind: total + guard_cells: + - column: A + row: 4 + expected_value: 'At 30 June 2023–2025' + label: point-in-time release period + - column: A + row: 29 + expected_value: '2. Boundaries at 1 January 2025.' + label: regional boundary vintage + - column: A + row: 34 + expected_value: P provisional + label: publication status + - value_id: nz + label: "New Zealand" + ordinal: 16 + row_number: 25 + expected_row_header_column: A + expected_row_header: "New Zealand(3)" + geography_id: "NZ" + geography_level: country + geography_name: "New Zealand" + geography_vintage: current + table_record_kind: total + guard_cells: + - column: A + row: 4 + expected_value: 'At 30 June 2023–2025' + label: point-in-time release period + - column: A + row: 29 + expected_value: '2. Boundaries at 1 January 2025.' + label: regional boundary vintage + - column: A + row: 34 + expected_value: P provisional + label: publication status +- <<: *population_set + record_set_id: stats_nz.cy2025.subnational_population.age_0_14 + provenance_class: census + record_set_spec_id: stats_nz.subnational_population.age_0_14.v1 + source_record_id_prefix: stats_nz.cy2025.subnational_population.age_0_14 + sheet_name: Table 3 + shared_filters: + person.age_band: "0_14" + shared_constraints: + - variable: person.age_band + operator: == + value: "0_14" + label: Publisher broad age group + - variable: person.age + operator: '>=' + value: 0 + unit: years + label: Age lower bound + - variable: person.age + operator: < + value: 15 + unit: years + label: Age upper bound (exclusive) + measures: + - <<: *population_measure + column: D + expected_column_header: "0–14" + rows: &broad_age_rows + - value_id: regc_01 + label: "Northland region" + ordinal: 0 + row_number: 10 + expected_row_header_column: A + expected_row_header: "Northland region" + geography_id: "01" + geography_level: region + geography_name: "Northland Region" + geography_vintage: regc_2025 + table_record_kind: total + guard_cells: + - column: A + row: 4 + expected_value: 'At 30 June 2023–2025' + label: point-in-time release period + - column: A + row: 68 + expected_value: '2. Boundaries at 1 January 2025.' + label: regional boundary vintage + - column: A + row: 74 + expected_value: P provisional + label: publication status + - column: C + row: 10 + expected_value: 2025 P + label: historical 2025 reference-date row + - value_id: regc_02 + label: "Auckland region" + ordinal: 1 + row_number: 13 + expected_row_header_column: A + expected_row_header: "Auckland region" + geography_id: "02" + geography_level: region + geography_name: "Auckland" + geography_vintage: regc_2025 + table_record_kind: total + guard_cells: + - column: A + row: 4 + expected_value: 'At 30 June 2023–2025' + label: point-in-time release period + - column: A + row: 68 + expected_value: '2. Boundaries at 1 January 2025.' + label: regional boundary vintage + - column: A + row: 74 + expected_value: P provisional + label: publication status + - column: C + row: 13 + expected_value: 2025 P + label: historical 2025 reference-date row + - value_id: regc_03 + label: "Waikato region" + ordinal: 2 + row_number: 16 + expected_row_header_column: A + expected_row_header: "Waikato region" + geography_id: "03" + geography_level: region + geography_name: "Waikato Region" + geography_vintage: regc_2025 + table_record_kind: total + guard_cells: + - column: A + row: 4 + expected_value: 'At 30 June 2023–2025' + label: point-in-time release period + - column: A + row: 68 + expected_value: '2. Boundaries at 1 January 2025.' + label: regional boundary vintage + - column: A + row: 74 + expected_value: P provisional + label: publication status + - column: C + row: 16 + expected_value: 2025 P + label: historical 2025 reference-date row + - value_id: regc_04 + label: "Bay of Plenty region" + ordinal: 3 + row_number: 19 + expected_row_header_column: A + expected_row_header: "Bay of Plenty region" + geography_id: "04" + geography_level: region + geography_name: "Bay of Plenty Region" + geography_vintage: regc_2025 + table_record_kind: total + guard_cells: + - column: A + row: 4 + expected_value: 'At 30 June 2023–2025' + label: point-in-time release period + - column: A + row: 68 + expected_value: '2. Boundaries at 1 January 2025.' + label: regional boundary vintage + - column: A + row: 74 + expected_value: P provisional + label: publication status + - column: C + row: 19 + expected_value: 2025 P + label: historical 2025 reference-date row + - value_id: regc_05 + label: "Gisborne region" + ordinal: 4 + row_number: 22 + expected_row_header_column: A + expected_row_header: "Gisborne region" + geography_id: "05" + geography_level: region + geography_name: "Gisborne Region" + geography_vintage: regc_2025 + table_record_kind: total + guard_cells: + - column: A + row: 4 + expected_value: 'At 30 June 2023–2025' + label: point-in-time release period + - column: A + row: 68 + expected_value: '2. Boundaries at 1 January 2025.' + label: regional boundary vintage + - column: A + row: 74 + expected_value: P provisional + label: publication status + - column: C + row: 22 + expected_value: 2025 P + label: historical 2025 reference-date row + - value_id: regc_06 + label: "Hawke's Bay region" + ordinal: 5 + row_number: 25 + expected_row_header_column: A + expected_row_header: "Hawke's Bay region" + geography_id: "06" + geography_level: region + geography_name: "Hawke's Bay Region" + geography_vintage: regc_2025 + table_record_kind: total + guard_cells: + - column: A + row: 4 + expected_value: 'At 30 June 2023–2025' + label: point-in-time release period + - column: A + row: 68 + expected_value: '2. Boundaries at 1 January 2025.' + label: regional boundary vintage + - column: A + row: 74 + expected_value: P provisional + label: publication status + - column: C + row: 25 + expected_value: 2025 P + label: historical 2025 reference-date row + - value_id: regc_07 + label: "Taranaki region" + ordinal: 6 + row_number: 28 + expected_row_header_column: A + expected_row_header: "Taranaki region" + geography_id: "07" + geography_level: region + geography_name: "Taranaki Region" + geography_vintage: regc_2025 + table_record_kind: total + guard_cells: + - column: A + row: 4 + expected_value: 'At 30 June 2023–2025' + label: point-in-time release period + - column: A + row: 68 + expected_value: '2. Boundaries at 1 January 2025.' + label: regional boundary vintage + - column: A + row: 74 + expected_value: P provisional + label: publication status + - column: C + row: 28 + expected_value: 2025 P + label: historical 2025 reference-date row + - value_id: regc_08 + label: "Manawatū-Whanganui region" + ordinal: 7 + row_number: 31 + expected_row_header_column: A + expected_row_header: "Manawatū-Whanganui region" + geography_id: "08" + geography_level: region + geography_name: "Manawatū-Whanganui Region" + geography_vintage: regc_2025 + table_record_kind: total + guard_cells: + - column: A + row: 4 + expected_value: 'At 30 June 2023–2025' + label: point-in-time release period + - column: A + row: 68 + expected_value: '2. Boundaries at 1 January 2025.' + label: regional boundary vintage + - column: A + row: 74 + expected_value: P provisional + label: publication status + - column: C + row: 31 + expected_value: 2025 P + label: historical 2025 reference-date row + - value_id: regc_09 + label: "Wellington region" + ordinal: 8 + row_number: 34 + expected_row_header_column: A + expected_row_header: "Wellington region" + geography_id: "09" + geography_level: region + geography_name: "Wellington Region" + geography_vintage: regc_2025 + table_record_kind: total + guard_cells: + - column: A + row: 4 + expected_value: 'At 30 June 2023–2025' + label: point-in-time release period + - column: A + row: 68 + expected_value: '2. Boundaries at 1 January 2025.' + label: regional boundary vintage + - column: A + row: 74 + expected_value: P provisional + label: publication status + - column: C + row: 34 + expected_value: 2025 P + label: historical 2025 reference-date row + - value_id: regc_16 + label: "Tasman region" + ordinal: 9 + row_number: 37 + expected_row_header_column: A + expected_row_header: "Tasman region" + geography_id: "16" + geography_level: region + geography_name: "Tasman Region" + geography_vintage: regc_2025 + table_record_kind: total + guard_cells: + - column: A + row: 4 + expected_value: 'At 30 June 2023–2025' + label: point-in-time release period + - column: A + row: 68 + expected_value: '2. Boundaries at 1 January 2025.' + label: regional boundary vintage + - column: A + row: 74 + expected_value: P provisional + label: publication status + - column: C + row: 37 + expected_value: 2025 P + label: historical 2025 reference-date row + - value_id: regc_17 + label: "Nelson region" + ordinal: 10 + row_number: 40 + expected_row_header_column: A + expected_row_header: "Nelson region" + geography_id: "17" + geography_level: region + geography_name: "Nelson Region" + geography_vintage: regc_2025 + table_record_kind: total + guard_cells: + - column: A + row: 4 + expected_value: 'At 30 June 2023–2025' + label: point-in-time release period + - column: A + row: 68 + expected_value: '2. Boundaries at 1 January 2025.' + label: regional boundary vintage + - column: A + row: 74 + expected_value: P provisional + label: publication status + - column: C + row: 40 + expected_value: 2025 P + label: historical 2025 reference-date row + - value_id: regc_18 + label: "Marlborough region" + ordinal: 11 + row_number: 43 + expected_row_header_column: A + expected_row_header: "Marlborough region" + geography_id: "18" + geography_level: region + geography_name: "Marlborough Region" + geography_vintage: regc_2025 + table_record_kind: total + guard_cells: + - column: A + row: 4 + expected_value: 'At 30 June 2023–2025' + label: point-in-time release period + - column: A + row: 68 + expected_value: '2. Boundaries at 1 January 2025.' + label: regional boundary vintage + - column: A + row: 74 + expected_value: P provisional + label: publication status + - column: C + row: 43 + expected_value: 2025 P + label: historical 2025 reference-date row + - value_id: regc_12 + label: "West Coast region" + ordinal: 12 + row_number: 46 + expected_row_header_column: A + expected_row_header: "West Coast region" + geography_id: "12" + geography_level: region + geography_name: "West Coast Region" + geography_vintage: regc_2025 + table_record_kind: total + guard_cells: + - column: A + row: 4 + expected_value: 'At 30 June 2023–2025' + label: point-in-time release period + - column: A + row: 68 + expected_value: '2. Boundaries at 1 January 2025.' + label: regional boundary vintage + - column: A + row: 74 + expected_value: P provisional + label: publication status + - column: C + row: 46 + expected_value: 2025 P + label: historical 2025 reference-date row + - value_id: regc_13 + label: "Canterbury region" + ordinal: 13 + row_number: 49 + expected_row_header_column: A + expected_row_header: "Canterbury region" + geography_id: "13" + geography_level: region + geography_name: "Canterbury Region" + geography_vintage: regc_2025 + table_record_kind: total + guard_cells: + - column: A + row: 4 + expected_value: 'At 30 June 2023–2025' + label: point-in-time release period + - column: A + row: 68 + expected_value: '2. Boundaries at 1 January 2025.' + label: regional boundary vintage + - column: A + row: 74 + expected_value: P provisional + label: publication status + - column: C + row: 49 + expected_value: 2025 P + label: historical 2025 reference-date row + - value_id: regc_14 + label: "Otago region" + ordinal: 14 + row_number: 52 + expected_row_header_column: A + expected_row_header: "Otago region" + geography_id: "14" + geography_level: region + geography_name: "Otago Region" + geography_vintage: regc_2025 + table_record_kind: total + guard_cells: + - column: A + row: 4 + expected_value: 'At 30 June 2023–2025' + label: point-in-time release period + - column: A + row: 68 + expected_value: '2. Boundaries at 1 January 2025.' + label: regional boundary vintage + - column: A + row: 74 + expected_value: P provisional + label: publication status + - column: C + row: 52 + expected_value: 2025 P + label: historical 2025 reference-date row + - value_id: regc_15 + label: "Southland region" + ordinal: 15 + row_number: 55 + expected_row_header_column: A + expected_row_header: "Southland region" + geography_id: "15" + geography_level: region + geography_name: "Southland Region" + geography_vintage: regc_2025 + table_record_kind: total + guard_cells: + - column: A + row: 4 + expected_value: 'At 30 June 2023–2025' + label: point-in-time release period + - column: A + row: 68 + expected_value: '2. Boundaries at 1 January 2025.' + label: regional boundary vintage + - column: A + row: 74 + expected_value: P provisional + label: publication status + - column: C + row: 55 + expected_value: 2025 P + label: historical 2025 reference-date row + - value_id: nz + label: "New Zealand" + ordinal: 16 + row_number: 64 + expected_row_header_column: A + expected_row_header: "New Zealand(4)" + geography_id: "NZ" + geography_level: country + geography_name: "New Zealand" + geography_vintage: current + table_record_kind: total + guard_cells: + - column: A + row: 4 + expected_value: 'At 30 June 2023–2025' + label: point-in-time release period + - column: A + row: 68 + expected_value: '2. Boundaries at 1 January 2025.' + label: regional boundary vintage + - column: A + row: 74 + expected_value: P provisional + label: publication status + - column: C + row: 64 + expected_value: 2025 P + label: historical 2025 reference-date row +- <<: *population_set + record_set_id: stats_nz.cy2025.subnational_population.age_15_39 + provenance_class: census + record_set_spec_id: stats_nz.subnational_population.age_15_39.v1 + source_record_id_prefix: stats_nz.cy2025.subnational_population.age_15_39 + sheet_name: Table 3 + shared_filters: + person.age_band: "15_39" + shared_constraints: + - variable: person.age_band + operator: == + value: "15_39" + label: Publisher broad age group + - variable: person.age + operator: '>=' + value: 15 + unit: years + label: Age lower bound + - variable: person.age + operator: < + value: 40 + unit: years + label: Age upper bound (exclusive) + measures: + - <<: *population_measure + column: E + expected_column_header: "15–39" + rows: *broad_age_rows +- <<: *population_set + record_set_id: stats_nz.cy2025.subnational_population.age_40_64 + provenance_class: census + record_set_spec_id: stats_nz.subnational_population.age_40_64.v1 + source_record_id_prefix: stats_nz.cy2025.subnational_population.age_40_64 + sheet_name: Table 3 + shared_filters: + person.age_band: "40_64" + shared_constraints: + - variable: person.age_band + operator: == + value: "40_64" + label: Publisher broad age group + - variable: person.age + operator: '>=' + value: 40 + unit: years + label: Age lower bound + - variable: person.age + operator: < + value: 65 + unit: years + label: Age upper bound (exclusive) + measures: + - <<: *population_measure + column: F + expected_column_header: "40–64" + rows: *broad_age_rows +- <<: *population_set + record_set_id: stats_nz.cy2025.subnational_population.age_65_plus + provenance_class: census + record_set_spec_id: stats_nz.subnational_population.age_65_plus.v1 + source_record_id_prefix: stats_nz.cy2025.subnational_population.age_65_plus + sheet_name: Table 3 + shared_filters: + person.age_band: "65_plus" + shared_constraints: + - variable: person.age_band + operator: == + value: "65_plus" + label: Publisher broad age group + - variable: person.age + operator: '>=' + value: 65 + unit: years + label: Age lower bound + measures: + - <<: *population_measure + column: G + expected_column_header: "65+" + rows: *broad_age_rows diff --git a/tests/test_chronicle_bundle.py b/tests/test_chronicle_bundle.py index 6a00ef8..d6ea0e9 100644 --- a/tests/test_chronicle_bundle.py +++ b/tests/test_chronicle_bundle.py @@ -73,16 +73,16 @@ def test_build_bundle_writes_merged_consumer_contract(tmp_path): "aggregate_duplicate_key_count": 0, "entity_count": 12, "error_count": 0, - "fact_count": 171855, - "geography_count": 12536, + "fact_count": 171940, + "geography_count": 12553, "period_count": 192, "semantic_duplicate_key_count": 121, "skipped_source_count": 10, - "source_count": 43, - "source_package_count": 151, + "source_count": 44, + "source_package_count": 152, "warning_count": 1, } - assert len(rows) == 171855 + assert len(rows) == 171940 assert {row["provenance_class"] for row in rows} <= { "administrative", "census", @@ -100,7 +100,7 @@ def test_build_bundle_writes_merged_consumer_contract(tmp_path): ) assert rows[0]["aggregate_fact_key"].startswith("ledger.aggregate_fact.v2:") assert rows[0]["semantic_fact_key"].startswith("ledger.semantic_fact.v2:") - assert source_packages["source_package_count"] == 151 + assert source_packages["source_package_count"] == 152 assert source_packages["skipped_source_count"] == 10 assert sorted(item["source"] for item in source_packages["skipped_sources"]) == [ "census-acs-s0101-congressional-district-age-2024", @@ -114,7 +114,7 @@ def test_build_bundle_writes_merged_consumer_contract(tmp_path): "jct-obbba-revenue-estimates-2025", "jct-tax-expenditures-2024", ] - assert coverage["fact_count"] == 171855 + assert coverage["fact_count"] == 171940 assert coverage["counts"]["by_source"] == { "bea": 445, "bfp_economic_outlook": 5, @@ -156,12 +156,20 @@ def test_build_bundle_writes_merged_consumer_contract(tmp_path): "statbel_fiscal_income": 565, "statbel_fiscal_income_distribution": 14600, "statbel_population_structure": 36, + "stats_nz": 85, "usda_snap": 852, "voa": 3001, "welshgov": 216, } table_counts = coverage["counts"]["by_source_table"] - assert len(table_counts) == 146 + assert len(table_counts) == 147 + assert ( + table_counts[ + "stats_nz:Subnational population estimates: At 30 June 2025 " + "(provisional), Tables 1 and 3" + ] + == 85 + ) assert ( table_counts[ "dwp:Universal Credit childcare element statistics to August 2025, Table 1" @@ -533,7 +541,7 @@ def test_build_bundle_writes_merged_consumer_contract(tmp_path): "calendar_year:2022": 2075, "calendar_year:2023": 6343, "calendar_year:2024": 33936, - "calendar_year:2025": 4571, + "calendar_year:2025": 4656, "calendar_year:2026": 341, "calendar_year:2027": 320, "calendar_year:2028": 320, @@ -689,7 +697,10 @@ def test_build_bundle_writes_merged_consumer_contract(tmp_path): ) assert coverage["counts"]["by_geography"]["country:K02000001"] == 4297 assert coverage["counts"]["by_geography"]["country:K03000001"] == 497 - assert len(coverage["counts"]["by_geography"]) == 12536 + assert coverage["counts"]["by_geography"]["country:NZ"] == 5 + assert coverage["counts"]["by_geography"]["region:01"] == 5 + assert coverage["counts"]["by_geography"]["region:02"] == 5 + assert len(coverage["counts"]["by_geography"]) == 12553 assert coverage["counts"]["by_entity"] == { "benefit_unit": 233, "dwelling": 12733, @@ -699,7 +710,7 @@ def test_build_bundle_writes_merged_consumer_contract(tmp_path): "household": 40724, "institutional_sector": 133, "pension_plan": 2, - "person": 60466, + "person": 60551, "return": 14600, "social_protection_scheme": 36, "tax_unit": 40069, diff --git a/tests/test_chronicle_nz_population.py b/tests/test_chronicle_nz_population.py new file mode 100644 index 0000000..a6a5f5f --- /dev/null +++ b/tests/test_chronicle_nz_population.py @@ -0,0 +1,322 @@ +"""Source-only regression gates for the partial 30 June 2025 NZ population package.""" + +from __future__ import annotations + +from collections import Counter +from dataclasses import replace +import hashlib +import html +from io import BytesIO +import json +from pathlib import Path + +import openpyxl +import pytest +import yaml + +from chronicle.bundle import build_bundle, build_bundle_coverage +from chronicle.consumer_contract import ( + consumer_fact_rows, + validate_consumer_fact_contract, +) +from chronicle.core import validate_facts +from chronicle.source_package import ( + SOURCE_PACKAGE_ALIASES, + load_source_package, + validate_source_package, +) +from chronicle.sources.cells import build_source_cell_key, validate_source_cells + +ALIAS = "stats-nz-subnational-population-estimates-2025" +REPO_ROOT = Path(__file__).resolve().parents[1] +DATA_ROOT = REPO_ROOT / "db" / "data" / "stats_nz" +WORKBOOK_SHA256 = "001e8a896cfb50f5ed17836dc815b235e3bcca55ee91c9869a2afaeb054b50a6" +REGC_SHA256 = "e5d53fa6abf742121d123bff970c77658a16e8c9b405b83d1a978cc22920d660" +NATIONAL_PAGE_SHA256 = ( + "b2d2751800857895b1498d87df6f03ac3511ec8c7a9b17cc3150c8ab56533053" +) +# Order and rows are from Tables 1 and 3 of the pinned publisher workbook. +REGIONS = ( + ("01", 7, 10), + ("02", 8, 13), + ("03", 9, 16), + ("04", 10, 19), + ("05", 11, 22), + ("06", 12, 25), + ("07", 13, 28), + ("08", 14, 31), + ("09", 15, 34), + ("16", 16, 37), + ("17", 17, 40), + ("18", 18, 43), + ("12", 19, 46), + ("13", 20, 49), + ("14", 21, 52), + ("15", 22, 55), + ("NZ", 25, 64), +) +# These are the source's four broad bands, not five-year-age target bands. +AGE_BANDS = { + "0_14": ("D", 0, 15), + "15_39": ("E", 15, 40), + "40_64": ("F", 40, 65), + "65_plus": ("G", 65, None), +} + + +def _artifact(package_name): + directory = DATA_ROOT / package_name + manifest = yaml.safe_load((directory / "manifest.yaml").read_text()) + entry = manifest["files"][2025] + return (directory / entry["filename"]).read_bytes(), entry + + +@pytest.fixture(scope="module") +def population(): + package = load_source_package(ALIAS) + cells = package.build_source_cells(2025) + facts = package.build_facts(2025, cells=cells) + return package, cells, facts + + +@pytest.fixture(scope="module") +def workbook(): + content, _ = _artifact("subnational_population_estimates_2025") + book = openpyxl.load_workbook(BytesIO(content), data_only=True) + yield book + book.close() + + +@pytest.mark.parametrize( + ("package_name", "expected_sha", "expected_size"), + [ + ("subnational_population_estimates_2025", WORKBOOK_SHA256, 97990), + ("regional_council_2025_codes", REGC_SHA256, 3381), + ("national_population_estimates_2025", NATIONAL_PAGE_SHA256, 61286), + ], +) +def test_publisher_artifact_bytes_and_country_storage_are_pinned( + package_name, expected_sha, expected_size +): + content, entry = _artifact(package_name) + assert hashlib.sha256(content).hexdigest() == entry["sha256"] == expected_sha + assert len(content) == entry["size_bytes"] == expected_size + assert entry["storage"]["r2"]["uri"].startswith( + f"r2://ledger-raw/raw/nz/stats_nz/{package_name}/2025/{expected_sha}/" + ) + + +def test_partial_package_counts_and_whole_sheet_preservation(population): + package, cells, facts = population + report = validate_source_package(ALIAS, year=2025) + assert report.valid + assert not report.warnings + assert report.counts["record_set_count"] == 5 + assert report.counts["source_record_count"] == len(facts) == 85 + assert "partial" in package.label.lower() + assert package.artifact.sheets == ("Table 1", "Table 3") + # Full used ranges, including unused years and source footnotes: 35x9 + 75x26. + assert len(cells) == 2265 + assert Counter(cell.sheet_name for cell in cells) == { + "Table 1": 315, + "Table 3": 1950, + } + assert validate_source_cells(cells).valid + assert validate_facts(facts).valid + + +def test_every_fact_equals_one_published_cell_with_no_scaling(population, workbook): + package, cells, facts = population + actual = { + (fact.geography.id, fact.filters.get("person.age_band", "all")): fact + for fact in facts + } + cell_keys = {build_source_cell_key(cell) for cell in cells} + assert len(actual) == 85 + for geography_id, total_row, broad_age_row in REGIONS: + assert ( + actual[(geography_id, "all")].value + == workbook["Table 1"][f"D{total_row}"].value + ) + for age_band, (column, _lower, _upper) in AGE_BANDS.items(): + assert ( + actual[(geography_id, age_band)].value + == workbook["Table 3"][f"{column}{broad_age_row}"].value + ) + for fact, spec in zip(facts, package.build_source_record_specs(2025), strict=True): + assert spec.value_scale == 1 + assert spec.divisor_selector is None + assert spec.round_to is None + assert spec.selector.end_address is None + assert fact.source_record_id == spec.source_record_id + assert fact.source_cell_keys + assert set(fact.source_cell_keys) <= cell_keys + assert fact.source.source_sha256 == WORKBOOK_SHA256 + assert fact.aggregation.method == "sum" + assert fact.measure.unit == "count" + assert type(fact.value) is int + + +def test_regc_codes_and_names_match_official_2025_classification(population): + _package, _cells, facts = population + content, _ = _artifact("regional_council_2025_codes") + classification = json.loads(content) + assert not classification.get("exceededTransferLimit", False) + codes = { + row["attributes"]["REGC2025_V1_00"]: row["attributes"]["REGC2025_V1_00_NAME"] + for row in classification["features"] + } + assert len(codes) == 17 # Sixteen regions plus publisher code 99 (outside region). + regional_facts = [fact for fact in facts if fact.geography.level == "region"] + assert {fact.geography.id for fact in regional_facts} == codes.keys() - {"99"} + assert len(regional_facts) == 80 + assert all(fact.geography.vintage == "regc_2025" for fact in regional_facts) + assert all( + fact.geography.name == codes[fact.geography.id] for fact in regional_facts + ) + assert ( + codes["02"] == "Auckland" + ) # Workbook calls the same region "Auckland region". + + +def test_national_total_is_published_not_reconciled_from_regions(population, workbook): + _package, _cells, facts = population + totals = {fact.geography.id: fact for fact in facts if not fact.filters} + # Table 1 footnote 3 includes areas outside a region (for example Chatham). + assert totals["NZ"].value == workbook["Table 1"]["D25"].value == 5_324_700 + assert totals["NZ"].value != sum( + fact.value for code, fact in totals.items() if code != "NZ" + ) + assert "99" not in totals # Do not construct an outside-region residual. + assert totals["NZ"].geography.level == "country" + assert totals["NZ"].source_record_id.endswith(".all_ages.nz.population") + + +def test_age_universes_are_only_published_broad_bands_and_all_sexes(population): + _package, _cells, facts = population + assert {fact.filters.get("person.age_band", "all") for fact in facts} == { + "all", + *AGE_BANDS, + } + for fact in facts: + assert "person.sex" not in fact.filters + assert not any( + constraint.variable == "person.sex" for constraint in fact.constraints + ) + assert fact.entity.name == "person" + assert fact.entity.role == "resident_population" + if not fact.filters: + assert not fact.constraints + continue + _column, lower, upper = AGE_BANDS[fact.filters["person.age_band"]] + age_constraints = { + (item.operator, item.value, item.unit) + for item in fact.constraints + if item.variable == "person.age" + } + expected = {(">=", lower, "years")} + if upper is not None: + expected.add(("<", upper, "years")) + assert age_constraints == expected + + +@pytest.mark.parametrize("requested_year", [2023, 2025, 2026]) +def test_requested_build_year_cannot_uprate_or_relabel_pinned_vintage( + population, requested_year +): + package, cells, original = population + facts = package.build_facts(requested_year, cells=cells) + assert facts == original + assert package.artifact.artifact_year == 2025 + for fact in facts: + assert fact.period.type == "calendar_year" + assert fact.period.value == 2025 + assert fact.period_coverage.start_date == "2025-06-30" + assert fact.period_coverage.end_date == "2025-06-30" + assert fact.period_coverage.basis == "calendar" + assert ( + fact.period_coverage.source_period_label == "At 30 June 2025 (provisional)" + ) + assert fact.source.vintage == "stats_nz_subnational_population_2025_10_29" + assert fact.assertion == "observation" + assert fact.provenance_class == "census" + + +@pytest.mark.parametrize( + ("sheet", "address", "replacement"), + [ + ("Table 1", "D6", "2026 P"), + ("Table 1", "A7", "Auckland region"), + ("Table 1", "A4", "Year ended 30 June 2023–2025"), + ("Table 1", "A29", "2. Boundaries at 1 January 2026."), + ("Table 1", "A34", "F final"), + ("Table 3", "C13", "2024 P"), + ("Table 3", "A13", "Northland region"), + ("Table 3", "E6", "15–64"), + ("Table 3", "G6", "60+"), + ("Table 3", "A68", "2. Boundaries at 1 January 2026."), + ("Table 3", "A74", "F final"), + ], +) +def test_source_semantic_drift_fails_closed(population, sheet, address, replacement): + package, cells, _facts = population + mutated = [ + replace(cell, raw_value=replacement, display_value=replacement) + if (cell.sheet_name, cell.address) == (sheet, address) + else cell + for cell in cells + ] + with pytest.raises(ValueError, match="expected"): + package.build_facts(2025, cells=mutated) + + +def test_consumer_rows_preserve_source_only_scope_and_lineage(population): + _package, _cells, facts = population + rows = consumer_fact_rows(facts) + assert validate_consumer_fact_contract(facts).valid + assert len({row["aggregate_fact_key"] for row in rows}) == 85 + assert len({row["semantic_fact_key"] for row in rows}) == 85 + for row in rows: + assert row["concept_alignment"]["relation"] == "source_label" + notes = row["concept_alignment"]["evidence_notes"] + assert "not five-year or single-year ages" in notes + assert "there is no sex-specific split" in notes + assert "outside regions" in notes + assert row["source"]["raw_r2_key"].startswith("raw/nz/stats_nz/") + assert row["lineage"]["source_cell_keys"] + assert row["lineage"]["source_record_id"] + assert ( + not {"target", "solver", "calibration", "uprating_factor", "weight"} + & row.keys() + ) + + +def test_national_landing_page_is_evidence_not_an_age_table_package(): + content, _ = _artifact("national_population_estimates_2025") + page = html.unescape(content.decode()).replace("\\/", "/") + assert "single-year of age" in page + assert "Infoshare" in page + assert "2025-08-19 10:45:00" in page + assert "DocumentBlock" not in page + assert "stats-nz-national-population-estimates-2025" not in SOURCE_PACKAGE_ALIASES + assert not ( + REPO_ROOT + / "packages/stats_nz/national_population_estimates_2025/source_package.yaml" + ).exists() + + +def test_explicit_single_package_bundle_passes_all_source_gates(tmp_path): + report = build_bundle(tmp_path / "bundle", sources=[ALIAS], year=2025) + assert report.valid + rows = [ + json.loads(line) + for line in (tmp_path / "bundle/consumer_facts.jsonl").read_text().splitlines() + ] + coverage = build_bundle_coverage(rows) + assert coverage["fact_count"] == 85 + assert coverage["counts"]["by_source"] == {"stats_nz": 85} + assert coverage["counts"]["by_period"] == {"calendar_year:2025": 85} + assert coverage["counts"]["by_geography"]["country:NZ"] == 5 + assert len(coverage["counts"]["by_geography"]) == 17 + assert not coverage["duplicates"]["aggregate_fact_keys"] + assert not coverage["duplicates"]["semantic_fact_keys"]