Question and data

How did internet use in 2024 differ across World Bank income groups?

I chose this question because the reach of an online service depends partly on how many people use the internet. I used the World Bank’s World Development Indicators dataset, specifically Individuals using the Internet (% of population) (IT.NET.USER.ZS). The underlying provider is the International Telecommunication Union (ITU). The indicator measures the percentage of people who used the internet in the previous three months.

I downloaded the official CSV package on September 25, 2026. Its data update is dated July 13, 2026. I use 2024 to compare the same year across economies; 2025 has much less coverage in this download. Income groups come from the country metadata included in the package, so they reflect that download’s classifications, not reconstructed historical classifications.

Import and clean the data

The CSV has introductory lines, a separate column for each year, blank values, and rows for regional and income-group aggregates. I skip the introductory lines, remove the unnamed trailing column, select 2024, and join the country metadata by country code. Rows without a region or a valid income group are excluded so that aggregates are not counted alongside individual economies. Missing percentages are excluded from the analysis rather than treated as zero.

library(dplyr)
library(ggplot2)

data_file <- list.files(
  "data", pattern = "^API_IT[.]NET[.]USER[.]ZS.*[.]csv$",
  full.names = TRUE
)
metadata_file <- list.files(
  "data", pattern = "^Metadata_Country_API_IT[.]NET[.]USER[.]ZS.*[.]csv$",
  full.names = TRUE
)
stopifnot(length(data_file) == 1, length(metadata_file) == 1)

raw_data <- read.csv(
  data_file, skip = 4, check.names = FALSE,
  na.strings = c("", "NA", ".."), fileEncoding = "UTF-8-BOM"
)
metadata <- read.csv(
  metadata_file, check.names = FALSE,
  na.strings = c("", "NA", ".."), fileEncoding = "UTF-8-BOM"
)

# The downloaded CSV files have an empty column after the final comma.
raw_data <- raw_data[, nzchar(names(raw_data)), drop = FALSE]
metadata <- metadata[, nzchar(names(metadata)), drop = FALSE]

income_order <- c(
  "Low income", "Lower middle income",
  "Upper middle income", "High income"
)

country_groups <- metadata %>%
  transmute(
    country_code = toupper(trimws(`Country Code`)),
    region = trimws(Region),
    income_group = trimws(IncomeGroup)
  )

internet_2024 <- raw_data %>%
  transmute(
    country = trimws(`Country Name`),
    country_code = toupper(trimws(`Country Code`)),
    internet_pct = as.numeric(`2024`)
  )

stopifnot(
  all(raw_data$`Indicator Code` == "IT.NET.USER.ZS"),
  !anyDuplicated(internet_2024$country_code),
  !anyDuplicated(country_groups$country_code)
)

economies <- internet_2024 %>%
  left_join(country_groups, by = "country_code") %>%
  filter(
    !is.na(region), region != "",
    income_group %in% income_order
  ) %>%
  mutate(income_group = factor(income_group, levels = income_order))

clean_data <- economies %>%
  filter(!is.na(internet_pct))

stopifnot(all(clean_data$internet_pct >= 0 &
              clean_data$internet_pct <= 100))

The original file has 265 rows. Excluding 48 aggregate or unclassified rows leaves 217 economies. Of these, 35 have no 2024 value, leaving 182 economies for the comparison. No missing values were imputed. The table below shows both usable observations and missing values by income group.

Insight 1: Average internet use rises across income groups

I use group_by() and summarize() to compare the mean and median internet usage rates. Each economy receives equal weight; these are not population-weighted rates for the combined populations of the groups.

coverage <- economies %>%
  group_by(income_group) %>%
  summarize(missing_2024 = sum(is.na(internet_pct)), .groups = "drop")

income_summary <- clean_data %>%
  group_by(income_group) %>%
  summarize(
    economies = n(),
    mean_users = mean(internet_pct),
    median_users = median(internet_pct),
    .groups = "drop"
  ) %>%
  left_join(coverage, by = "income_group")

knitr::kable(
  income_summary %>%
    select(income_group, economies, missing_2024, mean_users, median_users),
  digits = 1,
  col.names = c("Income group", "With data", "Missing", "Mean (%)", "Median (%)")
)
Income group With data Missing Mean (%) Median (%)
Low income 19 6 23.0 20.5
Lower middle income 43 4 56.6 57.3
Upper middle income 54 5 79.3 81.8
High income 66 20 91.7 93.8

Average internet use was 23.0% in the low-income group and 91.7% in the high-income group, a gap of 68.7 percentage points. Both middle-income groups fall between these extremes, and the medians follow the same ordering. This suggests that the typical reach of an online service differs substantially across income groups, although the comparison does not establish a causal relationship.

internet_plot <- ggplot(income_summary, aes(x = income_group, y = mean_users)) +
  geom_col(fill = "#38688C", width = 0.65) +
  geom_text(
    aes(label = paste0(sprintf("%.1f", mean_users), "%")),
    vjust = -0.6, size = 4.3
  ) +
  scale_x_discrete(labels = c(
    "Low\nincome", "Lower middle\nincome",
    "Upper middle\nincome", "High\nincome"
  )) +
  scale_y_continuous(
    limits = c(0, 100), breaks = seq(0, 100, 20),
    labels = function(x) paste0(x, "%"),
    expand = expansion(mult = c(0, 0.04))
  ) +
  labs(
    title = "Internet use by income group, 2024",
    subtitle = "Unweighted mean across economies with reported data",
    x = NULL, y = "Individuals using the internet (% of population)",
    caption = "Source: World Bank WDI / ITU, IT.NET.USER.ZS. Missing values excluded."
  ) +
  theme_minimal(base_size = 12) +
  theme(
    plot.background = element_rect(fill = "white", colour = NA),
    panel.background = element_rect(fill = "white", colour = NA),
    panel.grid.major.x = element_blank(),
    panel.grid.minor = element_blank(),
    plot.title = element_text(face = "bold"),
    plot.caption = element_text(hjust = 0, size = 9)
  )

internet_plot

Bar chart of unweighted mean internet use in 2024: 23.0 percent in low-income, 56.6 percent in lower-middle-income, 79.3 percent in upper-middle-income, and 91.7 percent in high-income economies.

Insight 2: Internet use below 50% is concentrated in lower-income groups

The averages do not show how many economies still have fewer than half of their residents using the internet. I use count() to classify economies against this 50% threshold, then calculate the share within each income group. The threshold is an analytical choice, not a World Bank classification.

usage_counts <- clean_data %>%
  mutate(below_50 = internet_pct < 50) %>%
  count(income_group, below_50, name = "economies")

threshold_summary <- usage_counts %>%
  group_by(income_group) %>%
  summarize(
    total = sum(economies),
    below_half = sum(economies[below_50]),
    .groups = "drop"
  ) %>%
  mutate(share_below = 100 * below_half / total)

knitr::kable(
  threshold_summary, digits = 1,
  col.names = c("Income group", "With data", "Below 50%", "Share below 50% (%)")
)
Income group With data Below 50% Share below 50% (%)
Low income 19 19 100.0
Lower middle income 43 18 41.9
Upper middle income 54 1 1.9
High income 66 0 0.0

All 19 low-income economies with data had internet use below 50%. The same was true for 18 of 43 lower-middle-income economies (41.9%), compared with 1 of 54 upper-middle-income economies and 0 of 66 high-income economies. For a business considering online customer acquisition, this suggests checking internet use at the country level before relying on a digital-only approach.

These findings apply to the economies with reported values. Missingness varies by group, and omitted economies could change the results. Internet use also does not measure affordability, connection quality, or willingness to buy online.

Sources