Skip to content

Commit c8be244

Browse files
chekosclaude
andauthored
feat(spatial): Replace custom TIGER/Line downloads with pygris (#308)
* feat(spatial): Replace custom TIGER/Line downloads with pygris Use pygris for all shapefile downloads instead of manually constructing Census Bureau URLs. This adds automatic caching, better year/vintage handling, and fixes broken ZCTA/PUMA downloads for pre-2020 years. - Replace _GEO_TO_TIGER URL templates with _GEO_TO_PYGRIS function dispatch - Add GEOID column normalization for vintage variants (GEOID20, GEOID10) - Add CRS assertion to guarantee EPSG:4269 - Enable pygris caching by default - Add pygris>=0.1.7 to spatial optional deps - Update docs to mention pygris and automatic caching Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(spatial): Address PR review feedback - Remove AFFGEOID from GEOID normalization candidates (different format would silently break merges) - Only pass resolution kwarg to pygris functions that accept it (tracts, block_groups, places, zctas, pumas do not) - Add helpful ImportError when pygris function is missing - Add cache parameter to _fetch_tiger_shapes and attach_geometry so users can force fresh downloads - Cap pygris version to <1 to guard against breaking API changes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(spatial): Address remaining review nits - Include minimum version (>=0.1.7) in pygris ImportError message - Add requires_state validation so tract/block group/place/puma raise a clear ValueError when state is missing, instead of a cryptic pygris error downstream Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: Update all references from TIGER/Line to pygris Comprehensive docs sweep across 13 files: - Replace "TIGER/Line" with "cartographic boundary" or "via pygris" in all guides, migration docs, and getting-started pages - Add 0.3 and 0.3.1 changelog entries with full details - Update installation docs to mention pygris as spatial dependency - Add cache=True to attach_geometry() signature in spatial guide - Add ValueError troubleshooting entry for missing state param - Update caching guide: remove stale geography/ dir, add pygris cache section with OS-specific paths - Fix example version number (0.2 -> 0.3) in installation.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent bb1d622 commit c8be244

16 files changed

Lines changed: 222 additions & 87 deletions

docs/getting-started/installation.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ If you plan to work with shapefiles, choropleths, or any geometry (the `geometry
3030
uv add "pypums[spatial]"
3131
```
3232

33-
This adds [`geopandas`](https://geopandas.org/) and its dependencies (`shapely`, `pyproj`, `fiona`), which enable PyPUMS to fetch TIGER/Line shapefiles and return `GeoDataFrame` objects.
33+
This adds [`geopandas`](https://geopandas.org/) and [`pygris`](https://github.com/walkerke/pygris) (plus their dependencies), enabling `geometry=True` to return `GeoDataFrame` objects. pygris handles shapefile downloads with automatic local caching — files are only downloaded once.
3434

3535
## Get a Census API key
3636

@@ -124,7 +124,7 @@ Run this in your terminal to confirm PyPUMS is installed:
124124
python -c "import pypums; print(pypums.__version__)"
125125
```
126126

127-
You should see the version number printed (e.g. `0.2`).
127+
You should see the version number printed (e.g. `0.3`).
128128

129129
To verify the CLI is available:
130130

@@ -166,7 +166,7 @@ If this prints your key without raising an error, you are ready to go.
166166

167167
If empty, follow the [configuration steps](#configure-your-api-key) above.
168168

169-
??? question "I get `ImportError: geopandas` when using `geometry=True`"
169+
??? question "I get `ImportError: geopandas` or `ImportError: pygris` when using `geometry=True`"
170170
You need the spatial extras. Install them with:
171171

172172
```bash

docs/getting-started/quickstart.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ print(type(la_poverty)) # <class 'geopandas.geodataframe.GeoDataFrame'>
9191
2. **variables** -- `B17001_002` is the count of people whose income is below the poverty level (from table B17001).
9292
3. **state** -- Required for tract-level queries so the API knows which state to pull tracts from.
9393
4. **county** -- `"037"` is the FIPS code for Los Angeles County. Use `pypums.datasets.fips.lookup_fips(state="California", county="Los Angeles County")` to look up codes.
94-
5. **geometry** -- When `True`, PyPUMS fetches TIGER/Line cartographic boundary shapefiles and merges them with the data. The result is a `GeoDataFrame` with a `geometry` column.
94+
5. **geometry** -- When `True`, PyPUMS downloads cartographic boundary shapefiles (via pygris, cached locally) and merges them with the data. The result is a `GeoDataFrame` with a `geometry` column.
9595

9696
Now plot it with [Altair](https://altair-viz.github.io/):
9797

@@ -211,11 +211,13 @@ The resulting map shows poverty counts by Census tract across Los Angeles County
211211
}
212212
```
213213

214-
!!! info "What is a TIGER/Line shapefile?"
214+
!!! info "How does `geometry=True` work?"
215215
The Census Bureau publishes free geographic boundary files called
216-
TIGER/Line shapefiles. When you set `geometry=True`, PyPUMS automatically
217-
downloads the correct shapefile for your geography level and year, then
218-
joins it to your data on the `GEOID` column.
216+
TIGER/Line shapefiles. When you set `geometry=True`, PyPUMS uses
217+
[pygris](https://github.com/walkerke/pygris) to download the correct
218+
shapefile for your geography level and year, then joins it to your data
219+
on the `GEOID` column. Files are cached locally so subsequent calls are
220+
fast.
219221

220222
**What to try next:** [Spatial Data & Mapping guide](../guides/spatial.md) for dot-density maps, population-weighted interpolation, and custom resolutions.
221223

docs/guides/acs-data.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -410,8 +410,8 @@ age_with_total["share"] = (
410410

411411
## Geometry support
412412

413-
Set `geometry=True` to return a GeoDataFrame with TIGER/Line
414-
cartographic boundary shapes joined to your data.
413+
Set `geometry=True` to return a GeoDataFrame with cartographic boundary
414+
shapes joined to your data (downloaded via pygris, cached locally).
415415

416416
```python
417417
ca_counties_geo = pypums.get_acs(
@@ -759,4 +759,4 @@ See the [Finding Variables](variables.md) guide for full details.
759759
- [Finding Variables](variables.md) — Discovering variable codes with `load_variables()`
760760
- [Geography & FIPS](geography.md) — Understanding geography levels and FIPS code lookups
761761
- [Margins of Error](margins-of-error.md) — MOE propagation formulas and statistical testing
762-
- [Spatial Data](spatial.md) — Attaching TIGER/Line geometry to query results
762+
- [Spatial Data](spatial.md) — Attaching geometry to query results

docs/guides/caching.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,20 @@ structure:
114114
api/ # API response cache (get_acs, get_decennial, etc.)
115115
variables/ # Variable table cache (load_variables)
116116
pums_vars/ # PUMS variable dictionary cache
117-
geography/ # Geometry / shapefile cache
118117
```
119118

119+
!!! note "Shapefile cache is managed by pygris"
120+
When you use `geometry=True`, shapefiles are cached separately by
121+
[pygris](https://github.com/walkerke/pygris) in its own directory:
122+
123+
- **macOS:** `~/Library/Caches/pygris/`
124+
- **Linux:** `~/.cache/pygris/`
125+
- **Windows:** `C:\Users\{user}\AppData\Local\pygris\Cache\`
126+
127+
To clear the shapefile cache, delete that directory manually.
128+
`CensusCache.clear()` only clears the PyPUMS API response caches above,
129+
not the pygris shapefile cache.
130+
120131
You can inspect the cache directory at any time:
121132

122133
```bash

docs/guides/decennial-data.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ print(hispanic_pop.head())
228228

229229
## Geometry support
230230

231-
Set `geometry=True` to return a GeoDataFrame with TIGER/Line shapes:
231+
Set `geometry=True` to return a GeoDataFrame with cartographic boundary shapes:
232232

233233
```python
234234
county_geo = pypums.get_decennial(
@@ -605,5 +605,5 @@ print(pop_vars[["name", "label"]].head(5))
605605

606606
- [API Reference](../reference/api.md) — Full `get_decennial()` function signature
607607
- [Geography & FIPS](geography.md) — Understanding geography levels and FIPS code lookups
608-
- [Spatial Data](spatial.md) — Attaching TIGER/Line geometry to query results
608+
- [Spatial Data](spatial.md) — Attaching geometry to query results
609609
- [ACS Data](acs-data.md) — For when you need margins of error and more recent annual data

docs/guides/migration-flows.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ df = get_flows(
2121
state=None, # state FIPS, abbreviation, or name
2222
county=None, # county FIPS code
2323
msa=None, # metropolitan statistical area code
24-
geometry=False, # attach TIGER/Line shapes
24+
geometry=False, # attach cartographic boundary shapes
2525
moe_level=90, # confidence level: 90, 95, or 99
2626
cache_table=False, # cache API response on disk
2727
show_call=False, # print the API URL
@@ -218,8 +218,8 @@ where z-scores are 1.645 (90%), 1.960 (95%), and 2.576 (99%).
218218

219219
## Geometry support
220220

221-
Set `geometry=True` to attach TIGER/Line cartographic boundary shapes to
222-
the origin geography. This returns a GeoDataFrame you can plot directly:
221+
Set `geometry=True` to attach cartographic boundary shapes to the origin
222+
geography. This returns a GeoDataFrame you can plot directly:
223223

224224
```python
225225
ca_flows_geo = get_flows(

docs/guides/population-estimates.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,7 @@ print(housing.head(3))
439439

440440
## Geometry support
441441

442-
Set `geometry=True` to get a GeoDataFrame with TIGER/Line shapes:
442+
Set `geometry=True` to get a GeoDataFrame with cartographic boundary shapes:
443443

444444
```python
445445
pop_geo = pypums.get_estimates(

docs/guides/spatial.md

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
# Spatial Data & Mapping
22

3-
PyPUMS can attach TIGER/Line cartographic boundary shapefiles to any Census
3+
PyPUMS can attach Census cartographic boundary shapefiles to any Census
44
query, returning a `GeoDataFrame` that is ready for mapping, spatial joins, and
5-
geospatial analysis.
5+
geospatial analysis. Shapefile downloads are handled by
6+
[pygris](https://github.com/walkerke/pygris), with automatic local caching so
7+
files are only downloaded once.
68

79
!!! info "Requires the spatial extras"
8-
Geometry features depend on **geopandas** and its dependencies (`shapely`,
9-
`pyproj`, `fiona`). Install them with:
10+
Geometry features depend on **geopandas** and **pygris** (plus their
11+
dependencies). Install them with:
1012

1113
```bash
1214
uv add "pypums[spatial]"
@@ -18,8 +20,9 @@ geospatial analysis.
1820

1921
The fastest way to get spatial data is to pass `geometry=True` to any of the
2022
main data retrieval functions. PyPUMS will automatically download the
21-
corresponding TIGER/Line shapefile, merge it with the tabular data on the
22-
`GEOID` column, and return a `GeoDataFrame`.
23+
corresponding cartographic boundary shapefile (via pygris), merge it with the
24+
tabular data on the `GEOID` column, and return a `GeoDataFrame`. Downloaded
25+
shapefiles are cached locally so subsequent calls are fast.
2326

2427
=== "get_acs()"
2528

@@ -226,7 +229,7 @@ corresponding TIGER/Line shapefile, merge it with the tabular data on the
226229
## Coordinate reference system
227230

228231
All geometry returned by PyPUMS is in **NAD83 (EPSG:4269)**, which is the
229-
native CRS of the Census Bureau's TIGER/Line files. You can verify this on any
232+
native CRS of the Census Bureau's cartographic boundary files. You can verify this on any
230233
`GeoDataFrame`:
231234

232235
```python
@@ -291,7 +294,7 @@ You will often need to reproject to a different CRS depending on your use case.
291294

292295
## Shapefile resolution
293296

294-
TIGER/Line cartographic boundary files come in three resolutions. The default
297+
Cartographic boundary files come in three resolutions. The default
295298
is `500k`, which strikes a good balance between detail and file size.
296299

297300
| Resolution | Description | Best for |
@@ -308,7 +311,7 @@ always uses `500k`.
308311

309312
## Supported geographies
310313

311-
The following geography levels have matching TIGER/Line shapefiles:
314+
The following geography levels have matching cartographic boundary shapefiles:
312315

313316
| Geography | Requires `state`? | Notes |
314317
|------------------------|--------------------|----------------------------------------|
@@ -325,8 +328,8 @@ The following geography levels have matching TIGER/Line shapefiles:
325328

326329
!!! warning "Sub-state geographies require the `state` parameter"
327330
For `tract`, `block group`, `place`, and `puma`, the Census Bureau
328-
publishes shapefiles per state. PyPUMS needs the `state` parameter to
329-
know which file to download.
331+
publishes shapefiles per state. PyPUMS will raise a `ValueError` if you
332+
omit the `state` parameter for these geography levels.
330333

331334
---
332335

@@ -409,6 +412,7 @@ attach_geometry(
409412
state=None, # state FIPS or abbreviation
410413
year=2023, # data year
411414
resolution="500k", # "500k", "5m", or "20m"
415+
cache=True, # cache shapefiles locally (via pygris)
412416
) -> GeoDataFrame
413417
```
414418

@@ -960,21 +964,27 @@ memory. A few tips for working with large datasets:
960964
(1318, 6)
961965
```
962966

963-
- **Cache your queries** with `cache_table=True` so repeated runs do not
964-
re-fetch the shapefile from the Census Bureau servers.
967+
- **Shapefile caching is automatic.** PyPUMS uses pygris with caching
968+
enabled, so shapefiles are downloaded once and reused from a local
969+
cache directory (`~/.cache/pygris/` on Linux,
970+
`~/Library/Caches/pygris/` on macOS).
965971

966972
---
967973

968974
## Troubleshooting
969975

970976
**`ImportError: geopandas is required for spatial operations`**
971977
: Install the spatial extra: `uv add "pypums[spatial]"`. This pulls in
972-
`geopandas`, `shapely`, and `pyproj`.
978+
`geopandas`, `pygris`, `shapely`, and `pyproj`.
979+
980+
**`ValueError: geography='tract' requires a state parameter`**
981+
: Sub-state geographies (`tract`, `block group`, `place`, `puma`) need a
982+
`state` argument. Pass a FIPS code, abbreviation, or full name.
973983

974984
**Geometry column is all `None`**
975-
: The Census TIGER/Line server may not have shapefiles for the geography
976-
level and year you requested. Try a different year or a broader
977-
geography (e.g., county instead of block group).
985+
: The Census Bureau may not have shapefiles for the geography level and
986+
year you requested. Try a different year or a broader geography (e.g.,
987+
county instead of block group).
978988

979989
**CRS mismatch when combining DataFrames**
980990
: All PyPUMS geometry is returned in EPSG:4269 (NAD83). If you are

docs/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ print(df.head())
4646

4747
---
4848

49-
Add `geometry=True` to any query and get a GeoDataFrame with TIGER/Line boundaries.
49+
Add `geometry=True` to any query and get a GeoDataFrame with cartographic boundary shapes (via pygris, cached locally).
5050

5151
[:octicons-arrow-right-24: Spatial guide](guides/spatial.md)
5252

docs/migration/from-census-ftp.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ df = pypums.get_pums(
7373
| **Survey design** | Manual SDR calculation | `to_survey()` helper |
7474
| **Caching** | Manage files yourself | Built-in with TTL |
7575
| **Summary tables** | Not available (PUMS only) | `get_acs()` for pre-tabulated data |
76-
| **Geometry** | Separate TIGER/Line download | `geometry=True` parameter |
76+
| **Geometry** | Separate TIGER/Line download | `geometry=True` (pygris handles the download) |
7777

7878
## When You Still Need FTP
7979

0 commit comments

Comments
 (0)