It was always the case that cendat would eventually support geometry/polygon fetching for aggregate products, but it took a little while to implement because I didn’t want to tack it on as a sequential process–I wanted it to run concurrently with the data fetching, and this required a pretty substantial rewrite of get_data and the addition of several new internal methods. That work is now complete enough for me to share an alpha/preview release version which you can install with pip:
pip install --pre--upgrade cendat
That’ll get you ver 0.7.0a3. Note that you will need pandas and geopandas to unlock the new geometry fetching features. New syntax is reflected in the landing page for this version at https://pypi.org/project/cendat/0.7.0a3/, but the short version is that there’s now an include_geometry parameter in get_data and there is a new to_gpd method in the CenDatResponse class that joins the fetched data and polygons and outputs a Pandas GeoDataFrame. Here’s an example:
✅ API key loaded successfully.
✅ Product set: 'ACS 5-Year Detailed Tables (2023/acs/acs5)' (Vintage: [2023])
✅ Groups set: B02001
--- Group: B02001 (Race) ---
Product: ACS 5-Year Detailed Tables (2023/acs/acs5) (Vintage: 2023)
B02001_001E: Total:
B02001_002E: White alone
B02001_003E: Black or African American alone
B02001_004E: American Indian and Alaska Native alone
B02001_005E: Asian alone
B02001_006E: Native Hawaiian and Other Pacific Islander alone
B02001_007E: Some Other Race alone
B02001_008E: Two or More Races:
B02001_009E: Two races including Some Other Race
B02001_010E: Two races excluding Some Other Race, and three or more races
Here we’re looking at the core race group in the 2023 ACS 5-year product. We will bypass variable listing/setting and just pull everything from the group (this was a new feature in ver 0.6.0 that I never got around to writing a post about).
✅ Geographies set: 'county' (requires `within` for: state)
✅ Parameters created for 1 geo-variable/group combinations.
✅ Successfully fetched map servers.
✅ Successfully fetched map server layers.
ℹ️ Discovering parent geographies for: ['state']
✅ Found 52 combinations. Building API queries...
ℹ️ Making 52 API call(s)...
ℹ️ Pre-querying for geometry counts to determine pagination...
- Found 9 geometries for WHERE 'STATE IN ('09')...'. Building paginated tasks.
- Found 46 geometries for WHERE 'STATE IN ('45')...'. Building paginated tasks.
- Found 56 geometries for WHERE 'STATE IN ('30')...'. Building paginated tasks.
- Found 5 geometries for WHERE 'STATE IN ('44')...'. Building paginated tasks.
- Found 83 geometries for WHERE 'STATE IN ('26')...'. Building paginated tasks.
- Found 105 geometries for WHERE 'STATE IN ('20')...'. Building paginated tasks.
- Found 115 geometries for WHERE 'STATE IN ('29')...'. Building paginated tasks.
- Found 100 geometries for WHERE 'STATE IN ('37')...'. Building paginated tasks.
- Found 93 geometries for WHERE 'STATE IN ('31')...'. Building paginated tasks.
- Found 30 geometries for WHERE 'STATE IN ('02')...'. Building paginated tasks.
- Found 64 geometries for WHERE 'STATE IN ('08')...'. Building paginated tasks.
- Found 120 geometries for WHERE 'STATE IN ('21')...'. Building paginated tasks.
- Found 58 geometries for WHERE 'STATE IN ('06')...'. Building paginated tasks.
- Found 92 geometries for WHERE 'STATE IN ('18')...'. Building paginated tasks.
- Found 64 geometries for WHERE 'STATE IN ('22')...'. Building paginated tasks.
- Found 99 geometries for WHERE 'STATE IN ('19')...'. Building paginated tasks.
- Found 67 geometries for WHERE 'STATE IN ('42')...'. Building paginated tasks.
- Found 87 geometries for WHERE 'STATE IN ('27')...'. Building paginated tasks.
- Found 77 geometries for WHERE 'STATE IN ('40')...'. Building paginated tasks.
- Found 44 geometries for WHERE 'STATE IN ('16')...'. Building paginated tasks.
- Found 36 geometries for WHERE 'STATE IN ('41')...'. Building paginated tasks.
- Found 21 geometries for WHERE 'STATE IN ('34')...'. Building paginated tasks.
- Found 17 geometries for WHERE 'STATE IN ('32')...'. Building paginated tasks.
- Found 102 geometries for WHERE 'STATE IN ('17')...'. Building paginated tasks.
- Found 29 geometries for WHERE 'STATE IN ('49')...'. Building paginated tasks.
- Found 88 geometries for WHERE 'STATE IN ('39')...'. Building paginated tasks.
- Found 75 geometries for WHERE 'STATE IN ('05')...'. Building paginated tasks.
- Found 15 geometries for WHERE 'STATE IN ('04')...'. Building paginated tasks.
- Found 53 geometries for WHERE 'STATE IN ('38')...'. Building paginated tasks.
- Found 5 geometries for WHERE 'STATE IN ('15')...'. Building paginated tasks.
- Found 82 geometries for WHERE 'STATE IN ('28')...'. Building paginated tasks.
- Found 62 geometries for WHERE 'STATE IN ('36')...'. Building paginated tasks.
- Found 95 geometries for WHERE 'STATE IN ('47')...'. Building paginated tasks.
- Found 16 geometries for WHERE 'STATE IN ('23')...'. Building paginated tasks.
- Found 3 geometries for WHERE 'STATE IN ('10')...'. Building paginated tasks.
- Found 14 geometries for WHERE 'STATE IN ('25')...'. Building paginated tasks.
- Found 159 geometries for WHERE 'STATE IN ('13')...'. Building paginated tasks.
- Found 254 geometries for WHERE 'STATE IN ('48')...'. Building paginated tasks.
- Found 10 geometries for WHERE 'STATE IN ('33')...'. Building paginated tasks.
- Found 66 geometries for WHERE 'STATE IN ('46')...'. Building paginated tasks.
- Found 67 geometries for WHERE 'STATE IN ('12')...'. Building paginated tasks.
- Found 1 geometries for WHERE 'STATE IN ('11')...'. Building paginated tasks.
- Found 24 geometries for WHERE 'STATE IN ('24')...'. Building paginated tasks.
- Found 55 geometries for WHERE 'STATE IN ('54')...'. Building paginated tasks.
- Found 33 geometries for WHERE 'STATE IN ('35')...'. Building paginated tasks.
- Found 14 geometries for WHERE 'STATE IN ('50')...'. Building paginated tasks.
- Found 39 geometries for WHERE 'STATE IN ('53')...'. Building paginated tasks.
- Found 67 geometries for WHERE 'STATE IN ('01')...'. Building paginated tasks.
- Found 78 geometries for WHERE 'STATE IN ('72')...'. Building paginated tasks.
- Found 133 geometries for WHERE 'STATE IN ('51')...'. Building paginated tasks.
- Found 23 geometries for WHERE 'STATE IN ('56')...'. Building paginated tasks.
- Found 72 geometries for WHERE 'STATE IN ('55')...'. Building paginated tasks.
ℹ️ Fetching geometries across 52 paginated calls...
✅ Data fetching complete. Stacking results.
✅ Geometry fetching complete. Stacking results.
CPU times: user 8.54 s, sys: 1.23 s, total: 9.78 s
Wall time: 24.4 s
We choose county-level data and indicate that geometries should be fetched alongside the data. Note that the TIGERweb REST Service has different limitations than the data API, and national queries that would work on the data side are rejected for geometries. For all queries where include_geometry=True, we give up some of the wildcarding capability that we would otherwise have: optionalWithWCFor is ignored and the least granular wildcards level is ignored–for counties that means that states can no longer be omitted or wildcarded. We can see that in this example each state was queried individually (still in a concurrent thread pool, once the record counts are obtained), while for a data-only request, only a single API query would be issued.
Note also the use of ‘paginated’ in the informational prints–the TIGERweb REST Service limits queries to 1,000 records, so any query that goes over is broken into multiple ‘paginated’ requests. No state has more than 1,000 counties, so we don’t leverage that here, but you would see it, for example, in a request for all places in the nation.
These ‘paginated’ queries are issued concurrently in a dedicated thread pool that is itself executed in its own thread concurrently with the main data API queries. So, while fetching geometries can make get_data calls take longer than they otherwise would, in those cases it takes no longer than it would to fetch the geometries alone. We can see evidence of that in this example: the data fetching success message happens before geometries are fetched and stacked, even though the two tasks are kicked off simultaneously.
Just like with the to_pandas and to_polars methods, the new to_gpd method is invoked on the CenDatResponse object:
For many aggregate products supported by cendat there is no specific TIGERweb map server (i.e., vintage) and in these cases, the ‘Current’ version is used. That could result in data with no corresponding geometry, or geometries with no data. In these cases, if join_strategy='left', the GeoDataFrame will have rows with empty geometries. You can set join_strategy='inner' to filter the output to data rows with matched geometries.
Now that geometries are supported, users can do all the cool things that enables, like plotting choropleth maps. One of my next development goals, in fact, is to add in one or more methods to support just that.
As always, more to come…
Citation
BibTeX citation:
@online{couzens2025,
author = {Couzens, Lance},
title = {Cendat Ver 0.7.0a3},
date = {2025-09-03},
url = {https://mostlyunoriginal.github.io/posts/2025-09-03-cendat-geo-fetching-alpha-release/},
langid = {en}
}