library(ipumsr)
library(tidyverse)
library(survey)
library(srvyr)
library(scales)
library(gt)
options(survey.lonely.psu = "adjust")SSI Eligibility and Participation in Texas
Purpose
This analysis estimates how many Texans are financially eligible for Supplemental Security Income (SSI) but not receiving it, and how much federal money that gap represents. Eligibility comes from 2024 ACS 1-Year data (IPUMS USA,) and enrollment comes from SSA’s December 2024 Texas caseload, since survey-reported SSI receipt can be underreported.1
This covers SSI only. SSDI requires work history and a medical determination that ACS can’t measure, so the SSDI section at the end is a receipt proxy, not an eligibility estimate. SSA application and award data cover the part of Lynn’s question about people who give up mid-process.
Parameters
Data Sources:
SSI Recipients by State and County, 2024 - caseload numbers from Dec 2024
SSI Annual Statistical Report, 2024 - Table 11 (average monthly payment by state and age), Table 31 (noncitizen recipients by state), Table 41 (recipients who work by state), Tables 61 and 67 (applications and awards by state), Table 69 (outcomes by year of application)
NCOA Benefits Participation Map – I compare my results to Urban Institute’s / NCOA’s to see if I am anywhere close to what they got for 65+ people
# acs vintage
acs_year <- 2024
# SSI federal benefit rate, monthly (using 2024 rates since I'm using 2024 ACS data)
fbr_ind <- 943
fbr_couple <- 1415
# deeming allocation for an ineligible spouse = couple FBR - individual FBR
deem_alloc <- fbr_couple - fbr_ind
# income that SSA ignores
gen_exclusion <- 20 # general income exclusion (first $20 of any income is free)
earn_exclusion <- 65 # earned income exclusion (plus first $65 of wages)
earn_rate <- 0.5 # then only half of leftover wages counts
# SSA TX caseload, December 2024 (Table 3). these are my participation numerators
ssa_tx_total <- 576630
ssa_tx_u18 <- 101147
ssa_tx_1864 <- 287181
ssa_tx_65plus <- 188302
# eligibility category, not age. everyone in the aged category is 65+, so the rest of the 65+ caseload came in through disability
ssa_tx_aged_cat <- 100912
# recipients who also receive OASDI (Old-Age, Survivors, and Disability Insurance), and total payments for the month
ssa_tx_oasdi <- 199424
ssa_tx_pay_dec <- 401953 # in thousands (dec 2024)
# TX average monthly payment by age, Dec 2024
asr_tx_avg_pay_1864 <- 718.03
asr_tx_avg_pay_65plus <- 466.59
# TX noncitizen recipients 65+, Dec 2024
asr_tx_noncit_65plus <- 39970
# TX blind and disabled recipients and those who work, Dec 2024 (all ages)
asr_tx_bd_total <- 483642
asr_tx_bd_work <- 21311
# TX applications and awards by age, 2024
asr_tx_apps_1864 <- 94465
asr_tx_apps_65plus <- 20384
asr_tx_awards_1864 <- 23028
asr_tx_awards_65plus <- 10609
# national outcomes for adults 18-64 who applied in 2019, followed to a final decision
asr_out_total <- 1295675
asr_out_pending <- 3794
asr_out_tech_den <- 177415
asr_out_med_den <- 606333 + 4163
asr_out_sub_den <- 79077 # allowed medically, denied afterward on income or resources
asr_out_awards <- 424893
# same table, 2022 applicants still undecided as of Dec 2024
asr_out22_total <- 1011037
asr_out22_pending <- 134774
# NCOA figures for external benchmark validation (2023 ACS 1-year, TX, age 65+)
urban_tx_eligible <- 501510
urban_tx_enrolled <- 182048
urban_tx_part_rate <- 0.363SSA caseload
Who is already on SSI in Texas, and what SSA pays them. I use the age columns as my numerators and the payment and OASDI columns as benchmarks to check my simulation against later on.
# benchmarks
ssa_disab_65plus <- ssa_tx_65plus - ssa_tx_aged_cat
enrolled_adult <- ssa_tx_1864 + ssa_tx_65plus
ssa_avg_payment_adult <- (ssa_tx_1864 * asr_tx_avg_pay_1864 +
ssa_tx_65plus * asr_tx_avg_pay_65plus) / enrolled_adult
ssa_oasdi_share <- ssa_tx_oasdi / ssa_tx_total
tibble(
Group = c("Under 18", "18-64", "65+", "Total"),
Recipients = c(ssa_tx_u18, ssa_tx_1864, ssa_tx_65plus, ssa_tx_total)
) |>
mutate(Share = Recipients / ssa_tx_total) |>
gt() |>
fmt_number(Recipients, decimals = 0) |>
fmt_percent(Share, decimals = 1) |>
tab_header(
title = "Texas SSI recipients, December 2024",
subtitle = "Federally administered payments only"
)| Texas SSI recipients, December 2024 | ||
| Federally administered payments only | ||
| Group | Recipients | Share |
|---|---|---|
| Under 18 | 101,147 | 17.5% |
| 18-64 | 287,181 | 49.8% |
| 65+ | 188,302 | 32.7% |
| Total | 576,630 | 100.0% |
Two things I want to flag here:
87,390 of the 188,302 recipients 65+ (46%) qualified as blind or disabled, not aged. So most older recipients aged into the program with a disability rather than becoming poor at 65. This is also why I use the age column and not the category column as my numerator, since my denominator counts everyone 65+ no matter how they got on.
Texas already draws $402.0 million a month, or about $4.8 billion a year. Useful for putting whatever gap I find in perspective.
Load and clean
ddi <- read_ipums_ddi("usa_00067.xml")
raw <- read_ipums_micro(ddi)
names(raw) <- toupper(names(raw))Cleaning steps that change results if skipped:
Group quarters: Urban’s universe is the household population, and institutionalized SSI recipients are excluded from their eligibility counts.2 I decided to do the same, since I use the NCOA figures as a benchmark comparison. I can always change this later on, though.
“Although the ACS surveys people living in group quarters (e.g., nursing homes, dormitories, military barracks, correctional facilities, and so on) in addition to surveying households, ATTIS operates only on the household population.” - NCOA Urban Institute report
INCSUPP: this variable should not be included in countable income. SSI is not countable income for SSI purposes, and it is our outcome variable. This is why income is built from components rather than fromINCTOT.
inc_vars <- c("INCSUPP", "INCSS", "INCEARN", "INCWELFR",
"INCRETIR", "INCINVST", "INCOTHER", "INCTOT")
tx <- raw |>
filter(
STATEFIP == 48,
GQ %in% c(1, 2, 5) # hh pop only
) |>
zap_labels() |>
# N/A and unknown codes
mutate(
INCTOT = na_if(na_if(INCTOT, 9999999), 9999998),
INCINVST = na_if(INCINVST, 999999),
INCRETIR = na_if(INCRETIR, 999999),
INCSS = na_if(INCSS, 99999),
INCWELFR = na_if(INCWELFR, 99999),
INCSUPP = na_if(INCSUPP, 99999),
INCOTHER = na_if(na_if(INCOTHER, 99999), 99998)
) |>
# N/A = under 15, outside ACS income universe
mutate(across(all_of(inc_vars), \(x) replace_na(x, 0)))
stopifnot(
nrow(tx) == 278666,
max(tx$INCTOT) < 9999998,
max(tx$INCINVST) < 999999,
max(tx$INCRETIR) < 999999,
max(tx$INCSS) < 99999,
max(tx$INCWELFR) < 99999,
max(tx$INCSUPP) < 99999,
max(tx$INCOTHER) < 99998
)
nrow(tx)[1] 278666
Income components
Methods constraint to think about: ACS collects annual income over a rolling past 12 months window and SSI is determined monthly. Here I divide by 12 to get an average month. This is a limitation because someone whose income varies month by month can be eligible in some months and not others, so it understates the number eligible at some point in the year.
tx <- tx |>
mutate(
# earned income (wages + self employment)
earned_m = INCEARN / 12,
# unearned income (INCSUPP is excluded)
unearned_m = (INCSS + INCWELFR + INCRETIR + INCINVST + INCOTHER) / 12,
# survey reported SSI receipt (underreporting diagnostics only)
reports_ssi = INCSUPP > 0
)Countable income function
This part implements the SSI sequence (the general exclusion applies to unearned income first, any remainder carries to earned income, then the earned exclusion, then half of what is left.)
countable_income <- function(unearned, earned) {
# general exclusion against unearned income first
unearned_ct <- pmax(0, unearned - gen_exclusion)
gen_left <- pmax(0, gen_exclusion - unearned)
# remaining general exclusion + earned exclusion, then half the rest
earned_ct <- pmax(0, earned - earn_exclusion - gen_left) * earn_rate
unearned_ct + earned_ct
}Categorical eligibility
SSI requires age 65+, blindness, or disability.3
ACS can’t measure SSA’s disability standard (see 5 step eligibility process). So, I use a conservative proxy built from the survey’s disability questions and treat the under-65 estimate as an upper bound.
IPUMS DIFF* coding: 0 = N/A, 1 = no difficulty, 2 = has difficulty.4 5 6 7 8 9
tx <- tx |>
mutate(
# restrictive (limitations most predictive of inability to work --I can change these too if necessary! wasn't too sure which ones to include)
disab_strict = DIFFCARE == 2 | DIFFMOB == 2 | DIFFREM == 2,
# broad (any of the six ACS disability questions)
disab_broad = DIFFCARE == 2 | DIFFMOB == 2 | DIFFREM == 2 |
DIFFPHYS == 2 | DIFFEYE == 2 | DIFFHEAR == 2,
aged = AGE >= 65,
categorical = aged | (AGE >= 18 & AGE < 65 & disab_strict)
)Noncitizen restrictions
The largest single source of overcount in any Texas SSI estimate. SSI is limited to citizens and certain qualified noncitizens, with a five-year bar and rules.10 ACS records citizenship but not legal status, so noncitizens who qualify cannot be identified. This is where I use the observable screen and carry a sensitivity scenario.
IPUMS CITIZEN: 0 = born in US, 1 = born abroad to American parents, 2 = naturalized, 3 = not a citizen.11
tx <- tx |>
mutate(
noncitizen = CITIZEN == 3,
yrs_in_us = acs_year - YRIMMIG,
recent_entry = noncitizen & !is.na(YRIMMIG) & yrs_in_us < 5,
# baseline (exclude only barred)
citizen_ok = !recent_entry,
# conservative sensitivity (exclude all noncitizens)
citizen_ok_strict = !noncitizen
)Assistance units and spousal deeming
SSI is determined for individuals or couples, not households. Married couples where both members are 65+ or disabled file jointly against the couple FBR. Where one spouse is ineligible, a portion of their income is deemed available.12
SPLOC gives the spouse’s person number within the household; 0 means no spouse present.13
spouse_info <- tx |>
select(SERIAL, PERNUM,
sp_earned = earned_m,
sp_unearned = unearned_m,
sp_categorical = categorical) |>
rename(SPLOC = PERNUM)
tx <- tx |>
left_join(spouse_info, by = c("SERIAL", "SPLOC")) |>
mutate(
has_spouse = SPLOC > 0 & !is.na(sp_categorical),
spouse_elig = has_spouse & sp_categorical,
spouse_inelig = has_spouse & !sp_categorical,
across(c(sp_earned, sp_unearned), \(x) replace_na(x, 0))
)Deeming rule: if the ineligible spouse’s own income falls at or below the allocation (couple FBR minus individual FBR), no deeming occurs and the applicant is treated as an individual. Above that, income is combined and tested against the couple standard.14
tx <- tx |>
mutate(
sp_income = sp_earned + sp_unearned,
unit_type = case_when(
spouse_elig ~ "couple",
spouse_inelig & sp_income > deem_alloc ~ "deemed",
TRUE ~ "individual"
),
unit_earned = if_else(unit_type %in% c("couple", "deemed"),
earned_m + sp_earned, earned_m),
unit_unearned = if_else(unit_type %in% c("couple", "deemed"),
unearned_m + sp_unearned, unearned_m),
#deemed cases test and pay against the couple standard
fbr_test = if_else(unit_type %in% c("couple", "deemed"),
fbr_couple, fbr_ind),
fbr_pay = if_else(unit_type %in% c("couple", "deemed"),
fbr_couple, fbr_ind),
countable_m = countable_income(unit_unearned, unit_earned)
)No asset test
SSI’s resource limits are 2,000 dollars (individual) and 3,000 dollars (couple). ACS contains no asset data at all. Urban imputes asset values. Every eligibility count below is therefore too high, and this is the second-largest overcount after immigration status.
Eligibility determination
tx <- tx |>
mutate(
income_eligible = countable_m < fbr_test,
ssi_eligible = categorical & citizen_ok & income_eligible,
#simulated monthly benefit
benefit_m = if_else(ssi_eligible, pmax(0, fbr_pay - countable_m), 0),
#split couple payment between two eligible members so summing across people doesn't double count unit's benefit
benefit_m_person = if_else(unit_type == "couple", benefit_m / 2, benefit_m),
benefit_annual = benefit_m_person * 12,
# sensitivity variants
ssi_eligible_strict = categorical & citizen_ok_strict & income_eligible,
ssi_eligible_broad = (aged | (AGE >= 18 & AGE < 65 & disab_broad)) &
citizen_ok & income_eligible
)Survey design
Note: We need to be careful with standard errors on a subgroup this small
tx_svy <- tx |>
as_survey_rep(
weights = PERWT,
repweights = matches("^REPWTP[0-9]+$"),
type = "successive-difference",
mse = TRUE
)
stopifnot(sum(grepl("^REPWTP[0-9]+$", names(tx))) == 80)Results
Eligible is my ACS simulation, enrolled is the SSA caseload from above. Children are not here because I don’t model child eligibility (see the methods notes).
res <- tx_svy |>
filter(AGE >= 18) |>
mutate(band = if_else(AGE >= 65, "65+", "18-64")) |>
group_by(band) |>
summarise(
eligible = survey_total(ssi_eligible, vartype = "ci"),
eligible_broad = survey_total(ssi_eligible_broad),
reported_ssi = survey_total(reports_ssi),
benefits = survey_total(benefit_annual)
) |>
ungroup() |>
left_join(
tibble(band = c("65+", "18-64"),
enrolled = c(ssa_tx_65plus, ssa_tx_1864)),
by = "band"
)
# add an all adult row first then calc rates so total row stays consistent
res_tbl <- res |>
select(band, eligible, enrolled, benefits) |>
bind_rows(
res |> summarise(band = "All adults 18+",
across(c(eligible, enrolled, benefits), sum))
) |>
mutate(
participation = enrolled / eligible,
gap = eligible - enrolled,
foregone = benefits * (gap / eligible)
)
# scalars I reuse below
elig_65 <- res$eligible[res$band == "65+"]
part_65 <- ssa_tx_65plus / elig_65
u65_strict <- res$eligible[res$band == "18-64"]
u65_broad <- res$eligible_broad[res$band == "18-64"]
res_tbl |>
select(Group = band,
`Eligible (ACS)` = eligible,
`Enrolled (SSA)` = enrolled,
Participation = participation,
`Not enrolled` = gap,
`Foregone $/yr` = foregone) |>
gt() |>
fmt_number(c(`Eligible (ACS)`, `Enrolled (SSA)`, `Not enrolled`), decimals = 0) |>
fmt_percent(Participation, decimals = 1) |>
fmt_currency(`Foregone $/yr`, decimals = 1, suffixing = TRUE) |>
tab_header(
title = "SSI eligibility, enrollment, and the gap in Texas",
subtitle = paste("ACS", acs_year, "1-year simulation vs SSA December 2024 caseload")
)| SSI eligibility, enrollment, and the gap in Texas | |||||
| ACS 2024 1-year simulation vs SSA December 2024 caseload | |||||
| Group | Eligible (ACS) | Enrolled (SSA) | Participation | Not enrolled | Foregone $/yr |
|---|---|---|---|---|---|
| 18-64 | 546,861 | 287,181 | 52.5% | 259,680 | $2.2B |
| 65+ | 668,501 | 188,302 | 28.2% | 480,199 | $2.9B |
| All adults 18+ | 1,215,362 | 475,483 | 39.1% | 739,879 | $5.3B |
Counting anyone who reports any disability raises the 18-64 eligible count from 546,861 to 743,483, and participation from 52.5% to 38.6%.
The gap between the strict and broad definitions is large enough that the estimate is really driven by which disability questions we count. So if we go forward with these estimate, I definitely think we should report the range, not a single number. Urban doesn’t publish an under 65 figure, so there’s no outside estimate to check ours against (that I know of).
NOTE
People who do not enroll skew toward smaller benefit amounts (a partial $40 monthly award is worth less hassle than a full one). Applying the mean benefit of all eligibles to non-participants overstates the total. So, I think the foregone dollars column should be treated as an upper bound.
Validation
Against Urban Institute / NCOA
tibble(
Source = c("ETX analysis", "Urban Institute / NCOA (2023)"),
Year = c(acs_year, 2023),
Eligible = c(elig_65, urban_tx_eligible),
Enrolled = c(ssa_tx_65plus, urban_tx_enrolled),
`Participation rate` = c(part_65, urban_tx_part_rate)
) |>
gt() |>
fmt_number(c(Eligible, Enrolled), decimals = 0) |>
fmt_percent(`Participation rate`, decimals = 1) |>
tab_header(
title = "Benchmark check, 65+",
subtitle = "Urban applies asset and legal-status imputation; we do not"
)| Benchmark check, 65+ | ||||
| Urban applies asset and legal-status imputation; we do not | ||||
| Source | Year | Eligible | Enrolled | Participation rate |
|---|---|---|---|---|
| ETX analysis | 2024 | 668,501 | 188,302 | 28.2% |
| Urban Institute / NCOA (2023) | 2023 | 501,510 | 182,048 | 36.3% |
My eligible count should be above Urban’s (the asset test alone should make it higher).
Against SSA administrative data
chk <- tx_svy |>
filter(AGE >= 18, ssi_eligible) |>
summarise(
mean_benefit_m = survey_mean(benefit_m_person),
concurrent = survey_mean(as.numeric(INCSS > 0))
)
reported_adult <- sum(res$reported_ssi)
tibble(
Check = c("Mean monthly benefit",
"Share also receiving OASDI (SSA column all ages)",
"Number enrolled"),
Simulated = c(dollar(chk$mean_benefit_m, accuracy = 1),
percent(chk$concurrent, accuracy = 0.1),
comma(reported_adult, accuracy = 1)),
SSA = c(dollar(ssa_avg_payment_adult, accuracy = 1),
percent(ssa_oasdi_share, accuracy = 0.1),
comma(enrolled_adult, accuracy = 1))
) |>
gt() |>
tab_header(
title = "Internal checks against SSA",
subtitle = "Simulated is eligible adults 18+; SSA is actual recipients, adults 18+ except where noted"
)| Internal checks against SSA | ||
| Simulated is eligible adults 18+; SSA is actual recipients, adults 18+ except where noted | ||
| Check | Simulated | SSA |
|---|---|---|
| Mean monthly benefit | $594 | $618 |
| Share also receiving OASDI (SSA column all ages) | 37.3% | 34.6% |
| Number enrolled | 545,858 | 475,483 |
Mean monthly benefit: mine should land below SSA’s, since non-participants skew toward smaller awards. SSA’s figure is their published Texas averages for 18-64 and 65+ weighted by caseload, so children are out of both columns.
OASDI share: The simulated OASDI share is based off
INCSS, which is a proxy for OASDI. This is the one row where the two columns are not the same universe. SSA only publishes this count for all ages, and I would rather show it as published than divide it by an adult caseload it doesn’t belong to. Children are 17.5% of the caseload and almost never have OASDI, so the published share is pulled down. The share for adults is higher than 34.6% and my number does sit below it, but SSA doesn’t publish the pieces to show that cleanly.Number enrolled: the simulated column here is just the ACS survey estimate. Interestingly, the ACS estimate is higher than the SSA figure, despite some research showing that surveys tend to under report SSI income.
Earnings and the disability proxy
SSA publishes how many recipients work. Since work capacity is part of SSA’s disability determination but not of the ACS questions, the share of simulated eligible people with earnings is a check on the disability proxy.
earn <- tx_svy |>
filter(AGE >= 18, AGE < 65, ssi_eligible) |>
summarise(has_earnings = survey_mean(as.numeric(earned_m > 0)))
tibble(
Check = "Share with earned income",
Simulated = percent(earn$has_earnings, accuracy = 0.1),
SSA = percent(asr_tx_bd_work / asr_tx_bd_total, accuracy = 0.1)
) |>
gt() |>
tab_header(
title = "Earnings check",
subtitle = "Simulated is eligible adults 18-64; SSA is TX blind and disabled recipients of all ages"
)| Earnings check | ||
| Simulated is eligible adults 18-64; SSA is TX blind and disabled recipients of all ages | ||
| Check | Simulated | SSA |
|---|---|---|
| Share with earned income | 30.8% | 4.4% |
The SSA column counts children and 65+ recipients in its denominator, so the real 18-64 working rate is higher than shown. Even so the gap is wide, and I think it is a genuine feature of the eligible population rather than a bug. Earnings above $1,971 a month zero out the benefit entirely, and anything approaching that shrinks the award enough that a year-long application stops being worth the trouble. Working is a large part of what keeps this group off SSI. It also means the 18-64 eligible count leans on people whose simulated award would be small, which is the same caution as the foregone dollars column.
So, the earnings gap is mainly evidence that the ACS disability questions capture many people who work and would not meet SSA’s standard. This reinforces treating the 18–64 count as an upper bound and reporting a range.
Applications and awards
The second half of Lynn’s question is about people who give up partway through the process. ACS cannot see that at all, so everything here is SSA administrative data.
tibble(
Group = c("18-64", "65+"),
Applications = c(asr_tx_apps_1864, asr_tx_apps_65plus),
Awards = c(asr_tx_awards_1864, asr_tx_awards_65plus)
) |>
mutate(Ratio = Awards / Applications) |>
gt() |>
fmt_number(c(Applications, Awards), decimals = 0) |>
fmt_percent(Ratio, decimals = 1) |>
tab_header(
title = "Texas SSI applications and awards, 2024",
subtitle = "Awards made in 2024 come partly from applications filed in earlier years, so the ratio is not a cohort rate"
)| Texas SSI applications and awards, 2024 | |||
| Awards made in 2024 come partly from applications filed in earlier years, so the ratio is not a cohort rate | |||
| Group | Applications | Awards | Ratio |
|---|---|---|---|
| 18-64 | 94,465 | 23,028 | 24.4% |
| 65+ | 20,384 | 10,609 | 52.0% |
Following one cohort from application to final decision is only possible nationally. This is where applicants actually drop out:
decided <- asr_out_total - asr_out_pending
tibble(
Outcome = c("Denied on income or resources",
"Denied on medical grounds",
"Allowed medically, denied afterward",
"Awarded"),
Applicants = c(asr_out_tech_den, asr_out_med_den, asr_out_sub_den, asr_out_awards)
) |>
mutate(Share = Applicants / decided) |>
gt() |>
fmt_number(Applicants, decimals = 0) |>
fmt_percent(Share, decimals = 1) |>
tab_header(
title = "Outcomes for adults 18-64 who applied in 2019",
subtitle = "National, all levels, followed to a final decision"
)| Outcomes for adults 18-64 who applied in 2019 | ||
| National, all levels, followed to a final decision | ||
| Outcome | Applicants | Share |
|---|---|---|
| Denied on income or resources | 177,415 | 13.7% |
| Denied on medical grounds | 610,496 | 47.3% |
| Allowed medically, denied afterward | 79,077 | 6.1% |
| Awarded | 424,893 | 32.9% |
Note:
13.7% of applicants never reach a medical decision, and another 6.1% clear the medical standard and are denied afterward anyway. Both are income and resource denials, the same screen I already apply. So we cannot multiply my eligible count by the award rate to get the number who would actually get on, because that applies the income test twice.
Of everyone who applied in 2022, 13.3% still had no final decision as of December 2024, two years later. That is the delay Lynn is describing, and it is measurable.
SSDI receipt proxy (not an eligibility estimate)
INCSS reports how much pre-tax income (if any) the respondent received from Social Security pensions, survivors benefits, or permanent disability insurance, as well as U.S. government Railroad Retirement insurance payments, during the previous year.15 SSDI cannot be separated out.
Social Security retirement cannot begin before 62.16 So among people under 62 who are not widow(er)s, INCSS > 0 is overwhelmingly SSDI.
ssdi <- tx_svy |>
filter(AGE >= 18, AGE < 62, MARST != 5) |> # exclude widow(er)s
summarise(
likely_ssdi = survey_total(INCSS > 0, vartype = "ci"),
mean_benefit = survey_mean(if_else(INCSS > 0, INCSS, NA_real_),
na.rm = TRUE, vartype = "ci")
)
ssdi |>
pivot_longer(everything(), names_to = "measure", values_to = "estimate") |>
gt() |>
fmt_number(estimate, decimals = 0) |>
tab_header(
title = "Texans under 62 likely receiving SSDI",
subtitle = "Receipt proxy only (says nothing about who is eligible and unenrolled)"
)| Texans under 62 likely receiving SSDI | |
| Receipt proxy only (says nothing about who is eligible and unenrolled) | |
| measure | estimate |
|---|---|
| likely_ssdi | 265,206 |
| likely_ssdi_low | 248,944 |
| likely_ssdi_upp | 281,468 |
| mean_benefit | 12,385 |
| mean_benefit_low | 11,893 |
| mean_benefit_upp | 12,878 |
Sensitivity analysis
sens <- tx_svy |>
filter(AGE >= 65) |>
summarise(
baseline = survey_total(ssi_eligible, vartype = "ci"),
all_noncit_exclud = survey_total(ssi_eligible_strict, vartype = "ci")
)
sens |>
pivot_longer(everything(), names_to = "scenario", values_to = "estimate") |>
gt() |>
fmt_number(estimate, decimals = 0) |>
tab_header(title = "Sensitivity: immigration status assumptions, 65+")| Sensitivity: immigration status assumptions, 65+ | |
| scenario | estimate |
|---|---|
| baseline | 668,501 |
| baseline_low | 648,459 |
| baseline_upp | 688,543 |
| all_noncit_exclud | 562,800 |
| all_noncit_exclud_low | 544,401 |
| all_noncit_exclud_upp | 581,199 |
These two rows give a high and low estimate based only on immigration status. Two other SSI rules that the data can’t capture, the asset limit and month by month income counting, would lower the number further.
The strict row overshoots. SSA counts 39,970 noncitizen SSI recipients aged 65+ in Texas (21.2% of the 65+ caseload, double the national rate), so dropping every noncitizen removes a fifth of my numerator. The true number is inside the spread, closer to the baseline.
Methods Notes
Estimates of SSI-eligible Texans are simulated from the 2024 American Community Survey 1-year microdata (IPUMS USA), applying SSI income rules, assistance-unit construction, and spousal deeming. Enrollment counts come from Social Security Administration administrative data rather than survey self-reports, which substantially underreport SSI receipt.
The estimates are subject to the following upward bias:
- No asset test: ACS contains no asset data, and SSI’s 2,000 and 3,000 dollar resource limits disqualify people this analysis counts as eligible.17
- Immigration status: ACS records citizenship but not legal status, so qualified non-citizens rules and sponsor deeming cannot be fully applied.
- Annual accounting: SSI is determined monthly, while ACS reports income over a 12-month period.
- Disability definition: for adults under 65, ACS disability questions do not correspond to SSA’s disability standard, and the under-65 estimate should be read as an upper bound.
Two things make participation look better than it is:
- Children: 101,147 Texas SSI recipients are under 18, which is 17.5% of the caseload. Child SSI runs on parental deeming and a separate childhood disability standard, neither of which I model here, so children are out of this analysis entirely and there is no child gap estimate.
- Institutionalized recipients: my denominator is household population only, but the SSA counts I use as numerators include people in institutions. NCOA put that at about 2% of the 65+ caseload, but SSA doesn’t publish it for 18-64.
The ranges shown around each estimate account for the fact that these numbers come from a survey sample, not a full count.
Footnotes
U.S. Census Bureau, Evaluation of Social Security Reporting in the Survey of Income and Program Participation: 2017.↩︎
NCOA, Estimation of National, State, and Substate Program Participation Rates for Adults 65 and Older, 2023.↩︎
SSA, Understanding Supplemental Security Income SSI Eligibility Requirements – 2026 Edition.↩︎
AARP, How does marriage affect Supplemental Security Income?↩︎