Executive Summary

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:

  • Multiple counties show energy burdens exceeding 30%, with the highest reaching 35%
  • Low-income homeowners face higher burdens (26%) than renters (18%)
  • Fuel oil and propane users pay 40%+ of income on energy
  • Pre-1960 homes show 23.4% burden vs 21.9% for newer construction

1. Introduction

What is Energy Burden?

Energy burden is the percentage of household income spent on home energy costs. The U.S. Department of Energy considers:

  • ≤6%: Affordable energy burden
  • 6-10%: Moderate burden
  • >10%: Severe burden

Low-income households often face severe energy burdens due to:

  1. Older, less efficient housing
  2. Limited ability to invest in upgrades
  3. Expensive heating fuel types
  4. Deferred maintenance

About the Data

We analyze data from the LEAD Tool (2022), which uses American Community Survey data (2018-2022) to estimate:

  • Household income
  • Energy expenditures (electricity, gas, other fuels)
  • Housing characteristics (age, type, heating fuel)
  • Geographic distribution across Michigan’s 83 counties

The data is highly granular, with each row representing a unique combination of:

  • County
  • Federal Poverty Level (FPL) category
  • Housing tenure (owner vs renter)
  • Building age
  • Building type
  • Heating fuel type

2. Understanding the Dataset

Loading and Exploring the Data

# 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.

Key Columns Explained

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

# Check Federal Poverty Level categories
cat("Federal Poverty Level Categories:\n")
## Federal Poverty Level Categories:
unique(mi_fpl$FPL150)
## [1] "0-100%"   "100-150%" "150-200%" "200-400%" "400%+"
# Check housing tenure types
cat("\nHousing Tenure Types:\n")
## 
## Housing Tenure Types:
unique(mi_fpl$TEN)
## [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")
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

Critical Understanding: Data Structure

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.


Calculating Energy Burden

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")
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.


3. Simple Analysis: County-Level Energy Burden

Which Counties Face the Biggest Crisis?

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)"
  )
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%

Visualization: Top 20 Counties

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:

  • Multiple rural and northern Michigan counties exceed 30% energy burden
  • All top 20 counties are well above the 6% affordability threshold (red line)
  • Geographic concentration suggests regional factors beyond individual household characteristics

4. Housing Characteristics Analysis

Does Building Age Matter?

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"
  )
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%

Visualization: Building Age Impact

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:

  • Building age shows modest impact (~2 percentage points difference)
  • Even newer homes show burdens 3-4x the affordability threshold
  • Pattern suggests climate and income—not just housing quality—drive the crisis

Homeowners vs Renters

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"
  )
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%

Visualization: Tenure Comparison

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:

  • Low-income homeowners show 26% burden vs 18% for renters
  • Pattern reverses conventional expectations about rental housing
  • Suggests factors like deferred maintenance, rural location, or expensive fuel types among low-income homeowners

5. Deep Dive: Heating Fuel Analysis

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)"
  )
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%

Visualization: Heating Fuel Crisis

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:

  • Fuel oil users: 40%+ burden (smaller household count)
  • Utility gas: 24% burden (largest population affected)
  • Electricity-only: 19% burden (lowest among major fuel types)

6. Statewide Overview

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"
  )
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%

Visualization: Energy Burden Across Income Levels

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:

  • 422,000 households below poverty face 22.2% burden
  • Even households at 150-200% poverty (middle-income) exceed the 6% threshold
  • Only households above 400% FPL achieve affordable energy burden

7. Geographic Targeting: Priority Areas

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.

Developing the Priority Score

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"
  )
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

Visualization: Priority Counties

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:

  • Wayne County ranks highest due to large population (113K low-income households)
  • Rural counties with smaller populations but high burdens also feature prominently
  • Dual-axis shows both scale (bar height) and severity (blue dots)

8. Summary

This analysis reveals the extent of energy affordability challenges facing Michigan households across multiple dimensions:

Scale

  • 422,000 households below poverty face average 23.9% energy burden
  • Nearly 1 million households across all income levels exceed the 6% affordability threshold
  • Total excess energy costs estimated at hundreds of millions annually

Key Patterns

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

Data Limitations

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

Technical Notes

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


Appendix: Data Processing Details

Aggregation Approach

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.

Software Environment

  • R version: 4.3.0+
  • Key packages: tidyverse 2.0.0, scales 1.2.1, knitr 1.42
  • Document generation: R Markdown with knitr

Reproducibility

To reproduce this analysis:

  1. Download Michigan LEAD data from DOE LEAD Tool website
  2. Install required R packages: install.packages(c("tidyverse", "scales", "knitr"))
  3. Update file path in the R Markdown document
  4. Knit document to HTML

Analysis completed: 2025-11-10

Document generated with R Markdown