reproducibility.md
in the project root.
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.
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 usepipdirectly. - 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
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
CENSUS_API_KEY). The pipeline reads this variable
automatically at runtime.
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 values02_fetch_employment.py— County Business Patterns employment data03_fetch_finance.py— Census of Governments expenditure and revenue files04_fetch_urban_area.py— UA-County relationship files (2010 and 2020 vintages)05_fetch_land_area.py— Census Gazetteer total land area files06_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
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
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) |
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 |
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.
Running the test suite
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:
- Write tests first in
tests/test_<module>.py - Create test fixtures in
data/fixtures/(small sample data, no network needed) - Implement the module in
scripts/<module>.py - Verify:
uv run pytest tests/test_<module>.py -v
Adding a new specification
To add a 7th (or Nth) specification to the ladder:
-
Open
scripts/run_regressions.pyand locate theSPEC_CONFIGSdictionary. - 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.
-
Run
uv run python scripts/20_run_regressions.pyto re-estimate all regressions including the new specification. -
Run
uv run python scripts/30_build_dashboard.pyto regenerate the dashboard with the new spec’s results.
Adding new variables
To incorporate a new independent variable into the analysis:
-
Update
config.py: Add the new variable’s API codes, item codes, or file references to the appropriate mapping dictionary. - 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.
-
Update
scripts/build_panel.py: Add the merge and computation logic for the new variable in the panel construction step. -
Update
scripts/run_regressions.py: Add the new variable to the appropriate specification configs inSPEC_CONFIGS. - 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:
-
Edit the
STATESdictionary inconfig.pyto add or remove states (with their FIPS codes). - If adding states, update the state fixed-effect dummy variables accordingly (remember to keep one state omitted as the reference category).
- Re-run the full pipeline from script 01 through script 30 to fetch data for the new states and rebuild all outputs.