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.
- starsim_data/__init__.py +57 -0
- starsim_data/cache.py +220 -0
- starsim_data/data/README.md +41 -0
- starsim_data/data/adjacency.h5 +0 -0
- starsim_data/data/demographics-2024.h5 +0 -0
- starsim_data/data/fetched.json +416 -0
- starsim_data/data/locations.csv +188 -0
- starsim_data/data/nodes.h5 +0 -0
- starsim_data/data/prem/prem2017.h5 +0 -0
- starsim_data/data/prem2021.h5 +0 -0
- starsim_data/fetching.py +422 -0
- starsim_data/loaders.py +289 -0
- starsim_data/locations.py +280 -0
- starsim_data/nodes.py +223 -0
- starsim_data/plotting.py +267 -0
- starsim_data/rasters.py +390 -0
- starsim_data/registry.py +366 -0
- starsim_data/settings.py +108 -0
- starsim_data/shapes.py +330 -0
- starsim_data/version.py +9 -0
- starsim_data-2.0.0.dist-info/METADATA +265 -0
- starsim_data-2.0.0.dist-info/RECORD +30 -0
- starsim_data-2.0.0.dist-info/WHEEL +5 -0
- starsim_data-2.0.0.dist-info/licenses/LICENSE +35 -0
- starsim_data-2.0.0.dist-info/licenses/NOTICE.md +70 -0
- starsim_data-2.0.0.dist-info/licenses/licenses/LICENSE_EPYDEMIX +674 -0
- starsim_data-2.0.0.dist-info/licenses/licenses/LICENSE_MISTRY2021 +201 -0
- starsim_data-2.0.0.dist-info/licenses/licenses/LICENSE_PREM2017 +674 -0
- starsim_data-2.0.0.dist-info/licenses/licenses/LICENSE_PREM2021 +674 -0
- starsim_data-2.0.0.dist-info/top_level.txt +1 -0
starsim_data/__init__.py
ADDED
|
@@ -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
|