A Data Story
The World Cup Over The Years
13 → 48
Teams, 1930 to 2026
96
Years of World Cups
3
Host countries in 2026

The Story

World Cup Through The Years

The World Cup grew from 13 teams in Uruguay in 1930 to 48 teams across three countries in 2026, yet the same small group of nations keeps winning. We are going to follow the data to look at how the World Cup, the most watched sporting event, has changed over the course of the last 96 years.

Hosts

Who is Hosting the World Cup

Hosting the World Cup is rare, and only a handful of countries have done it more than once. There have been only a few countries who hosted twice: France, Germany, Italy, Brazil and the US. 2026 changed the trends. It was the first World Cup shared by three countries, with 16 host cities: 11 in the United States, 3 in Mexico and 2 in Canada. Mexico became the first country to host three men’s World Cups.

host_raw <- df[, .(host_country = unique(host_country)), by = world_cup_year]

host_split <- host_raw[, .(host_country = str_trim(str_split(host_country, "[/,]")[[1]])),
                       by = world_cup_year]

host_split[, host_country_clean := fifelse(host_country %in% germany_names,
                                           "Germany", host_country)]

host_summary <- host_split[, .(Tournaments_Hosted = .N,
                               Years = paste(sort(unique(world_cup_year)), collapse = ", ")),
                           by = host_country_clean]
setorder(host_summary, -Tournaments_Hosted)
setnames(host_summary, "host_country_clean", "host_country")

host_summary[, plotly_name := host_country]
host_summary[host_country == "England", plotly_name := "United Kingdom"]
host_summary[host_country == "Korea Republic", plotly_name := "South Korea"]

plot_ly(
  data = host_summary,
  type = "choropleth",
  locations = ~plotly_name,
  locationmode = "country names",
  z = ~Tournaments_Hosted,
  text = ~paste0("<b>", host_country, "</b><br>",
                 "Tournaments Hosted: ", Tournaments_Hosted, "<br>",
                 "Year(s): ", Years),
  hoverinfo = "text",
  colorscale = list(c(0, "#2B63D9"), c(0.5, cyan), c(1, amber)),
  marker = list(line = list(color = card, width = 0.5))) %>%
  colorbar(title = "Tournaments\nHosted") %>%
  layout(
    title = list(text = "World Cup Host Countries",
                 font = list(color = ink, size = 18)),
    font = list(family = "Inter, sans-serif", color = ink),
    paper_bgcolor = card,
    plot_bgcolor = card,
    geo = list(
      showframe = FALSE,
      showcoastlines = TRUE,
      coastlinecolor = "#3A4A70",
      showcountries = TRUE,
      countrycolor = "#2B3A5E",
      showland = TRUE,
      landcolor = "#1A2640",
      resolution = 50,
      projection = list(type = "natural earth"),
      bgcolor = card))

Home Advantage

Does hosting make teams more successful?

We all think of playing at home being an advantage, but is it? Let’s look into the data.

host_by_year <- host_split[, .(world_cup_year,
                               Team = fifelse(host_country %in% germany_names,
                                              "Germany", host_country))]

team_matches <- rbind(
  df[, .(world_cup_year, Team = team1_clean, Won = winner_clean == team1_clean)],
  df[, .(world_cup_year, Team = team2_clean, Won = winner_clean == team2_clean)])
team_matches[is.na(Won), Won := FALSE]

team_matches <- merge(team_matches,
                      unique(host_by_year)[, .(world_cup_year, Team, Is_Host = TRUE)],
                      by = c("world_cup_year", "Team"), all.x = TRUE)
team_matches[is.na(Is_Host), Is_Host := FALSE]

host_rate <- team_matches[, .(Matches = .N, Win_Rate = mean(Won)), by = Is_Host]
host_rate[, Group := fifelse(Is_Host, "Host nations", "Everyone else")]

ggplot(host_rate, aes(x = Group, y = Win_Rate, fill = Group)) +
  geom_col(width = 0.5, show.legend = FALSE) +
  geom_text(aes(label = percent(Win_Rate, accuracy = 0.1)),
            vjust = -0.5, size = 4.5, fontface = "bold", color = ink) +
  scale_fill_manual(values = c("Host nations" = amber, "Everyone else" = blue)) +
  scale_y_continuous(labels = percent, expand = expansion(mult = c(0, 0.15))) +
  labs(
    title = "Share of Matches Won: Hosts vs. Everyone Else",
    x = NULL,
    y = "Matches Won") +
  theme_wc()

Winners

Past Winners

We have had a select few countries that have hosted but is there a similar trend to winners? We know countries have a higher win percentage when playing on home soil but does that translate into World Cup titles?

team_match_wins <- df[!is.na(winner_clean) & winner_clean != "Draw",
                      .(Wins = .N), by = winner_clean]
team_match_wins <- team_match_wins[order(-Wins)]

top10_teams <- head(team_match_wins, 10)

finals <- df[stage == "Final"]
world_titles <- finals[!is.na(winner_clean), .(Titles = .N), by = winner_clean]

world_map <- map_data("world")

world_titles[, region := fifelse(winner_clean == "England", "UK", winner_clean)]

world_map_titles <- merge(world_map, world_titles, by = "region", all.x = TRUE)
world_map_titles <- world_map_titles[order(world_map_titles$group, world_map_titles$order), ]

plot_titles_map <- function(map_title, xlim = NULL, ylim = NULL) {
  ggplot(world_map_titles, aes(x = long, y = lat, group = group, fill = Titles)) +
    geom_polygon(color = card, linewidth = 0.1) +
    scale_fill_gradient(low = "#3A5BA8", high = amber,
                        na.value = "#1A2640", labels = comma,
                        breaks = pretty_breaks(n = 5),
                        name = "World Cup\nTitles") +
    coord_fixed(1.3, xlim = xlim, ylim = ylim) +
    labs(title = map_title) +
    theme_wc_map()
}

plot_titles_map("World Cup Titles by Country")

plot_titles_map("World Cup Titles by Country: Europe",
                xlim = c(-25, 45), ylim = c(35, 70))

plot_titles_map("World Cup Titles by Country: South America",
                xlim = c(-85, -30), ylim = c(-58, 15))

Every country to win the World Cup has also hosted it. On six occasions out of the 22 editions, the home country has won the tournament. While it increases the chances of winning it isn’t a direct pass to success.

Consistency

Success Through Decades or Inconsistent Peak Nations?

Winning a lot can mean different things. Some teams win tournament after tournament. Others have very successful tournaments and either don’t qualify or fail to progress deep into the tournament. The stacked bar splits each team’s total wins by tournament year, and the trellis puts every team on the same timeline so the shape of each record is visible.

top10_by_year <- df[!is.na(winner_clean) & winner_clean %in% top10_teams$winner_clean,
                    .(Wins = .N), by = .(winner_clean, world_cup_year)]

ggplot(top10_by_year, aes(x = Wins,
                          y = factor(winner_clean, levels = rev(top10_teams$winner_clean)),
                          fill = factor(world_cup_year))) +
  geom_bar(stat = "identity", position = position_stack(reverse = TRUE),
           color = card, linewidth = 0.2) +
    geom_text(data = top10_teams,
            aes(x = Wins,
                y = factor(winner_clean, levels = rev(top10_teams$winner_clean)),
                label = Wins),
            hjust = -0.3, size = 4, color = ink,
            inherit.aes = FALSE) +
  scale_x_continuous(labels = comma, expand = expansion(mult = c(0, 0.12))) +
  scale_fill_manual(values = year_pal, name = "Year") +
  labs(
    title = "Top 10 Teams: Wins by Tournament Year",
    x = "Wins",
    y = "Team") +
  theme_wc()

full_grid <- CJ(winner_clean = top10_teams$winner_clean, world_cup_year = all_years)

top10_full <- merge(full_grid, top10_by_year, by = c("winner_clean", "world_cup_year"), all.x = TRUE)
top10_full[is.na(Wins), Wins := 0]

mid_year <- all_years[ceiling(length(all_years) / 2)]
year_breaks <- c(min(all_years), mid_year, max(all_years))

ggplot(top10_full, aes(x = world_cup_year, y = Wins)) +
  geom_col(fill = blue, width = 3) +
  facet_wrap(~ factor(winner_clean, levels = top10_teams$winner_clean),
             axes = "all_x") +
  scale_x_continuous(breaks = year_breaks, labels = comma_format(big.mark = "")) +
  scale_y_continuous(labels = comma) +
  labs(
    title = "Wins per Tournament by Team",
    x = "World Cup Year",
    y = "Wins") +
  theme_wc() +
  theme(axis.text.x = element_text(size = 8),
        strip.text = element_text(size = 10, face = "bold", color = amber))

Wins and Goals

Do Wins Provide Goal Entertainment?

Over the course of World Cup history, as teams win do goals correlate? Are our most successful teams the most entertaining? Do they score the most goals?

team_order <- rev(top10_teams$winner_clean)

goals_team1 <- df[, .(Team = team1_clean, Goals = fulltime_score_team1)]
goals_team2 <- df[, .(Team = team2_clean, Goals = fulltime_score_team2)]

team_goals <- rbind(goals_team1, goals_team2)[, .(Total_Goals = sum(Goals, na.rm = TRUE)), by = Team]
team_goals_top10 <- team_goals[Team %in% top10_teams$winner_clean]

team_totals <- top10_by_year[, .(Total_Wins = sum(Wins)), by = winner_clean]

scale_factor <- max(team_totals$Total_Wins) / max(team_goals_top10$Total_Goals)

ggplot() +
  geom_bar(data = top10_by_year,
           aes(x = Wins, y = factor(winner_clean, levels = team_order), fill = factor(world_cup_year)),
           stat = "identity", position = position_stack(reverse = TRUE),
           color = card, linewidth = 0.2) +
  geom_point(data = team_goals_top10,
             aes(x = Total_Goals * scale_factor, y = Team),
             shape = 21, color = bg, fill = "white", size = 2.8, stroke = 1) +
  scale_x_continuous(
    labels = comma,
    expand = expansion(mult = c(0, 0.12)),
    sec.axis = sec_axis(~ . / scale_factor, name = "Total Goals Scored", labels = comma)) +
  scale_fill_manual(values = year_pal, name = "Year") +
  labs(
    title = "Top Teams: Wins & Goals Scored",
    x = "Wins",
    y = "Team") +
  theme_wc() +
  theme(axis.title.x.top = element_text(size = 11))

goals_team1_yr <- df[, .(Team = team1_clean, world_cup_year, Goals = fulltime_score_team1)]
goals_team2_yr <- df[, .(Team = team2_clean, world_cup_year, Goals = fulltime_score_team2)]

goals_by_team_year <- rbind(goals_team1_yr, goals_team2_yr)[
  , .(Total_Goals = sum(Goals, na.rm = TRUE)), by = .(Team, world_cup_year)]

goals_by_team_year_top10 <- goals_by_team_year[Team %in% top10_teams$winner_clean]

full_grid <- CJ(Team = top10_teams$winner_clean, world_cup_year = all_years)
heat_data <- merge(full_grid, goals_by_team_year_top10, by = c("Team", "world_cup_year"), all.x = TRUE)
heat_data[is.na(Total_Goals), Total_Goals := 0]

shared_limits <- range(heat_data$Total_Goals)

era1 <- heat_data[world_cup_year >= 1930 & world_cup_year <= 1966]
era2 <- heat_data[world_cup_year >= 1970 & world_cup_year <= 1998]
era3 <- heat_data[world_cup_year >= 2002 & world_cup_year <= 2026]

plot_era <- function(dt, era_title) {
  ggplot(dt, aes(x = factor(world_cup_year),
                 y = factor(Team, levels = rev(top10_teams$winner_clean)),
                 fill = Total_Goals)) +
    geom_tile(color = card, linewidth = 0.4) +
    # light text on dark tiles, dark text on bright tiles
    geom_text(aes(label = Total_Goals,
                  color = ifelse(Total_Goals > 0.55 * max(shared_limits), bg, ink)),
              size = 3) +
    scale_color_identity() +
    scale_fill_gradient(low = "#1A2744", high = amber,
                        limits = shared_limits, name = "Goals") +
    labs(title = era_title, x = "World Cup Year", y = "Team") +
    theme_wc() +
    theme(axis.text.x = element_text(angle = 45, hjust = 1, size = 9),
          panel.grid.major = element_blank())
}

plot_era(era1, "Goals by Top Team: 1930-1966")

plot_era(era2, "Goals by Top Team: 1970-1998")

plot_era(era3, "Goals by Top Team: 2002-2026")

Goal Totals

Goal Entertainment…

Spectators generally associate goals as the main source of entertainment during a match. So how has goals over the year trended?

goals_by_year <- df[, .(Total_Goals = sum(fulltime_score_team1, fulltime_score_team2, na.rm = TRUE)),
                    by = world_cup_year]
setorder(goals_by_year, world_cup_year)

max_point <- goals_by_year[which.max(Total_Goals)]
min_point <- goals_by_year[which.min(Total_Goals)]

ggplot(goals_by_year, aes(x = world_cup_year, y = Total_Goals)) +
  geom_line(color = blue, linewidth = 0.9) +
  geom_point(shape = 21, color = blue, fill = card, size = 2, stroke = 1) +
  geom_point(data = max_point, shape = 21, color = bg, fill = amber, size = 3.5, stroke = 1) +
  geom_point(data = min_point, shape = 21, color = bg, fill = amber, size = 3.5, stroke = 1) +
  geom_label_repel(data = rbind(max_point, min_point),
                   aes(label = comma(Total_Goals)),
                   box.padding = 1, point.padding = 1, size = 4,
                   fill = "#1B2745", color = ink, segment.color = amber) +
  scale_x_continuous(breaks = pretty_breaks(n = 10)) +
  scale_y_continuous(labels = comma) +
  labs(
    title = "Total Goals by Tournament",
    x = "World Cup Year",
    y = "Total Goals Scored") +
  theme_wc()

Scoring Rate

Times Have Changed, Has the Scoring?

The total goals have increased over the years, but so has the tournament. We have seen the tournament grow from 13 teams to 48 teams in 2026. Is the goal increase over the years a consequence of teams scoring more or just because there has been more games played?

goals_per_game <- df[, .(Total_Goals = sum(fulltime_score_team1, fulltime_score_team2, na.rm = TRUE),
                         Matches = .N),
                     by = world_cup_year]
goals_per_game[, Goals_Per_Game := Total_Goals / Matches]
setorder(goals_per_game, world_cup_year)

max_gpg <- goals_per_game[which.max(Goals_Per_Game)]
min_gpg <- goals_per_game[which.min(Goals_Per_Game)]
overall_avg <- goals_per_game[, sum(Total_Goals) / sum(Matches)]

ggplot(goals_per_game, aes(x = world_cup_year, y = Goals_Per_Game)) +
  geom_hline(yintercept = overall_avg, linetype = "dashed", color = "#6B7794") +
  annotate("text", x = min(goals_per_game$world_cup_year), y = overall_avg,
           label = paste("Overall avg:", number(overall_avg, accuracy = 0.01)),
           hjust = 0, vjust = -0.8, size = 3.5, color = muted) +
  geom_line(color = blue, linewidth = 0.9) +
  geom_point(shape = 21, color = blue, fill = card, size = 2, stroke = 1) +
  geom_point(data = max_gpg, shape = 21, color = bg, fill = amber, size = 3.5, stroke = 1) +
  geom_point(data = min_gpg, shape = 21, color = bg, fill = amber, size = 3.5, stroke = 1) +
  geom_label_repel(data = rbind(max_gpg, min_gpg),
                   aes(label = paste0(world_cup_year, ": ", number(Goals_Per_Game, accuracy = 0.01))),
                   box.padding = 1, point.padding = 1, size = 4,
                   fill = "#1B2745", color = ink, segment.color = amber) +
  scale_x_continuous(breaks = pretty_breaks(n = 10)) +
  scale_y_continuous(labels = number_format(accuracy = 0.1)) +
  labs(
    title = "Average Goals per Match by Tournament",
    x = "World Cup Year",
    y = "Goals per Match") +
  theme_wc()

Final Whistle

Conclusion

We have seen eight teams over the course of 22 editions win the tournament. Those teams have come from two continent federations and all the teams are among the top 10 most successful teams. The last time a new team won the World Cup was in 2010. Additionally, goals have steadily increased over the years. When we account for matches played, goals per match decreased significantly in 1960 and have been around or below the average (2.76) ever since…until 2026.

So has soccer been entertaining over the years as play styles have changed? Who will break the into exclusive group of champions and will we see similar trends for years to come?