starsim-data 2.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. starsim_data-2.0.0/.claude/skills/add-dataset/SKILL.md +81 -0
  2. starsim_data-2.0.0/.github/workflows/docs.yml +110 -0
  3. starsim_data-2.0.0/.github/workflows/publish.yml +45 -0
  4. starsim_data-2.0.0/.github/workflows/tests.yml +87 -0
  5. starsim_data-2.0.0/.gitignore +110 -0
  6. starsim_data-2.0.0/CHANGELOG.md +52 -0
  7. starsim_data-2.0.0/CLAUDE.md +108 -0
  8. starsim_data-2.0.0/CONTRIBUTING.md +47 -0
  9. starsim_data-2.0.0/LICENSE +35 -0
  10. starsim_data-2.0.0/MANIFEST.in +5 -0
  11. starsim_data-2.0.0/NOTICE.md +70 -0
  12. starsim_data-2.0.0/PKG-INFO +265 -0
  13. starsim_data-2.0.0/README.md +227 -0
  14. starsim_data-2.0.0/docs/01-getting-started.qmd +158 -0
  15. starsim_data-2.0.0/docs/02-tiers.qmd +83 -0
  16. starsim_data-2.0.0/docs/03-datasets.qmd +269 -0
  17. starsim_data-2.0.0/docs/04-locations.qmd +146 -0
  18. starsim_data-2.0.0/docs/05-spatial.qmd +217 -0
  19. starsim_data-2.0.0/docs/06-datasources.qmd +188 -0
  20. starsim_data-2.0.0/docs/07-related-projects.qmd +94 -0
  21. starsim_data-2.0.0/docs/08-architecture.qmd +182 -0
  22. starsim_data-2.0.0/docs/09-provenance.qmd +116 -0
  23. starsim_data-2.0.0/docs/10-building-the-data.qmd +128 -0
  24. starsim_data-2.0.0/docs/assets/favicon.ico +0 -0
  25. starsim_data-2.0.0/docs/assets/icon-dark.png +0 -0
  26. starsim_data-2.0.0/docs/assets/icon.png +0 -0
  27. starsim_data-2.0.0/docs/assets/logo-dark.png +0 -0
  28. starsim_data-2.0.0/docs/assets/logo.png +0 -0
  29. starsim_data-2.0.0/docs/assets/starsim-theme.html +71 -0
  30. starsim_data-2.0.0/great-docs.yml +83 -0
  31. starsim_data-2.0.0/licenses/LICENSE_EPYDEMIX +674 -0
  32. starsim_data-2.0.0/licenses/LICENSE_MISTRY2021 +201 -0
  33. starsim_data-2.0.0/licenses/LICENSE_PREM2017 +674 -0
  34. starsim_data-2.0.0/licenses/LICENSE_PREM2021 +674 -0
  35. starsim_data-2.0.0/plans/laser-init-merge.md +273 -0
  36. starsim_data-2.0.0/pyproject.toml +84 -0
  37. starsim_data-2.0.0/scripts/README.md +87 -0
  38. starsim_data-2.0.0/scripts/build_demographics.py +243 -0
  39. starsim_data-2.0.0/scripts/build_hdf5.py +297 -0
  40. starsim_data-2.0.0/scripts/build_locations.py +215 -0
  41. starsim_data-2.0.0/scripts/build_nodes.py +383 -0
  42. starsim_data-2.0.0/scripts/download_epydemix.py +89 -0
  43. starsim_data-2.0.0/scripts/download_wpp.py +160 -0
  44. starsim_data-2.0.0/scripts/provenance.py +49 -0
  45. starsim_data-2.0.0/scripts/publish.py +134 -0
  46. starsim_data-2.0.0/setup.cfg +4 -0
  47. starsim_data-2.0.0/starsim_data/__init__.py +57 -0
  48. starsim_data-2.0.0/starsim_data/cache.py +220 -0
  49. starsim_data-2.0.0/starsim_data/data/README.md +41 -0
  50. starsim_data-2.0.0/starsim_data/data/adjacency.h5 +0 -0
  51. starsim_data-2.0.0/starsim_data/data/demographics-2024.h5 +0 -0
  52. starsim_data-2.0.0/starsim_data/data/fetched.json +416 -0
  53. starsim_data-2.0.0/starsim_data/data/locations.csv +188 -0
  54. starsim_data-2.0.0/starsim_data/data/nodes.h5 +0 -0
  55. starsim_data-2.0.0/starsim_data/data/prem/prem2017.h5 +0 -0
  56. starsim_data-2.0.0/starsim_data/data/prem2021.h5 +0 -0
  57. starsim_data-2.0.0/starsim_data/fetching.py +422 -0
  58. starsim_data-2.0.0/starsim_data/loaders.py +289 -0
  59. starsim_data-2.0.0/starsim_data/locations.py +280 -0
  60. starsim_data-2.0.0/starsim_data/nodes.py +223 -0
  61. starsim_data-2.0.0/starsim_data/plotting.py +267 -0
  62. starsim_data-2.0.0/starsim_data/rasters.py +390 -0
  63. starsim_data-2.0.0/starsim_data/registry.py +366 -0
  64. starsim_data-2.0.0/starsim_data/settings.py +108 -0
  65. starsim_data-2.0.0/starsim_data/shapes.py +330 -0
  66. starsim_data-2.0.0/starsim_data/version.py +9 -0
  67. starsim_data-2.0.0/starsim_data.egg-info/PKG-INFO +265 -0
  68. starsim_data-2.0.0/starsim_data.egg-info/SOURCES.txt +84 -0
  69. starsim_data-2.0.0/starsim_data.egg-info/dependency_links.txt +1 -0
  70. starsim_data-2.0.0/starsim_data.egg-info/requires.txt +15 -0
  71. starsim_data-2.0.0/starsim_data.egg-info/top_level.txt +1 -0
  72. starsim_data-2.0.0/tests/README.md +34 -0
  73. starsim_data-2.0.0/tests/conftest.py +59 -0
  74. starsim_data-2.0.0/tests/run_tests +10 -0
  75. starsim_data-2.0.0/tests/test_cache.py +153 -0
  76. starsim_data-2.0.0/tests/test_demographics.py +98 -0
  77. starsim_data-2.0.0/tests/test_demographics_data.py +77 -0
  78. starsim_data-2.0.0/tests/test_docs.py +124 -0
  79. starsim_data-2.0.0/tests/test_fetching.py +213 -0
  80. starsim_data-2.0.0/tests/test_load.py +201 -0
  81. starsim_data-2.0.0/tests/test_locations.py +169 -0
  82. starsim_data-2.0.0/tests/test_nodes.py +197 -0
  83. starsim_data-2.0.0/tests/test_plotting.py +129 -0
  84. starsim_data-2.0.0/tests/test_provenance.py +111 -0
  85. starsim_data-2.0.0/tests/test_registry.py +116 -0
  86. starsim_data-2.0.0/tests/test_spatial.py +187 -0
@@ -0,0 +1,81 @@
1
+ ---
2
+ name: add-dataset
3
+ description: Add a new data source to starsim_data — another country's subnational data, a new contact source, populations, commuting or mobility flows, or any other dataset — by writing its download script and build step and wiring it into the registry, provenance, docs and tests. Use when asked to add, import or package data for a new location or source (e.g. "add Germany's districts", "import Canada's provinces from epydemix", "add the commuter matrix").
4
+ ---
5
+
6
+ # Add a dataset to starsim_data
7
+
8
+ A dataset here is three things: a **download script** that copies the upstream data into `raw_data/` as readable CSVs, a **build step** that turns those CSVs into a self-describing HDF5 store, and the **wiring** (license, docs, tests) that the repo's invariants require. The US states (`mistry2021_us`, `population_us`) are the worked example to copy: `scripts/download_epydemix.py`, and `build_population_us()` and the tables at the top of `scripts/build_hdf5.py`.
9
+
10
+ Read `CLAUDE.md` first: the tier invariant, the registry, and "Keeping the docs in step" all apply.
11
+
12
+ ## 1. Settle these before writing code
13
+
14
+ Ask Cliff about any of these you can't answer from the source itself. They decide everything after.
15
+
16
+ - **License.** Find the exact license of the data (not of the repo it sits in: epydemix-data is GPL-3.0 as a whole, but its Census numbers are public domain). If it can't be redistributed, it can't be a store here; it can only be fetched from upstream at use time, as GADM is (`redistributable=False`). Record the license as it will appear in `NOTICE.md`.
17
+ - **Tier.** Bundled (tier 0, in the wheel) only if it is small and most users need it; Cliff wants the wheel's size unchanged unless he agrees otherwise. Everything else is a fetched store (tier 1) in `fetched/`.
18
+ - **Location keys.** A subnational unit is keyed `<Country>:<Unit>`, and the key must be the unit's `dot_name` in the node table, so that `resolve()`, `load_nodes()` and the new data agree. Check with `ssd.load_nodes(country, level=1).dot_name`. Only levels 0 and 1 are bundled. For level 2 and finer, get the names from `ssd.fetch_nodes(country, level=2)` (needs `[spatial]`), and if the source's names don't match them (e.g. `Landkreis Harburg` against `Harburg`), build a crosswalk from codes rather than fuzzy-matching names. Commit the crosswalk as a CSV in `raw_data/`.
19
+ - **Layout.** Reuse one if it fits: `matrix` (square, by age), `population` and `mortality` (a year × sex × age array), `crude` (one value per year), `nodes`. A new shape, such as origin–destination flows, needs a new shaper in `starsim_data/loaders.py`, registered with `ssd.registry.register_layout()` and documented in `docs/08-architecture.qmd`. Return the same columns an existing dataset returns wherever the meaning is the same: `population_us` uses `population`'s `Time, Sex, AgeGrp, Value`, with `Sex='Both sexes'` because the source doesn't split by sex.
20
+ - **Name.** Lowercase, source then scope: `mistry2021_us`, `population_us`. Don't put new locations into a store that is already published (for example, adding states to `mistry2021`): it changes the store's bytes, and its pinned download, for everyone.
21
+ - **Upstream version.** Pin a commit, release or vintage, so that rerunning the download gives the same files.
22
+
23
+ ## 2. Download script: `scripts/download_<source>.py`
24
+
25
+ Model it on `scripts/download_epydemix.py`:
26
+
27
+ - A module docstring saying what it downloads, from where, into which files, and the command to run next.
28
+ - The upstream version pinned in a constant (`COMMIT = ...`), and URLs built from it.
29
+ - Output to `raw_data/`, as plain CSVs a person can read and diff: contact matrices as `raw_data/contact_matrices/<Country>__<Unit>/<source>_contacts_<kind>.csv` (headerless, square; the `__` stands for `:`, which isn't legal in a folder name everywhere), and anything else as one tidy CSV keyed by `location` (e.g. `raw_data/population_us.csv`: `location, age, population`).
30
+ - Check every location against the node table before writing anything, and raise, listing the ones that don't match.
31
+ - Any token goes in `scripts/.env` (git-ignored), as `download_wpp.py` does, and never in the code.
32
+ - Read every CSV with `keep_default_na=False` or the `csv` module (Namibia's ISO-2 code is `NA`).
33
+ - Keep downloading separate from building, so that the build and its `--check` run offline from the committed CSVs.
34
+
35
+ Tell Cliff how big the download and the raw CSVs will be before running it. Raw CSVs are committed, so 30 MB of them is 30 MB in git history.
36
+
37
+ ## 3. Build step
38
+
39
+ All fetched stores are listed in `scripts/build_hdf5.py`, which writes the manifest `starsim_data/data/fetched.json` from them. Add the new store there:
40
+
41
+ - Add `name` to `FETCHED_SOURCES`, and bump `RELEASE` to a new `data-*` tag: the new store is downloaded from that GitHub release, which Cliff creates with `scripts/publish.py` once the change is on `main`.
42
+ - `SOURCES[name]`: `source`, `source_url`, `via` (if it came through another project), `license`, `citation`, `note`. This is written into the store as its provenance.
43
+ - `DESCRIPTIONS[name]`: one line for `list_datasets()`.
44
+ - Contact matrices need nothing more than their CSVs and an `AGE_WIDTH` entry: sources are discovered from the file names.
45
+ - Anything else needs a `build_<name>(check=False)` function, like `build_population_us()`. It writes one dataset per location key and a root `spec` attribute for the layout, for example `dict(layout='population', years=[...], sexes=[...], ages=[...], age_col='AgeGrp', value_col='Value')`, plus `provenance` from `stamp()`. Under `check`, it reads each value back and compares it with its CSV. Call it from `main()` when its name is passed to `--fetched`.
46
+ - A bundled store goes into `starsim_data/data/` instead, needs an entry in `registry.DESCRIPTIONS`, and is discovered from its file. Bundling something new is Cliff's call (see Tier).
47
+
48
+ Build only the new store: `python scripts/build_hdf5.py --check --fetched <name>`. Never rebuild a published store, since that changes its bytes and breaks its pinned download. Every run also rewrites the bundled `prem2021.h5` and `prem/prem2017.h5`, with a new build time and the same data, so restore them afterwards with `git restore starsim_data/data/prem2021.h5 starsim_data/data/prem/prem2017.h5` unless their data really changed.
49
+
50
+ Levels are worked out from the keys (`United_States:Massachusetts` is level 1). `fetch()` reads any fetched store with no further code, and `load()` points to it from other datasets of the same layout ("it is in fetch(...)").
51
+
52
+ ## 4. Wiring
53
+
54
+ Everything below is required: the tests check some of it, and none of it updates itself.
55
+
56
+ - `NOTICE.md`: a section for a new source (or a bullet under the existing one for the same study), and the list of fetched stores in its introduction.
57
+ - `tests/test_provenance.py::test_licenses`: the new dataset and its license.
58
+ - Tests: one compact test of what's new, like `tests/test_load.py::test_us_states` (shape, schema, a sanity total, and the error that points to it). The offline suite reads fetched stores from `fetched/` through `tests/conftest.py`, so no network is needed. Update `tests/README.md` if a file's coverage changes.
59
+ - Docs: `docs/03-datasets.qmd` (the schema and an example; downloading examples must be static ```` ```python ```` blocks), `docs/06-datasources.qmd` (coverage, license, limitations), `docs/02-tiers.qmd` (the tier table), `docs/10-building-the-data.qmd`, and `docs/04-locations.qmd` if naming changes. Update the comparison table in `docs/07-related-projects.qmd` if coverage changes.
60
+ - `README.md` (the source table), `CHANGELOG.md` (one line), `raw_data/README.md`, `scripts/README.md`, and the commands in `CLAUDE.md`.
61
+
62
+ ## 5. Check
63
+
64
+ ```bash
65
+ python scripts/build_hdf5.py --check
66
+ pytest -m "not network"
67
+ pytest tests/test_docs.py
68
+ great-docs build # if Quarto is available
69
+ ```
70
+
71
+ Then try it the way a user would, including the errors: a misspelled unit, a unit the dataset lacks, and an argument that doesn't apply (it must be rejected, not ignored). Don't commit; leave everything for Cliff, and remind Cliff to create the new data release once the change is on `main`.
72
+
73
+ ## Worked sketch: Germany's districts (not built yet)
74
+
75
+ Requested for MEmilio users: the roughly 400 districts (Kreise), with populations by age group, centroids, and the commuter matrix between them. Verify every URL, table number and license below before relying on it.
76
+
77
+ - **Populations:** Destatis GENESIS-Online, table 12411-0017 (population by district and age group) or its successor. The API needs an account; put the token in `scripts/.env`. The license is probably Datenlizenz Deutschland – Namensnennung 2.0 (`dl-de/by-2-0`), which allows redistribution with attribution. Use the `population` layout with Germany's age groups, keyed `Germany:<Kreis>`.
78
+ - **Commuter flows:** the Federal Employment Agency's commuter statistics (Pendlerstatistik), one spreadsheet per state. MEmilio's `getCommuterMobility` (in `memilio.epidata`) shows the URLs and how to parse them. Check the agency's terms of reuse. This needs a new layout, e.g. `flows`, returning `origin, destination, value` for a location (`Germany`), from a sparse array stored as three columns.
79
+ - **Keys:** districts are identified by their AGS code (Amtlicher Gemeindeschlüssel). Get the level-2 `dot_name`s from `fetch_nodes('Germany', level=2)` and crosswalk AGS to `dot_name`, using codes where the boundary source has them and a committed crosswalk CSV where it doesn't.
80
+ - **Places:** `load_nodes('Germany', level=2)` needs level 2 in the bundled node table, which is a `scripts/build_nodes.py` change and a size decision for Cliff. Otherwise `fetch_nodes()` already gives centroids at level 2.
81
+ - **Size:** about 400 × 400 sparse flows and 400 × (6–21) age groups, so a fetched store of well under 1 MB.
@@ -0,0 +1,110 @@
1
+ name: CI Docs
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ pull_request:
8
+
9
+ jobs:
10
+ build-docs:
11
+ name: "Build Docs"
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v6
15
+ with:
16
+ fetch-depth: 0 # Full history for accurate page timestamps
17
+
18
+ - uses: actions/setup-python@v6
19
+ with:
20
+ python-version: "3.13"
21
+
22
+ - name: Install package and dependencies
23
+ run: |
24
+ python -m pip install --upgrade pip
25
+ python -m pip install -e ".[dev,docs]"
26
+ python -m pip install great-docs
27
+
28
+ - name: Set up Quarto
29
+ uses: quarto-dev/quarto-actions/setup@v2
30
+
31
+ - name: Build docs
32
+ run: great-docs build
33
+
34
+ - name: Save docs artifact
35
+ uses: actions/upload-artifact@v7
36
+ with:
37
+ name: docs-html
38
+ path: great-docs/_site
39
+ include-hidden-files: true
40
+
41
+ - name: Upload build timings
42
+ uses: actions/upload-artifact@v7
43
+ with:
44
+ name: build-timings
45
+ path: great-docs/_site/build-timings.json
46
+
47
+ publish-docs:
48
+ name: "Publish Docs"
49
+ runs-on: ubuntu-latest
50
+ needs: "build-docs"
51
+ if: github.ref == 'refs/heads/main'
52
+ permissions:
53
+ pages: write
54
+ id-token: write
55
+ environment:
56
+ name: github-pages
57
+ url: ${{ steps.deployment.outputs.page_url }}
58
+ steps:
59
+ - uses: actions/download-artifact@v7
60
+ with:
61
+ name: docs-html
62
+ path: great-docs/_site
63
+
64
+ - name: Upload Pages artifact
65
+ uses: actions/upload-pages-artifact@v5
66
+ with:
67
+ path: great-docs/_site
68
+ include-hidden-files: true
69
+
70
+ - name: Deploy to GitHub Pages
71
+ id: deployment
72
+ uses: actions/deploy-pages@v5
73
+
74
+ preview-docs:
75
+ name: "Preview Docs"
76
+ runs-on: ubuntu-latest
77
+ needs: "build-docs"
78
+ if: github.event_name == 'pull_request'
79
+ permissions:
80
+ deployments: write
81
+ pull-requests: write
82
+ steps:
83
+ - uses: actions/download-artifact@v7
84
+ with:
85
+ name: docs-html
86
+ path: great-docs/_site
87
+
88
+ # Start deployment
89
+ - name: Configure pull release name
90
+ if: ${{ github.event_name == 'pull_request' }}
91
+ run: |
92
+ echo "RELEASE_NAME=pr-${{ github.event.number }}" >> $GITHUB_ENV
93
+
94
+ - name: Configure branch release name
95
+ if: ${{ github.event_name != 'pull_request' }}
96
+ run: |
97
+ # use branch name, but replace slashes. E.g. feat/a -> feat-a
98
+ echo "RELEASE_NAME=${GITHUB_REF_NAME//\//-}" >> $GITHUB_ENV
99
+
100
+ # Deploy
101
+ - name: Create Github Deployment
102
+ uses: bobheadxi/deployments@v1
103
+ id: deployment
104
+ if: ${{ !github.event.pull_request.head.repo.fork }}
105
+ with:
106
+ step: start
107
+ token: ${{ secrets.GITHUB_TOKEN }}
108
+ env: ${{ env.RELEASE_NAME }}
109
+ ref: ${{ github.head_ref }}
110
+ logs: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
@@ -0,0 +1,45 @@
1
+ name: Publish
2
+
3
+ # Publishes the package to PyPI when a `v*` release is published (e.g. `v2.0.0`), by
4
+ # trusted publishing (no token; set up once on pypi.org). Make the release with
5
+ # `python scripts/publish.py <data tag>`, which also creates the data release the
6
+ # fetched stores are downloaded from. This refuses to publish a package whose fetched
7
+ # stores can't be downloaded.
8
+
9
+ on:
10
+ release:
11
+ types: [published]
12
+
13
+ jobs:
14
+ pypi:
15
+ name: Publish to PyPI
16
+ if: startsWith(github.event.release.tag_name, 'v')
17
+ runs-on: ubuntu-latest
18
+ environment: pypi
19
+ permissions:
20
+ id-token: write # trusted publishing
21
+ steps:
22
+ - name: Checkout sources
23
+ uses: actions/checkout@v7
24
+ - uses: actions/setup-python@master
25
+ with:
26
+ python-version: '3.14'
27
+ - name: Check the version matches the tag, and the fetched stores can be downloaded
28
+ run: |
29
+ python - <<'EOF'
30
+ import json, pathlib, re, urllib.request
31
+ tag = '${{ github.event.release.tag_name }}'
32
+ version = re.search(r"__version__ = '(.+)'", pathlib.Path('starsim_data/version.py').read_text())[1]
33
+ assert tag == f'v{version}', f'Tag {tag} does not match version {version}'
34
+ manifest = json.loads(pathlib.Path('starsim_data/data/fetched.json').read_text())
35
+ for name, entry in manifest.items():
36
+ with urllib.request.urlopen(urllib.request.Request(entry['url'], method='HEAD')) as response:
37
+ assert response.status == 200, f'{name}: {entry["url"]} returned {response.status}'
38
+ print(f'Version {version}; {len(manifest)} fetched stores are downloadable')
39
+ EOF
40
+ - name: Build
41
+ run: |
42
+ pip install build
43
+ python -m build
44
+ - name: Publish
45
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,87 @@
1
+ name: starsim_data CI
2
+
3
+ on:
4
+ workflow_call:
5
+ workflow_dispatch:
6
+ push:
7
+ branches:
8
+ - main
9
+ pull_request:
10
+
11
+ jobs:
12
+ install_and_test:
13
+ name: Tests
14
+ timeout-minutes: 10
15
+ strategy:
16
+ fail-fast: false
17
+ max-parallel: 8
18
+ matrix:
19
+ os: ['ubuntu-latest']
20
+ python-version: ['3.14']
21
+ runs-on: ${{ matrix.os }}
22
+ steps:
23
+ - name: Checkout sources
24
+ uses: actions/checkout@v7
25
+ - uses: actions/setup-python@master
26
+ with:
27
+ python-version: ${{ matrix.python-version }}
28
+ architecture: x64
29
+ - name: Install starsim_data
30
+ run: pip install -e .[dev]
31
+ - name: Run tests
32
+ # -m "not network" so the suite stays offline by default; the bundled data
33
+ # makes every core test hermetic, and that property is worth protecting.
34
+ run: pytest tests -v -m "not network" --durations=0 --junitxml=test-results.xml
35
+ - name: Publish test report
36
+ uses: mikepenz/action-junit-report@v6
37
+ if: always()
38
+ with:
39
+ report_paths: './test-results.xml'
40
+ check_name: 'Test Report (Python ${{ matrix.python-version }})'
41
+
42
+ check_data_is_reproducible:
43
+ name: Data rebuild is reproducible
44
+ timeout-minutes: 10
45
+ runs-on: ubuntu-latest
46
+ steps:
47
+ - name: Checkout sources
48
+ uses: actions/checkout@v7
49
+ - uses: actions/setup-python@master
50
+ with:
51
+ python-version: '3.14'
52
+ architecture: x64
53
+ - name: Install starsim_data
54
+ run: pip install -e .[dev]
55
+ # The bundled .h5 stores are build artifacts committed to the repo, so they can
56
+ # drift from the raw data they claim to be built from. Rebuilding and validating
57
+ # here is what keeps `raw_data/` the actual source of truth.
58
+ - name: Rebuild contact matrices and validate against the CSVs
59
+ run: python scripts/build_hdf5.py --check
60
+ - name: Rebuild demographics and validate via the loader
61
+ run: python scripts/build_demographics.py --check
62
+ - name: Check locations.csv is up to date
63
+ run: python scripts/build_locations.py --check
64
+ - name: Tests still pass against the rebuilt data
65
+ run: pytest tests -q -m "not network"
66
+
67
+ # The node table is deliberately left out of the job above: rebuilding it pulls ~420 MB
68
+ # of boundary data from GitHub, which is too much to spend on every pull request. Run it
69
+ # by hand when the boundary sources change.
70
+ check_nodes_are_reproducible:
71
+ name: Node table rebuild is reproducible
72
+ if: github.event_name == 'workflow_dispatch'
73
+ timeout-minutes: 30
74
+ runs-on: ubuntu-latest
75
+ steps:
76
+ - name: Checkout sources
77
+ uses: actions/checkout@v7
78
+ - uses: actions/setup-python@master
79
+ with:
80
+ python-version: '3.14'
81
+ architecture: x64
82
+ - name: Install starsim_data
83
+ run: pip install -e .[dev,spatial]
84
+ - name: Rebuild the node table and validate via the loader
85
+ run: python scripts/build_nodes.py --check
86
+ - name: Tests still pass against the rebuilt node table
87
+ run: pytest tests -q -m "not network"
@@ -0,0 +1,110 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # Installer logs
30
+ pip-log.txt
31
+ pip-delete-this-directory.txt
32
+
33
+ # Unit test / coverage reports
34
+ htmlcov/
35
+ .tox/
36
+ .nox/
37
+ .coverage
38
+ .coverage.*
39
+ .cache
40
+ nosetests.xml
41
+ coverage.xml
42
+ *.cover
43
+ *.py,cover
44
+ .hypothesis/
45
+ .pytest_cache/
46
+ cover/
47
+
48
+ # Environments
49
+ .env
50
+ .venv
51
+ env/
52
+ venv/
53
+ ENV/
54
+ env.bak/
55
+ venv.bak/
56
+
57
+ # Jupyter Notebook
58
+ .ipynb_checkpoints
59
+
60
+ # IPython
61
+ profile_default/
62
+ ipython_config.py
63
+
64
+ # pyenv
65
+ .python-version
66
+
67
+ # mypy / ruff / type checkers
68
+ .mypy_cache/
69
+ .dmypy.json
70
+ dmypy.json
71
+ .ruff_cache/
72
+
73
+ # IDEs
74
+ .idea/
75
+ .vscode/
76
+ *.swp
77
+ *.swo
78
+
79
+ # OS
80
+ .DS_Store
81
+ Thumbs.db
82
+
83
+ # Build artifacts (HDF5 data stores are generated from the CSVs; not committed)
84
+ *.h5
85
+ *.hdf5
86
+
87
+ # ...except the bundled national-tier stores, which ship inside the package.
88
+ # These are small (~MBs), built by scripts/build_hdf5.py, and committed so that
89
+ # `pip install` is self-contained. Larger subnational tiers stay uncommitted.
90
+ !starsim_data/data/*.h5
91
+ !starsim_data/data/prem/*.h5
92
+
93
+ # ...and the contact stores that are fetched on demand rather than bundled: committed
94
+ # here, outside the package, and downloaded by fetch() from a pinned commit.
95
+ !fetched/*.h5
96
+
97
+ # UN WPP demographics source. The normalized CSVs (raw_data/demographics/wpp_*.csv)
98
+ # and README are tracked as the demographic source of truth; only the bulky raw
99
+ # downloads (xlsx/gz/zip) and logs are scratch and ignored.
100
+ raw_data/demographics/*.xlsx
101
+ raw_data/demographics/*.csv.gz
102
+ raw_data/demographics/*.zip
103
+ raw_data/demographics/*.log
104
+
105
+ # great-docs build directory (ephemeral; great-docs.yml is the committed source)
106
+ great-docs/
107
+
108
+ # Quarto's execution cache. Deliberately not committed: letting CI execute the code cells
109
+ # afresh means the docs build also checks that the documented examples still run.
110
+ _freeze/
@@ -0,0 +1,52 @@
1
+ # Changelog
2
+
3
+ All notable changes to `starsim_data` are recorded here. Versions follow [semantic versioning](https://semver.org); the *data* is versioned too, so a change to a bundled dataset is a release, not an amendment.
4
+
5
+ ## 2.0.0 (2026-10-22)
6
+
7
+ - **Contact matrices for 187 locations by default.** `ssd.load('kenya')` returns the contact matrices of Prem et al. 2021 (`prem2021`), and for the ten locations that study left out (Japan, Australia, Haiti and seven others) those of Prem et al. 2017 (`prem2017`). Pass `which='prem2021'` for that study alone.
8
+ - **A smaller package.** The other contact sources, `prem2017` and `mistry2021`, are downloaded the first time they are used, e.g. `ssd.fetch('france', which='mistry2021')`, rather than shipped with the package.
9
+ - **Subnational populations and borders, offline.** `ssd.load_nodes('kenya', level=1)` now gives the population of every state or province (from WorldPop, 2020), and `ssd.adjacency('kenya', level=1)` says which of them share a border.
10
+ - **Finer levels on request.** `ssd.fetch_nodes('kenya', level=2)` builds the same table (centroids, areas, populations and borders) at any level, e.g. Kenya's 290 sub-counties.
11
+ - **US states.** `ssd.fetch('United States:Massachusetts', which='mistry2021_us')` gives a state's contact matrices (Mistry et al. 2021), and `which='population_us'` its population by single year of age (US Census Bureau, 2023). A subnational unit is named `'<country>:<unit>'`, as in the node table.
12
+ - `fetch()` takes `kind=` for the contact matrices, and rejects arguments that do not apply.
13
+ - The fetched stores are downloaded from a GitHub release rather than from raw.githubusercontent.com, which rate-limits heavily.
14
+ - Requires Sciris 3.5.2 or later.
15
+
16
+ ## 1.0.0 (2026-08-08)
17
+
18
+ Merged with the [laser-init](https://github.com/laser-base/laser-init) project with additional features for geospatial modeling, name resolution, etc.
19
+
20
+ ### Resolution and provenance
21
+
22
+ - **Location resolution.** Locations can now be given as an ISO-2 or ISO-3 code, an official name, a translation, or a common abbreviation: `ssd.load('USA', ...)`, `ssd.load('KEN', ...)`, `ssd.load("Côte d'Ivoire", ...)` and `ssd.load('DRC', ...)` all work. Matching is exact against a table generated into `data/locations.csv`; fuzzy matching (via `sc.suggest`) is used only to suggest alternatives in the error message when a lookup fails, never to resolve one.
23
+ - `ssd.resolve()`, `ssd.location_info()` and `ssd.list_all_locations()` for working with the location table directly.
24
+ - `ssd.parse_level()`, accepting `'admin1'` / `'ADM2'` / `'3'` / `2`. Only level 0 has data today; this fixes the vocabulary before subnational data arrives.
25
+ - **Provenance.** Every bundled store now records its source, URL, license, citation and build details, written into the HDF5 at build time so it cannot be separated from the data. Read it with `ssd.provenance(which)`, or from `df.attrs['provenance']` on any loaded frame. Tests assert that the licenses recorded in the data match the ones asserted in `NOTICE.md`.
26
+ - **Revision-versioned demographics.** UN WPP data is now stored as `demographics-<revision>.h5`. `load()` uses the newest bundled revision by default; `load(..., revision=2024)` pins one, and `ssd.list_revisions()` lists them. A future WPP release becomes an additive rebuild rather than a silent change to data already in use.
27
+ - `iso3`, `official_name` and `aliases` columns in `data/locations.csv`, generated by the new `scripts/build_locations.py` from `pycountry` (a build-time dependency only).
28
+ - CI (`.github/workflows/tests.yml`), pytest markers (`network`, `slow`, `integration`), and this changelog. CI additionally rebuilds every store from `raw_data/` and re-runs the tests against the result, so the committed build artifacts cannot drift from their sources.
29
+
30
+ ### Data loading
31
+
32
+ - **`fetch()` — the data that is deliberately not bundled.** `load()` is still instant, offline and frozen; `fetch()` closes the three gaps the README has always named: annual resolution instead of decadal, complete life tables (`qx`, `lx`, `ex`, ...) instead of just `mx`, and other projection variants instead of Medium only. It reads the UN's public bulk CSVs — no API token, unlike the Data Portal — and caches them. `ssd.fetch('kenya', which='deaths', years=(2000, 2025))`, `ssd.fetch('kenya', which='life_table', years=2020)`, `ssd.fetch('kenya', which='births', variant='High')`. For the four series that are also bundled it returns exactly the schema `load()` returns, verified against the bundled data; `life_table` and `indicators` are fetch-only. See `ssd.list_fetchable()`.
33
+ - **A download cache**, with a provenance sidecar per file recording its URL, timestamp, size and SHA-256. `ssd.cache_dir()`, `ssd.cache_info()`, `ssd.cache_provenance()`, `ssd.clear_cache()`. The location defaults to a platform-appropriate directory and is overridable via `ssd.options.cache_dir` or `STARSIM_DATA_CACHE_DIR`.
34
+ - **`ssd.options`**, following Starsim's `ss.options` conventions, with `set()`, `reset()`, `help()` and a `context()` manager. Settings are read from the environment but acted on lazily: importing the package still creates no directories, opens no files and emits no warnings.
35
+ - **`plot()` — validation figures.** `ssd.plot('kenya')` draws a four-panel summary (age pyramid, crude birth rate, mortality curve, contact matrix); `ssd.plot('kenya', which='population')` draws one panel. The point is to catch the wrong country or the wrong units before the data reaches a simulation.
36
+ - **A dataset registry** (`ssd.registry`) holding a descriptor per dataset — layout, tier, admin level, license, revisions, location count — with public `register_dataset()` and `register_layout()` hooks. `ssd.list_datasets(detailed=True)` returns it as a dataframe, and `list_datasets(tier=0)` selects just the bundled data.
37
+ - **Tiers.** Datasets now declare whether they are bundled in the wheel (0), a downloaded release asset (1) or fetched from upstream (2). `load()` refuses anything above tier 0 and points at `fetch()`, so its "never touches the network" guarantee is now enforced by the code rather than by convention.
38
+
39
+ ### Nodes and distances
40
+
41
+ - **The node table** (`ssd.load_nodes`), bundled. For every administrative unit at levels 0 and 1: `nodeid`, `location`, `iso3`, `level`, `name`, `pcode`, `dot_name`, `parent`, `lat`, `lon`, `area_km2`, `population`, `source`. 3,254 nodes across 184 countries in 1.4 MB, readable with no geospatial dependencies — which settles the question the plan left open: it bundles rather than shipping as a release asset. Built by `scripts/build_nodes.py` from UNOCHA COD-AB where it exists (the source of the p-codes, ~110 countries) and geoBoundaries CGAZ v6.0.0 elsewhere; the two disagree in places about how a country subdivides, so every row records which source it came from. `population` is national-only by design.
42
+ - **`ssd.distances()`** — the great-circle distance matrix between node centroids, which with `load_nodes()` gives a gravity model its `P_i`, `P_j` and `D_ij`. The gravity formulation itself stays in the model.
43
+ - **`ssd.fetch_shapes()`** — administrative boundaries with full geometry, from UNOCHA, geoBoundaries or GADM, normalized onto one schema and joining to the node table on `pcode`. `source=None` tries UNOCHA then geoBoundaries. **GADM is never reached automatically**: it is non-commercial only, so it must be named explicitly, and the constraint is enforced as data (`ShapeSource.redistributable`) rather than remembered — the node-table build refuses any source that sets it False.
44
+ - **`ssd.fetch_raster()`, `ssd.aggregate_raster()`, `ssd.fetch_nodes()`** — WorldPop 1 km gridded population, and summing it inside each administrative unit. This is the step that turns a boundary file into a metapopulation. `ssd.rescale_to_national()` reconciles WorldPop's modeled totals with UN WPP's (they differ by about 1.2% for Kenya 2020).
45
+ - **`ssd.choropleth()`**, and a `nodes` panel for `plot()` that draws centroids without needing any geospatial dependency.
46
+ - **`[spatial]` extra** (`geopandas`, `pyogrio`, `rastertoolkit`), imported lazily: `import starsim_data` works without them, and only the spatial functions raise if they are missing.
47
+ - **A documentation site**, built with [great-docs](https://posit-dev.github.io/great-docs/): ten narrative pages in `docs/` covering the tier model, the schema of every dataset, location resolution, the spatial tier, the source comparison, how this package relates to [epydemix-data](https://github.com/epistorm/epydemix-data), [laser-init](https://github.com/laser-base/laser-init) and the other projects distributing the same data, the internal architecture, provenance, and how the bundled files are generated — plus an API reference generated from the docstrings. Code examples are executed at build time, so the frames and figures shown are the package's actual output. Both the sidebar and the API reference are built by great-docs itself — from the pages' `guide-section` frontmatter and from the public API — so neither can fall out of step with the code; the only hand-written styling is three overrides great-docs has no setting for (figure scaling, navbar centering, and the Starsim footer); `.github/workflows/docs.yml` builds and deploys it. NOTICE.md gains entries for UNOCHA, geoBoundaries, WorldPop and GADM.
48
+
49
+
50
+ ## Version 0.1.0
51
+
52
+ Initial release: UN WPP demographics and `prem2017` / `prem2021` / `mistry2021` contact matrices, bundled as HDF5 and read with `load()`.
@@ -0,0 +1,108 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## What this is
6
+
7
+ `starsim_data` distributes UN WPP demographics, contact matrices, and administrative geography for [Starsim](https://starsim.org) models. It returns dataframes and nothing else — no model code, no simulation logic. See `docs/07-related-projects.qmd` for where that line is drawn and why.
8
+
9
+ ## Commands
10
+
11
+ ```bash
12
+ pip install -e ".[dev]" # runtime + pytest + pycountry + great-docs
13
+ pip install -e ".[dev,spatial]" # also geopandas/pyogrio/rastertoolkit, needed by build_nodes.py
14
+
15
+ pytest # the offline suite
16
+ pytest -m "not network" # what CI runs
17
+ pytest -m network # also hits UN/WorldPop servers (a few hundred MB)
18
+ pytest tests/test_load.py -k matrix # one file, one pattern
19
+ pytest tests/test_load.py::test_matrix
20
+
21
+ great-docs build # -> great-docs/_site/ (needs Quarto)
22
+ great-docs preview # live reload on localhost:3000
23
+ ```
24
+
25
+ Markers are declared in `pyproject.toml`: `network`, `slow`, `integration`, `spatial` (the last skips automatically without the extra). `tests/conftest.py` redirects the cache to a temp dir so tests never touch the user's real one; set `STARSIM_DATA_CACHE_DIR` to reuse downloads across network-test runs.
26
+
27
+ ### Rebuilding the bundled data
28
+
29
+ The `.h5` files in `starsim_data/data/` are **committed build artifacts**, generated from `raw_data/` by `scripts/`. Never hand-edit them. Every script takes `--check` (validate without writing), which is what CI runs to stop the artifacts drifting from their sources:
30
+
31
+ ```bash
32
+ python scripts/build_locations.py --check # derived columns of locations.csv, from pycountry
33
+ python scripts/build_hdf5.py --check # prem2021.h5, prem/prem2017.h5 and fetched.json, from raw_data/contact_matrices/ (--fetched also rebuilds fetched/*.h5)
34
+ python scripts/build_demographics.py --check # demographics, from raw_data/demographics/
35
+ python scripts/build_nodes.py --check # node table; ~1.4 GB cold, [spatial] required
36
+ python scripts/build_nodes.py --limit 5 # a quick subset while developing
37
+ python scripts/download_wpp.py # refresh raw_data/demographics/ (needs a token in scripts/.env)
38
+ python scripts/download_epydemix.py # refresh the US states in raw_data/ (then build_hdf5.py --fetched mistry2021_us population_us)
39
+ ```
40
+
41
+ `build_nodes.py` is excluded from the per-PR CI job because of its size; it has a `workflow_dispatch`-only job in `.github/workflows/tests.yml`.
42
+
43
+ ## Architecture
44
+
45
+ ### The tier invariant
46
+
47
+ Every function belongs to exactly one tier, and this is the design constraint the rest of the package is arranged around:
48
+
49
+ - **Tier 0** — `load()`, `load_nodes()`, `distances()`. Reads only what is inside the wheel. **Never touches the network**, so it is instant, offline and byte-identical for everyone on a given version.
50
+ - **Tier 1** — `fetch()`, `fetch_shapes()`, `fetch_nodes()`, `fetch_raster()`. Downloads and caches, with a provenance sidecar per file.
51
+
52
+ Tier is a field on the dataset descriptor and `load()` enforces it: asking `load()` for a tier-1 dataset raises and names `fetch()` instead. Do not add a network call to a tier-0 path — CI runs `-m "not network"` and `tests/test_docs.py` fails the build if an executed docs cell would fetch.
53
+
54
+ ### Registry-driven dispatch
55
+
56
+ `registry.py` holds two tables, and nearly every extension goes through one of them rather than through an `if` branch:
57
+
58
+ - `DATASETS` — name → `Dataset` descriptor (layout, tier, levels, license, revisions, location count, store). Bundled descriptors are **discovered** by opening each `.h5` and reading its root attributes, so the stores are self-describing: adding a new contact source is dropping CSVs in `raw_data/` and re-running `build_hdf5.py`. The contact sources that are fetched rather than bundled (`prem2017`, `mistry2021`, in `fetched/` outside the wheel) are described by the manifest `data/fetched.json`, which `build_hdf5.py` writes with each store's URL (a GitHub release asset), SHA-256, locations and provenance. The default contact matrices, `prem`, are `prem2021` plus the `prem2017` matrices for the ten locations it lacks (bundled in `data/prem/prem2017.h5`, a subfolder so discovery doesn't list it as its own dataset); discovery describes them from both stores, with a `loader` that reads whichever has the location.
59
+ - `LAYOUTS` — layout name → the shaper that turns a stored array into a dataframe (`matrix`, `crude`, `mortality`, `population`, `nodes`).
60
+
61
+ `load()` resolves the descriptor, picks the source for the revision, resolves the location scoped to that dataset, then hands off to the shaper with an `opts` dict carrying layout-specific arguments (`kind` for matrices, `level` for nodes). An argument a layout has no use for is **rejected, not ignored**. `register_dataset()`, `register_layout()` and `plotting.PLOTTERS` are the public hooks; a descriptor with a `loader` makes `load()` delegate entirely, which is how downstream code adds data this package doesn't ship.
62
+
63
+ Descriptor discovery is cached — call `registry.refresh()` if the files change inside a running process (e.g. a test that rebuilds a store).
64
+
65
+ ### Import conventions
66
+
67
+ Follow Starsim's convention: **`from` imports live only in `__init__.py`**. Every other module does `import starsim_data as ssd` and reaches everything as `ssd.load`, `ssd.registry.datasets()`, so modules can refer to each other freely without cycles. The import order in `__init__.py` is load-bearing: `registry` must exist before `loaders` (which registers layouts) and `fetching` (which registers tier-1 datasets).
68
+
69
+ Importing the package must stay side-effect free — no directories created, no files opened, no warnings. `options` reads the environment at import but acts on it lazily; the cache dir is resolved and created only when something is written. The `[spatial]` imports are lazy for the same reason: `import starsim_data` works without geopandas, and only the spatial functions raise.
70
+
71
+ ### Invariants that are enforced as data, not as memory
72
+
73
+ - **Provenance travels with the data.** `scripts/provenance.py` stamps source/URL/license/citation/build details into each store at build time; `load()` and `fetch()` put the same record in `df.attrs['provenance']`. `tests/test_provenance.py` asserts the licenses recorded in the stores match `NOTICE.md`.
74
+ - **GADM is never redistributed.** Its `ShapeSource` sets `redistributable=False`, `fetch_shapes()` leaves it out of the UNOCHA→geoBoundaries fallback chain, and `build_nodes.py` refuses any source with that flag. Keep the constraint on the descriptor rather than in a comment or a conditional.
75
+ - **Demographic stores are versioned by WPP revision**, as `demographics-<revision>.h5`. A new UN release is an additive rebuild, never an edit to data already in use.
76
+ - **Namibia's ISO-2 code is the string `NA`.** Any CSV read must pass `keep_default_na=False` or use the `csv` module, or pandas turns Namibia into a missing value. There is a regression test for it in `tests/test_locations.py`.
77
+ - **Location resolution is exact.** Fuzzy matching (`sc.suggest`) only builds the "did you mean" error message; it never resolves a name.
78
+
79
+ ### Licensing
80
+
81
+ Code is MIT; each bundled dataset keeps its own upstream license (GPL-3.0 for the Prem matrices, Apache-2.0 for `mistry2021`, CC BY 3.0 IGO for WPP). `NOTICE.md` is the operative attribution text and `licenses/` holds the full upstream texts. If you add or change a data source, update `NOTICE.md` — a test checks it against what the stores record.
82
+
83
+ ## Documentation
84
+
85
+ Narrative pages are `.qmd` files in `docs/`, built by [great-docs](https://posit-dev.github.io/great-docs/). There is no navigation or API-reference config to maintain: the sidebar comes from each page's `guide-section` frontmatter and the reference is discovered from the public API. Rules `tests/test_docs.py` enforces:
86
+
87
+ - Every page needs `title` and a `guide-section` from `Getting started` / `The data` / `Internals`, and sections must stay **contiguous in filename order** — inserting a page in an earlier section means renumbering the later ones.
88
+ - Renumbering is safe: site URLs strip the numeric prefix (`docs/09-provenance.qmd` → `user-guide/provenance.html`), so cross-page links are written as bare `provenance.qmd`.
89
+ - Only the landing page (`01-getting-started.qmd`) prefixes links with `user-guide/`.
90
+ - Executed `{python}` cells must not fetch anything; write downloading examples as static ` ```python ` blocks.
91
+
92
+ `docs/08-architecture.qmd` is the long-form version of the Architecture section above, and stays in step with the code by executing against it.
93
+
94
+ ### Keeping the docs in step
95
+
96
+ Docs are part of a change, not a follow-up to it. Before treating any change as done, check what it obliges you to update.
97
+
98
+ **When you add or remove a docs page:**
99
+
100
+ 1. Give it `title` and a `guide-section`, then **renumber siblings so the sections stay contiguous** in filename order — a new `The data` page means bumping every `Internals` page. Use `git mv`; site URLs strip the numeric prefix, so no links break.
101
+ 2. **Do not add navigation config to `great-docs.yml`.** A new page needs no entry there, by design — the sidebar is built from `guide-section` and the reference from the public API, so neither can fall out of step with the code. `test_docs.py::test_config` fails if `sections` or a hand-written `reference.contents` shows up. Wanting to edit that file is usually a sign something else is wrong. The one real exception: introducing a *new* `guide-section` means adding it to `SECTIONS` in `tests/test_docs.py`, which defines both the allowed values and their order.
102
+ 3. Link it from at least one sibling page — otherwise it is reachable only from the sidebar.
103
+ 4. Update the hand-maintained places that count or list pages: the "N narrative pages" sentence in `CHANGELOG.md`, and the per-file table in `tests/README.md` when tests change.
104
+ 5. Run `pytest tests/test_docs.py`, then `great-docs build` if Quarto is available. The tests catch frontmatter, dead cross-links and accidental network use; only a real build catches a cell that raises.
105
+
106
+ **When code changes:** the API reference regenerates itself, but prose does not. A new dataset, layout or source touches `docs/03-datasets.qmd` (schema), `docs/06-datasources.qmd` (coverage, licensing, limitations) and `NOTICE.md`; a new extension point touches `docs/08-architecture.qmd`. Docs cells execute against the package at build time, so a stale *example* fails docs CI — but a stale *sentence* will not, and nothing will catch it for you.
107
+
108
+ README links to the built site are `https://starsimhub.github.io/starsim_data/user-guide/<name>.html`.