A Quantitative Framework for Mapping Socioeconomic Mobility Barriers Using Public Census and Labor Data with Geospatial Visualization
December 2025 - March 2026
This past year (2025-2026), I was enrolled in Stanford's e-Entrepreneurship (SEUS) program. It's a cohort-based program (free of charge!) where a group of 20 U.S. high school students spend 5 months probing the question of what it actually means to build something that matters. We had venture capitalists, social workers, lawyers, and AI entrepreneurs speak with us. There were design thinking workshops, debates on UBI and public policy, and whether Silicon Valley's "move fast and break things" ethos was a feature or a bug (I maintain it's a bug dressed up in a TED Talk).
One of my own recurring takeaways across every lecture and discussion post was this: the most dangerous products are not the ones that fail. They're the ones that succeed partially, and only on a select group of people. A social enterprise that helps some communities while ignoring others isn't solving a problem. It's rearranging it. I have organized a significant portion of my life around this belief, first in DermEquity, where I spent a year documenting how AI dermatology tools produce a 6.8% higher melanoma miss rate for dark skin patients while standard accuracy metrics show "no alarm," and now in STRATUM, which is a completely different domain but, as it turns out, the exact same problem.
The Opportunity Atlas (Chetty et al., 2018) is one of the most striking datasets I have ever encountered. It maps, at the county and census tract level, the probability that a child born in the 25th percentile of the income distribution will reach the median or above by age 35. The variation is genuinely staggering. A child born in one county in Iowa has roughly the same economic prospects as a child born in Denmark. A child born in an adjacent county might have prospects closer to Honduras. And both of those counties show up as "functional" on most national economic indicators.
The data, extraordinary as it is, has the same problem which guided my previous work in AI diagnostics: you can see that something is wrong. You cannot easily see why. Is it the lack of broadband that cuts families off from remote work and online education? Is it housing cost burden trapping people in poverty through rent? Is it low educational attainment, high unemployment, income so depressed that savings are structurally impossible? Policy organizations have data on all of these things, but nobody has built an open-source, transparent, publicly accessible tool that fuses them into a decomposable score and shows you, at the county level, which specific factors are driving the barrier.
So I built one.
Cheers,
Angie X.
The academic literature on socioeconomic mobility is deep and rich. Raj Chetty, Nathaniel Hendren, and their colleagues at Harvard's Opportunity Insights lab have produced some of the most rigorous empirical economics of the last decade, demonstrating with longitudinal IRS tax records that where you grow up has a measurable and persistent effect on where you end up, and that this is driven by specific structural features of communities rather than individual characteristics.
The policy tool landscape is a different story.
The tools that exist fall into two categories. The first is proprietary and expensive: Bloomberg terminals, ESRI's ArcGIS platform, institutional data subscriptions. These require licensing and technical expertise that most county-level policymakers, nonprofit directors, and researchers outside well-funded institutions do not have. The second category is open yet siloed: the Opportunity Insights website publishes beautiful visualizations of their mobility data, the Census Bureau's data explorer lets you look up any ACS variable by geography, the Bureau of Labor Statistics publishes monthly unemployment by county. But these are separate systems. You cannot fuse them into a composite index and decompose that index to see which factors are driving a specific county's outcome. You see the what. Not the why.
There is a third failure mode: tools that report a single aggregate number and call it analysis. A county's average economic indicator looks fine. A subcohort within that county is structurally trapped. The aggregate number is technically accurate and completely misleading. I kept describing this in the context of DermEquity, where a model's average accuracy of 76.4% hid a 6.8% melanoma miss rate gap between skin tones. STRATUM is built on this very same conviction: averages hide the things that actually matter, and the job of a good tool is to surface what such averages conceal.
Project objective: Build an open-source Python tool that computes a county-level Mobility Barrier Index (MBI) across all U.S. counties, uses PCA to derive data-driven weights rather than arbitrary assignments, renders an interactive geospatial dashboard where any user can drill down to a specific county and see its factor breakdown, and is documented rigorously enough that a county commissioner, nonprofit director, or researcher could actually use it.
A good rule of my thumb is to never start coding until you understand what you're building well enough to explain it to someone who doesn't care. With DermEquity, I spent weeks reading Koh and Liang's influence functions paper, the Fitzpatrick17k dataset documentation, Hardt et al.'s fairness framework, etc. before touching a Kaggle notebook. With NEXUS, I read Wilder's original 1978 RSI paper and Harvey's 1988 yield curve dissertation before writing a single line of JavaScript. With STRATUM, I read economists.
Of course, my background in NEXUS had given me a relatively solid foundation in finance. However, economics is a slightly whole different shebang. Prior to this project, (I haven't taken AP Econ yet) my relationship with economics was shabby. I am now somewhat better informed.
On intergenerational mobility and what it actually measures: the Chetty et al. framework defines upward mobility as the expected income rank at age 35 for a child born in the 25th percentile of the national income distribution. This is longitudinal: it requires tracking people from childhood to adulthood, which is why it uses IRS records rather than surveys. The finding that this varies by a factor of almost two across U.S. counties, and that it has not improved since the 1980s despite overall economic growth, is one of the more uncomfortable empirical results in modern social science. The mobility we tell ourselves exists is, in substantial parts of the country, largely mythological.
But what drives it? The Opportunity Atlas identifies five factors at the commuting zone level: residential segregation, income inequality, local school quality, social capital, and family stability. These are correlational findings. STRATUM takes a complementary angle: rather than asking what predicts mobility empirically, it asks what structural barriers exist that prevent mobility from occurring.
On PCA and why it's more defensible than making up weights: the core methodological question in building any composite index is how to weight the components. If you say income counts for 25% and education counts for 20%, you're making a causal claim without evidence. Two reasonable researchers could defend completely different weight choices and produce completely different final scores from identical data.
PCA extracts weights from the covariance structure of the data itself. The first principal component is the direction of maximum variance across all six dimensions simultaneously. Counties that score high on one barrier tend to score high on others: the poverty rate, broadband exclusion, low income, and low education are correlated. PC1 captures this dominant pattern of co-variation, and its loadings become the weights. This is more defensible not because it's "objective" (the choice of variables still involves judgment), but because it's consistent with the data's own structure rather than someone's intuition about importance.
PC1 explaining 58% of variance in our run means that 58% of the information in all six dimensions can be captured by a single weighted composite. If PC1 explained 17%, the dimensions would be essentially uncorrelated and combining them would be meaningless.
On Moran's I and spatial autocorrelation: a key empirical question about mobility barriers is whether they cluster geographically. If high-barrier counties cluster near other high-barrier counties, it suggests regional structural causes: persistent poverty zones, deindustrialized corridors, historical disinvestment patterns. If barriers are randomly distributed, county-specific dynamics dominate.
Moran's I quantifies this. Values near +1 indicate that similar values cluster; values near 0 indicate random distribution. For U.S. county-level socioeconomic data, Moran's I typically runs between 0.4 and 0.7. STRATUM's result of 0.27 (moderate positive autocorrelation) reflects the state-level grouping approximation we use rather than full queen contiguity, which would likely produce a higher value.
Thus, the policy implication matters: geographic clustering means county-level interventions will have spillover effects on neighbors. Regional policy is warranted alongside county-specific targeting.
Before writing code, I designed the full pipeline on paper. The architecture reflects a lesson from every prior project: separation of concerns is not just good software engineering, it is survivability. When the Opportunity Insights data URLs went dead mid-build (both of them, simultaneously, which I can only describe as cosmically timed), the fallback was a one-function fix in opportunity.py that didn't touch anything else. When Alpha Vantage changed their API terms during NEXUS development, it was a one-file fix. Good architecture means you can change one thing without breaking everything adjacent to it.
Data ingestion layer (src/data/). Two modules handle all external data communication. census.py fetches ACS 5-year estimates for all 3,221 U.S. counties via the Census Bureau API (free, requires a key you can get in 2 minutes at api.census.gov/data/key_signup.html), derives rate variables from raw counts, and caches locally to avoid redundant API calls. opportunity.py attempts to download county-level mobility data from the Opportunity Insights public repository; if both known URLs are dead (as they were when I built this), it falls back to proxying mobility deficit from the poverty and income dimensions already available from Census.
Analysis layer (src/analysis/). mbi.py is the computational core: six barrier dimensions, PCA weighting, weighted composite MBI scaled to 0-100, factor contribution decomposition, and OLS factor regression for partial R-squared. spatial.py implements Moran's I using state-level grouping as a spatial proxy and a Census region summary.
Visualization layer (src/visualization/). dashboard.py builds the Plotly Dash interactive dashboard with a national choropleth map, a county-level factor breakdown panel triggered by map clicks, a national MBI distribution histogram, a Census region comparison bar chart, PCA weight visualization, and top/bottom county tables.
Entry point. stratum.py orchestrates the pipeline with a CLI (run with --report for terminal summary, --no-dashboard to skip web server).
The PCA weighting decision: this is the methodological choice I actually thought hardest about. The alternative to PCA is equal weighting (each dimension contributes 1/6 of the score) or hand-assigned weights. Equal weighting has the virtue of simplicity, but treats broadband exclusion as equally important as poverty, which may not reflect reality. Hand-assigned weights incorporate domain knowledge but are inherently subjective and hard to defend publicly.
I chose PCA for defensibility, documented the limitation explicitly (PCA weights reflect statistical co-variation, not causal importance), and included individual factor regression as a complement. The partial R-squared from a univariate regression of MBI on each dimension tells you how much that factor independently explains MBI variation, which is a different and complementary piece of information from the PCA weight.
The normalization decision: all six dimensions must be on a common scale before PCA. I used min-max normalization to [0,1], oriented so 1.0 = maximum barrier. Income is inverted (low income maps to 1.0), education is inverted, and poverty and housing burden are direct. The limitation: min-max normalization is sensitive to outliers. One county with extreme income pulls the scale for everyone else. I documented this and recommend examining outlier counties when interpreting results.
The mobility fallback: the Opportunity Insights URLs are currently dead. Rather than fail, mbi.py checks whether the upward_mobility column has any non-null values before using it; if not, it proxies mobility deficit as the average of poverty barrier and income barrier. The correlation between poverty, income, and actual Chetty mobility scores is approximately 0.7, which makes this a reasonable approximation for a framework that's transparent about its inputs.
Running STRATUM on live ACS data produces MBI scores for 3,221 U.S. counties. Several findings are consistent across runs.
PC1 explains 58% of variance across the six barrier dimensions. This validates the composite index: the six dimensions share substantial underlying structure, meaning there is a real latent construct being measured. The dominant pattern is that counties with high poverty exposure almost always also have low income, low educational attainment, and high housing cost burden. These things move together. PCA captures this co-movement and weights the dimensions by how much each contributes to it.
The regional pattern is consistent with the mobility literature: the South has substantially higher mean MBI than the Northeast or West. Rural Appalachian counties, the Mississippi Delta, and significant portions of the Deep South consistently appear among the highest-barrier counties. This reflects persistent poverty regions documented extensively in the economics literature and visible in Chetty's mobility maps.
Broadband exclusion and educational attainment consistently receive among the highest PCA weights, which is notable. Broadband has become as structurally determinative of economic opportunity as income itself. Counties without reliable internet access are systematically excluded from remote work, online education, telehealth, and e-commerce. This is a relatively recent development (the broadband era is barely 25 years old) that has compounded fast.
The factor regression produces an important divergence from the PCA weights: poverty has the highest partial R-squared (it is the single best predictor of MBI if you had to pick one), but a moderate PCA weight, because much of poverty's variance is shared with income and education. Counties with high poverty almost always have low income and low education. The PCA down-weights poverty's unique contribution because the composite already captures that information through other dimensions. Both numbers are useful for different questions.
The practical output: clicking any county on the dashboard's choropleth map produces a factor decomposition showing which specific dimensions are driving that county's MBI score. Two counties with the same MBI of 70 might look completely different in the breakdown: one driven by broadband exclusion (rural area cut off from the digital economy), another by housing cost burden (formerly affordable area experiencing rapid rent inflation). These counties need different interventions. The MBI alone doesn't tell you which is which. The decomposition does.
Here's a few of my takeaways:
On overfitting and intellectual honesty: the PCA weighting decision forced me to confront the same issue I keep running into in quantitative work- when you choose methodology by looking at your data, you are in some sense tuning your results to that data. PCA weights are not objective truths. They are descriptions of statistical co-variation in a specific dataset from a specific ACS vintage year. If I had used different variables, the weights would differ. If I had used a different year, they would again shift. The honest claim is not that PCA weights are correct. It is that they are data-consistent, and that this is a more defensible starting point than arbitrary assignment. I documented this limitation, and any system that does not is either naive or dishonest.
On composite indices and their discontents: building a composite index compels you to confront every methodological assumption you have. Which variables to include, how to normalize them, how to handle missing data, aggregate, etc. Every choice shapes the result. Therefore, the only truly honest response is to document every choice, explain the rationale, make the code fully open-source, and to acknowledge your limitations explicitly. The most important limitation of STRATUM is that MBI measures structural barriers, not causes. High housing cost burden in a county might reflect rapid economic growth (housing is expensive because jobs are arriving) rather than structural deprivation. The index treats them identically.
On the relationship between data availability and insight: everything STRATUM uses is free and public (the gap here was not the data). Stratum is a tool that combines the data and makes it accessible to non-experts. This is a pattern I have now encountered throughout all of my recent projects. The Fitzpatrick17k dataset existed before DermEquity (although the lack of diverse representation in medical datasets is a different issue entirely- I wrote about this in my first Substack article). The FRED macroeconomic API existed before NEXUS. Similarly, the Opportunity Atlas existed before STRATUM. In this specific case, the bottleneck is the methodology to use the data well and the engineering to make the methodology accessible.
STRATUM is a tool, not an answer. It can tell you that a county has a high mobility barrier and decompose that score into factors. It cannot tell you what to do about it, and it cannot definitively attribute the barrier to any single cause. What it can do is make a specific kind of question answerable that was previously not: not which counties have low mobility, but which counties have low mobility driven primarily by broadband exclusion versus housing cost burden versus educational access. Those are different places. They need different things. Making that distinction visible, freely, for every county in the country, seemed worth building.
Cheers,
Angie X.
This project is open source at github.com/axshoe/Stratum.