This report analyzes energy affordability in Michigan using the Department of Energy’s Low-Income Energy Affordability Data (LEAD) Tool. We examine 422,000 low-income households facing an average 23.9% energy burden—nearly four times the federal affordability threshold of 6%.
Key Findings:
Energy burden is the percentage of household income spent on home energy costs. The U.S. Department of Energy considers:
Low-income households often face severe energy burdens due to:
We analyze data from the LEAD Tool (2022), which uses American Community Survey data (2018-2022) to estimate:
The data is highly granular, with each row representing a unique combination of:
# Load required packages
library(tidyverse) # Data manipulation and visualization
library(scales) # Number formatting
library(knitr) # Table formatting
# Suppress scientific notation for readability
options(scipen = 999)# Read the data
# Note: Adjust the path to where your CSV file is located
mi_fpl <- read_csv("C:/Users/GhoshS4/Downloads/MI-2022-LEAD-data/MI FPL Counties 2022.csv")
# Display dataset structure
glimpse(mi_fpl)## Rows: 83,337
## Columns: 31
## $ ABV <chr> "MI", "MI", "MI", "…
## $ STATE <dbl> 26, 26, 26, 26, 26,…
## $ FIP <dbl> 26001, 26001, 26001…
## $ NAME <chr> "Alcona County", "A…
## $ FPL150 <chr> "0-100%", "0-100%",…
## $ TEN <chr> "OWN", "OWN", "OWN"…
## $ `TEN-YBL6` <chr> "OWNER 1940-59", "O…
## $ `TEN-BLD` <chr> "OWNER 1 ATTACHED",…
## $ `TEN-HFL` <chr> "OWNER BOTTLED GAS"…
## $ UNITS <dbl> 0.0050954, 0.034164…
## $ FREQUENCY <dbl> 0.00000, 0.29200, 0…
## $ `HINCP*UNITS` <dbl> 0.00000000000000, 1…
## $ `ELEP*UNITS` <dbl> 0.00000, 60.09103, …
## $ `GASP*UNITS` <dbl> 0.00000000000, 14.4…
## $ `FULP*UNITS` <dbl> 0.00000000000000, 1…
## $ `HINCP UNITS` <dbl> 0.000000, 0.034164,…
## $ `ELEP UNITS` <dbl> 0.000000, 0.034164,…
## $ `GASP UNITS` <dbl> 0.000000, 0.034164,…
## $ `FULP UNITS` <dbl> 0.000000, 0.034164,…
## $ `WHITE ALONE HISPANIC OR LATINO` <dbl> 0.0000000, 0.000000…
## $ `WHITE ALONE NOT HISPANIC OR LATINO` <dbl> 0.0000000, 0.196979…
## $ `BLACK OR AFRICAN AMERICAN ALONE` <dbl> 0.00000000, 0.00000…
## $ `AMERICAN INDIAN AND ALASKA NATIVE ALONE` <dbl> 0, 0, 0, 0, 0, 0, 0…
## $ `ASIAN ALONE` <dbl> 0, 0, 0, 0, 0, 0, 0…
## $ `NATIVE HAWAIIAN AND OTHER PACIFIC ISLANDER ALONE` <dbl> 0, 0, 0, 0, 0, 0, 0…
## $ `SOME OTHER RACE ALONE` <dbl> 0, 0, 0, 0, 0, 0, 0…
## $ `TWO OR MORE RACES` <dbl> 0.00000000, 0.00000…
## $ `LESS THAN HIGH SCHOOL` <dbl> 0.00000000, 0.00000…
## $ `HIGH SCHOOL` <dbl> 0.0000000, 0.000000…
## $ `ASSOCIATES OR SOME COLLEGE` <dbl> 0.000000000, 0.0086…
## $ `BACHELORS OR HIGHER` <dbl> 0.00000000, 0.00000…
What we have: 83,337 rows and 31 columns.
The dataset has three main types of columns:
1. Geographic Identifiers: - NAME:
County name - FIP: County FIPS code
2. Household Characteristics (Filters): -
FPL150: Federal Poverty Level category (0-100%, 100-150%,
etc.) - TEN: Housing tenure (OWN = owner, REN = renter) -
TEN-YBL6: Building age (e.g., “OWNER 1960-79”) -
TEN-HFL: Heating fuel type (e.g., “OWNER UTILITY GAS”)
3. Aggregate Metrics: - UNITS: Number
of households in this category - HINCP*UNITS: Total income
across all households - ELEP*UNITS: Total electricity costs
- GASP*UNITS: Total gas costs - FULP*UNITS:
Total other fuel costs - HINCP UNITS: Number of households
with valid income data
## Federal Poverty Level Categories:
## [1] "0-100%" "100-150%" "150-200%" "200-400%" "400%+"
##
## Housing Tenure Types:
## [1] "OWN" "REN"
# Show sample of the data
head(mi_fpl %>% select(NAME, FPL150, TEN, UNITS, `HINCP*UNITS`, `ELEP*UNITS`), 10) %>%
kable(caption = "Sample of Michigan LEAD Data")| NAME | FPL150 | TEN | UNITS | HINCP*UNITS | ELEP*UNITS |
|---|---|---|---|---|---|
| Alcona County | 0-100% | OWN | 0.0050954 | 0.000 | 0.00000 |
| Alcona County | 0-100% | OWN | 0.0341640 | 1402.223 | 60.09103 |
| Alcona County | 0-100% | OWN | 0.0470704 | 0.000 | 0.00000 |
| Alcona County | 0-100% | OWN | 23.0116018 | 137397.413 | 28247.46331 |
| Alcona County | 0-100% | OWN | 1.7492915 | 27901.834 | 3227.25797 |
| Alcona County | 0-100% | OWN | 0.8247630 | 13987.624 | 591.80581 |
| Alcona County | 0-100% | OWN | 0.3252978 | 0.000 | 0.00000 |
| Alcona County | 0-100% | OWN | 28.9579893 | 297129.528 | 24880.91458 |
| Alcona County | 0-100% | OWN | 16.2640502 | 144438.427 | 13440.72830 |
| Alcona County | 0-100% | OWN | 0.0187520 | 0.000 | 0.00000 |
Important: Each row is NOT an individual household. Rather, each row represents an aggregated group of households sharing the same characteristics.
For example, one row might represent: - Wayne County + Below Poverty + Renters + Built 1960-1979 + Heated with Natural Gas
To analyze at the county level or by poverty level, we must aggregate these micro-categories first, then calculate averages.
The formula for energy burden is straightforward:
\[\text{Energy Burden (\%)} = \frac{\text{Total Annual Energy Cost}}{\text{Annual Household Income}} \times 100\]
However, because our data is aggregated, we calculate:
\[\text{Average Energy Burden} = \frac{\sum(\text{Energy Cost} \times \text{Households})}{\sum(\text{Income} \times \text{Households})} \times 100\]
# Calculate averages for each micro-category
# This is done at the row level first
mi_metrics <- mi_fpl %>%
mutate(
# Calculate average income and costs for this specific group
avg_income = ifelse(`HINCP UNITS` > 0, `HINCP*UNITS` / `HINCP UNITS`, NA),
avg_elec_cost = ifelse(`ELEP UNITS` > 0, `ELEP*UNITS` / `ELEP UNITS`, NA),
avg_gas_cost = ifelse(`GASP UNITS` > 0, `GASP*UNITS` / `GASP UNITS`, NA),
avg_fuel_cost = ifelse(`FULP UNITS` > 0, `FULP*UNITS` / `FULP UNITS`, NA),
# Total energy cost (sum of all fuel types)
total_energy_cost = coalesce(avg_elec_cost, 0) +
coalesce(avg_gas_cost, 0) +
coalesce(avg_fuel_cost, 0),
# Energy burden as percentage
energy_burden_pct = ifelse(avg_income > 0,
(total_energy_cost / avg_income) * 100,
NA)
)
# Show example calculation
mi_metrics %>%
filter(NAME == "Wayne County", FPL150 == "0-100%") %>%
select(NAME, UNITS, avg_income, total_energy_cost, energy_burden_pct) %>%
head(5) %>%
kable(digits = 2, caption = "Example: Energy Burden Calculation for Wayne County")| NAME | UNITS | avg_income | total_energy_cost | energy_burden_pct |
|---|---|---|---|---|
| Wayne County | 29.36 | 9025.28 | 795.82 | 8.82 |
| Wayne County | 1.17 | NA | 0.00 | NA |
| Wayne County | 1.77 | NA | 0.00 | NA |
| Wayne County | 242.67 | 9195.24 | 2312.19 | 25.15 |
| Wayne County | 168.27 | 13781.41 | 8354.57 | 60.62 |
Key Assumption: We treat missing energy costs as zero, which is conservative. This assumes households without reported electricity costs (for example) are not spending money on electricity.
Now that we understand the data structure, let’s aggregate to the county level to answer: Which Michigan counties have the highest energy burden for low-income households?
Methodology: 1. Filter to households below 100% Federal Poverty Level 2. Aggregate all micro-categories within each county 3. Calculate weighted average burden using household counts as weights
# Aggregate to county + poverty level
county_fpl_summary <- mi_metrics %>%
group_by(NAME, FPL150) %>%
summarise(
total_households = sum(UNITS, na.rm = TRUE),
# Sum all income and energy costs
total_income_weighted = sum(`HINCP*UNITS`, na.rm = TRUE),
households_with_income = sum(`HINCP UNITS`, na.rm = TRUE),
total_energy_weighted = sum(`ELEP*UNITS`, na.rm = TRUE) +
sum(`GASP*UNITS`, na.rm = TRUE) +
sum(`FULP*UNITS`, na.rm = TRUE),
households_with_energy = sum(`ELEP UNITS`, na.rm = TRUE),
.groups = "drop"
) %>%
mutate(
# Calculate simple averages
avg_income = total_income_weighted / households_with_income,
avg_total_energy = total_energy_weighted / households_with_energy,
energy_burden_pct = (avg_total_energy / avg_income) * 100
)
# Show top 10 worst counties for low-income households
county_fpl_summary %>%
filter(FPL150 == "0-100%") %>%
select(NAME, total_households, avg_income, avg_total_energy, energy_burden_pct) %>%
arrange(desc(energy_burden_pct)) %>%
head(10) %>%
mutate(
avg_income = dollar(avg_income),
avg_total_energy = dollar(avg_total_energy),
energy_burden_pct = paste0(round(energy_burden_pct, 1), "%")
) %>%
kable(
col.names = c("County", "Households", "Avg Income", "Avg Energy Cost", "Energy Burden"),
caption = "Top 10 Counties by Energy Burden (Households <100% FPL)"
)| County | Households | Avg Income | Avg Energy Cost | Energy Burden |
|---|---|---|---|---|
| Keweenaw County | 91.16011 | $9,298.36 | $3,099.14 | 33.3% |
| Ionia County | 2038.29407 | $10,814.59 | $3,567.65 | 33% |
| Lake County | 667.01724 | $9,409.30 | $2,971.73 | 31.6% |
| Leelanau County | 513.08713 | $8,817.05 | $2,769.38 | 31.4% |
| Lenawee County | 3194.35653 | $10,089.36 | $3,116.49 | 30.9% |
| Crawford County | 722.59224 | $9,810.12 | $2,995.52 | 30.5% |
| Alcona County | 596.70953 | $9,666.83 | $2,935.83 | 30.4% |
| Manistee County | 939.14194 | $8,737.67 | $2,643.34 | 30.3% |
| Oscoda County | 438.69438 | $10,681.22 | $3,203.32 | 30% |
| Missaukee County | 512.18246 | $11,241.62 | $3,281.99 | 29.2% |
county_fpl_summary %>%
filter(FPL150 == "0-100%", !is.na(energy_burden_pct), total_households > 10) %>%
arrange(desc(energy_burden_pct)) %>%
slice_head(n = 20) %>%
ggplot(aes(x = reorder(NAME, energy_burden_pct), y = energy_burden_pct)) +
geom_col(aes(fill = energy_burden_pct), show.legend = FALSE) +
geom_hline(yintercept = 6, linetype = "dashed", color = "red", linewidth = 1) +
scale_fill_gradient(low = "#56B4E9", high = "#D55E00") +
scale_y_continuous(labels = function(x) paste0(x, "%")) +
coord_flip() +
labs(
title = "Counties with Highest Energy Burden for Low-Income Households",
subtitle = "Households below 100% Federal Poverty Level | Red line = 6% DOE affordability threshold",
x = NULL,
y = "Energy Burden (% of household income)",
caption = "Source: LEAD Tool 2022 (ACS 2018-2022)\n0-100% FPL: Household income below federal poverty line (~$15,060 for individual, $31,200 for family of 4 in 2024)"
) +
theme_minimal(base_size = 11) +
theme(
plot.title = element_text(face = "bold", size = 14),
panel.grid.major.y = element_blank()
)Key Observations:
Older homes are typically less energy efficient due to poor insulation, outdated heating systems, and single-pane windows. Let’s test this hypothesis.
Methodology: 1. Categorize buildings into age groups 2. Compare energy burden across age categories 3. Look at both low-income and near-poverty households
building_age_data <- mi_fpl %>%
filter(FPL150 %in% c("0-100%", "100-150%")) %>%
mutate(
age_category = case_when(
str_detect(`TEN-YBL6`, "2020|2000") ~ "Post-2000",
str_detect(`TEN-YBL6`, "1980|1960") ~ "1960-1999",
str_detect(`TEN-YBL6`, "BEFORE|1940") ~ "Pre-1960",
TRUE ~ NA_character_
)
) %>%
filter(!is.na(age_category)) %>%
group_by(age_category, FPL150) %>%
summarise(
total_households = sum(UNITS, na.rm = TRUE),
total_income_weighted = sum(`HINCP*UNITS`, na.rm = TRUE),
households_with_income = sum(`HINCP UNITS`, na.rm = TRUE),
total_energy_weighted = sum(`ELEP*UNITS`, na.rm = TRUE) +
sum(`GASP*UNITS`, na.rm = TRUE) +
sum(`FULP*UNITS`, na.rm = TRUE),
households_with_energy = sum(`ELEP UNITS`, na.rm = TRUE),
.groups = "drop"
) %>%
mutate(
avg_income = total_income_weighted / households_with_income,
avg_energy_cost = total_energy_weighted / households_with_energy,
energy_burden_pct = (avg_energy_cost / avg_income) * 100
)
# Display the data
building_age_data %>%
select(age_category, FPL150, total_households, energy_burden_pct) %>%
arrange(FPL150, desc(energy_burden_pct)) %>%
mutate(
total_households = comma(total_households),
energy_burden_pct = paste0(round(energy_burden_pct, 1), "%")
) %>%
kable(
col.names = c("Building Age", "Income Level", "Households", "Energy Burden"),
caption = "Energy Burden by Building Age"
)| Building Age | Income Level | Households | Energy Burden |
|---|---|---|---|
| Pre-1960 | 0-100% | 179,592 | 23.4% |
| Post-2000 | 0-100% | 41,690 | 21.9% |
| 1960-1999 | 0-100% | 200,718 | 21.1% |
| Pre-1960 | 100-150% | 123,705 | 10.1% |
| 1960-1999 | 100-150% | 143,542 | 9.1% |
| Post-2000 | 100-150% | 29,354 | 8.4% |
ggplot(building_age_data, aes(x = age_category, y = energy_burden_pct, fill = FPL150)) +
geom_col(position = "dodge") +
geom_hline(yintercept = 6, linetype = "dashed", color = "red") +
geom_text(aes(label = sprintf("%.1f%%", energy_burden_pct)),
position = position_dodge(width = 0.9), vjust = -0.5) +
scale_fill_manual(
values = c("0-100%" = "#66c2a5", "100-150%" = "#fc8d62"),
labels = c("Below Poverty (0-100% FPL)", "Near Poverty (100-150% FPL)"),
name = "Income Level"
) +
labs(
title = "Older Homes Drive Energy Burden Crisis",
subtitle = "Energy burden by building age and income level",
x = "Building Age",
y = "Energy Burden (% of income)",
caption = "Source: LEAD Tool 2022\n0-100% FPL: Income below poverty line | 100-150% FPL: Income 1-1.5x poverty line (~$15K-$23K individual)"
) +
theme_minimal(base_size = 12) +
theme(
plot.title = element_text(face = "bold"),
legend.position = "top"
)Key Observations:
Traditional housing policy assumes renters face higher energy burdens due to the “split-incentive problem”—landlords don’t pay utilities, so they don’t invest in efficiency. Let’s test this.
tenure_analysis <- mi_fpl %>%
group_by(FPL150, TEN) %>%
summarise(
total_households = sum(UNITS, na.rm = TRUE),
total_income_weighted = sum(`HINCP*UNITS`, na.rm = TRUE),
households_with_income = sum(`HINCP UNITS`, na.rm = TRUE),
total_energy_weighted = sum(`ELEP*UNITS`, na.rm = TRUE) +
sum(`GASP*UNITS`, na.rm = TRUE) +
sum(`FULP*UNITS`, na.rm = TRUE),
households_with_energy = sum(`ELEP UNITS`, na.rm = TRUE),
.groups = "drop"
) %>%
mutate(
avg_income = total_income_weighted / households_with_income,
avg_energy_cost = total_energy_weighted / households_with_energy,
energy_burden_pct = (avg_energy_cost / avg_income) * 100
) %>%
filter(!is.na(energy_burden_pct))
# Display the comparison
tenure_analysis %>%
select(FPL150, TEN, total_households, avg_income, avg_energy_cost, energy_burden_pct) %>%
mutate(
TEN = ifelse(TEN == "OWN", "Homeowners", "Renters"),
total_households = comma(total_households),
avg_income = dollar(avg_income),
avg_energy_cost = dollar(avg_energy_cost),
energy_burden_pct = paste0(round(energy_burden_pct, 1), "%")
) %>%
kable(
col.names = c("Income Level", "Tenure", "Households", "Avg Income", "Avg Energy Cost", "Energy Burden"),
caption = "Energy Burden: Homeowners vs Renters"
)| Income Level | Tenure | Households | Avg Income | Avg Energy Cost | Energy Burden |
|---|---|---|---|---|---|
| 0-100% | Homeowners | 182,836 | $10,524 | $2,737.07 | 26% |
| 0-100% | Renters | 239,164 | $10,036 | $1,832.18 | 18.3% |
| 100-150% | Homeowners | 155,663 | $25,977 | $2,777.47 | 10.7% |
| 100-150% | Renters | 140,937 | $23,516 | $1,787.16 | 7.6% |
| 150-200% | Homeowners | 193,274 | $37,017 | $2,773.82 | 7.5% |
| 150-200% | Renters | 120,933 | $34,071 | $1,894.60 | 5.6% |
| 200-400% | Homeowners | 872,079 | $63,451 | $2,800.18 | 4.4% |
| 200-400% | Renters | 337,169 | $53,280 | $1,833.86 | 3.4% |
| 400%+ | Homeowners | 1,502,618 | $162,206 | $2,902.50 | 1.8% |
| 400%+ | Renters | 264,580 | $114,416 | $1,683.53 | 1.5% |
ggplot(tenure_analysis, aes(x = FPL150, y = energy_burden_pct, fill = TEN)) +
geom_col(position = "dodge", width = 0.7) +
geom_hline(yintercept = 6, linetype = "dashed", color = "red", linewidth = 1) +
geom_text(aes(label = sprintf("%.1f%%", energy_burden_pct)),
position = position_dodge(width = 0.7), vjust = -0.5, size = 3.5) +
scale_fill_manual(
values = c("OWN" = "#3498db", "REN" = "#e74c3c"),
labels = c("Homeowners", "Renters"),
name = NULL
) +
scale_y_continuous(
labels = function(x) paste0(x, "%"),
breaks = seq(0, 30, 5)
) +
labs(
title = "Low-Income Homeowners Face Higher Energy Burden Than Renters",
subtitle = "Challenging the conventional split-incentive narrative",
x = "Federal Poverty Level",
y = "Energy Burden (% of household income)",
caption = "Source: LEAD Tool 2022 (ACS 2018-2022) | Red line = 6% DOE threshold\n0-100%: Below poverty | 100-150%: 1-1.5x poverty | 150-200%: 1.5-2x poverty | 200-400%: 2-4x poverty | 400%+: Above 4x poverty"
) +
theme_minimal(base_size = 12) +
theme(
plot.title = element_text(face = "bold", size = 14),
plot.subtitle = element_text(size = 10, color = "gray30"),
legend.position = "top",
panel.grid.minor = element_blank()
)Key Observations:
Michigan’s cold climate makes heating fuel choice critical. Let’s examine which fuel types create the highest burden.
fuel_analysis <- mi_fpl %>%
filter(FPL150 == "0-100%") %>%
mutate(
fuel_type = str_remove(`TEN-HFL`, "^(OWNER|RENTER) "),
fuel_type = str_to_title(fuel_type)
) %>%
group_by(fuel_type) %>%
summarise(
total_households = sum(UNITS, na.rm = TRUE),
total_income_weighted = sum(`HINCP*UNITS`, na.rm = TRUE),
households_with_income = sum(`HINCP UNITS`, na.rm = TRUE),
total_energy_weighted = sum(`ELEP*UNITS`, na.rm = TRUE) +
sum(`GASP*UNITS`, na.rm = TRUE) +
sum(`FULP*UNITS`, na.rm = TRUE),
households_with_energy = sum(`ELEP UNITS`, na.rm = TRUE),
.groups = "drop"
) %>%
filter(total_households > 50) %>% # Meaningful sample size
mutate(
avg_income = total_income_weighted / households_with_income,
avg_energy_cost = total_energy_weighted / households_with_energy,
energy_burden_pct = (avg_energy_cost / avg_income) * 100
) %>%
arrange(desc(energy_burden_pct))
# Display top heating fuels by burden
fuel_analysis %>%
select(fuel_type, total_households, avg_energy_cost, energy_burden_pct) %>%
mutate(
total_households = comma(total_households),
avg_energy_cost = dollar(avg_energy_cost),
energy_burden_pct = paste0(round(energy_burden_pct, 1), "%")
) %>%
kable(
col.names = c("Heating Fuel", "Households", "Avg Annual Cost", "Energy Burden"),
caption = "Energy Burden by Heating Fuel Type (Low-Income Households)"
)| Heating Fuel | Households | Avg Annual Cost | Energy Burden |
|---|---|---|---|
| Fuel Oil | 3,190 | $3,902.14 | 40.2% |
| Bottled Gas | 27,233 | $3,801.98 | 37% |
| Wood | 8,669 | $3,164.51 | 26.3% |
| Other | 4,510 | $2,388.84 | 24.3% |
| Utility Gas | 295,642 | $2,255.72 | 21.6% |
| Electricity | 76,998 | $1,606.68 | 17.1% |
| Solar | 261 | $1,176.42 | 16.4% |
| None | 5,312 | $1,081.13 | 12.8% |
| Coal | 185 | $1,595.15 | 9.7% |
ggplot(fuel_analysis,
aes(x = reorder(fuel_type, energy_burden_pct),
y = energy_burden_pct)) +
geom_col(aes(fill = energy_burden_pct), show.legend = FALSE) +
geom_point(aes(size = total_households), alpha = 0.6, color = "black") +
scale_fill_gradient(low = "#2ecc71", high = "#c0392b") +
scale_size_continuous(
range = c(3, 10),
labels = comma,
name = "Households"
) +
coord_flip() +
labs(
title = "Fuel Oil & Propane Users Face Severe Energy Burden",
subtitle = "Low-income households (<100% FPL) by primary heating fuel | Dot size = # of households",
x = NULL,
y = "Energy Burden (% of household income)",
caption = "Source: LEAD Tool 2022 | Households below 100% Federal Poverty Level (~$15K individual, $31K family of 4)"
) +
theme_minimal(base_size = 11) +
theme(
plot.title = element_text(face = "bold", size = 14),
panel.grid.major.y = element_blank()
)Key Observations:
Let’s step back and look at the big picture across all income levels.
statewide_summary <- mi_fpl %>%
group_by(FPL150) %>%
summarise(
total_households = sum(UNITS, na.rm = TRUE),
total_income_weighted = sum(`HINCP*UNITS`, na.rm = TRUE),
households_with_income = sum(`HINCP UNITS`, na.rm = TRUE),
total_energy_weighted = sum(`ELEP*UNITS`, na.rm = TRUE) +
sum(`GASP*UNITS`, na.rm = TRUE) +
sum(`FULP*UNITS`, na.rm = TRUE),
households_with_energy = sum(`ELEP UNITS`, na.rm = TRUE),
.groups = "drop"
) %>%
mutate(
avg_income = total_income_weighted / households_with_income,
avg_energy_cost = total_energy_weighted / households_with_energy,
energy_burden_pct = (avg_energy_cost / avg_income) * 100
)
# Display statewide summary
statewide_summary %>%
mutate(
total_households = comma(total_households),
avg_income = dollar(avg_income),
avg_energy_cost = dollar(avg_energy_cost),
energy_burden_pct = paste0(round(energy_burden_pct, 1), "%")
) %>%
select(FPL150, total_households, avg_income, avg_energy_cost, energy_burden_pct) %>%
kable(
col.names = c("Income Level", "Households", "Avg Income", "Avg Energy Cost", "Energy Burden"),
caption = "Michigan Statewide Energy Burden by Income Level"
)| Income Level | Households | Avg Income | Avg Energy Cost | Energy Burden |
|---|---|---|---|---|
| 0-100% | 422,000 | $10,267 | $2,282.66 | 22.2% |
| 100-150% | 296,600 | $24,903 | $2,366.44 | 9.5% |
| 150-200% | 314,206 | $35,994 | $2,481.79 | 6.9% |
| 200-400% | 1,209,248 | $60,927 | $2,568.20 | 4.2% |
| 400%+ | 1,767,198 | $156,023 | $2,749.98 | 1.8% |
ggplot(statewide_summary, aes(x = FPL150)) +
geom_col(aes(y = energy_burden_pct, fill = FPL150),
alpha = 0.8, show.legend = FALSE) +
geom_hline(yintercept = 6, linetype = "dashed", color = "red", linewidth = 1) +
geom_hline(yintercept = 10, linetype = "dotted", color = "darkred", linewidth = 1) +
geom_text(
aes(y = energy_burden_pct,
label = sprintf("%.1f%%\n%s HH",
energy_burden_pct,
comma(total_households, accuracy = 1))),
vjust = -0.5, size = 3.5, fontface = "bold"
) +
scale_fill_brewer(palette = "YlOrRd", direction = -1) +
scale_y_continuous(
labels = function(x) paste0(x, "%"),
breaks = seq(0, 30, 5),
expand = expansion(mult = c(0, 0.1))
) +
annotate("text", x = 5.3, y = 6, label = "6% Affordability\nThreshold",
hjust = 1, size = 3, color = "red") +
annotate("text", x = 5.3, y = 10, label = "10% Severe\nBurden",
hjust = 1, size = 3, color = "darkred") +
labs(
title = "Michigan's Energy Affordability Crisis By Income Level",
subtitle = "Even middle-income households face energy burdens above affordability thresholds",
x = "Federal Poverty Level",
y = "Average Energy Burden (% of income)",
caption = "Source: LEAD Tool 2022 (ACS 2018-2022)\n0-100%: Below poverty | 100-150%: 1-1.5x poverty | 150-200%: 1.5-2x poverty | 200-400%: 2-4x poverty | 400%+: >4x poverty\nNumbers show burden % and household count"
) +
theme_minimal(base_size = 12) +
theme(
plot.title = element_text(face = "bold", size = 14),
panel.grid.major.x = element_blank()
)Key Observations:
Understanding which counties face the most severe energy burden helps identify where interventions could have the greatest impact. We develop a priority score that considers both the severity of burden and the number of households affected.
Our prioritization methodology combines three factors:
1. Energy Burden Severity - How high is the average
burden in the county?
2. Household Count - How many low-income households are
affected?
3. Fuel Cost Factor - What percentage use expensive
heating fuels (oil/propane)?
The Priority Score is calculated as:
\[\text{Priority Score} = \text{Energy Burden} \times \text{Households} \times (1 + \frac{\text{% High-Cost Fuel}}{100})\]
This approach ensures we identify: - Counties with catastrophically high burdens (even if small populations) - Large urban counties with moderate burdens but massive scale - Rural areas where expensive fuel types compound the problem
county_hotspots <- mi_fpl %>%
filter(FPL150 == "0-100%") %>%
mutate(
fuel_type = str_remove(`TEN-HFL`, "^(OWNER|RENTER) "),
high_cost_fuel = fuel_type %in% c("FUEL OIL", "BOTTLED GAS")
) %>%
group_by(NAME) %>%
summarise(
total_low_income_hh = sum(UNITS, na.rm = TRUE),
high_cost_fuel_hh = sum(UNITS[high_cost_fuel], na.rm = TRUE),
pct_high_cost_fuel = (high_cost_fuel_hh / total_low_income_hh) * 100,
total_income_weighted = sum(`HINCP*UNITS`, na.rm = TRUE),
households_with_income = sum(`HINCP UNITS`, na.rm = TRUE),
total_energy_weighted = sum(`ELEP*UNITS`, na.rm = TRUE) +
sum(`GASP*UNITS`, na.rm = TRUE) +
sum(`FULP*UNITS`, na.rm = TRUE),
households_with_energy = sum(`ELEP UNITS`, na.rm = TRUE),
avg_energy_burden = (total_energy_weighted / households_with_energy) /
(total_income_weighted / households_with_income) * 100,
# Priority score: burden × households × fuel cost factor
priority_score = avg_energy_burden * total_low_income_hh *
(1 + pct_high_cost_fuel/100),
.groups = "drop"
) %>%
filter(total_low_income_hh > 50) %>%
arrange(desc(priority_score))
# Show top 15 priority counties
county_hotspots %>%
slice_head(n = 15) %>%
select(NAME, total_low_income_hh, avg_energy_burden, pct_high_cost_fuel, priority_score) %>%
mutate(
total_low_income_hh = comma(total_low_income_hh),
avg_energy_burden = paste0(round(avg_energy_burden, 1), "%"),
pct_high_cost_fuel = paste0(round(pct_high_cost_fuel, 1), "%"),
priority_score = comma(round(priority_score))
) %>%
kable(
col.names = c("County", "Low-Income HH", "Energy Burden", "% High-Cost Fuel", "Priority Score"),
caption = "Top 15 Priority Counties for Intervention"
)| County | Low-Income HH | Energy Burden | % High-Cost Fuel | Priority Score |
|---|---|---|---|---|
| Wayne County | 112,698 | 23.2% | 1.2% | 2,646,244 |
| Oakland County | 36,236 | 23.3% | 2.3% | 862,172 |
| Macomb County | 29,756 | 20.6% | 2.4% | 627,891 |
| Genesee County | 20,984 | 24.8% | 2% | 530,923 |
| Kent County | 20,733 | 14.6% | 2.6% | 311,548 |
| Ingham County | 14,521 | 17.7% | 3.5% | 265,314 |
| Saginaw County | 10,557 | 23.5% | 5.5% | 261,630 |
| Washtenaw County | 16,036 | 15.5% | 1.8% | 253,678 |
| Berrien County | 8,156 | 25.1% | 5.1% | 215,493 |
| Kalamazoo County | 11,007 | 17% | 5.7% | 197,517 |
| St. Clair County | 6,285 | 21.5% | 9.3% | 147,729 |
| Jackson County | 6,428 | 20.6% | 4.4% | 138,286 |
| Muskegon County | 6,674 | 19.5% | 5.8% | 137,628 |
| Calhoun County | 5,495 | 20.5% | 7.2% | 120,853 |
| Isabella County | 4,422 | 23.3% | 13.1% | 116,472 |
top_15_priority <- county_hotspots %>% slice_head(n = 15)
ggplot(top_15_priority, aes(x = reorder(NAME, priority_score))) +
geom_col(aes(y = priority_score/1000), fill = "#e74c3c", alpha = 0.7) +
geom_point(aes(y = avg_energy_burden * 10, size = total_low_income_hh),
color = "#3498db") +
scale_y_continuous(
name = "Intervention Priority Score (thousands)",
sec.axis = sec_axis(~ . / 10, name = "Energy Burden (%)")
) +
scale_size_continuous(
range = c(3, 12),
labels = comma,
name = "Low-Income\nHouseholds"
) +
coord_flip() +
labs(
title = "Top 15 Counties for Energy Burden Intervention Priority",
subtitle = "Priority score combines burden severity, household count, and fuel cost factors",
x = NULL,
caption = "Source: LEAD Tool 2022 | Blue dots show energy burden % | Dot size indicates household count\nPriority Score = Energy Burden × Households × (1 + % High-Cost Fuel/100)"
) +
theme_minimal(base_size = 11) +
theme(
plot.title = element_text(face = "bold", size = 13),
legend.position = "right"
)Key Observations:
This analysis reveals the extent of energy affordability challenges facing Michigan households across multiple dimensions:
Geographic Concentration: - Multiple counties exceed 30% energy burden for low-income households - Rural and northern counties show disproportionate impact - Urban counties (Wayne, Oakland, Macomb) affect largest absolute populations
Housing Characteristics: - Building age shows modest impact (~2 percentage points difference) - Low-income homeowners face higher burdens (26%) than renters (18%) - Pattern suggests factors beyond housing quality drive the crisis
Heating Fuel Impact: - Fuel oil and propane users face 40%+ burdens - Natural gas users represent largest affected population - Electricity-only homes show lowest burden among major fuel types
This analysis uses 2018-2022 American Community Survey data and cannot: - Capture recent energy price volatility - Identify individual households for direct outreach - Assess whether conditions are improving or worsening over time - Account for household-specific factors like disability or age
Data Source: U.S. Department of Energy Low-Income Energy Affordability Data (LEAD) Tool, 2022 edition
Methodology: Two-stage aggregation approach with household-weighted averages to properly account for sample sizes across 83,337 micro-categories
Key Assumptions: - Missing energy costs treated as zero (conservative) - Priority scoring combines burden severity, household count, and fuel type - Federal Poverty Level categories based on 2024 thresholds
The LEAD dataset provides 83,337 rows representing unique combinations of county, income level, tenure, building age, building type, and heating fuel. To analyze at the county or income level, we:
Stage 1: Calculate row-level averages
avg_income[row] = HINCP*UNITS[row] / HINCP_UNITS[row]
avg_energy[row] = (ELEP*UNITS[row] + GASP*UNITS[row] + FULP*UNITS[row]) / ELEP_UNITS[row]
Stage 2: Aggregate with household weights
weighted_avg_burden[county] = Σ(energy_burden[row] × UNITS[row]) / Σ(UNITS[row])
This prevents small sample categories from distorting county-level estimates.
To reproduce this analysis:
install.packages(c("tidyverse", "scales", "knitr"))Analysis completed: 2025-11-10
Document generated with R Markdown