Does Higher Healthcare Spending Guarantee Better Coverage?

Comparing healthcare spending to coverage (in the year 2020). Higher spending doesn’t always equate to better health coverage, with notable outliers like the United States showing high costs but low coverage, while countries like Brunei, Turkey and Thailand achieve high coverage with lower spending.

A4 Size Viz
Our World in Data
Public Health
Author

Aditya Dahiya

Published

June 4, 2024

Health Expenditure vs. Coverage: Are We Balancing the Scales?

The scatterplot for the year 2020, using data from Our World in Data and the Institute of Health Metrics and Evaluation (IHME), illustrates the relationship between per capita health expenditure as a percentage of GDP and the Universal Health Coverage (UHC) Service Index across various countries. The graphic reveals that while many countries align along a central diagonal, indicating a proportional relationship between health spending and coverage, significant outliers exist. Countries such as Turkey, Thailand, and Brunei achieve higher UHC indices despite relatively low spending, highlighted in green. Conversely, nations like the United States, Afghanistan, Argentina, and Brazil, along with several small island nations (e.g., Tuvalu, Palau, Nauru, Lesotho, Marshall Islands), exhibit lower coverage relative to their health expenditure, marked in red. Notably, the United States stands out as a wealthy country with exceptionally high health spending but inadequate coverage. These findings suggest that the efficiency and allocation of healthcare spending are crucial for achieving extensive and equitable health coverage, rather than the sheer amount of money spent.

The top scatterplot compares per capita health expenditure as a percentage of GDP (x-axis) to the Universal Health Coverage Service Index (y-axis) for the year 2020, with each dot representing a country. The bottom faceted scatterplot series displays the same indicators across five time points: 2000, 2005, 2010, 2015, and 2020. Each panel shows the percentage of GDP spent on healthcare (x-axis) against the UHC Service Index (y-axis), with each dot representing a country. Over the two decades, healthcare spending has generally increased, but the UHC Index has not risen proportionately, suggesting growing inequity in healthcare fund utilization.

How I made this graphic?

Getting the data

Code
# Data Import and Wrangling Tools
library(tidyverse)            # All things tidy
library(owidR)                # Get data from Our World in R

# Final plot tools
library(scales)               # Nice Scales for ggplot2
library(fontawesome)          # Icons display in ggplot2
library(ggtext)               # Markdown text support for ggplot2
library(showtext)             # Display fonts in ggplot2
library(colorspace)           # To lighten and darken colours

# The Expansion pack to ggplot2
library(ggforce)              # To learn some new geom-extensions
library(wbstats)              # To fetch World Bank data on Country
                              # Codes and GDP per capita
# Credits: https://stackoverflow.com/questions/48199791/rounded-corners-in-ggplot2
# Credits @X: @TeunvandenBrand
# devtools::install_github("teunbrand/elementalist")
library(elementalist)         # Rounded corners of panels


# Data from 1990 to 2021
# Coverage of essential health services, as defined by the 
# UHC service coverage index - Both Sexes - Estimate
rawdf1 <- owidR::owid("healthcare-access-quality-ihme")

# Our World in Data
# Current health expenditure per capita, PPP (current international $)
# GDP per capita Population (historical estimates) Countries Continents
rawdf2 <- owidR::owid("healthcare-expenditure-vs-gdp")

# Getting country ISO2 codes for geom_flag()
rawdf3 <- wbstats::wb_countries()

Visualization Parameters

Code
# Font for titles
font_add_google("Glegoo",
  family = "title_font"
) 

# Font for the caption
font_add_google("Stint Ultra Condensed",
  family = "caption_font"
) 

# Font for plot text
font_add_google("Maiden Orange",
  family = "body_font"
) 

showtext_auto()

# Colour Palette
mypal <- paletteer::paletteer_d("PNWColors::Mushroom")
mypal

# Background Colour
bg_col <- mypal[6]
text_col <- mypal[1]
text_hil <- mypal[2]

# Base Text Size
bts <- 80

plot_title <- "Does Higher Healthcare Spending Guarantee Better Coverage?"

plot_subtitle <- str_wrap("Comparing healthcare spending to coverage (in the year 2020). Higher spending doesn't always equate to better health coverage, with notable outliers like the United States showing high costs but low coverage, while countries like Brunei, Turkey and Thailand achieve high coverage with lower spending.", 160)
str_view(plot_subtitle)

# Caption stuff for the plot
sysfonts::font_add(
  family = "Font Awesome 6 Brands",
  regular = here::here("docs", "Font Awesome 6 Brands-Regular-400.otf")
)
github <- "&#xf09b"
github_username <- "aditya-dahiya"
xtwitter <- "&#xe61b"
xtwitter_username <- "@adityadahiyaias"
social_caption_1 <- glue::glue("<span style='font-family:\"Font Awesome 6 Brands\";'>{github};</span> <span style='color: {text_hil}'>{github_username}  </span>")
social_caption_2 <- glue::glue("<span style='font-family:\"Font Awesome 6 Brands\";'>{xtwitter};</span> <span style='color: {text_hil}'>{xtwitter_username}</span>")


plot_caption <- paste0(
  "**Data:** Our World in Data | World Health Organization  |  ",
  "**Code:** ", 
  social_caption_1, 
  " |  **Graphics:** ", 
  social_caption_2
  )

about_the_data <- "About the Data: The data for the Universal Health Coverage (UHC) Service Coverage Index, which assesses the extent of health services coverage in various countries, is sourced from the World Health Organization (WHO) Global Health Observatory. The index reflects the breadth and quality of essential health services provided to populations without financial hardship. The per capita current health expenditure and GDP per capita data are obtained from the Institute for Health Metrics and Evaluation (IHME). These indicators help measure the financial resources allocated to health relative to the overall economic output of a country, providing insights into the priority given to health in national budgets."

inset_title <- "Trends in Health Expenditure and Coverage: Spending More, Covering Less Equitably?"

inset_subtitle <- str_wrap("The evolution of healthcare spending vs. service coverage from 2000 to 2020. While countries are increasingly allocating a higher percentage of their GDP to healthcare, the Universal Health Coverage Service Index has not risen proportionately. This trend suggests a growing inequity in how healthcare funds are being utilized, indicating that higher spending does not necessarily translate to more comprehensive coverage. (Each dot is a country)", 225)
str_view(inset_subtitle)

Exploratory Data Analysis and Data Wrangling

Code
# temp1 <- owidR::owid_search("Healthcare")
# temp2 <- owidR::owid_search("health")

# Universal Health Coverage Service Index
df1 <- rawdf1 |> 
  as_tibble() |>
  rename(coverage = `Coverage of essential health services, as defined by the UHC service coverage index - Both Sexes - Estimate`) |> 
  rename(
    country = entity
  )


# Current Health Expenditure (Per Person) and GDP Per Capita
df2 <- rawdf2 |> 
  as_tibble() |> 
  janitor::clean_names() |> 
  select(-countries_continents, -population_historical) |> 
  drop_na() |> 
  rename(
    country = entity,
    health_exp = current_health_expenditure_per_capita_ppp_current_international
  )

plotdf <- df1 |> 
  left_join(df2) |> 
  drop_na()

plotdf |> 
  count(year, sort = T) |> 
  ggplot(
    aes(y = n, x = year)
  ) +
  geom_point() +
  scale_x_continuous(breaks = 2000:2021)

plotdf |> 
  pull(year) |> 
  range()

highlight_countries <- c(
  "Turkey", "Thailand", "Brunei", "United States", 
  "Afghanistan", "Argentina", "Brazil", "India",
  "China", "Mexico", "Armenia", "Tajikistan"
)


merge_codes <- rawdf3 |> 
  select(iso3c, iso2c) |> 
  mutate(iso2c = str_to_lower(iso2c)) |> 
  rename(code = iso3c)

plotdf1 <- plotdf |>
  mutate(
    ratio = health_exp / gdp_per_capita,
    col_var = sqrt(coverage) / sqrt(ratio),
    size_var = country %in% highlight_countries
  ) |> 
  arrange(desc(size_var)) |> 
  left_join(merge_codes)

range(plotdf1$col_var)

Visualization

Code
g_base <- plotdf1 |>  
  filter(year == 2020) |>
  ggplot(
    mapping = aes(
      x = ratio,
      y = coverage,
      label = country
    )
  ) + 
  ggflags::geom_flag(
    mapping = aes(country = iso2c),
    size = 7
  ) +
  # During iterations, I use geom_point() instead of geom_flag() to save rendering time
  # geom_point(
  #   alpha = 0.4
  # ) +
  geom_text(
    mapping = aes(size = size_var, colour = col_var),
    check_overlap = TRUE,
    family = "body_font",
    nudge_y = -1.5
  ) +
  scale_size_manual(
    values = c(bts/6, bts/3)
  ) +
  geom_abline(
    colour = mypal[5],
    linewidth = 80,
    alpha = 0.2,
    slope = 80/0.11,
    intercept = 9,
    lineend = "round"
  ) +
  annotate(
    geom = "text",
    x = 0.005, y = 80,
    family = "caption_font",
    size = bts / 6,
    label = str_wrap(about_the_data, 80),
    hjust = 0,
    vjust = 1,
    lineheight = 0.3,
    colour = text_col
  ) +
  scale_x_continuous(
    limits = c(0, 0.2),
    labels = scales::label_percent(),
    oob = scales::squish,
    expand = expansion(c(0, 0.05))
  ) +
  scale_y_continuous(
    limits = c(20, 80)
  ) +
  paletteer::scale_color_paletteer_c(
    "ggthemes::Red-Green-Gold Diverging",
    direction = 1
  ) +
  labs(
    title = plot_title,
    subtitle = plot_subtitle,
    caption = plot_caption,
    y = "Universal Health Coverage Service Index",
    x = "Percentage of GDP spent on healthcare"
  ) +
  theme_classic(
    base_family = "body_font",
    base_size = bts
  ) +
  theme(
    legend.position = "none",
    plot.title.position = "plot",
    plot.title = element_text(
      # family = "title_font",
      size = 2 * bts,
      hjust = 0,
      lineheight = 0.35,
      colour = text_hil,
      margin = margin(20,0,0,0, "mm")
    ),
    plot.subtitle = element_text(
      lineheight = 0.3,
      hjust = 0,
      size = bts,
      colour = text_hil,
      margin = margin(10,0,5,0, "mm")
    ),
    plot.caption = element_textbox(
      family = "caption_font",
      colour = text_hil,
      hjust = 0.5,
      margin = margin(175,0,0,20, "mm")
    ),
    axis.line = element_line(
      colour = text_col,
      linewidth = 0.5,
      arrow = arrow(
        angle = 20,
        length = unit(bts / 7, "mm")
      )
    ),
    axis.ticks = element_blank(),
    axis.ticks.length = unit(0, "mm"),
    axis.text = element_text(
      colour = text_col,
      margin = margin(0,0,0,0, "mm")
    ),
    axis.title = element_text(
      colour = text_col,
      margin = margin(0,0,0,0, "mm")
    )
  )

btsi = bts * 0.75

g2 <- plotdf1 |> 
  filter(year %in% seq(2000, 2020, 5)) |> 
  ggplot(
    mapping = aes(
      x = ratio, 
      y = coverage,
      colour = col_var,
      group = year
    )
  ) + 
  geom_point(
    pch = 20,
    size = 3.5,
    alpha = 0.8
  ) +
  geom_abline(
    colour = mypal[5],
    linewidth = 20,
    alpha = 0.2,
    slope = 80/0.1,
    intercept = 4,
    lineend = "round"
  ) +
  facet_wrap(
    ~ year,
    nrow = 1
  ) +
  scale_x_continuous(
    limits = c(0, 0.15),
    oob = scales::squish,
    expand = expansion(c(0, 0.02)),
    labels = scales::label_percent()
  ) + 
  scale_y_continuous(
    expand = expansion(0)
  ) + 
  paletteer::scale_color_paletteer_c(
    "ggthemes::Red-Green-Gold Diverging",
    direction = 1
  ) +
  labs(
    y = "Universal Health Coverage Service Index",
    x = "Percentage of GDP spent on healthcare",
    title = inset_title,
    subtitle = inset_subtitle
  ) +
  theme_classic(
    base_family = "body_font",
    base_size = btsi
  ) + 
  theme(
    plot.background = element_rect(
      fill = "transparent",
      colour = "transparent"
    ),
    panel.background = element_rect(
      fill = "transparent",
      colour = "transparent"
    ),
    legend.position = "none",
    plot.subtitle = element_text(
      colour = text_col,
      lineheight = 0.3,
      hjust = 0.5,
      margin = margin(2,0,2,0, "mm")
    ),
    plot.title = element_text(
      colour = text_col,
      margin = margin(0,0,0,0, "mm"),
      hjust = 0.5,
      size = btsi * 1.5
    ),
    axis.line = element_line(
      colour = text_col,
      linewidth = 0.3,
      arrow = arrow(length = unit(3, "mm"))
    ),
    axis.text = element_text(
      colour = text_col
    ),
    axis.title = element_text(
      colour = text_col
    ),
    strip.background = element_rect(
      colour = "transparent",
      fill = "transparent"
    ),
    strip.text = element_text(
      colour = text_col,
      margin = margin(0,0,0,0, "mm"),
      size = btsi * 2
    ),
    axis.ticks.length = unit(0, "mm"),
    plot.title.position = "plot"
  )

Add annotations and insets

Code
# QR Code for the plot
url_graphics <- paste0(
  "https://aditya-dahiya.github.io/projects_presentations/projects/",
  # The file name of the current .qmd file
  "owid_health_exp_cover",
  ".qmd"
)
# remotes::install_github('coolbutuseless/ggqr')
# library(ggqr)
plot_qr <- ggplot(
  data = NULL, 
  aes(x = 0, y = 0, label = url_graphics)
  ) + 
  ggqr::geom_qr(
    colour = text_hil, 
    fill = bg_col,
    size = 1.5
    ) +
  # labs(caption = "Scan for the Interactive Version") +
  coord_fixed() +
  theme_void() +
  theme(plot.background = element_rect(
    fill = NA, 
    colour = NA
    ),
    plot.caption = element_text(
      hjust = 0.5,
      margin = margin(0,0,0,0, "mm"),
      family = "caption_font",
      size = bts/1.8
    )
  )

library(patchwork)
g <- g_base +
  inset_element(
    p = plot_qr,
    left = 0.81, right = 0.91,
    top = 0.92, bottom = 0.82,
    align_to = "full",
    on_top = TRUE
  ) +
  inset_element(
    p = g2,
    left = 0, right = 1,
    top = 0.33, bottom = 0.04,
    align_to = "full"
  ) +
  plot_annotation(
    theme = theme(
      plot.background = element_rect(
        fill = "transparent",
        colour = "transparent"
      ),
      panel.background = element_rect(
        fill = "transparent",
        colour = "transparent"
      )
    )
  )

Save graphic and a thumbnail

Code
ggsave(
  filename = here::here("data_vizs", "a4_owid_health_exp_cover.png"),
  plot = g,
  height = 297 * 2,
  width = 210 * 2,
  units = "mm",
  bg = bg_col
)

library(magick)
# Saving a thumbnail for the webpage
image_read(here::here("data_vizs", 
                      "a4_owid_health_exp_cover.png")) |> 
  image_resize(geometry = "400") |> 
  image_write(here::here("data_vizs", "thumbnails", 
                         "owid_health_exp_cover.png"))

A poster for the print

Code
bts = 100

g_base <- plotdf1 |>  
  filter(year == 2020) |>
  ggplot(
    mapping = aes(
      x = ratio,
      y = coverage,
      label = country
    )
  ) + 
  ggflags::geom_flag(
    mapping = aes(country = iso2c),
    size = 10
  ) +
  
  # During iterations, I use geom_point() instead of geom_flag() to save rendering time
  # geom_point(
  #   alpha = 0.4,
  #   size = 9
  # ) +
  geom_text(
    mapping = aes(
      size = size_var, 
      colour = col_var
    ),
    check_overlap = TRUE,
    family = "body_font",
    nudge_y = -0.8
  ) +
  scale_size_manual(
    values = c(12, 24)
  ) +
  geom_abline(
    colour = mypal[5],
    linewidth = 80,
    alpha = 0.2,
    slope = 80/0.11,
    intercept = 9,
    lineend = "round"
  ) +
  annotate(
    geom = "text",
    x = 0.005, y = 80,
    family = "caption_font",
    size = 16,
    label = str_wrap(about_the_data, 80),
    hjust = 0,
    vjust = 1,
    lineheight = 0.3,
    colour = text_col
  ) +
  scale_x_continuous(
    limits = c(0, 0.2),
    labels = scales::label_percent(),
    oob = scales::squish,
    expand = expansion(c(0, 0.05))
  ) +
  scale_y_continuous(
    limits = c(20, 80)
  ) +
  coord_cartesian(
    clip = "on"
  ) +
  paletteer::scale_color_paletteer_c(
    "ggthemes::Red-Green-Gold Diverging",
    direction = 1
  ) +
  labs(
    title = plot_title,
    subtitle = plot_subtitle,
    caption = plot_caption,
    y = "Universal Health Coverage Service Index",
    x = "Percentage of GDP spent on healthcare"
  ) +
  theme_classic(
    base_family = "body_font",
    base_size = bts
  ) +
  theme(
    legend.position = "none",
    plot.title.position = "plot",
    plot.title = element_text(
      size = 2.5 * bts,
      hjust = 0,
      lineheight = 0.35,
      colour = text_hil,
      margin = margin(10,0,0,0, "mm")
    ),
    plot.subtitle = element_text(
      lineheight = 0.35,
      hjust = 0,
      size = bts,
      colour = text_hil,
      margin = margin(5,0,5,0, "mm")
    ),
    plot.caption = element_textbox(
      family = "caption_font",
      colour = text_hil,
      hjust = 0.5,
      margin = margin(250,0,0,0, "mm")
    ),
    plot.margin = margin(10,10,5,10, "mm"),
    axis.line = element_line(
      colour = text_col,
      linewidth = 0.5,
      arrow = arrow(
        angle = 20,
        length = unit(bts / 8, "mm")
      )
    ),
    axis.ticks = element_line(
      linetype = 1,
      colour = text_col,
      linewidth = 0.3
    ),
    axis.ticks.length = unit(4, "mm"),
    axis.text = element_text(
      colour = text_col,
      margin = margin(0,0,0,0, "mm")
    ),
    axis.title = element_text(
      colour = text_col,
      margin = margin(0,0,0,0, "mm")
    )
  )

btsi = bts * 0.75

g2 <- plotdf1 |> 
  filter(year %in% seq(2000, 2020, 5)) |> 
  ggplot(
    mapping = aes(
      x = ratio, 
      y = coverage,
      colour = col_var,
      group = year
    )
  ) + 
  ggflags::geom_flag(
    mapping = aes(country = iso2c),
    size = 5
  ) +
  # geom_point(
  #   pch = 20,
  #   size = 3.5
  # ) +
  geom_abline(
    colour = mypal[5],
    linewidth = 20,
    alpha = 0.2,
    slope = 80/0.1,
    intercept = 4,
    lineend = "round"
  ) +
  facet_wrap(
    ~ year,
    nrow = 1
  ) +
  scale_x_continuous(
    limits = c(0, 0.15),
    oob = scales::squish,
    expand = expansion(c(0, 0.02)),
    labels = scales::label_percent()
  ) + 
  scale_y_continuous(
    expand = expansion(0)
  ) + 
  paletteer::scale_color_paletteer_c(
    "ggthemes::Red-Green-Gold Diverging",
    direction = 1
  ) +
  labs(
    y = "Universal Health Coverage Service Index",
    x = "Percentage of GDP spent on healthcare",
    title = inset_title,
    subtitle = inset_subtitle
  ) +
  theme_classic(
    base_family = "body_font",
    base_size = btsi
  ) + 
  theme(
    plot.background = element_rect(
      fill = "transparent",
      colour = "transparent"
    ),
    panel.background = element_rect(
      fill = "transparent",
      colour = "transparent"
    ),
    legend.position = "none",
    plot.subtitle = element_text(
      colour = text_col,
      size = btsi * 0.9,
      lineheight = 0.3,
      hjust = 0.5,
      margin = margin(2,0,2,0, "mm")
    ),
    plot.title = element_text(
      colour = text_col,
      margin = margin(0,0,0,0, "mm"),
      hjust = 0.5,
      size = btsi * 1.5
    ),
    axis.line = element_line(
      colour = text_col,
      linewidth = 0.3,
      arrow = arrow(length = unit(3, "mm"))
    ),
    axis.text = element_text(
      colour = text_col
    ),
    axis.title = element_text(
      colour = text_col
    ),
    strip.background = element_rect(
      colour = "transparent",
      fill = "transparent"
    ),
    strip.text = element_text(
      colour = text_col,
      margin = margin(0,0,0,0, "mm"),
      size = btsi * 2
    ),
    axis.ticks.length = unit(0, "mm"),
    plot.title.position = "plot",
    panel.grid = element_line(
      linetype = 3,
      colour = darken(bg_col, 0.4),
      linewidth = 0.4
    )
  )


# QR Code for the plot
url_graphics <- paste0(
  "https://aditya-dahiya.github.io/projects_presentations/projects/",
  # The file name of the current .qmd file
  "owid_health_exp_cover",
  ".qmd"
)
# remotes::install_github('coolbutuseless/ggqr')
# library(ggqr)
plot_qr <- ggplot(
  data = NULL, 
  aes(x = 0, y = 0, label = url_graphics)
  ) + 
  ggqr::geom_qr(
    colour = text_hil, 
    fill = "transparent",
    size = 1.5
    ) +
  # labs(caption = "Scan for the Interactive Version") +
  coord_fixed() +
  theme_void() +
  theme(plot.background = element_rect(
    fill = "transparent", 
    colour = "transparent"
    )
  )

library(patchwork)
g <- g_base +
  inset_element(
    p = plot_qr,
    left = 0.02, right = 0.1,
    top = 0.83, bottom = 0.75,
    align_to = "panel",
    on_top = TRUE
  ) +
  inset_element(
    p = g2,
    left = 0, right = 1,
    top = 0.28, bottom = 0.01,
    align_to = "full"
  ) +
  plot_annotation(
    theme = theme(
      plot.background = element_rect(
        fill = "transparent",
        colour = "transparent"
      ),
      panel.background = element_rect(
        fill = "transparent",
        colour = "transparent"
      )
    )
  )

ggsave(
  filename = here::here("data_vizs", "poster_owid_health_exp_cover.png"),
  plot = g,
  height = 36,
  width = 24,
  units = "in",
  bg = bg_col
)