Skip to content

Repository files navigation

Where Have All the Families Gone? — Boston Metro

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":

  1. A choropleth of the Youth-to-Woman Ratio (YWR) by neighborhood.
  2. YWR vs. the average age of a neighborhood's homes — the lifecycle "hump."

map curve

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.

What this is (and isn't)

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).

Data sources

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.

Layout

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)

Run it

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.py

data/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.

Definitions & caveats

  • 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.

Appendix: Session context (resume with zero background)

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.

Goal

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.

Decisions already made (by the user, mid-session)

  • 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 branch claude/boston-family-housing-plots-vmolbx was 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.

Data access (current)

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 .env file as CENSUS_API_KEY=... (gitignored, never committed), loaded with python-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.

Where the data actually comes from

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_001E total pop (→ pop), children + women cells.
  • B25034 (Year Structure Built) brackets _002E.._011E + _001E total 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 + female B01001_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.

Current state — DONE

  • fetch_data.pydata/boston_tracts.geojson (1028 tracts; YWR valid for 1013, home_age for 1012), built live from census.gov. Committed.
  • plots.pyoutputs/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 in plots.py now (the committed PNGs).

Possible next steps (not yet done)

  • Add the NH portion of the Boston MSA — now just pull cb_2023_33_tract_500k geometry 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).

Reproduce from scratch

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

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages