A reproduction, for the Boston metro area, of the two figures in Colin Williams' Institute for Family Studies essay "Where Have All the Families Gone? Aging Empty Nesters and Housing Lock-In":
- A choropleth of the Youth-to-Woman Ratio (YWR) by neighborhood.
- YWR vs. the average age of a neighborhood's homes — the lifecycle "hump."
The urban core (downtown, Back Bay, the university belt) is orange/red — few children per working-age woman — while the suburbs are green. Plotting YWR against housing age reproduces the article's hump: child density peaks in neighborhoods whose homes average ~45–55 years old (built in the 1960s–70s) and falls off for both the newest downtown housing and the very oldest urban cores.
The original essay combines two inputs that are restricted/proprietary and cannot be downloaded: the Census Bureau's Gridded Environmental Impact Frame (~1 km² population grid) and CoreLogic parcel records (home age/size). This project reproduces the same concepts from public data, using census tracts as the neighborhood unit.
| Article concept | Public proxy used here |
|---|---|
| Youth-to-Woman Ratio | children under 18 ÷ working-age women (18–64), ACS table B01001 |
| Average age of homes | survey year − unit-weighted mean year built, ACS table B25034 |
| ~1 km² grid cell | census tract |
| Single metro snapshot | the latest released vintage, ACS 2019–2023 5-year |
Geography: the Massachusetts counties of the Boston-Cambridge-Newton metro — Suffolk, Middlesex, Norfolk, Essex, Plymouth (~1,000 tracts).
All data is pulled directly from the U.S. Census Bureau (census.gov):
- ACS 2019–2023 5-year age/sex (B01001) and year-built (B25034/B25035)
tables, from the live Census Data API (
api.census.gov). This needs a free API key — see Run it. - Tract geometry: the Census Bureau's 2023 cartographic boundary file
(
www2.census.gov, TIGER/GENZ2023, 1:500k), which uses 2020 census-tract boundaries — the same tract vintage the ACS 5-year estimates are tabulated on.
fetch_data.py Build data/boston_tracts.geojson from the Census API + census.gov geometry
plots.py Render the two static figures into outputs/
app.py Interactive Streamlit version of both figures
data/boston_tracts.geojson Processed, per-tract dataset (committed; app reads this)
pip install -r requirements.txt
# (optional) rebuild the processed dataset from census.gov.
# Needs a free Census API key in a local .env file:
# echo "CENSUS_API_KEY=your_key_here" > .env
# Request one at https://api.census.gov/data/key_signup.html
python fetch_data.py
# regenerate the static PNGs
python plots.py
# interactive app
streamlit run app.pydata/boston_tracts.geojson is committed, so plots.py and app.py work
without re-running fetch_data.py (and without an API key). The key is only
needed to rebuild the dataset.
- YWR = children under 18 per working-age woman (18–64). It is a migration-and-fertility composite, not a pure birth rate — it captures where families raise children.
- Average home age uses B25034 year-built bracket midpoints (the open
"1939 or earlier" bracket is assigned 1925) — a true unit-weighted average,
matching the article's wording. The ACS median-year-built field (B25035) is
also pulled and stored (
median_year_built) for reference. - Tracts are coarser than the article's 1 km² grid, and this shows a single time point rather than the 2000→2020 change the essay also analyzes.
This section is a handoff note so a brand-new session (or person) can continue without any prior context. It records the goal, the decisions made, the environment quirks discovered, and exactly how the data was obtained.
Reproduce, for Boston, the plots in the IFStudies essay "Where Have All the Families Gone? Aging Empty Nesters and Housing Lock-In" (Colin Williams). The essay has three figures: (1) a YWR map of two metros, (2) YWR vs. average home age (a "hump" curve, 2000 vs 2020), (3) % change in YWR 2000→2020 by home age.
- Latest time point only — no cross-time comparison. So figure (3), which needs two periods, was dropped. We build only the map + the YWR-vs-home-age curve, for one vintage.
- Python + Streamlit for the interactive viz.
- Push directly to
main, no pull requests. (This overrides the default dev-branch workflow. The unused branchclaude/boston-family-housing-plots-vmolbxwas created by the harness but is not used.) - Geography = Massachusetts portion of the Boston-Cambridge-Newton MSA (Suffolk, Middlesex, Norfolk, Essex, Plymouth). NH counties (Rockingham, Strafford) are in the MSA but were excluded (the MA core is recognizably "Boston"). With the live Census API this is now just a county filter, so adding NH would be straightforward.
Everything comes straight from the U.S. Census Bureau (census.gov). An earlier
build of this project ran in an environment where census.gov was network-blocked
and used public S3/GitHub mirrors instead; that workaround has been removed.
- ACS data: the live Census Data API (
api.census.gov). Requires a free API key — stored in a local.envfile asCENSUS_API_KEY=...(gitignored, never committed), loaded withpython-dotenv. Request one at https://api.census.gov/data/key_signup.html - Geometry: the Census Bureau cartographic boundary files at
www2.census.gov/geo/tiger/GENZ2023/...
Note: the Census API returns the missing/invalid key HTML page on the first
request if the key was just issued/activated; a retry succeeds. The fetcher uses
urllib and follows redirects, so a hard failure surfaces as a JSON-decode error
on the HTML error page.
We hit the Census Data API, ACS 2019–2023 5-year (acs/acs5), tract
level, all of Massachusetts in one call per table (then filter to the 5 metro
counties). Variables are pulled with &get=, capped at 50 per call (we use ~22
and ~12). ACS marks suppressed cells with large negative "jam" values, which the
fetcher coerces to NaN.
- B01001 (Sex by Age):
B01001_001Etotal pop (→pop), children + women cells. - B25034 (Year Structure Built) brackets
_002E.._011E+_001Etotal units, and B25035_001E median year built (now populated on the live API).
Endpoint shape:
https://api.census.gov/data/2023/acs/acs5?get=<vars>&for=tract:*&in=state:25&key=<KEY>
Column → metric mapping (see fetch_data.py for the authoritative version):
- children under 18 = male
B01001_003E..006E+ femaleB01001_027E..030E - working-age women 18–64 = female
B01001_031E..043E - YWR = children_u18 / women_18_64
- avg year built = unit-weighted mean of B25034 bracket midpoints (2020.5, 2014.5, 2004.5, 1994.5, 1984.5, 1974.5, 1964.5, 1954.5, 1944.5, 1925.0 for the open "1939 or earlier" bracket)
- home_age = 2023 − avg_year_built
Tract geometry: Census Bureau 2023 cartographic boundary file
https://www2.census.gov/geo/tiger/GENZ2023/shp/cb_2023_25_tract_500k.zip
(read directly with geopandas.read_file; columns include GEOID, NAMELSAD,
COUNTYFP). fetch_data.py caches it to data/ma_tracts_2023.geojson on first
run; the cache is gitignored. Population/housing counts come from the ACS, not the
geometry file.
fetch_data.py→data/boston_tracts.geojson(1028 tracts; YWR valid for 1013, home_age for 1012), built live fromcensus.gov. Committed.plots.py→outputs/ywr_map_boston.png,outputs/ywr_vs_homeage_boston.png. Committed. Both visually match the article (red urban core / green suburbs; hump peaking ~50-yr-old homes).app.py→ Streamlit app: an interactive Leaflet/folium choropleth (streamlit-folium) where hovering a tract highlights it and shows its name + stats; the plotly "hump" curve; and two sortable tables (top-10 by YWR and the full raw dataset, sorted by YWR, re-sortable on any column). The static matplotlib map lives only inplots.pynow (the committed PNGs).
- Add the NH portion of the Boston MSA — now just pull
cb_2023_33_tract_500kgeometry and add counties 015/017 to the county filter. - Bump the vintage when a newer ACS 5-year is released (change
ACS_YEAR). - Optional: the article's 2000→2020 change figure (would need a second vintage, e.g. ACS 2008–2012, plus harmonizing tract boundaries).
pip install -r requirements.txt
echo "CENSUS_API_KEY=your_key_here" > .env # https://api.census.gov/data/key_signup.html
python fetch_data.py # rebuilds data/boston_tracts.geojson live from census.gov
python plots.py # regenerates the two PNGs in outputs/
streamlit run app.py # interactive app
