The Story
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
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
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
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
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
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
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
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
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?