Skip to main content

spacescans

Link time-referenced geolocation data to the spatial and contextual exposome.

spacescans links location episodes to environmental and contextual exposures. A location episode is a participant identifier, a location (longitude and latitude) and the first and last day spent there; a residential history is a series of them. For every episode spacescans computes a buffer-weighted value of each exposure source (or the distance to the nearest feature) and weights it by the days the episode overlaps each source period. By default the result has one row per participant: each exposure averaged over all of the participant's episodes, weighted by days. One row per location episode is available with output_level="episode".

Exposures come from a catalog of 11 public data products (each site downloads them itself; see Getting the exposure data) or from datasets you register: tables keyed by census geography, rasters, and point, line or polygon features. The same engine runs behind the Python API, the command line, the R package in r/ and the SPACESCANS web application.

Python 3.10–3.13 Website

Install

pip install "spacescans[geo]"

The development version installs from GitHub:

pip install "spacescans[geo] @ git+https://github.com/IU-Ultraman/spacescans.git"

You install and import spacescans. (Before 0.3.0 the distribution was called spacescans-pipeline; it was never on PyPI.) It needs Python 3.10 to 3.13 on Linux or macOS. The base install is light; the extras add what each kind of source needs:

Extra Needed for Adds
(none) the catalog, spacescans data, cohort checks, exposure-value aggregation from existing weights pandas, DuckDB, pyarrow, pydantic
geo spatial weighting, which every catalog source needs; registered rasters and features; the quickstart geopandas, rasterio, shapely, exactextract
rda sources distributed as R data files: EPA Walkability Index, Community Organization Density, FARA Food Access, Neighborhood Deprivation Index pyreadr
nc ACAG PM2.5 Composition xarray, netCDF4
hdf4 TEMIS UV Exposure pyhdf (wheels for common platforms; otherwise it builds against the system HDF4 library)
all everything above

pip install "spacescans[geo,rda,nc]" covers every catalog source except TEMIS UV Exposure. An optional reader or pattern whose extra is missing raises MissingExtraError, which names the pip install to run.

Quickstart (no data needed)

spacescans quickstart --output-dir ./demo-out

The quickstart needs the geo extra. It runs one spatial-weighting step (C3) on synthetic sample data that comes with the package: 270 m buffers around 10 synthetic locations, intersected with three sample county polygons. It writes the spatial weights to ./demo-out/quickstart_county_weights.parquet. It does not link exposure values; the next section does.

Your cohort

One row per location episode; a participant who moved has one row per location.

Column Content
pid participant identifier (a PATID column is accepted too)
startDate, endDate first and last day at the location: 2017-08-19 (ISO, preferred) or 8/19/2017
longitude, latitude the location, in decimal degrees

Every row needs a pid and a location: the cohort check refuses a file with a blank pid or a missing, non-numeric or out-of-range coordinate, or with an endDate before its startDate, before anything runs. A blank date is allowed, with a warning: that episode contributes no days. Only these five columns are written to disk, into a private (0700) run directory; other columns stay in your session. Cohort values (identifiers, dates, coordinates) never appear in messages, events, warnings or provenance.

import pandas as pd
from spacescans import data
from spacescans.orchestrate import link

# Once: download NPS Soundscape Noise (1.2 GB) from the project mirror.
data.fetch(["noise"], data_dir="~/ss-data")

cohort = pd.DataFrame({
    "pid":       ["P1", "P1", "P2", "P3"],
    "startDate": ["2015-01-01", "2017-06-01", "2016-03-15", "2014-07-01"],
    "endDate":   ["2017-05-31", "2019-12-31", "2019-12-31", "2018-06-30"],
    "longitude": [-86.158, -86.131, -85.680, -87.630],
    "latitude":  [39.768, 39.790, 40.193, 41.880],
})

res = link(cohort, ["noise"], data_dir="~/ss-data")
res.exposures    # one row per participant: pid, l50dba_exi, l50dba_imp, l50dba_nat
res.coverage     # linkage rate of each variable: the share of participants with a value
res.features     # feature dictionary: what each output column is, with its unit
res.provenance   # versions, data receipts, cache keys and settings; no cohort values

by_episode = link(cohort, ["noise"], data_dir="~/ss-data", output_level="episode")

cohort can also be the path of a CSV file. Other arguments: buffer_m (buffer radius in meters, default 270), raster_res_m (default 25), cache (a directory that keeps spatial weights between calls, so further sources on the same geography reuse them), custom (registered datasets, below), on_error ("stop" or "continue" when a source fails), threads and memory_limit (DuckDB limits per step). spacescans.catalog.variables() lists the catalog keys; spacescans.catalog.coverage(cohort, keys) checks before a run which episodes fall inside each source's spatial and temporal coverage.

Participant level and episode level

With output_level="participant" (the default) the result has one row per participant id in the cohort, in order of first appearance: pid, then the requested variables, with no dates, coordinates or episode ids. For participant i and variable v:

x̄(i,v) = Σ_e Σ_t d(e,t) · z(e,t,v)  /  Σ_e Σ_t d(e,t)

where e runs over the participant's episodes, t over the source periods (year, two-week period, day; a single period for time-invariant sources) that overlap the episode, z(e,t,v) is the spatial estimate for the episode's location in that period (the buffer-weighted mean, or the distance to the nearest feature), and d(e,t) is the number of days of the episode in that period, first and last day included. Only periods with a defined estimate count, separately for each variable; a participant with no contributing day keeps their row, with missing values. This is the same as averaging the episode values, each weighted by its contributing days.

  • Overlapping location episodes. When two episodes of one participant overlap in time, each contributes its own days: a calendar day that both cover counts once for each of the two locations, so the participant value is a mean over episode-days, not over calendar days. Trim the overlaps in your cohort if a day should count only once.
  • Derived variables are derived from the participant's means: FARA Food Access's flags (such as LILATracts_1And10) are recoded from the participant's mean rates and urban share, so they stay 0 or 1, and each ACAG _nbm (non-biomass) column is the participant's mean of the species minus the participant's mean of its biomass part (_bm). At the episode level the same rules apply to each episode's means.

With output_level="episode" the result has one row per location episode, with episode_id (the 0-based row number of the episode in your input) and the variables.

Linkage rates are computed at the level of the output: the share of participants with a value, or the share of episodes in episode mode.

Register your own datasets

spacescans.custom keeps a library of datasets you add. Registration validates the files and prepares them once (normalized Parquet for tables, a checked grid and period index for rasters, GeoParquet in EPSG:5070 for features), stores them with their metadata (dataset.json), and every later linkage reuses the prepared files.

from pathlib import Path
from spacescans import custom

custom.register(["clinics.gpkg"], name="clinics", kind="features", temporal="static")
custom.register(sorted(Path("pm_biweekly").glob("*.tif")), name="pm_biweekly",
                kind="raster", temporal="biweekly")

custom.list_datasets()           # DatasetRecord: id, name, kind, temporal, variables, metadata
res = link(cohort, ["noise"], data_dir="~/ss-data", custom=["clinics", "pm_biweekly"])
custom.remove_dataset("pm_biweekly")
kind Files temporal Linked as
polygon_table CSV keyed by 2010 census tract, block group, ZIP Code Tabulation Area or county static, yearly (a year column), daily (a date column) area-weighted mean over the buffer
raster GeoTIFF: one, or one per period on a common grid static, yearly, biweekly, daily buffer-weighted mean of the cells
features points, lines or polygons (one geometry type per dataset) with a coordinate reference system, as GeoPackage, GeoJSON, zipped Shapefile or GeoParquet static, yearly (one file per year, or a year column) distance in meters from the location to the nearest feature (<name>_dist_m unless you name the variable)

register() also takes key (a table's geography and key column, such as {"boundary": "tract", "column": "GEOID"}; for features optionally a year column or a layer), variables (name, unit and description of each value), metadata (source, version, description, license note, visibility, owner) and overwrite. A record has an id, a name unique in its library, the checksum of the original files and the spacescans version that prepared it. The library lives in root=, else $SPACESCANS_DATASETS_DIR, else the user data directory (platformdirs.user_data_dir("spacescans") followed by datasets). Each record's visibility ("private" or "deployment") is informational in the package; the web application enforces it, sharing a "deployment" dataset with every user of that deployment. get_dataset(name_or_id) returns one record. The reference is docs/custom-datasets.md.

custom also accepts in-memory definitions made with custom.define_polygon() and custom.define_raster(), as a mapping of variable key to definition.

Getting the exposure data

No exposure data comes with spacescans. spacescans data knows where each catalog variable's data comes from and what a complete copy looks like under the data directory (spacescans/data/manifest.json); it needs no extras.

spacescans data status ndi walkability --data-dir ~/ss-data   # present, missing, sizes, next step
spacescans data fetch noise tiger_proximity --data-dir ~/ss-data --dry-run
spacescans data fetch noise tiger_proximity --data-dir ~/ss-data   # resumable; re-run after a drop
spacescans data verify --data-dir ~/ss-data                   # sha256 of everything placed
spacescans data adopt --data-dir /existing/web/pipeline-data  # receipts for data placed earlier
  • Files the project mirrors download from the project's OneDrive mirror (or --mirror URL|DIR, $SPACESCANS_DATA_MIRROR), resume after interruptions, are checked against their published sha256, and are recorded in <data-dir>/.spacescans/receipts/.
  • EPA FAQSD Air Quality, TEMIS UV Exposure and ACAG PM2.5 Composition download from their providers once you accept the provider's terms in the call (--accept-terms ID; status and fetch --dry-run print each ID with its terms). VIIRS Nighttime Lights needs a manual download; status prints the instructions, license and attribution. Provider terms are never accepted on your behalf.
  • The same functions are available from Python: spacescans.data.status(), fetch(), verify(), adopt(), terms(), import_files().

The catalog (every source covers the conterminous United States; the time-varying sources cover 2013–2019, the Neighborhood Deprivation Index 2012–2022):

Key Source Spatial data Time step Extras
walkability EPA Walkability Index polygon (block group) time-invariant geo, rda
cbp_zcta5 Community Organization Density polygon (ZCTA, county fallback) yearly geo, rda
fara_tract FARA Food Access polygon (census tract) yearly geo, rda
ndi Neighborhood Deprivation Index polygon (block group, 2010 and 2020) yearly geo, rda
faqsd EPA FAQSD Air Quality polygon (census tract) daily geo
noise NPS Soundscape Noise raster (270 m) time-invariant geo
vnl VIIRS Nighttime Lights raster (15″) yearly geo
acag ACAG PM2.5 Composition raster (0.01°) biweekly geo, nc
temis TEMIS UV Exposure raster (0.25°) daily geo, hdf4
nhd_bluespace NHD Bluespace line (distance to the nearest water feature) time-invariant geo
tiger_proximity TIGER Road Distance line (distance to the nearest road) yearly geo

Command line

export SPACESCANS_DATA_DIR=/path/to/exposome-data
export SPACESCANS_OUTPUT_DIR=/path/to/results

# Copy the editable template configs, then point their paths at your data
spacescans init-config --out ./configs

# Run the pipelines in dependency order
spacescans run ./configs/c3/county.yaml
spacescans run ./configs/c3/zcta5.yaml
spacescans run ./configs/c4/zbp.yaml            # depends on C3 zcta5
spacescans run ./configs/c4/cbp_fallback.yaml   # depends on C3 county + C4 zbp

A C4 config's time.output_grouping is patient (one row per participant, the default) or episode (one row per location episode); the templates state it. Paths resolve in three tiers, CLI flag > $SPACESCANS_DATA_DIR > YAML base_dir:, with ${SPACESCANS_DATA_DIR} expansion inside YAML paths.

How it works

Every linkage runs in two steps, both driven by YAML configs and the same DuckDB engine:

  1. Spatial weighting (C3, needs geo): the share of each location's buffer that falls in each polygon or raster cell, or the distance from the location to the nearest line or feature. The weights depend only on the cohort's locations, the source geography, the buffer radius and the rasterization resolution, so they are computed once per link() call (and across calls with a cache directory) and reused by every further source on the same geography.
  2. Exposure-value aggregation (C4, base install): the weights applied to each source period's values, weighted in turn by the days each episode overlaps the period, per episode or per participant.

C3 patterns

Pattern What it does Typical data
boundary_overlap_fast buffer × polygon area-overlap weights (per-tile bulk rasterization) block group, tract, ZCTA, county
boundary_overlap the same without per-tile rasterization (slower)
grid_weights buffer × raster-cell coverage weights ACAG, noise, TEMIS, VIIRS
tiger_proximity distance from each location to the nearest primary and secondary road, per year TIGER roads
nhd_proximity distance from each location to the nearest flowline, waterbody, area and coastline NHD

Distances to your own point, line or polygon features take one of two routes, which give the same exact nearest-feature distance:

  • Registered features (custom.register(..., kind="features"), then link(custom=[...]); also R's ss_register_dataset() and the web application's feature uploads): the distances from each location to the nearest feature are computed at the start of each link (spacescans.custom.features, in EPSG:5070 meters) and aggregated by the static_areal or yearly_areal C4 pattern. No C3 step.
  • Hand-written YAML run with spacescans run: the proximity pattern does both steps in one. Its config names the features (source.file: one vector or GeoParquet file, or one per year of time.years, or one file with source.year_col), the output column (exposure.value_cols, default distance_m) and optionally source.category_col (one column per category); distances are in meters in buffer.aea_crs (an Albers equal-area projection of the conterminous US) and time.output_grouping applies as in every C4 config.

C4 patterns

Pattern Time What it does Typical data
static_areal time-invariant area- or cell-weighted value, weighted by episode days walkability, noise
yearly_areal yearly area-weighted yearly values, weighted by the days in each year ZCTA business patterns
yearly_areal_bg_vintage yearly yearly_areal over 2010 and 2020 block groups, by year NDI
cbp_fallback yearly ZCTA values, falling back to county values where there is no ZCTA value Community Organization Density
fara_tract yearly yearly_areal with the FARA flag recodes (needs rda) FARA
faqsd_daily_areal daily daily tract values with day-level overlap EPA FAQSD
gridded yearly, biweekly, daily per-period values over the C3 weights of raster cells or polygons (needs geo) VIIRS, TEMIS; registered daily tables and daily or biweekly rasters
acag_multi biweekly the ACAG species over the C3 cell weights (needs geo, nc) ACAG
precomputed_areal yearly per-(location, year) values such as road distances, weighted by days TIGER distances
precomputed_static time-invariant per-location values such as water distances, weighted by days NHD distances

Limits

  • Catalog sources cover the conterminous United States and, when they vary over time, at least 2013–2019.
  • Each pipeline step runs as a single process, using about one processor core. Support for several cores is planned.
  • The daily sources need memory roughly in proportion to the number of episodes: EPA FAQSD Air Quality and TEMIS UV Exposure ran out of memory in a 24 GiB container at 100,000 episodes. Measured peaks per source are in docs/memory.md.

R package

The R package in r/ (ss_link(), ss_fetch(), ss_register_dataset() and friends) runs this package in a separate Python process through a JSON-file bridge (docs/bridge-protocol.md). It is released with the same version as the Python package. Install it with remotes::install_github("IU-Ultraman/spacescans", subdir = "r"), then run ss_install() once; see its README.

Project layout

src/spacescans/
├── orchestrate/         link(): a cohort and variables in, exposures out; run dirs, cache, merge
├── catalog/             the catalog variables, feature dictionary, recipes and templates
├── custom/              registered datasets and uploaded values (tables, rasters, features)
├── data/                spacescans data: manifest, status, fetch, verify, adopt
├── cohort.py            cohort checks and date parsing
├── bridge/              python -m spacescans.bridge, the JSON-file protocol the R package uses
├── cli/                 run / quickstart / init-config / data
├── config_resolution.py 3-tier path resolution (CLI > env > YAML) + ${VAR} expansion
├── pipeline/            config loader, pattern registry and runner
├── models/              pydantic config schema
├── engine/              DuckDB aggregation engine
├── io/                  readers (Parquet, CSV, .Rda) and writers
├── geometry/            buffers, overlap, grid weights, proximity (geo)
├── transforms/          filter / recode / derive / date_parse / spatial_fix
├── linkage/             linkage-pattern implementations
├── plugins/             source-specific readers (extras-gated)
└── resources/           synthetic sample data and template configs

import spacescans loads only the base dependencies.

Development

pip install -e '.[all,dev]'
pytest                  # full suite (base + any extras present in the environment)
pytest -m "not extras"  # base-only tests
pytest -m geo           # geo-extra tests

Opt-in suites (real data, golden references, web parity) are described in docs/tier2-notes.md. Changes are listed in CHANGELOG.md; the release procedure is docs/release.md.

License

spacescans is released under the MIT License (LICENSE); the R package in r/ is released under the same license. The license covers the software only: each catalog source keeps its provider's terms, which spacescans data status prints.


Learn more at www.spacescans.com.

Metadata

Release files for spacescans 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for spacescans 0.3.0
File Size Uploaded Provenance
spacescans-0.3.0.tar.gz 489.7 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for spacescans 0.3.0
File Interpreter ABI Platform
spacescans-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 955.7 kB

Release files / spacescans-0.3.0.tar.gz

Download URL spacescans-0.3.0.tar.gz
Size 489.7 kB
Tags Source
SHA-256 checksum
How to use checksums
7cbec9d747821c7b7bf114df53b7fcd586f14042d5a5488ab6d8ca70ccd12552
BLAKE2b-256 checksum
How to use checksums
e6420701c0c8eec74da107fa6424135d5722a7cce67e4c9dbf1542eedfc0a945
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.

Transparency log

Release files / spacescans-0.3.0-py3-none-any.whl

Download URL spacescans-0.3.0-py3-none-any.whl
Size 466.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e0c3e955e351a28b0bfd1765d74f1ace2dfbcb827e09f1fd6381575eefe5c792
BLAKE2b-256 checksum
How to use checksums
c1e257258b55b6e91bac8e06f420c04bada4876619e9e1587303e306332c73c6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.

Transparency log

Security for spacescans 0.3.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for spacescans-0.3.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page