starsim-data 2.0.0__py3-none-any.whl

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.
@@ -0,0 +1,57 @@
1
+ """
2
+ starsim_data: national-level demographic data and contact matrices for disease
3
+ modeling with Starsim.
4
+
5
+ This package bundles UN World Population Prospects demographics (births, deaths,
6
+ migration, population) and a trimmed, reorganized subset of the contact-matrix data
7
+ from epydemix-data (https://github.com/epistorm/epydemix-data), national-level (level
8
+ 0) only. See NOTICE.md for per-source data licenses and attributions.
9
+
10
+ The data ships bundled as HDF5 under `starsim_data/data/` (e.g. `prem2021.h5`,
11
+ `demographics-2024.h5`). `load` returns a `sciris.dataframe` (a drop-in replacement for
12
+ a pandas `DataFrame`); for contact matrices the rows and columns are indexed by the
13
+ start age of each bin:
14
+
15
+ ```python
16
+ import starsim_data as ssd
17
+ M = ssd.load(location='kenya') # square, age-indexed (0, 5, ..., 75)
18
+ M.loc[25, 30] # contacts between the 25-29 and 30-34 bins
19
+ ```
20
+
21
+ Locations can be given as a name, ISO-2/ISO-3 code, official name or alias -- `'USA'`,
22
+ `'US'`, `'united states'` and `'United_States'` all resolve to the same data. Every
23
+ dataset records where it came from; see `provenance()`.
24
+
25
+ `load` never touches the network. What is not bundled -- annual series, complete life
26
+ tables, other projection variants, the full `prem2017` and `mistry2021` contact matrices
27
+ -- is available from `fetch()`, which downloads and caches; see `cache_info()` and
28
+ `options`. `plot()` draws the data for a quick check before it goes into a model.
29
+ """
30
+
31
+ __version__ = '0.4.0'
32
+
33
+ # This is the one module that uses `from` imports, to assemble the public namespace.
34
+ # Everywhere else in the package, follow Starsim's convention: `import starsim_data as
35
+ # ssd` and reach things as `ssd.load`, `ssd.registry.datasets()` and so on. The order
36
+ # below matters, because a few modules register themselves on import: `registry` has to
37
+ # exist before `loaders` adds its layouts, and before `fetching` adds its datasets.
38
+ from .settings import options
39
+ from . import registry
40
+ from .registry import register_dataset, register_layout
41
+ from .locations import resolve, location_info, list_all_locations, parse_level
42
+ from .loaders import load, list_datasets, list_locations, list_revisions, provenance
43
+ from .cache import cache_dir, cache_info, cache_provenance, cached_download, clear_cache
44
+ from .fetching import fetch, list_fetchable
45
+ from .plotting import plot, choropleth
46
+ from .nodes import load_nodes, distances, adjacency, node_name
47
+ from .shapes import fetch_shapes, list_shape_sources
48
+ from .rasters import fetch_nodes, fetch_raster, aggregate_raster, rescale_to_national
49
+
50
+ __all__ = ['load', 'fetch', 'plot', 'choropleth', 'provenance', 'options', 'registry',
51
+ 'load_nodes', 'distances', 'adjacency', 'node_name',
52
+ 'fetch_shapes', 'list_shape_sources',
53
+ 'fetch_nodes', 'fetch_raster', 'aggregate_raster', 'rescale_to_national',
54
+ 'list_datasets', 'list_locations', 'list_revisions', 'list_fetchable',
55
+ 'resolve', 'location_info', 'list_all_locations', 'parse_level',
56
+ 'register_dataset', 'register_layout',
57
+ 'cache_dir', 'cache_info', 'cache_provenance', 'cached_download', 'clear_cache']
starsim_data/cache.py ADDED
@@ -0,0 +1,220 @@
1
+ """
2
+ Download cache for data that is too big to bundle.
3
+
4
+ Tier-0 data ships inside the wheel and `load()` never touches the network. Anything
5
+ larger -- the UN WPP bulk files behind `fetch()`, and later the spatial sources -- is
6
+ downloaded once into a local cache and reused from then on.
7
+
8
+ Every cached file gets a provenance sidecar (`<file>.provenance.json`) recording the URL
9
+ it came from, when it was downloaded, its size and its SHA-256. That is `laser-init`'s
10
+ idea, and it is the right one for fetched files: unlike bundled data, where provenance
11
+ is written into the HDF5 itself, a cache entry is an ordinary file on disk and a sidecar
12
+ is the only place the lineage can live.
13
+
14
+ ```python
15
+ import starsim_data as ssd
16
+ ssd.cache_dir() # where things are cached
17
+ ssd.cache_info() # what is cached, from where, and when it was fetched
18
+ ssd.clear_cache() # delete it all
19
+ ```
20
+ """
21
+
22
+ import os
23
+ import hashlib
24
+ import platform
25
+ import shutil
26
+ import sciris as sc
27
+ import starsim_data as ssd
28
+
29
+ __all__ = ['cache_dir', 'cache_info', 'cache_provenance', 'cached_download', 'clear_cache']
30
+
31
+ SIDECAR_SUFFIX = '.provenance.json'
32
+
33
+ # Read in chunks rather than all at once: cached WPP files run to a few hundred MB.
34
+ HASH_CHUNK = 1 << 20
35
+
36
+
37
+ def _default_cache_dir():
38
+ """Return the platform-appropriate cache location, without creating it."""
39
+ home = sc.path('~').expanduser()
40
+ system = platform.system()
41
+ if system == 'Windows':
42
+ root = os.environ.get('LOCALAPPDATA') or home / 'AppData' / 'Local'
43
+ return sc.path(root) / 'starsim_data' / 'Cache'
44
+ elif system == 'Darwin':
45
+ return home / 'Library' / 'Caches' / 'starsim_data'
46
+ else:
47
+ root = os.environ.get('XDG_CACHE_HOME') or home / '.cache'
48
+ return sc.path(root) / 'starsim_data'
49
+
50
+
51
+ def cache_dir(subdir=None, create=False):
52
+ """Return the directory where downloaded data is cached.
53
+
54
+ Resolved at call time from `ssd.options.cache_dir`, falling back to a platform default
55
+ (`~/.cache/starsim_data` on Linux, `~/Library/Caches/starsim_data` on macOS,
56
+ `%LOCALAPPDATA%\\starsim_data\\Cache` on Windows). Nothing is created unless
57
+ `create=True`, so merely asking where the cache is has no side effects.
58
+
59
+ Args:
60
+ subdir (str): optional subdirectory within the cache, e.g. `'wpp'`.
61
+ create (bool): whether to create the directory if it does not exist.
62
+
63
+ Returns:
64
+ pathlib.Path: the cache directory.
65
+
66
+ Example:
67
+ >>> import starsim_data as ssd
68
+ >>> ssd.cache_dir().name
69
+ 'starsim_data'
70
+ """
71
+ if ssd.options.cache_dir:
72
+ path = sc.path(ssd.options.cache_dir).expanduser()
73
+ else:
74
+ path = _default_cache_dir()
75
+ if subdir:
76
+ path = path / subdir
77
+ if create:
78
+ path.mkdir(parents=True, exist_ok=True)
79
+ return path
80
+
81
+
82
+ def _sidecar(path):
83
+ """The path of a cached file's provenance sidecar."""
84
+ return sc.path(str(path) + SIDECAR_SUFFIX)
85
+
86
+
87
+ def _sha256(path):
88
+ """The SHA-256 of a file, read in chunks."""
89
+ digest = hashlib.sha256()
90
+ with open(path, 'rb') as f:
91
+ chunk = f.read(HASH_CHUNK)
92
+ while chunk:
93
+ digest.update(chunk)
94
+ chunk = f.read(HASH_CHUNK)
95
+ return digest.hexdigest()
96
+
97
+
98
+ def _write_sidecar(path, url):
99
+ """Record where a cached file came from, alongside it."""
100
+ facts = dict(
101
+ url = url,
102
+ filename = path.name,
103
+ downloaded = sc.now(utc=True).isoformat(timespec='seconds'),
104
+ bytes = path.stat().st_size,
105
+ sha256 = _sha256(path),
106
+ package_version = ssd.__version__,
107
+ )
108
+ sc.savejson(_sidecar(path), facts, sanitizepath=False) # Don't let Sciris rename the file
109
+ return facts
110
+
111
+
112
+ def cached_download(url, filename=None, subdir=None, refresh=False, verbose=None):
113
+ """Download `url` into the cache, or return the existing path if already cached.
114
+
115
+ Args:
116
+ url (str): the URL to fetch.
117
+ filename (str): name to save as (default: the last path component of the URL).
118
+ subdir (str): subdirectory of the cache to put it in, e.g. `'wpp'`.
119
+ refresh (bool): re-download even if a cached copy exists.
120
+ verbose (bool): report progress (default: `ssd.options.verbose`).
121
+
122
+ Returns:
123
+ pathlib.Path: the path to the cached file.
124
+ """
125
+ verbose = sc.ifelse(verbose, ssd.options.verbose)
126
+ filename = filename or url.rstrip('/').split('/')[-1]
127
+ path = cache_dir(subdir, create=True) / filename
128
+
129
+ if path.is_file() and (not refresh):
130
+ if verbose:
131
+ print(f'Using cached {path} ({sc.humanize_bytes(path.stat().st_size)})')
132
+ return path
133
+
134
+ if verbose:
135
+ print(f'Downloading {url}\n -> {path}')
136
+ sc.download(url, filename=str(path), convert=False, verbose=verbose) # convert=False keeps the gzipped WPP files as bytes rather than decoding them as text
137
+ _write_sidecar(path, url)
138
+ return path
139
+
140
+
141
+ def cache_provenance(path):
142
+ """Return the recorded provenance for a cached file, or None if there is none."""
143
+ sidecar = _sidecar(path)
144
+ if not sidecar.is_file():
145
+ return None
146
+ return sc.objdict(sc.loadjson(sidecar))
147
+
148
+
149
+ def _cached_files():
150
+ """Return every cached data file (i.e. excluding the provenance sidecars)."""
151
+ root = cache_dir()
152
+ if not root.is_dir():
153
+ return []
154
+ files = []
155
+ for path in sorted(root.rglob('*')):
156
+ if path.is_file() and (not path.name.endswith(SIDECAR_SUFFIX)):
157
+ files.append(path)
158
+ return files
159
+
160
+
161
+ def cache_info():
162
+ """Return a dataframe of what is currently cached: file, size, source URL and date.
163
+
164
+ Returns:
165
+ sciris.dataframe: with columns `[file, size, bytes, url, downloaded]`, empty if
166
+ nothing is cached.
167
+
168
+ Example:
169
+ >>> import starsim_data as ssd
170
+ >>> ssd.cache_info() # doctest: +SKIP
171
+ """
172
+ root = cache_dir()
173
+ rows = []
174
+ for path in _cached_files():
175
+ prov = cache_provenance(path) or {}
176
+ size = path.stat().st_size
177
+ rows.append(dict(
178
+ file = str(path.relative_to(root)),
179
+ size = sc.humanize_bytes(size),
180
+ bytes = size,
181
+ url = prov.get('url', ''),
182
+ downloaded = prov.get('downloaded', ''),
183
+ ))
184
+ return sc.dataframe(rows, columns=['file', 'size', 'bytes', 'url', 'downloaded'])
185
+
186
+
187
+ def clear_cache(pattern=None, verbose=None):
188
+ """Delete cached downloads. Bundled data is never touched.
189
+
190
+ Args:
191
+ pattern (str): a glob matched against each cached file's path relative to the
192
+ cache root, e.g. `'wpp/*'`. If None, the whole cache is removed.
193
+ verbose (bool): report what was deleted (default: `ssd.options.verbose`).
194
+
195
+ Returns:
196
+ list: the paths that were deleted.
197
+
198
+ Example:
199
+ >>> import starsim_data as ssd
200
+ >>> ssd.clear_cache('wpp/*') # doctest: +SKIP
201
+ """
202
+ verbose = sc.ifelse(verbose, ssd.options.verbose)
203
+ root = cache_dir()
204
+ if not root.is_dir():
205
+ return []
206
+
207
+ if pattern is None:
208
+ removed = [str(p.relative_to(root)) for p in _cached_files()]
209
+ shutil.rmtree(root)
210
+ else:
211
+ removed = []
212
+ for path in _cached_files():
213
+ if path.relative_to(root).match(pattern):
214
+ _sidecar(path).unlink(missing_ok=True)
215
+ path.unlink()
216
+ removed.append(str(path.relative_to(root)))
217
+
218
+ if verbose:
219
+ print(f'Removed {len(removed)} cached file(s) from {root}')
220
+ return removed
@@ -0,0 +1,41 @@
1
+ # starsim_data/data — bundled data files
2
+
3
+ These files ship inside the installed package and are the data that `starsim_data.load()` reads. The `.h5` stores are **generated — do not edit by hand** (regenerate them from the sources in `raw_data/`); `locations.csv` is the one file here with hand-maintained content, described below.
4
+
5
+ Each store carries a root `spec` attribute recording its `layout`, so `load()` returns the right shape for it.
6
+
7
+ **Contact matrices** (`layout='matrix'`), built from [`raw_data/contact_matrices/`](../../raw_data/contact_matrices/) by [`scripts/build_hdf5.py`](../../scripts/build_hdf5.py):
8
+
9
+ - `prem2021.h5` — the bundled contact source, keyed by location with one `float32` matrix per contact setting: `/<Location>/<setting>`.
10
+ - `prem/prem2017.h5` — the same, for the ten locations `prem2021` lacks, from `prem2017`. Together with `prem2021.h5` these are the default contact matrices, `load(which='prem')`. It is in a subfolder so that it is not discovered as a dataset of its own.
11
+ - `fetched.json` — the manifest of the stores that are fetched rather than bundled (`prem2017`, `mistry2021`, and for the US states `mistry2021_us` and `population_us`): for each, its URL (an asset of a GitHub release), SHA-256, locations and provenance. Their stores, built the same way, are in [`fetched/`](../../fetched/) at the top of the repository, outside the package; `fetch()` downloads them.
12
+
13
+ **Demographic series** from UN WPP, all grouped in `demographics-<revision>.h5` (e.g. `demographics-2024.h5`), built by [`scripts/build_demographics.py`](../../scripts/build_demographics.py). One group per series, each with its own `spec`:
14
+
15
+ - `/births`, `/migration` (`layout='crude'`) — one value per year (CBR, CNMR), per location.
16
+ - `/deaths` (`layout='mortality'`) — age/sex mortality rate `mx`, a `(year, sex, age)` array per location.
17
+ - `/population` (`layout='population'`) — population by 5-year age groups and sex (absolute persons), `(year, sex, age)` per location.
18
+
19
+ **Node table** (`layout='nodes'`), built from UNOCHA COD-AB and geoBoundaries by [`scripts/build_nodes.py`](../../scripts/build_nodes.py):
20
+
21
+ - `nodes.h5` — one compound dataset per location, holding every administrative unit at levels 0 and 1: `nodeid`, `location`, `iso3`, `level`, `name`, `pcode`, `dot_name`, `parent`, `lat`, `lon`, `area_km2`, `population`, `source`. Read with `starsim_data.load_nodes()`.
22
+ - `adjacency.h5` — for each location and level, the pairs of units that share a border. Read with `starsim_data.adjacency()`.
23
+
24
+ Plus `locations.csv` — the canonical country metadata file, read by all the build scripts and by the runtime location resolver. It is **part source, part generated**: `location`, `level`, `iso_code`, `primary_contact_source`, `contact_sources` and `population_source` are hand-maintained, while `iso3`, `official_name` and `aliases` are filled by [`scripts/build_locations.py`](../../scripts/build_locations.py) from `pycountry`. Edit the former by hand; regenerate the latter.
25
+
26
+ Note that this file must be read with `keep_default_na=False` (or with the `csv` module, as the resolver does): Namibia's ISO-2 code is the string `NA`, which pandas' defaults turn into NaN.
27
+
28
+ To regenerate:
29
+
30
+ ```bash
31
+ python scripts/build_locations.py --check # locations.csv derived columns
32
+ python scripts/build_hdf5.py --check # contact matrices, from raw_data/contact_matrices/
33
+ python scripts/build_demographics.py --check # demographics, from raw_data/demographics/
34
+ python scripts/build_nodes.py --check # node table, from the boundary sources
35
+ ```
36
+
37
+ `build_nodes.py` is the odd one out: its inputs are too big for `raw_data/` and are downloaded into the package cache instead, so a cold rebuild pulls ~1.4 GB. That is why it is not part of the automatic reproducibility check in CI.
38
+
39
+ Each `.h5` also carries a root `provenance` attribute recording its source, license, citation and build details; see `starsim_data.provenance()`.
40
+
41
+ The national-tier `.h5` files are small (a few MB each) and committed to the repo so that `pip install` is self-contained; this is a deliberate exception to the `*.h5` rule in `.gitignore`. Any larger (e.g. subnational) tiers added later should be distributed as release assets instead of committed here.
Binary file
Binary file