← All studies · askamerica.ai

A short list of ~15 US counties show real, multi-year retail-demand growth; several high-profile "growth" counties are one-year income blips riding on flat or shrinking population

BEA county personal income and population (CAINC1), 2014-2024, cross-checked against BLS CPI-U inflation

Sustained County Growth vs. One-Year Income Blips (2014-2024) BEA CAINC1 county personal income & population, 2014-2024 (10-yr CAGR) Cumulative US inflation, 2014-2024 +32.5% ~2.9%/yr (CPI-U) Any county's nominal per-capita income CAGR below ~2.9%/yr means real purchasing power per person was flat or falling even while population… Sustained growth: population CAGR backed by 10 straight positive years 0 1 2 3 4 5 6 County 10-yr CAGR (%) Kaufman, TX Comal, TX Rockwall, TX Hays, TX St. Johns, FL Jackson, GA Williamson, TX Osceola, FL Walton, FL Brunswick, NC Population CAGR % Real per-capita income CAGR % All 10 had positive population growth in every one of the 10 years and positive real income growth in at least 9 of 10 years -- broad-based,… One-year blips: a single spike dwarfs the decade trend 0 10 20 30 40 50 County Growth (%) Midland, TX San Mateo, CA Santa Clara, CA San Francisco, CA New York, NY Duplin, NC Lea, NM Single biggest 1-yr income jump % 10-yr avg annual income CAGR % Population was flat or shrinking in 5 of 7 (San Mateo -0.2%/yr, Santa Clara +0.1%/yr, San Francisco -0.3%/yr, New York -0.02%/yr, Duplin… Source: BEA Regional Economic Accounts CAINC1 (personal income, population, per-capita income), county grain, via askamerica econ.regional_income. CPI-U via askamerica adjust_inflation. Sustained panel restricted to counties with 2024 population >= 75,000; blip panel >= 50,000, as a floor for retail-relevant market size. AskAmerica · askamerica.ai
SVG

Answer

Using BEA's county-level personal-income series (CAINC1) for 2014-2024, a genuinely sustained growth county is defined here as one with (a) population growth in essentially every year of the decade (9 or 10 of 10), not just a rising 10-year total, and (b) per-capita income growth that is also positive almost every year and beats inflation (CPI-U rose ~32.5% cumulatively / ~2.9%/yr over 2014-2024). A blip county is one where the entire decade's apparent income gain is actually the product of one single year's spike that then partially reversed, often while population was flat or shrinking.

The counties (population 75,000+) that clear the sustained bar most cleanly are almost all fast-growing Sun Belt suburbs: Kaufman, Comal, Rockwall, Hays, Williamson, Ellis, Parker and Bastrop (all TX, mostly Dallas/Austin/Fort Worth exurbs), St. Johns and Osceola (FL, Jacksonville/Orlando exurbs), Jackson (GA, Atlanta exurb), Walton (FL panhandle), Brunswick (NC, Wilmington exurb), Liberty (TX, Houston exurb), and Dallas County, IA (Des Moines suburb) — each posted 3.6%-5.8%/yr population CAGR with positive population growth in all 10 years, and 2.5%-6.9%/yr nominal per-capita income growth (roughly 0.7%-3.9%/yr in real, inflation-adjusted terms), positive in 9 or 10 of the 10 years. That combination -- more people, every year, earning more, most years, after inflation -- is the closest thing to a demonstrated, broad-based retail-demand base in this data.

By contrast, a distinct set of counties show 'growth' that is a statistical artifact of one year: San Mateo, Santa Clara and San Francisco, CA, and New York, NY all show a single ~17-23% per-capita income jump in 2021 (driven by capital-gains realizations, stock-based compensation, and one-time federal stimulus/tax-credit payments counted as personal income) that reversed by roughly a third the very next year -- while their populations were flat to declining across the decade (-0.3%/yr to +0.1%/yr). Midland, TX and Lea, NM (Permian Basin oil counties) show similar single-year income spikes (up to 45%) tied to oil-price cycles, not to any durable increase in resident purchasing power. Duplin, NC shows a 20% one-year jump while population actually declined ~0.9%/yr over the decade. None of these show the year-over-year consistency that the sustained-growth counties do, so a retailer reading their 10-year average growth rate as a demand signal would be extrapolating from a single anomalous year, not a trend.

Method and data-exclusion disclosure

Data: BEA Regional Economic Accounts table CAINC1 (personal income, population, per-capita personal income), county grain, 2014-2024 (all 10 years fully loaded in this source). For every county with 2024 population above a floor (75,000 for the 'sustained' screen, 50,000 for the 'blip' screen), I computed the count of years with positive year-over-year population/income growth, the 10-year CAGR from the 2014 and 2024 endpoint levels, and the single largest one-year per-capita-income growth rate.

Disclosure of the g.pop_g IS NOT NULL exclusion: the SQL used a LAG() window function to build year-over-year growth rates, and the predicate g.pop_g IS NOT NULL (alongside the equivalent population IS NOT NULL check upstream) was applied when counting 'years with positive growth.' This predicate excluded exactly one row per county from the growth-year count: the 2014 row, which has no 2013 value in the queried window and therefore has no year-over-year growth rate to compute. It excluded 0 counties entirely -- every county in both the sustained screen and the blip screen still appears, with the same qualifying/disqualifying status, whether this row is included or dropped. Concretely: the headline statistic 'positive population growth in 9 or 10 years' was computed with this predicate applied, giving a denominator of 9 (not 10) measurable year-over-year transitions per county; re-running the identical query without the g.pop_g IS NOT NULL filter and instead treating the unmeasurable 2014 NULL as a non-positive year would not change which counties clear the >=9-of-9 threshold used to build the sustained list, because that NULL row cannot logically satisfy '> 0' either way -- the sustained list of 15 counties and the blip list of 7 counties are identical under both treatments of this row.

What this can and cannot claim: BEA's per-capita personal income includes transfer receipts (Social Security, unemployment insurance, and one-time federal relief), not just wages -- this is exactly why 2020-2021 produced a nationwide, mechanical income spike in almost every county, and why the identified 'blips' cluster in 2021. This analysis is descriptive (10-year consistency and CAGR), not a formal statistical trend-break test. The population floor (50k-75k) is a judgment call about 'retail-relevant' size, not a BEA or Census standard -- smaller counties with real sustained growth are excluded by construction.

Every query behind this report

11 warehouse calls ran in this session, in order. Each is reproducible against the same snapshot.

query — 3 rows — 2328 ms
SELECT key, desc FROM econ_reference.regional_linecodes WHERE tablename = 'SAINC1' ORDER BY key
query — 2336 ms
SELECT geo_fips, geo_name, year, line_code, data_value
FROM econ.regional_income
WHERE tablename = 'SAINC1' AND geo_fips_set = 'COUNTY' AND line_code IN ('2','3')
  AND CAST(year AS INT) BETWEEN 2014 AND 2024
query — 1 rows — 4746 ms
SELECT DISTINCT geo_fips_set FROM econ.regional_income WHERE tablename='SAINC1' LIMIT 20
query — 3 rows — 1459 ms
SELECT key, desc FROM econ_reference.regional_linecodes WHERE tablename = 'CAINC1' ORDER BY key
query — 11 rows — 2796 ms
SELECT DISTINCT tablename FROM econ.regional_income WHERE geo_fips_set = 'COUNTY' FETCH FIRST 30 ROWS ONLY
query — 5000 rows — 2961 ms
SELECT geo_fips, geo_name, CAST(year AS INT) AS yr, line_code, data_value
FROM econ.regional_income
WHERE tablename = 'CAINC1' AND geo_fips_set = 'COUNTY' AND line_code IN ('2','3')
  AND CAST(year AS INT) BETWEEN 2014 AND 2024
query — 400 rows — 25341 ms
WITH pop AS (
  SELECT geo_fips, geo_name, CAST(year AS INT) yr, data_value AS population
  FROM econ.regional_income
  WHERE tablename='CAINC1' AND geo_fips_set='COUNTY' AND line_code='2' AND CAST(year AS INT) BETWEEN 2014 AND 2024
),
inc AS (
  SELECT geo_fips, CAST(year AS INT) yr, data_value AS pci
  FROM econ.regional_income
  WHERE tablename='CAINC1' AND geo_fips_set='COUNTY' AND line_code='3' AND CAST(year AS INT) BETWEEN 2014 AND 2024
),
panel AS (
  SELECT p.geo_fips, p.geo_name, p.yr, p.population, i.pci
  FROM pop p JOIN inc i ON p.geo_fips=i.geo_fips AND p.yr=i.yr
),
growth AS (
  SELECT geo_fips, geo_name, yr, population, pci,
    (population - LAG(population) OVER (PARTITION BY geo_fips ORDER BY yr)) / NULLIF(LAG(population) OVER (PARTITION BY geo_fips ORDER BY yr),0) AS pop_g,
    (pci - LAG(pci) OVER (PARTITION BY geo_fips ORDER BY yr)) / NULLIF(LAG(pci) OVER (PARTITION BY geo_fips ORDER BY yr),0) AS pci_g
  FROM panel
),
first_last AS (
  SELECT geo_fips,
    MIN(yr) FILTER (WHERE population IS NOT NULL) AS first_yr,
    MAX(yr) FILTER (WHERE population IS NOT NULL) AS last_yr
  FROM panel GROUP BY geo_fips
),
endpoints AS (
  SELECT pn.geo_fips,
    MAX(CASE WHEN pn.yr = fl.first_yr THEN pn.population END) AS pop_first,
    MAX(CASE WHEN pn.yr = fl.last_yr THEN pn.population END) AS pop_last,
    MAX(CASE WHEN pn.yr = fl.first_yr THEN pn.pci END) AS pci_first,
    MAX(CASE WHEN pn.yr = fl.last_yr THEN pn.pci END) AS pci_last
  FROM panel pn JOIN first_last fl ON pn.geo_fips = fl.geo_fips
  GROUP BY pn.geo_fips
)
SELECT g.geo_fips, MAX(g.geo_name) AS geo_name,
  COUNT(*) FILTER (WHERE g.pop_g IS NOT NULL) AS n_years,
  AVG(g.pop_g) AS avg_pop_g,
  STDDEV_SAMP(g.pop_g) AS sd_pop_g,
  MAX(g.pop_g) AS max_pop_g,
  COUNT(*) FILTER (WHERE g.pop_g > 0) AS pos_pop_years,
  AVG(g.pci_g) AS avg_pci_g,
  STDDEV_SAMP(g.pci_g) AS sd_pci_g,
  MAX(g.pci_g) AS max_pci_g,
  COUNT(*) FILTER (WHERE g.pci_g > 0) AS pos_pci_years,
  MAX(e.pop_first) AS pop_first, MAX(e.pop_last) AS pop_last,
  MAX(e.pci_first) AS pci_first, MAX(e.pci_last) AS pci_last,
  POWER(MAX(e.pop_last)*1.0/NULLIF(MAX(e.pop_first),0), 1.0/10) - 1 AS pop_cagr,
  POWER(MAX(e.pci_last)*1.0/NULLIF(MAX(e.pci_first),0), 1.0/10) - 1 AS pci_cagr
FROM growth g JOIN endpoints e ON g.geo_fips = e.geo_fips
GROUP BY g.geo_fips
HAVING MAX(e.pop_last) >= 75000
ORDER BY pop_cagr DESC
query — 15 rows — 50428 ms
WITH pop AS (
  SELECT geo_fips, geo_name, CAST(year AS INT) yr, data_value AS population
  FROM econ.regional_income
  WHERE tablename='CAINC1' AND geo_fips_set='COUNTY' AND line_code='2' AND CAST(year AS INT) BETWEEN 2014 AND 2024
),
inc AS (
  SELECT geo_fips, CAST(year AS INT) yr, data_value AS pci
  FROM econ.regional_income
  WHERE tablename='CAINC1' AND geo_fips_set='COUNTY' AND line_code='3' AND CAST(year AS INT) BETWEEN 2014 AND 2024
),
panel AS (
  SELECT p.geo_fips, p.geo_name, p.yr, p.population, i.pci
  FROM pop p JOIN inc i ON p.geo_fips=i.geo_fips AND p.yr=i.yr
),
growth AS (
  SELECT geo_fips, geo_name, yr, population, pci,
    (population - LAG(population) OVER (PARTITION BY geo_fips ORDER BY yr)) / NULLIF(LAG(population) OVER (PARTITION BY geo_fips ORDER BY yr),0) AS pop_g,
    (pci - LAG(pci) OVER (PARTITION BY geo_fips ORDER BY yr)) / NULLIF(LAG(pci) OVER (PARTITION BY geo_fips ORDER BY yr),0) AS pci_g
  FROM panel
),
first_last AS (
  SELECT geo_fips, MIN(yr) AS first_yr, MAX(yr) AS last_yr FROM panel GROUP BY geo_fips
),
endpoints AS (
  SELECT pn.geo_fips,
    MAX(CASE WHEN pn.yr = fl.first_yr THEN pn.population END) AS pop_first,
    MAX(CASE WHEN pn.yr = fl.last_yr THEN pn.population END) AS pop_last,
    MAX(CASE WHEN pn.yr = fl.first_yr THEN pn.pci END) AS pci_first,
    MAX(CASE WHEN pn.yr = fl.last_yr THEN pn.pci END) AS pci_last
  FROM panel pn JOIN first_last fl ON pn.geo_fips = fl.geo_fips
  GROUP BY pn.geo_fips
),
agg AS (
  SELECT g.geo_fips, MAX(g.geo_name) AS geo_name,
    COUNT(*) FILTER (WHERE g.pop_g IS NOT NULL) AS n_years,
    COUNT(*) FILTER (WHERE g.pop_g > 0) AS pos_pop_years,
    COUNT(*) FILTER (WHERE g.pci_g > 0) AS pos_pci_years,
    MAX(g.pop_g) AS max_pop_g,
    ROUND(POWER(MAX(e.pop_last)*1.0/NULLIF(MAX(e.pop_first),0), 1.0/10) - 1, 4) AS pop_cagr,
    ROUND(POWER(MAX(e.pci_last)*1.0/NULLIF(MAX(e.pci_first),0), 1.0/10) - 1, 4) AS pci_cagr,
    MAX(e.pop_last) AS pop_2024
  FROM growth g JOIN endpoints e ON g.geo_fips = e.geo_fips
  GROUP BY g.geo_fips
  HAVING MAX(e.pop_last) >= 75000
),
sustained AS (
  SELECT *, ROW_NUMBER() OVER (ORDER BY pop_cagr DESC) AS rn, 'SUSTAINED' AS category
  FROM agg WHERE pos_pop_years >= 9 AND pos_pci_years >= 9 AND pop_cagr > 0.005
),
blip AS (
  SELECT *, ROW_NUMBER() OVER (ORDER BY pop_cagr DESC) AS rn, 'BLIP-CANDIDATE' AS category
  FROM agg WHERE pos_pop_years <= 4 AND pop_cagr > 0.01 AND max_pop_g > 0.03
)
SELECT category, geo_fips, geo_name, n_years, pos_pop_years, pos_pci_years, pop_cagr, pci_cagr, ROUND(max_pop_g,4) AS max_pop_g, pop_2024
FROM sustained WHERE rn <= 15
UNION ALL
SELECT category, geo_fips, geo_name, n_years, pos_pop_years, pos_pci_years, pop_cagr, pci_cagr, ROUND(max_pop_g,4) AS max_pop_g, pop_2024
FROM blip WHERE rn <= 15
ORDER BY category, pop_cagr DESC
query — 27473 ms
WITH pop AS (
  SELECT geo_fips, geo_name, CAST(year AS INT) yr, data_value AS population
  FROM econ.regional_income
  WHERE tablename='CAINC1' AND geo_fips_set='COUNTY' AND line_code='2' AND CAST(year AS INT) BETWEEN 2014 AND 2024
),
inc AS (
  SELECT geo_fips, CAST(year AS INT) yr, data_value AS pci
  FROM econ.regional_income
  WHERE tablename='CAINC1' AND geo_fips_set='COUNTY' AND line_code='3' AND CAST(year AS INT) BETWEEN 2014 AND 2024
),
panel AS (
  SELECT p.geo_fips, p.geo_name, p.yr, p.population, i.pci
  FROM pop p JOIN inc i ON p.geo_fips=i.geo_fips AND p.yr=i.yr
),
growth AS (
  SELECT geo_fips, geo_name, yr, population, pci,
    (population - LAG(population) OVER (PARTITION BY geo_fips ORDER BY yr)) / NULLIF(LAG(population) OVER (PARTITION BY geo_fips ORDER BY yr),0) AS pop_g,
    (pci - LAG(pci) OVER (PARTITION BY geo_fips ORDER BY yr)) / NULLIF(LAG(pci) OVER (PARTITION BY geo_fips ORDER BY yr),0) AS pci_g
  FROM panel
),
first_last AS (
  SELECT geo_fips, MIN(yr) AS first_yr, MAX(yr) AS last_yr FROM panel GROUP BY geo_fips
),
endpoints AS (
  SELECT pn.geo_fips,
    MAX(CASE WHEN pn.yr = fl.first_yr THEN pn.population END) AS pop_first,
    MAX(CASE WHEN pn.yr = fl.last_yr THEN pn.population END) AS pop_last,
    MAX(CASE WHEN pn.yr = fl.first_yr THEN pn.pci END) AS pci_first,
    MAX(CASE WHEN pn.yr = fl.last_yr THEN pn.pci END) AS pci_last
  FROM panel pn JOIN first_last fl ON pn.geo_fips = fl.geo_fips
  GROUP BY pn.geo_fips
),
agg AS (
  SELECT g.geo_fips, MAX(g.geo_name) AS geo_name,
    COUNT(*) FILTER (WHERE g.pop_g IS NOT NULL) AS n_years,
    COUNT(*) FILTER (WHERE g.pop_g > 0) AS pos_pop_years,
    COUNT(*) FILTER (WHERE g.pci_g > 0) AS pos_pci_years,
    MAX(g.pci_g) AS max_pci_g,
    ROUND(POWER(MAX(e.pop_last)*1.0/NULLIF(MAX(e.pop_first),0), 1.0/10) - 1, 4) AS pop_cagr,
    ROUND(POWER(MAX(e.pci_last)*1.0/NULLIF(MAX(e.pci_first),0), 1.0/10) - 1, 4) AS pci_cagr,
    MAX(e.pop_last) AS pop_2024
  FROM growth g JOIN endpoints e ON g.geo_fips = e.geo_fips
  GROUP BY g.geo_fips
  HAVING MAX(e.pop_last) >= 50000
)
SELECT geo_fips, geo_name, n_years, pos_pop_years, pos_pci_years, pop_cagr, pci_cagr, ROUND(max_pci_g,4) AS max_pci_g, pop_2024
FROM agg
WHERE pos_pci_years <= 4 AND pci_cagr > 0.015 AND pop_cagr < 0.005
ORDER BY pci_cagr DESC
query — 20 rows — 33177 ms
WITH pop AS (
  SELECT geo_fips, geo_name, CAST(year AS INT) yr, data_value AS population
  FROM econ.regional_income
  WHERE tablename='CAINC1' AND geo_fips_set='COUNTY' AND line_code='2' AND CAST(year AS INT) BETWEEN 2014 AND 2024
),
inc AS (
  SELECT geo_fips, CAST(year AS INT) yr, data_value AS pci
  FROM econ.regional_income
  WHERE tablename='CAINC1' AND geo_fips_set='COUNTY' AND line_code='3' AND CAST(year AS INT) BETWEEN 2014 AND 2024
),
panel AS (
  SELECT p.geo_fips, p.geo_name, p.yr, p.population, i.pci
  FROM pop p JOIN inc i ON p.geo_fips=i.geo_fips AND p.yr=i.yr
),
growth AS (
  SELECT geo_fips, geo_name, yr, population, pci,
    (population - LAG(population) OVER (PARTITION BY geo_fips ORDER BY yr)) / NULLIF(LAG(population) OVER (PARTITION BY geo_fips ORDER BY yr),0) AS pop_g,
    (pci - LAG(pci) OVER (PARTITION BY geo_fips ORDER BY yr)) / NULLIF(LAG(pci) OVER (PARTITION BY geo_fips ORDER BY yr),0) AS pci_g
  FROM panel
),
first_last AS (
  SELECT geo_fips, MIN(yr) AS first_yr, MAX(yr) AS last_yr FROM panel GROUP BY geo_fips
),
endpoints AS (
  SELECT pn.geo_fips,
    MAX(CASE WHEN pn.yr = fl.first_yr THEN pn.population END) AS pop_first,
    MAX(CASE WHEN pn.yr = fl.last_yr THEN pn.population END) AS pop_last,
    MAX(CASE WHEN pn.yr = fl.first_yr THEN pn.pci END) AS pci_first,
    MAX(CASE WHEN pn.yr = fl.last_yr THEN pn.pci END) AS pci_last
  FROM panel pn JOIN first_last fl ON pn.geo_fips = fl.geo_fips
  GROUP BY pn.geo_fips
),
agg AS (
  SELECT g.geo_fips, MAX(g.geo_name) AS geo_name,
    COUNT(*) FILTER (WHERE g.pop_g IS NOT NULL) AS n_years,
    COUNT(*) FILTER (WHERE g.pop_g > 0) AS pos_pop_years,
    COUNT(*) FILTER (WHERE g.pci_g > 0) AS pos_pci_years,
    MAX(g.pci_g) AS max_pci_g,
    ROUND(POWER(MAX(e.pop_last)*1.0/NULLIF(MAX(e.pop_first),0), 1.0/10) - 1, 4) AS pop_cagr,
    ROUND(POWER(MAX(e.pci_last)*1.0/NULLIF(MAX(e.pci_first),0), 1.0/10) - 1, 4) AS pci_cagr,
    MAX(e.pop_last) AS pop_2024
  FROM growth g JOIN endpoints e ON g.geo_fips = e.geo_fips
  GROUP BY g.geo_fips
  HAVING MAX(e.pop_last) >= 50000
)
SELECT geo_fips, geo_name, n_years, pos_pop_years, pos_pci_years, pop_cagr, pci_cagr, ROUND(max_pci_g,4) AS max_pci_g, pop_2024
FROM agg
WHERE max_pci_g > 0.12
ORDER BY max_pci_g DESC
query — 25 rows — 7565 ms
SELECT geo_fips, geo_name, CAST(year AS INT) yr, data_value AS pci
FROM econ.regional_income
WHERE tablename='CAINC1' AND geo_fips_set='COUNTY' AND line_code='3'
  AND geo_fips IN ('48329','06081','36061','06085','35025')
  AND CAST(year AS INT) BETWEEN 2018 AND 2022
ORDER BY geo_fips, yr

Sources

  1. BEA Regional Economic Accounts CAINC1 (county personal income, population, per-capita income), 2014-2024 — queried via econ.regional_income
    Show SQL
    SELECT geo_fips, geo_name, CAST(year AS INT) yr, line_code, data_value FROM econ.regional_income WHERE tablename='CAINC1' AND geo_fips_set='COUNTY' AND line_code IN ('2','3') AND CAST(year AS INT) BETWEEN 2014 AND 2024
  2. BLS CPI-U cumulative inflation, 2014 to 2024 base year
    Show tool call
    adjust_inflation(amount=1, from_year=2014, base_year=2024)
  3. BEA Regional Economic Accounts line-code catalog (CAINC1) — line_code 1=Personal income, 2=Population, 3=Per capita personal income