---
title: "UK Household Spending Inequality by Income Level in FYE 2024"
subtitle: "Housing costs create the largest spending gap between rich and poor households"
description: "UK household spending analysis reveals housing costs burden poor families disproportionately, creating the largest spending gap across income groups. Visualization explores spending inequality across all categories using ONS Family Spending Survey data."
date: "2025-09-24"
categories: ["MakeoverMonday", "2025"]
tags: [
"household-spending",
"income-inequality",
"housing-costs",
"uk-statistics",
"ons-data",
"data-storytelling",
"ggplot2",
"ridge-plots",
"dot-plots",
"social-policy"
]
image: "thumbnails/mm_2025_39.png"
format:
html:
toc: true
toc-depth: 5
code-link: true
code-fold: true
code-tools: true
code-summary: "Show code"
editor_options:
chunk_output_type: inline
execute:
freeze: true
cache: true
error: false
message: false
warning: false
eval: true
---
```{r}
#| label: setup-links
#| include: false
# CENTRALIZED LINK MANAGEMENT
## Project-specific info
current_year <- 2025
current_week <- 39
project_file <- "mm_2025_39.qmd"
project_image <- "mm_2025_39.png"
## Data Sources
data_main <- "https://data.world/makeovermonday/2025w39-family-spending-in-the-uk"
data_secondary <- "https://data.world/makeovermonday/2025w39-family-spending-in-the-uk"
## Repository Links
repo_main <- "https://github.com/poncest/personal-website/"
repo_file <- paste0("https://github.com/poncest/personal-website/blob/master/data_visualizations/MakeoverMonday/", current_year, "/", project_file)
## External Resources/Images
chart_original <- "https://raw.githubusercontent.com/poncest/MakeoverMonday/refs/heads/master/2025/Week_39/original_chart.png"
## Organization/Platform Links
org_primary <- "https://www.ons.gov.uk/peoplepopulationandcommunity/personalandhouseholdfinances/expenditure/bulletins/familyspendingintheuk/april2023tomarch2024#data-sources-and-quality"
org_secondary <- "https://www.ons.gov.uk/"
# Helper function to create markdown links
create_link <- function(text, url) {
paste0("[", text, "](", url, ")")
}
# Helper function for citation-style links
create_citation_link <- function(text, url, title = NULL) {
if (is.null(title)) {
paste0("[", text, "](", url, ")")
} else {
paste0("[", text, "](", url, ' "', title, '")')
}
}
```
### Original
The original visualization **Figure 5: Poorer households spent proportionally more on housing, fuel and power than richer households in FYE 2024 - Average weekly household expenditure as a percentage of total weekly expenditure, by quintile group, UK, financial year ending (FYE) 2024** comes from `r create_link("Family spending in the UK: April 2023 to March 2024", data_secondary)`
<!--  -->

### Makeover
{#fig-1}
### <mark> **Steps to Create this Graphic** </mark>
#### 1. Load Packages & Setup
```{r}
#| label: load
#| warning: false
#| message: false
#| results: "hide"
## 1. LOAD PACKAGES & SETUP ----
suppressPackageStartupMessages({
if (!require("pacman")) install.packages("pacman")
pacman::p_load(
tidyverse, # Easily Install and Load the 'Tidyverse'
ggtext, # Improved Text Rendering Support for 'ggplot2'
showtext, # Using Fonts More Easily in R Graphs
scales, # Scale Functions for Visualization
glue, # Interpreted String Literals
patchwork, # Using Fonts More Easily in R Graphs
ggridges # Ridgeline Plots in 'ggplot2'
)
})
### |- figure size ----
camcorder::gg_record(
dir = here::here("temp_plots"),
device = "png",
width = 12,
height = 10,
units = "in",
dpi = 320
)
# Source utility functions
suppressMessages(source(here::here("R/utils/fonts.R")))
source(here::here("R/utils/social_icons.R"))
source(here::here("R/utils/image_utils.R"))
source(here::here("R/themes/base_theme.R"))
```
#### 2. Read in the Data
```{r}
#| label: read
#| include: true
#| eval: true
#| warning: false
#|
households_spending_raw <- read_csv(
here::here('data/MakeoverMonday/2025/Figure_5__Poorer_households_spent_proportionally_more_on_housing,_fuel_and_power_than_richer_households_in_FYE_2024.csv')) |>
janitor::clean_names()
```
#### 3. Examine the Data
```{r}
#| label: examine
#| include: true
#| eval: true
#| results: 'hide'
#| warning: false
glimpse(households_spending_raw)
skimr::skim_without_charts(households_spending_raw)
```
#### 4. Tidy Data
```{r}
#| label: tidy
#| warning: false
# Create lookup tables
quintile_labels <- c(
"bottom_fifth" = "Bottom fifth",
"x2nd" = "2nd quintile",
"x3rd" = "3rd quintile",
"x4th" = "4th quintile",
"top_fifth" = "Top fifth",
"all_households" = "All households"
)
# Use the category names from the data
category_lookup <- c(
"Food and non-alcoholic drinks" = "Food & non-alcoholic drinks",
"Alcoholic drink tobacco and narcotics" = "Alcohol, tobacco & narcotics", # Note: no comma in original
"Clothing and footwear" = "Clothing & footwear",
"Housing (net) fuel and power" = "Housing, fuel & power", # Note: no comma in original
"Household goods and services" = "Household goods & services",
"Transport" = "Transport",
"Communication" = "Communication",
"Recreation and culture" = "Recreation & culture",
"Restaurants and hotels" = "Restaurants & hotels",
"Miscellaneous goods and services" = "Miscellaneous goods & services",
"Other expenditure items" = "Other expenditure"
)
# Data prep
households_spending_clean <- households_spending_raw |>
pivot_longer(
cols = -percent_of_total_weekly_expendtiure,
names_to = "income_quintile",
values_to = "percentage"
) |>
rename(category = percent_of_total_weekly_expendtiure) |>
mutate(
# Clean quintile names
income_quintile = quintile_labels[income_quintile],
income_quintile = factor(income_quintile, levels = quintile_labels),
# Clean category names using direct lookup
category_clean = category_lookup[category]
) |>
filter(income_quintile != "All households") |>
group_by(category_clean) |>
mutate(gap = max(percentage) - min(percentage)) |>
ungroup() |>
mutate(category_clean = fct_reorder(category_clean, gap, .desc = FALSE))
# Dot plot data
dot_data <- households_spending_clean |>
group_by(category_clean) |>
summarise(
poorest_pct = percentage[income_quintile == "Bottom fifth"],
richest_pct = percentage[income_quintile == "Top fifth"],
gap = first(gap),
.groups = "drop"
)
# Ridges plot data
ridges_data <- households_spending_clean
```
#### 5. Visualization Parameters
```{r}
#| label: params
#| include: true
#| warning: false
### |- plot aesthetics ----
# Get base colors with custom palette
colors <- get_theme_colors(palette = list(
primary = "#2C3E50",
secondary = "#E67E22",
accent = "#95A5A6",
light_gray = "#BDC3C7",
dark_gray = "#7F8C8D"
))
### |- titles and caption ----
title_text <- str_glue("UK Household Spending Inequality by Income Level in FYE 2024")
subtitle_text <-str_glue(
"Housing costs create the largest spending gap between rich and poor households"
)
# Create caption
caption_text <- create_mm_caption(
mm_year = current_year,
mm_week = current_week,
source_text = paste0("UK Office for National Statistics")
)
### |- fonts ----
setup_fonts()
fonts <- get_font_families()
### |- plot theme ----
# Start with base theme
base_theme <- create_base_theme(colors)
# Add weekly-specific theme elements
weekly_theme <- extend_weekly_theme(
base_theme,
theme(
# Text styling
plot.title = element_text(
size = rel(1.4), family = fonts$title, face = "bold",
color = colors$title, lineheight = 1.1, hjust = 0,
margin = margin(t = 5, b = 10)
),
plot.subtitle = element_text(
size = rel(0.9), family = fonts$subtitle, face = "italic",
color = alpha(colors$subtitle, 0.9), lineheight = 1.1,
margin = margin(t = 0, b = 20)
),
# Legend formatting
legend.position = "plot",
legend.justification = "top",
legend.margin = margin(l = 12, b = 5),
legend.key.size = unit(0.8, "cm"),
legend.box.margin = margin(b = 10),
legend.title = element_text(face = "bold"),
# Axis formatting
axis.ticks.y = element_blank(),
axis.ticks.x = element_line(color = "gray", linewidth = 0.5),
# axis.ticks.length = unit(0.2, "cm"),
axis.title.x = element_text(
face = "bold", size = rel(0.85),
margin = margin(t = 10)
),
axis.title.y = element_text(
face = "bold", size = rel(0.85),
margin = margin(r = 10)
),
axis.text.x = element_text(
size = rel(0.85), family = fonts$subtitle,
color = colors$text
),
axis.text.y = element_text(
size = rel(0.85), family = fonts$subtitle,
color = colors$text
),
# Grid lines
panel.grid.minor = element_line(color = "#ecf0f1", linewidth = 0.2),
panel.grid.major = element_line(color = "#ecf0f1", linewidth = 0.4),
# Margin
plot.margin = margin(20, 20, 20, 20)
)
)
# Set theme
theme_set(weekly_theme)
```
#### 6. Plot
```{r}
#| label: plot
#| warning: false
### |- P1: Range Dot Plot ----
p1 <- dot_data |>
ggplot(aes(y = category_clean)) +
# Geoms
geom_segment(aes(x = richest_pct, xend = poorest_pct, yend = category_clean),
linewidth = 1, color = colors$palette$light_gray, alpha = 0.6
) +
geom_point(aes(x = richest_pct), size = 4, color = colors$palette$primary) +
geom_point(aes(x = poorest_pct), size = 4, color = colors$palette$secondary) +
geom_text(
aes(
x = (richest_pct + poorest_pct) / 2,
label = glue("{round(gap,1)} pp")
),
vjust = -1.8, size = 3.3,
color = colors$palette$primary
) +
# Annotate
annotate("point", x = 22, y = 1.5, size = 4, color = colors$palette$primary) + # Richest
annotate("text",
x = 23, y = 1.5, label = "Richest households",
color = colors$palette$primary, size = 3.2, fontface = "bold", hjust = 0
) +
annotate("point", x = 22, y = 1, size = 4, color = colors$palette$secondary) + # Poorest
annotate("text",
x = 23, y = 1, label = "Poorest households",
color = colors$palette$secondary, size = 3.2, fontface = "bold", hjust = 0
) +
# Scales
scale_x_continuous(
labels = percent_format(scale = 1),
expand = expansion(mult = c(0.02, 0.1)),
limits = c(0, 40)
) +
# Labs
labs(
title = "Spending Inequality Across Categories",
subtitle = str_wrap("Range between poorest and richest households (percentage points gap shown)",
width = 65
),
x = "Percentage of total weekly expenditure",
y = NULL,
) +
# Theme
theme(
panel.grid.major.x = element_blank(),
panel.grid.major.y = element_line(color = "#F0F0F0", linewidth = 0.5),
panel.grid.minor = element_blank()
)
### |- P2: Ridgeline Plot ----
p2 <- ridges_data |>
ggplot(aes(x = percentage, y = category_clean)) +
# Geoms
geom_density_ridges(
alpha = 0.8,
scale = 1.2,
rel_min_height = 0.01,
color = "white",
linewidth = 0.8,
fill = colors$palette$accent
) +
# Scales
scale_x_continuous(
labels = percent_format(scale = 1),
expand = expansion(mult = c(0.02, 0.05))
) +
# Labs
labs(
title = "Distribution Across Quintiles",
subtitle = str_wrap("Spending patterns within each category",
width = 40
),
x = "Percentage of total weekly expenditure",
y = NULL,
) +
# Theme
theme(
axis.text.y = element_blank(),
axis.ticks.y = element_blank(),
panel.grid.major.x = element_blank(),
panel.grid.major.y = element_line(color = "#F0F0F0", linewidth = 0.5),
panel.grid.minor = element_blank()
)
### |- Combined Plots ----
combined_plots <- p1 + p2 +
plot_layout(ncol = 2, widths = c(1.4, 1.1))
combined_plots <- combined_plots +
plot_annotation(
title = title_text,
subtitle = subtitle_text,
caption = caption_text,
theme = theme(
plot.title = element_text(
size = rel(1.6),
family = fonts$title,
face = "bold",
color = colors$title,
lineheight = 1.1,
margin = margin(t = 5, b = 5)
),
plot.subtitle = element_markdown(
size = rel(0.95),
family = fonts$subtitle,
color = alpha(colors$subtitle, 0.9),
lineheight = 1.2,
margin = margin(t = 5, b = 10)
),
plot.caption = element_markdown(
size = rel(0.6),
family = fonts$caption,
color = colors$caption,
hjust = 0.5,
margin = margin(t = 05)
)
)
)
```
#### 7. Save
```{r}
#| label: save
#| warning: false
### |- plot image ----
save_plot_patchwork(
plot = combined_plots,
type = "makeovermonday",
year = current_year,
week = current_week,
width = 12,
height = 10
)
```
#### 8. Session Info
::: {.callout-tip collapse="true"}
##### Expand for Session Info
```{r, echo = FALSE}
#| eval: true
#| warning: false
sessionInfo()
```
:::
#### 9. GitHub Repository
::: {.callout-tip collapse="true"}
##### Expand for GitHub Repo
The complete code for this analysis is available in `r create_link(project_file, repo_file)`.
For the full repository, `r create_link("click here", repo_main)`.
:::
#### 10. References
::: {.callout-tip collapse="true"}
##### Expand for References
1. Data:
- Makeover Monday `r current_year` Week `r current_week`: `r create_link("Family spending in the UK", data_main)`
2. Article
- `r create_link("Family spending in the UK", data_secondary)`
:::