library(tidyverse)
library(tidycensus)
library(knitr)
census_api_key(Sys.getenv("MY_CENSUS_API_KEY"))
my_state <- "OH"Assignment 1: Census Data Quality for Policy Decisions
Evaluating Data Reliability for Algorithmic Decision-Making
Assignment Overview
Scenario
You are a data analyst for the Ohio Department of Human Services. The department is considering implementing an algorithmic system to identify communities that should receive priority for social service funding and outreach programs. Your supervisor has asked you to evaluate the quality and reliability of available census data to inform this decision.
Drawing on our Week 2 discussion of algorithmic bias, you need to assess not just what the data shows, but how reliable it is and what communities might be affected by data quality issues.
Learning Objectives
- Apply dplyr functions to real census data for policy analysis
- Evaluate data quality using margins of error
- Connect technical analysis to algorithmic decision-making
- Identify potential equity implications of data reliability issues
- Create professional documentation for policy stakeholders
Setup
State Selection: I have chosen Ohio for this analysis because it is my home state! I spent most of my life there and although I am excited for my new chapter in Philadelphia, the state of Ohio will always have a place in my heart.
Part 2: County-Level Resource Assessment
2.1 Data Retrieval
Data: - Geography: county level - Variables: median household income (B19013_001) and total population (B01003_001)
- Year: 2022 - Survey: acs5 - Output format: wide
county_data <- get_acs(
geography = "county",
variables = c(
median_income = "B19013_001", # Median household income
total_pop = "B01003_001" # Total population
),
state = my_state,
year = 2022,
output = "wide"
)
# Clean the county names to remove state name and "County"
words_to_remove <- c("County", "Ohio", ', ', ' ')
pattern <- paste0("\\b(", paste(words_to_remove, collapse = "|"), ")\\b")
county_data <- county_data %>%
mutate(county_name = str_remove_all(NAME, pattern))
# Display the first few rows
glimpse(county_data)Rows: 88
Columns: 7
$ GEOID <chr> "39001", "39003", "39005", "39007", "39009", "39011", "…
$ NAME <chr> "Adams County, Ohio", "Allen County, Ohio", "Ashland Co…
$ median_incomeE <dbl> 46234, 58976, 62254, 53663, 48750, 75231, 56943, 66677,…
$ median_incomeM <dbl> 4072, 2172, 2954, 2186, 3695, 2795, 2417, 3077, 1604, 3…
$ total_popE <dbl> 27509, 102087, 52522, 97666, 61276, 46263, 66554, 43715…
$ total_popM <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
$ county_name <chr> "Adams", "Allen", "Ashland", "Ashtabula", "Athens", "Au…
2.2 Data Quality Assessment
Your Task: Calculate margin of error percentages and create reliability categories.
Requirements: - Calculate MOE percentage: (margin of error / estimate) * 100 - Create reliability categories: - High Confidence: MOE < 5% - Moderate Confidence: MOE 5-10%
- Low Confidence: MOE > 10% - Create a flag for unreliable estimates (MOE > 10%)
Hint: Use mutate() with case_when() for the categories.
# Calculate MOE percentages
county_data <- county_data %>%
mutate(
median_income_moe_pct = (median_incomeM / median_incomeE) * 100,
)
county_data <- county_data %>%
mutate(
median_income_moe_cat = case_when(
median_income_moe_pct > 10 ~ "Low Confidence: MOE > 10%",
median_income_moe_pct >= 5 & median_income_moe_pct <= 10 ~ "Moderate Confidence: MOE 5-10%",
TRUE ~ "High Confidence: MOE < 5%"
),
)
# Summary showing count of counties in each reliability category
moe_cat_summary_table <- county_data %>%
count(median_income_moe_cat, name = "number_of_counties")
moe_cat_summary_table# A tibble: 3 × 2
median_income_moe_cat number_of_counties
<chr> <int>
1 High Confidence: MOE < 5% 57
2 Low Confidence: MOE > 10% 2
3 Moderate Confidence: MOE 5-10% 29
2.3 High Uncertainty Counties
Workflow: - Sort by MOE percentage (highest first) - Select the top 5 counties - Display: county name, median income, margin of error, MOE percentage, reliability category - Format as a professional table using kable()
# Create table of top 5 counties by MOE percentage
top_moe_counties <- county_data %>%
slice_max(median_income_moe_pct, n = 5) %>%
select(county_name, median_incomeE, median_incomeM, median_income_moe_pct, median_income_moe_cat)
kable(
top_moe_counties,
caption = "Top 5 Counties in Ohio by Median Income Margin of Error (MOE)",
col.names = c("County Name", "Median Income Estimate $", "Median Income MOE $", "Median Income MOE %", "Median Income MOE Category")
)| County Name | Median Income Estimate $ | Median Income MOE $ | Median Income MOE % | Median Income MOE Category |
|---|---|---|---|---|
| Vinton | 50967 | 6264 | 12.290306 | Low Confidence: MOE > 10% |
| Noble | 51547 | 5566 | 10.797913 | Low Confidence: MOE > 10% |
| Monroe | 55381 | 5415 | 9.777722 | Moderate Confidence: MOE 5-10% |
| Adams | 46234 | 4072 | 8.807371 | Moderate Confidence: MOE 5-10% |
| Pike | 50832 | 4258 | 8.376613 | Moderate Confidence: MOE 5-10% |
Data Quality Commentary:
These results are not surprising given that the counties are among the least populous in the state (Vinton, Noble, and Monroe have less than 15,000 people according to 2024 estimates). Since the ACS relies on sample data, rather than the total enumeration provided by the Census, small population groups will result in small sample sizes.
In this scenario: Any algorithmic system should account for the data inaccuracies arising from Ohio’s smaller counties.
Part 3: Neighborhood-Level Analysis
3.1 Focus Area Selection
Selection: Counties that represent different reliability levels (e.g., 1 high confidence, 1 moderate, 1 low confidence) to compare how data quality varies.
selected_counties <- county_data %>%
filter(county_name %in% c("Cuyahoga", "Marion", "Vinton"))
# Display the selected counties with their key characteristics
# Show: county name, median income, MOE percentage, reliability category
kable(
caption = "Selected Counties (1 in each reliability category)",
selected_counties %>% select(county_name, median_incomeE, median_income_moe_pct, median_income_moe_cat),
col.names = c("County Name", "Median Income Estimate $", "Median Income MOE %", "Median Income MOE Category")
)| County Name | Median Income Estimate $ | Median Income MOE % | Median Income MOE Category |
|---|---|---|---|
| Cuyahoga | 60074 | 1.163565 | High Confidence: MOE < 5% |
| Marion | 55106 | 8.313069 | Moderate Confidence: MOE 5-10% |
| Vinton | 50967 | 12.290306 | Low Confidence: MOE > 10% |
Comment on the output: Cuyahoga county is one of the most populous counties in Ohio, so the sample sizes used lent to its low MOE of ~1.16%. Although this can not be extrapolated to all counties, it is interesting to see a decline of median income as the MOE increases among the selected counties.
3.2 Tract-Level Demographics
- Geography: tract level
- Variables: white alone (B03002_003), Black/African American (B03002_004), Hispanic/Latino (B03002_012), total population (B03002_001)
- Use the same state and year as before
- Output format: wide
race_vars <- c(
white = "B03002_003",
black = "B03002_004",
hispanic = "B03002_012",
total = "B03002_001"
)
# Based on https://www.census.gov/programs-surveys/geography/guidance/geo-identifiers.html,
# GEOIDs for counties are made of two digit state codes and three digit county codes. This is visible
# in how GEOID in county_data stays constant for the first two digits, and is unique for the last 3.
# We can parse this and create a list:
selected_counties <- selected_counties %>%
mutate(county_code = str_sub(GEOID, 3, 5))
county_codes <- selected_counties %>% pull(county_code)
tract_demo_selected_counties = get_acs(
geography = "tract",
state = my_state,
county = county_codes,
variables = race_vars,
output = "wide"
)
# Calculate percentage of each group using mutate()
# Create percentages for white, Black, and Hispanic populations
tract_demo_selected_counties <- tract_demo_selected_counties %>%
mutate(
pct_white = 100 * whiteE / totalE,
pct_black = 100 * blackE / totalE,
pct_hispanic = 100 * hispanicE / totalE
)
# Add readable tract and county name columns using str_extract() or similar
tract_demo_selected_counties <- tract_demo_selected_counties %>%
mutate(
tract_name = str_extract(NAME, "^[^;]+"),
county_name = str_extract(NAME, "(?<=;\\s).*")
)
# We can clean up the county name as we did before:
words_to_remove <- c("County", "Ohio", '; ', ' ')
pattern <- paste0("\\b(", paste(words_to_remove, collapse = "|"), ")\\b")
tract_demo_selected_counties <- tract_demo_selected_counties %>%
mutate(county_name = str_remove_all(county_name, pattern))3.3 Demographic Analysis
# Find the tract with the highest percentage of Hispanic/Latino residents
# Hint: use arrange() and slice() to get the top tract
top_hispanic <- tract_demo_selected_counties %>%
slice_max(order_by = pct_hispanic, n = 1)
top_hispanic %>%
select(county_name, tract_name, pct_white, pct_black, pct_hispanic) %>%
kable(
caption = "Details of Census Tract with Highest Percentage Hispanic Residents",
col.names = c("County Name", "Tract", "% White", "% Black", "% Hispanic")
)| County Name | Tract | % White | % Black | % Hispanic |
|---|---|---|---|---|
| Cuyahoga | Census Tract 1029 | 18.89952 | 19.13876 | 61.48325 |
# Calculate average demographics by county using group_by() and summarize()
# Show: number of tracts, average percentage for each racial/ethnic group
selected_counties_summary <- tract_demo_selected_counties %>%
group_by(county_name) %>%
summarize(
n_of_tracts = n(),
avg_pct_white = mean(pct_white, na.rm = TRUE),
avg_pct_black = mean(pct_black, na.rm = TRUE),
avg_pct_hisp = mean(pct_hispanic, na.rm = TRUE),
)
# Create a nicely formatted table of your results using kable()
selected_counties_summary %>%
kable(
caption = "Average Demographics for Selected Ohio Counties",
digits = 2,
col.names = c("County Name", "Number of Tracts", "Avg % White", "Avg % Black", "Avg % Hispanic")
)| County Name | Number of Tracts | Avg % White | Avg % Black | Avg % Hispanic |
|---|---|---|---|---|
| Cuyahoga | 428 | 51.12 | 34.98 | 6.77 |
| Marion | 18 | 86.13 | 4.75 | 3.50 |
| Vinton | 3 | 96.42 | 0.49 | 0.07 |
Part 4: Comprehensive Data Quality Evaluation
4.1 MOE Analysis for Demographic Variables
Workflow: - Calculate MOE percentages for each demographic variable - Flag tracts where any demographic variable has MOE > 15% - Create summary statistics
# I noticed some of the estimates or margins were 0s, or NaNs, so I'm accounting for that here as well:
tract_demo_selected_counties <- tract_demo_selected_counties %>%
mutate(
white_moe_pct = if_else(whiteE > 0, (whiteM / whiteE), NA_real_) * 100,
black_moe_pct = if_else(blackE > 0, (blackM / blackE), NA_real_) * 100,
hispanic_moe_pct = if_else(hispanicE > 0, (hispanicM / hispanicE), NA_real_) * 100
)
# Create a flag for tracts with high MOE on any demographic variable
# Use logical operators (| for OR) in an ifelse() statement
tract_demo_selected_counties <- tract_demo_selected_counties %>%
mutate(
high_moe_flag = ifelse(
white_moe_pct > 15 | black_moe_pct > 15 | hispanic_moe_pct > 15,
TRUE,
FALSE
)
)
# Create summary statistics showing how many tracts have data quality issues
tract_demo_selected_counties %>%
group_by(county_name) %>%
summarize(
total_tracts = sum(!is.na(high_moe_flag), na.rm = TRUE),
high_moe_tracts = sum(high_moe_flag, na.rm = TRUE),
) %>%
kable(
caption = "Tract Data Quality by County",
digits = 0,
col.names = c("County Name", "Number of Tracts (excluding those without data)", "Tracts with Quality Issues (>15% MOE)")
)| County Name | Number of Tracts (excluding those without data) | Tracts with Quality Issues (>15% MOE) |
|---|---|---|
| Cuyahoga | 421 | 421 |
| Marion | 18 | 18 |
| Vinton | 2 | 2 |
I noticed that there were some tracts with no data, so the sum of tracts excludes those.
4.2 Pattern Analysis
# Group tracts by whether they have high MOE issues
# Calculate average characteristics for each group:
# - population size, demographic percentages
# Use group_by() and summarize() to create this comparison
# Create a professional table showing the patterns
tract_demo_selected_counties %>%
group_by(high_moe_flag) %>%
summarize(
avg_pct_white = mean(pct_white, na.rm = TRUE),
avg_pct_black = mean(pct_black, na.rm = TRUE),
avg_pct_hisp = mean(pct_hispanic, na.rm = TRUE),
n_tracts = n(),
.groups = "drop"
) %>%
kable(
caption = "Average Demographics by High MOE Status",
digits = 1,
col.names = c(
"High MOE Flag",
"Avg % White",
"Avg % Black",
"Avg % Hispanic",
"Number of Tracts"
)
)| High MOE Flag | Avg % White | Avg % Black | Avg % Hispanic | Number of Tracts |
|---|---|---|---|---|
| TRUE | 52.6 | 33.7 | 6.6 | 441 |
| NA | 98.9 | 0.0 | 0.0 | 8 |
This summary was simple, but when I did a visual inspection of the data, I see there’s some patterns that this table analysis is obscuring. I see that the MOE for Hispanic residents is above 15% for all tracts! I’ll do a second summary table, this time showing
tract_demo_selected_counties %>%
summarize(
n_tracts = n(),
white_high_moe = sum(white_moe_pct > 15, na.rm = TRUE),
black_high_moe = sum(black_moe_pct > 15, na.rm = TRUE),
hispanic_high_moe = sum(hispanic_moe_pct > 15, na.rm = TRUE),
pct_white_high = 100 * white_high_moe / n_tracts,
pct_black_high = 100 * black_high_moe / n_tracts,
pct_hispanic_high = 100 * hispanic_high_moe / n_tracts
) %>%
kable(
caption = "Tracts with MOE > 15% by Demographic Group",
digits = 1,
col.names = c(
"Total Tracts",
"White High MOE Tracts",
"Black High MOE Tracts",
"Hispanic High MOE Tracts",
"% of Tracts with White High MOE",
"% of Tracts with Black High MOE",
"% of Tracts with Hispanic High MOE"
)
)| Total Tracts | White High MOE Tracts | Black High MOE Tracts | Hispanic High MOE Tracts | % of Tracts with White High MOE | % of Tracts with Black High MOE | % of Tracts with Hispanic High MOE |
|---|---|---|---|---|---|---|
| 449 | 306 | 416 | 403 | 68.2 | 92.7 | 89.8 |
tract_demo_selected_counties %>%
group_by(county_name) %>%
summarize(
n_tracts = n(),
white_high_moe = sum(white_moe_pct > 15, na.rm = TRUE),
black_high_moe = sum(black_moe_pct > 15, na.rm = TRUE),
hispanic_high_moe = sum(hispanic_moe_pct > 15, na.rm = TRUE),
pct_white_high = 100 * white_high_moe / n_tracts,
pct_black_high = 100 * black_high_moe / n_tracts,
pct_hispanic_high = 100 * hispanic_high_moe / n_tracts,
.groups = "drop"
) %>%
kable(
caption = "County-Level Breakdown of Tracts with MOE > 15%, by Demographic Group",
digits = 1,
col.names = c(
"County",
"Total Tracts",
"White High MOE Tracts",
"Black High MOE Tracts",
"Hispanic High MOE Tracts",
"% of Tracts with White High MOE",
"% of Tracts with Black High MOE",
"% of Tracts with Hispanic High MOE"
)
)| County | Total Tracts | White High MOE Tracts | Black High MOE Tracts | Hispanic High MOE Tracts | % of Tracts with White High MOE | % of Tracts with Black High MOE | % of Tracts with Hispanic High MOE |
|---|---|---|---|---|---|---|---|
| Cuyahoga | 428 | 298 | 402 | 383 | 69.6 | 93.9 | 89.5 |
| Marion | 18 | 8 | 12 | 18 | 44.4 | 66.7 | 100.0 |
| Vinton | 3 | 0 | 2 | 2 | 0.0 | 66.7 | 66.7 |
Pattern Analysis: A larger share of census tracts flagged as High MOE (>15%) are driven primarily by high median income margins of error among Black or Hispanic populations. The last chart highlights that, with both communities having a higher MOE as a percentage of tracts being marked.
For Marion and Vinton counties, this is potentially explained by Black and Hispanic populations being a smaller percentage of the total. This would make the sample sizes for both communities fairly small in these counties. In Cuyahoga county, a visual inspection reveals tracts being flagged when they have small percentages of Hispanic, Black, or White population.
Part 5: Policy Recommendations
5.1 Analysis Integration and Professional Summary
Executive Summary:
Across the analyses, a pattern emerges: the MOE for demographic data is highly sensitive to both the population size of census tracts and the demographic composition within them. Small-population counties, such as Vinton County, show particularly large MOEs due to limited sample sizes in the ACS. Similarly, tracts with highly diverse populations tend to have inflated MOEs across multiple demographic groups, making nearly every group susceptible to high uncertainty in the estimates.
In examining equity implications, Black and Hispanic populations face the highest risk of unreliable data. While the largest MOE observed for White populations is around 200%, Black and Hispanic populations frequently have MOEs in the hundreds of percent and, in extreme cases, as high as 2000%. This suggests that algorithmic decisions relying on this demographic data can be highly unreliable for these communities.
We can classify the root causes of these data issues into two categories. First, small sample sizes in low-population tracts and counties amplify MOEs for all estimates. Second, demographic variability can take the form of both extremely small minority populations and highly heterogeneous tracts. This further inflates MOE. An example is Census Tract 1711.02, which is nearly 87% Black but flagged as “High MOE” because the MOE for the White population exceeds 40%.
To address these systematic issues, decision-makers should incorporate demographic composition into analyses, weighting MOEs appropriately when interpreting the demographic data. Underrepresented communities should be prioritized for follow-up investigations at the tract or county level to better understand them. By acknowledging and adjusting for these data limitations, we can improve the equity and reliability of policy decisions.
6.3 Specific Recommendations
# Create a summary table using your county reliability data
# Include: county name, median income, MOE percentage, reliability category
# Add a new column with algorithm recommendations using case_when():
# - High Confidence: "Safe for algorithmic decisions"
# - Moderate Confidence: "Use with caution - monitor outcomes"
# - Low Confidence: "Requires manual review or additional data"
# Format as a professional table with kable()
county_data %>%
mutate(
algorithm_recommendation = case_when(
median_income_moe_cat == "High Confidence: MOE < 5%" ~ "Safe for algorithmic decisions",
median_income_moe_cat == "Moderate Confidence: MOE 5-10%" ~ "Use with caution - monitor outcomes",
median_income_moe_cat == "Low Confidence: MOE > 10%" ~ "Requires manual review or additional data",
TRUE ~ "Check data"
)
) %>%
select(county_name, median_incomeE, median_income_moe_pct, median_income_moe_cat, algorithm_recommendation) %>%
kable(
caption = "County-Level Median Income Reliability and Algorithmic Recommendations",
digits = 1,
col.names = c(
"County Name",
"Median Income",
"MOE % Median Income",
"Reliability Category",
"Algorithm Recommendation"
)
)| County Name | Median Income | MOE % Median Income | Reliability Category | Algorithm Recommendation |
|---|---|---|---|---|
| Adams | 46234 | 8.8 | Moderate Confidence: MOE 5-10% | Use with caution - monitor outcomes |
| Allen | 58976 | 3.7 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Ashland | 62254 | 4.7 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Ashtabula | 53663 | 4.1 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Athens | 48750 | 7.6 | Moderate Confidence: MOE 5-10% | Use with caution - monitor outcomes |
| Auglaize | 75231 | 3.7 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Belmont | 56943 | 4.2 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Brown | 66677 | 4.6 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Butler | 77062 | 2.1 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Carroll | 59872 | 5.9 | Moderate Confidence: MOE 5-10% | Use with caution - monitor outcomes |
| Champaign | 70486 | 5.5 | Moderate Confidence: MOE 5-10% | Use with caution - monitor outcomes |
| Clark | 58954 | 2.6 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Clermont | 79573 | 2.9 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Clinton | 64210 | 7.1 | Moderate Confidence: MOE 5-10% | Use with caution - monitor outcomes |
| Columbiana | 55473 | 4.0 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Coshocton | 52048 | 5.9 | Moderate Confidence: MOE 5-10% | Use with caution - monitor outcomes |
| Crawford | 52486 | 4.2 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Cuyahoga | 60074 | 1.2 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Darke | 60237 | 6.5 | Moderate Confidence: MOE 5-10% | Use with caution - monitor outcomes |
| Defiance | 69302 | 4.8 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Delaware | 123995 | 1.9 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Erie | 65171 | 4.4 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Fairfield | 82969 | 3.9 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Fayette | 56773 | 6.2 | Moderate Confidence: MOE 5-10% | Use with caution - monitor outcomes |
| Franklin | 71070 | 1.3 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Fulton | 71453 | 4.5 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Gallia | 55533 | 6.4 | Moderate Confidence: MOE 5-10% | Use with caution - monitor outcomes |
| Geauga | 97162 | 3.9 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Greene | 81243 | 3.0 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Guernsey | 53901 | 7.7 | Moderate Confidence: MOE 5-10% | Use with caution - monitor outcomes |
| Hamilton | 68249 | 1.6 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Hancock | 67006 | 4.6 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Hardin | 55876 | 5.1 | Moderate Confidence: MOE 5-10% | Use with caution - monitor outcomes |
| Harrison | 54056 | 6.7 | Moderate Confidence: MOE 5-10% | Use with caution - monitor outcomes |
| Henry | 71616 | 5.9 | Moderate Confidence: MOE 5-10% | Use with caution - monitor outcomes |
| Highland | 60522 | 5.0 | Moderate Confidence: MOE 5-10% | Use with caution - monitor outcomes |
| Hocking | 59007 | 4.9 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Holmes | 72987 | 4.1 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Huron | 64144 | 4.1 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Jackson | 56549 | 7.8 | Moderate Confidence: MOE 5-10% | Use with caution - monitor outcomes |
| Jefferson | 53124 | 4.4 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Knox | 71246 | 5.2 | Moderate Confidence: MOE 5-10% | Use with caution - monitor outcomes |
| Lake | 76835 | 1.7 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Lawrence | 51846 | 5.2 | Moderate Confidence: MOE 5-10% | Use with caution - monitor outcomes |
| Licking | 78505 | 2.8 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Logan | 69125 | 4.8 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Lorain | 67272 | 1.7 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Lucas | 57265 | 1.7 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Madison | 77062 | 4.7 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Mahoning | 54279 | 2.0 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Marion | 55106 | 8.3 | Moderate Confidence: MOE 5-10% | Use with caution - monitor outcomes |
| Medina | 89968 | 2.7 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Meigs | 46255 | 6.9 | Moderate Confidence: MOE 5-10% | Use with caution - monitor outcomes |
| Mercer | 73278 | 4.5 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Miami | 71440 | 3.2 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Monroe | 55381 | 9.8 | Moderate Confidence: MOE 5-10% | Use with caution - monitor outcomes |
| Montgomery | 61942 | 1.6 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Morgan | 51056 | 7.8 | Moderate Confidence: MOE 5-10% | Use with caution - monitor outcomes |
| Morrow | 70412 | 6.4 | Moderate Confidence: MOE 5-10% | Use with caution - monitor outcomes |
| Muskingum | 56810 | 4.9 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Noble | 51547 | 10.8 | Low Confidence: MOE > 10% | Requires manual review or additional data |
| Ottawa | 69515 | 5.0 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Paulding | 65331 | 5.7 | Moderate Confidence: MOE 5-10% | Use with caution - monitor outcomes |
| Perry | 62899 | 7.3 | Moderate Confidence: MOE 5-10% | Use with caution - monitor outcomes |
| Pickaway | 67600 | 4.7 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Pike | 50832 | 8.4 | Moderate Confidence: MOE 5-10% | Use with caution - monitor outcomes |
| Portage | 69796 | 3.2 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Preble | 66355 | 5.9 | Moderate Confidence: MOE 5-10% | Use with caution - monitor outcomes |
| Putnam | 79453 | 4.0 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Richland | 56557 | 3.5 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Ross | 58048 | 4.1 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Sandusky | 60814 | 5.0 | Moderate Confidence: MOE 5-10% | Use with caution - monitor outcomes |
| Scioto | 46360 | 3.6 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Seneca | 62476 | 3.9 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Shelby | 73502 | 4.6 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Stark | 63130 | 1.8 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Summit | 68360 | 1.3 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Trumbull | 53537 | 3.6 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Tuscarawas | 61953 | 3.7 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Union | 104496 | 2.9 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| VanWert | 64841 | 5.4 | Moderate Confidence: MOE 5-10% | Use with caution - monitor outcomes |
| Vinton | 50967 | 12.3 | Low Confidence: MOE > 10% | Requires manual review or additional data |
| Warren | 103128 | 2.3 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Washington | 59053 | 4.2 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Wayne | 70320 | 2.7 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Williams | 60632 | 3.6 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Wood | 70537 | 3.2 | High Confidence: MOE < 5% | Safe for algorithmic decisions |
| Wyandot | 68552 | 7.6 | Moderate Confidence: MOE 5-10% | Use with caution - monitor outcomes |
Key Recommendations:
Counties suitable for immediate algorithmic implementation: Most Ohio counties are appropriate for immediate algorithmic use; however, some caveats apply. In counties with small or underrepresented populations, certain communities may face higher margins of error, and in highly diverse counties, careful attention or additional algorithmic analysis is needed.
Counties requiring additional oversight: Adams, Athens, Carroll, Champaign, Clinton, Coshocton, Darke, Fayette, Gallia, Guernsey, Hardin, Harrison, Henry, Highland, Jackson, Knox, Lawrence, Marion, Meigs, Monroe, Morgan, Morrow, Paulding, Perry, Pike, Preble, Sandusky, Van Wert, Wyandot.
- These counties can be further categorized based on population and demographic characteristics. In counties with very small populations, targeted local outreach may be sufficient to understand community needs. In larger or more diverse counties, more granular analysis at sub-county levels may be necessary to ensure accurate insights and equitable decision-making.
- Counties needing alternative approaches: Noble, Vinton.
- These counties have some of the lowest populations in Ohio, making traditional ACS estimates highly uncertain. Given the small population size, targeted local outreach—allowing for larger effective sample sizes than the ACS—may be the most feasible approach to accurately assess community needs.
Questions for Further Investigation
The relationship between demographics and population size in determining MOE warrants further investigation. The current analysis, which applies MOE cutoffs based on either demographic or county-level data, involves tradeoffs and may exclude or underrepresent certain populations. Developing a cutoff that incorporates both population size and demographic characteristics could provide a more accurate and equitable assessment, rather than relying single, uniform metrics.
Technical Notes
Data Sources: - U.S. Census Bureau, American Community Survey 2018-2022 5-Year Estimates - Retrieved via tidycensus R package in September of 2025 (final run conducted on September 28th 2025)
Reproducibility: - All analysis conducted in R version 4.5.1 (2025-06-13) - Census API key required for replication - Complete code and documentation available at: https://musa-5080-fall-2025.github.io/portfolio-setup-kakumanus/
Methodology Notes: Nothing of note in this analysis.
Limitations: For algorithmic decision-making at the state government level, this analysis serves as an initial assessment, providing key considerations rather than providing a final solution. Further exploration is needed, particularly because relying on ACS-based estimates can inadvertently exclude populations—especially in counties where demographic characteristics make sampling approaches less reliable.