Turn Geographic Data into Maps and Statistics SKILL.md
Slice and map geospatial data without a GIS license. Use for shapefiles, GeoPackage, GeoJSON, or GeoTIFF rasters; zonal statistics like mean elevation per county; buffers and spatial joins; or publication maps with choropleths, hillshade, insets, and zoom breakouts. Downloads come from vetted official sources such as USGS, NOAA, and the US Census Bureau.
---
name: geospatial-analyst
description: Slice and map geospatial data without a GIS license. Use for shapefiles, GeoPackage, GeoJSON, or GeoTIFF rasters; zonal statistics like mean elevation per county; buffers and spatial joins; or publication maps with choropleths, hillshade, insets, and zoom breakouts. Downloads come from vetted official sources such as USGS, NOAA, and the US Census Bureau.
category: Make sense of your data
type: bundled
requires:
packages:
- geopandas>=1.0
- rasterio>=1.4
- pyogrio>=0.10
- shapely>=2.0
- pyproj>=3.7
- mapclassify>=2.6
setup:
- "No connector is required; the skill runs on the local Python geo stack."
- "First use prompts to install geopandas, rasterio, and friends into the run_code environment; approve once."
- "Datasets download from vetted official sources (USGS, NOAA, Census, Natural Earth) with a recorded provenance file."
---
# Geospatial Analyst
An ArcGIS-class analysis and cartography workflow built on the open-source
Python geo stack (GeoPandas/Rasterio/Shapely), using redeep's normal tools:
`run_code` for exploration, `bash` for the bundled scripts, `analyze_image` to
inspect rendered maps. Everything is reversible: data, layers, and figures live
in a **map project** directory driven by a `layers.json` manifest.
## The map project (undo/redo backbone)
```
map_project/
layers.json <- the manifest: canvas + ordered layers + history
data/ <- downloaded datasets, each with _provenance.json
renders/ <- outputs (map.png, stats JSON/CSV)
```
Start every analysis/map by creating one:
```bash
python3 "<skill>/scripts/geokit.py" init map_project --crs EPSG:4326 --title "..."
```
Layers are the unit of undo. Each add/restyle/show/hide/remove is recorded in
`layers.json` history; reverting a change = editing the manifest, re-rendering
= `render_map.py`. **Never** composite everything into one script — add one
layer per geokit command so any element (basemap, choropleth, markers, inset)
can be independently styled, hidden, or deleted without touching others.
Typical z order: hillshade (1) < raster (5) < admin polygons (10) < choropleth
(20) < lines (30) < points (40) < labels (50) < graticule (60) <
scalebar/north_arrow (70) < inset/breakout (80).
## Workflow
1. **Acquire data — vetted sources only.** `fetch_data.py list` shows the
registry; `fetch_data.py get DATASET --out map_project/data` downloads and
records provenance (URL, SHA-256, date, license) in `_provenance.json`.
For elevation use `get usgs_3dep --bbox xmin,ymin,xmax,ymax` (TNM API; pick
a small bbox — tiles are ~1x1 degree). If the user's needed dataset is not
in the registry, prefer .gov/.edu/official-CDN hosts and add it to
`VETTED_SOURCES` in geokit.py after verifying the host, citing the agency.
2. **Explore & slice** in `run_code`: `import geopandas, rasterio`; read with
`gpd.read_file` / `rasterio.open`; profile columns, CRS, bounds, dtypes.
Reproject with `to_crs`, clip with `gdf.clip`, buffer with `buffer` — but only
after projecting to a metric CRS like EPSG:3857 or local UTM (**never buffer
in degrees**). Save intermediates under `data/` with pyogrio
(`gdf.to_file(..., engine="pyogrio")`).
3. **Zonal statistics** (raster stats per polygon):
`zonal_stats.py --raster dem.tif --zones counties.gpkg --stats mean min max
std count --id-column GEOID --out-json stats.json --out-csv stats.csv`.
Zones are reprojected to the raster CRS (the raster grid is never
resampled); zones with no valid pixels report null, not zero. Join stats
back to zones in `run_code` for choropleths (`zones.merge(stats, on=...)`).
4. **Build the map layer-by-layer** with `geokit.py add`. Style keys: `column`
(attribute to map), `cmap`, `color`, `alpha`, `scheme`
(quantile/equal_interval/fisherjenks) + `k`, `categorical: true`,
`markersize`, `edgecolor`, `linewidth`, `label_column` (annotate features).
Then `render_map.py map_project` -> `renders/map.png`. Inspect with
`analyze_image` and iterate: `geokit.py restyle --id choropleth --patch
'{"style":{"cmap":"magma"}}'` then re-render.
5. **Insets & zoom breakouts**: `geokit.py inset --kind breakout --rect
'[0.65,0.62,0.32,0.35]' --bbox '[xmin,miny,maxx,maxy]' --layers '[...]'` —
the renderer highlights the bbox on the main map and draws the detail
panel; `--kind inset` is a plain secondary map (e.g. a locator country).
6. **Publish-safe outputs**: renders are 300 dpi at journal column widths
(7.48 in double-column default). Verify legibility with `analyze_image` at
scale: titles >= 10pt, annotations >= 6pt, distinguishable colors, readable
legend, scalebar + north arrow for topographic-style figures. The figure
footer auto-cites each layer's data sources from provenance records.
7. **R option**: for raster-first workflows, R's `sf`/`terra` offer the same
capabilities (see references/r-spatial.md). Prefer Python unless the user
asks for R or is already in R.
## Script reference
| Script | Role |
|---|---|
| `geokit.py` | Project manifest CLI: init/add/inset/remove/show/hide/restyle/canvas/list/validate; vetted-source registry; importable from `run_code` (`import geokit`) |
| `fetch_data.py` | Download from vetted official sources with provenance; `list` / `get` |
| `zonal_stats.py` | Per-polygon raster statistics -> JSON + CSV |
| `render_map.py` | Manifest -> publication PNG; layers in z order, legends, insets/breakouts, provenance footer |
All scripts print JSON; consume their output programmatically. Scripts are
black boxes: run them, don't read them. If one fails, its JSON error names the
fix (e.g. missing geo stack -> install via the skill's `requires.packages`).
## Conventions & gotchas
- **CRS discipline**: know every layer's CRS (`gdf.crs`, `src.crs`); reproject
vectors to the map CRS before styling/plotting; project to metric CRS for
area/distance work. Mixing CRSes silently misplaces data by kilometers.
- **Degrees are not meters**: buffering/density in EPSG:4326 produces nonsense;
use EPSG:3857 or local UTM (EPSG:326xx/327xx) for geometry ops.
- **Nodata**: always check `src.nodata`; mask before stats; categorical-code
rasters need `categorical: true` styling, not continuous colormaps.
- **Attribute joins for choropleths**: match on exact keys (FIPS/GEOID), watch
dtype mismatches (str vs int), and sanity-check row counts before mapping —
a silent inner-join loss shows up as holes in the map.
- **Publication style**: subtle graticules, desaturated admin borders,
perceptually-uniform colormaps (viridis/magma/cividis) for continuous data,
colorblind-safe categorical palettes, compact legends. When in doubt, render
then `analyze_image` the PNG.
- **Provenance**: every figure is only as credible as its data lineage — cite
agency + product + date in the footer (auto via `_provenance.json`); record
rolling feeds (e.g. USGS earthquake GeoJSON) with their retrieval date.
- **File formats**: prefer GeoPackage (`.gpkg`) for project intermediates
(single file, no sidecar issues, multiple layers); GeoJSON for interchange;
keep original shapefile component sets together under `data/<dataset>/`.
## References (read on demand)
- `references/cheatsheet.md` — dense GeoPandas/Rasterio/pyproj recipe card
- `references/data-sources.md` — the vetted-source registry in depth: catalog
URLs, API patterns, how to vet a new source
- `references/r-spatial.md` — the same workflow in R (sf/terra/ggplot2)
- `references/cartography.md` — publication figure checklist, colormap/scheme
choice, inset/breakout design patterns, ArcGIS-equivalent recipes
Redeep