Reproducibility Guide

Urban Sprawl & the Cost of Public Services — Step-by-step instructions for reproducing the full analysis pipeline.

← Dashboard Methodology
AI agents: For a comprehensive step-by-step walkthrough optimized for automated reproduction, see reproducibility.md in the project root.
1. Overview

This project is a modern replication of Carruthers & Ulfarsson (2003), “Urban Sprawl and the Cost of Public Services,” which studied how urban development patterns relate to per capita public expenditure across metropolitan counties in 14 fast-growing US states. The original study used 1982–1992 data; this replication updates the analysis to the 2012–2022 period using Census Bureau data.

Key finding: The density–spending relationship identified in the original paper is specification-sensitive. Whether higher density is associated with lower per capita costs depends on the choice of fixed effects, the treatment of zero-expenditure observations, the inclusion of revenue controls, and the addition of demographic demand variables. The 6-specification ladder reveals which results are robust and which are artifacts of particular modeling choices.

What this repository produces

  • Analysis panel: A county-year panel dataset (1,148 observations across 394 counties, 74 columns) combining expenditure, revenue, demographic, employment, land area, and governance data for metropolitan counties in 14 states across 2012, 2017, and 2022.
  • 104 regressions: 72 main regressions (6 specifications × 12 expenditure categories) plus 32 robustness checks (4 checks × 4 categories × 2 specs).
  • Interactive dashboard: A self-contained HTML dashboard with coefficient comparison charts, specification details, and robustness results.
2. Setup

Prerequisites

  • Python 3.12+ — Required for the analysis scripts. The project uses modern Python features (type unions, match statements).
  • uv package manager — All Python commands must be run through uv. Never install packages to system Python or use pip directly.
  • Census API key — Required for fetching data from the Census Bureau APIs (ACS 5-Year, County Business Patterns).

Get a Census API key

Request a free API key from the Census Bureau at https://api.census.gov/data/key_signup.html. The key will be emailed to you within minutes.

Installation

# Set your Census API key as an environment variable
export CENSUS_API_KEY=your_key_here

# Clone the repository
git clone <repository-url>
cd urban-sprawl-update

# Install all dependencies via uv
uv sync
Important: Never hard-code your Census API key in source files. Always set it as an environment variable (CENSUS_API_KEY). The pipeline reads this variable automatically at runtime.
3. Running the Pipeline

The pipeline consists of numbered scripts that must be run in order. Each stage reads from the previous stage’s output and writes to a well-defined location.

Stage 1: Fetch raw data (Scripts 01–06)

Scripts 01 through 06 fetch raw data from Census Bureau APIs and bulk download files. All raw data is saved to data/raw/. This stage requires an active internet connection and a valid CENSUS_API_KEY.

  • 01_fetch_demographics.py — ACS population, poverty, age composition, property values
  • 02_fetch_employment.py — County Business Patterns employment data
  • 03_fetch_finance.py — Census of Governments expenditure and revenue files
  • 04_fetch_urban_area.py — UA-County relationship files (2010 and 2020 vintages)
  • 05_fetch_land_area.py — Census Gazetteer total land area files
  • 06_fetch_governance.py — Census of Governments organization files (government counts)

Stage 2: Build panel (Script 10)

Script 10_build_panel.py merges all raw data sources into a single analysis panel at data/final/analysis_panel.parquet. The panel contains approximately 1,148 rows (county-year observations) and 74 columns. CPI deflation is applied before per-capita computation. Government finance amounts (stored in thousands in the raw files) are multiplied by 1,000 during this step.

Stage 3: Run regressions (Script 20)

Script 20_run_regressions.py estimates all 104 regressions and writes results to data/final/regression_results.json. This includes 72 main regressions (6 specifications × 12 expenditure categories) and 32 robustness checks (4 checks × 4 categories × 2 specs).

Stage 4: Build dashboard (Script 30)

Script 30_build_dashboard.py reads the regression results JSON and generates a self-contained interactive HTML dashboard at output/dashboard.html.

Quick Start

Run the full pipeline with these commands:
uv run python scripts/01_fetch_demographics.py
uv run python scripts/02_fetch_employment.py
uv run python scripts/03_fetch_finance.py
uv run python scripts/04_fetch_urban_area.py
uv run python scripts/05_fetch_land_area.py
uv run python scripts/06_fetch_governance.py
uv run python scripts/10_build_panel.py
uv run python scripts/20_run_regressions.py
uv run python scripts/30_build_dashboard.py
4. Data Dictionary

The analysis panel (data/final/analysis_panel.parquet) contains 74 columns grouped by type. All dollar-denominated variables are in real 2022 dollars (CPI-deflated). Per capita variables are computed as value / population.

Identifiers

Column Type Description
state_fips str 2-digit state FIPS code (zero-padded)
county_fips str 3-digit county FIPS code (zero-padded)
year int Panel year (2012, 2017, or 2022)
county_id str 5-digit combined FIPS (state_fips + county_fips)
county_name str County name
state_name str State name

Expenditure per capita (12 categories)

Column Description
per_capita_total_direct Total direct expenditure per capita
per_capita_capital_facilities Capital facilities expenditure per capita
per_capita_roadways Roadways expenditure per capita
per_capita_other_transportation Other transportation expenditure per capita
per_capita_sewerage Sewerage expenditure per capita
per_capita_trash_collection Trash collection expenditure per capita
per_capita_housing_community_dev Housing and community development expenditure per capita
per_capita_police_protection Police protection expenditure per capita
per_capita_fire_protection Fire protection expenditure per capita
per_capita_parks_recreation Parks and recreation expenditure per capita
per_capita_education Education expenditure per capita
per_capita_libraries Libraries expenditure per capita

Revenue per capita (2)

Column Description
per_capita_local_tax_revenue Local tax revenue per capita
per_capita_intergov_revenue Intergovernmental revenue per capita

Density & land

Column Description
density (population + employment) / urban_land_acres
urban_land_acres Urbanized land area in acres (time-varying: 2010 or 2020 UA vintage)
total_land_area_sqmi Total county land area in square miles
pct_urbanized Fraction of county land area that is urbanized

Demographics

Column Description
population Total population (ACS 5-Year)
employment Total employment (County Business Patterns)
poverty_rate Share of population below the poverty line
pct_under_18 Share of population under 18 years of age
pct_65_plus Share of population aged 65 and over

Property

Column Description
agg_property_value Aggregate owner-occupied property value (real 2022$)
property_value_per_acre Aggregate property value / urban_land_acres

Governance

Column Description
municipal_count Number of municipal governments in the county
special_district_count Number of special district governments in the county
per_capita_municipal_govts Municipal governments per 1,000 residents
per_capita_special_districts Special districts per 1,000 residents
central_city 1 if county contains a CBSA principal city, 0 otherwise

Fixed effects

Column Description
state_AZ, state_CA, state_CO, state_FL, state_GA, state_ID, state_NV, state_NM, state_NC, state_OR, state_TN, state_UT, state_WA 13 state dummy variables (TX omitted as reference category)
year_2017 Year dummy for 2017 (2012 omitted as reference)
year_2022 Year dummy for 2022 (2012 omitted as reference)
5. The Specification Ladder

We estimate six progressively different specifications for each of the 12 expenditure categories (72 regressions total). This “spec ladder” approach reveals how sensitive the density–spending relationship is to modeling choices and helps distinguish robust findings from specification-dependent artifacts.

Spec 1 — Baseline (Original Paper Match)

Fixed effects: State FE (13 dummies, TX omitted) + Year FE

Standard errors: White’s HC1 heteroscedasticity-consistent

Dependent variable: ln(per capita expenditure), dropping zeros

Revenue included: Yes

Extra controls: None

Motivation: Direct comparison with the original 2003 findings.

Spec 2 — County Fixed Effects

Fixed effects: County FE + Year FE

Standard errors: Clustered by county

Dependent variable: ln(per capita expenditure), dropping zeros

Revenue included: Yes

Extra controls: None

Motivation: Absorbs all time-invariant county characteristics. Tests whether the cross-sectional density effect survives within-county identification.

Spec 3 — Need/Demand Controls

Fixed effects: County FE + Year FE

Standard errors: Clustered by county

Dependent variable: ln(per capita expenditure), dropping zeros

Revenue included: Yes

Extra controls: poverty_rate, pct_under_18, pct_65_plus

Motivation: Separates cost pressure from service demand. Counties with higher poverty or larger elderly/youth populations may spend more regardless of density.

Spec 4 — Zero-Inclusive (log1p)

Fixed effects: County FE + Year FE

Standard errors: Clustered by county

Dependent variable: ln(1 + per capita expenditure), retaining zeros

Revenue included: Yes

Extra controls: poverty_rate, pct_under_18, pct_65_plus

Motivation: Avoids sample selection bias from dropping zero-expenditure observations, particularly important for libraries, trash collection, and other transportation.

Spec 5 — Revenue Exclusion

Fixed effects: County FE + Year FE

Standard errors: Clustered by county

Dependent variable: ln(per capita expenditure), dropping zeros

Revenue included: No

Extra controls: poverty_rate, pct_under_18, pct_65_plus

Motivation: Revenue and expenditure are jointly determined through the budget identity. Excluding revenue tests how much the density estimate depends on conditioning on an endogenous variable.

Spec 6 — State FE with Modern Controls

Fixed effects: State FE (13 dummies, TX omitted) + Year FE

Standard errors: HC1

Dependent variable: ln(per capita expenditure), dropping zeros

Revenue included: Yes

Extra controls: poverty_rate, pct_under_18, pct_65_plus

Motivation: Shows what the original paper’s specification would look like with additional demographic controls. Helps disentangle whether differences between Spec 1 and Spec 3 come from the change in fixed effects or the addition of demographics.

Summary comparison

Spec FE SE DV Revenue Demographics
1 State HC1 ln(y) Yes No
2 County Cluster ln(y) Yes No
3 County Cluster ln(y) Yes Yes
4 County Cluster ln(1+y) Yes Yes
5 County Cluster ln(y) No Yes
6 State HC1 ln(y) Yes Yes
6. Robustness Checks

Four robustness checks are applied to two key specifications (Spec 1: state FE + HC1, and Spec 3: county FE + clustered SEs + demographic controls) across four expenditure categories central to the sprawl–cost debate: Total Direct, Roadways, Sewerage, and Police Protection. Each check produces 8 regressions (4 categories × 2 specs), for a total of 32 robustness regressions.

Balanced Panel

Modification: Restrict the sample to counties present in all three panel years (2012, 2017, 2022).

What it addresses: Ensures that composition changes in the unbalanced panel — counties entering or leaving the sample across years — do not drive the estimated density–spending relationship. If results change substantially, it signals that the unbalanced panel includes systematically different counties across years.

Minimum Urban Land Threshold (640 acres)

Modification: Exclude counties with fewer than 640 urban acres (1 square mile) of urbanized land.

What it addresses: Counties with very small urban footprints produce extreme density values because the denominator of the density ratio is tiny. These outlier observations can exert disproportionate leverage on OLS estimates. Removing them tests whether the density coefficient is driven by these high-leverage, small-footprint counties.

Winsorized Density (1st/99th percentile)

Modification: Winsorize the density variable at the 1st and 99th percentiles, capping extreme values rather than removing observations.

What it addresses: Tests whether extreme density outliers at either tail are driving the estimated relationship. Unlike the minimum urban land threshold, winsorization retains all observations but limits the influence of extreme values. If the coefficient changes substantially, the main result is sensitive to the tails of the density distribution.

Static UA Vintage

Modification: Use 2020 Urban Area boundaries for all panel years, making urban land area time-invariant rather than assigning 2010 UA data to 2012/2017 and 2020 UA data to 2022.

What it addresses: Tests whether results depend on the time-varying land area assignment. Under county fixed effects, a time-invariant land area is absorbed by the county dummy, so the density variable varies only through population and employment changes. This check reveals how much of the county-FE density effect comes from changes in the built environment versus changes in the numerator alone.

7. Verification & Testing

Running the test suite

uv run pytest tests/ -v

The project includes 393 automated tests that collectively verify every stage of the pipeline. All tests can run without network access or a Census API key because they use fixture data stored in data/fixtures/.

What the tests cover

Test Area Description
Data fetching fixtures Each fetch script (01–06) has tests that use small sample data in data/fixtures/ to verify parsing logic, column naming, FIPS code handling, and data type conversions without making network calls.
Panel construction Tests verify that the merge logic in script 10 correctly joins all data sources, applies CPI deflation, computes per-capita variables, creates density measures, and generates fixed-effect dummies.
Regression coefficient matching Tests verify that the regression engine produces the correct coefficient estimates, standard errors, and p-values by comparing against known results from small fixture datasets.
HTML output validation Tests verify that the dashboard and methodology page generators produce valid HTML with the expected structure, section IDs, and content.

TDD workflow

This project follows a test-driven development workflow. For every module:

  1. Write tests first in tests/test_<module>.py
  2. Create test fixtures in data/fixtures/ (small sample data, no network needed)
  3. Implement the module in scripts/<module>.py
  4. Verify: uv run pytest tests/test_<module>.py -v
8. Extending the Analysis

Adding a new specification

To add a 7th (or Nth) specification to the ladder:

  1. Open scripts/run_regressions.py and locate the SPEC_CONFIGS dictionary.
  2. Add a new entry following the existing pattern. Each spec config defines: the fixed-effects type (state or county), the standard error type (HC1 or clustered), the dependent variable transform (ln or log1p), whether revenue variables are included, and which extra control variables to add.
  3. Run uv run python scripts/20_run_regressions.py to re-estimate all regressions including the new specification.
  4. Run uv run python scripts/30_build_dashboard.py to regenerate the dashboard with the new spec’s results.

Adding new variables

To incorporate a new independent variable into the analysis:

  1. Update config.py: Add the new variable’s API codes, item codes, or file references to the appropriate mapping dictionary.
  2. Update the fetch script: If the variable comes from a new data source, create a new fetch script. If it comes from an existing source (e.g., ACS), modify the relevant fetch script to include the new API variable.
  3. Update scripts/build_panel.py: Add the merge and computation logic for the new variable in the panel construction step.
  4. Update scripts/run_regressions.py: Add the new variable to the appropriate specification configs in SPEC_CONFIGS.
  5. Write tests for each modified module before implementing changes.

Changing the sample

The 14-state sample is defined in config.py in the STATES dictionary, which maps state abbreviations to FIPS codes. To change the sample:

  1. Edit the STATES dictionary in config.py to add or remove states (with their FIPS codes).
  2. If adding states, update the state fixed-effect dummy variables accordingly (remember to keep one state omitted as the reference category).
  3. Re-run the full pipeline from script 01 through script 30 to fetch data for the new states and rebuild all outputs.