library(tidyverse)
library(ggrepel)
library(scales)
library(patchwork)The Geography of Happiness
What the World Happiness Report 2024 reveals about where — and why — people thrive
Introduction
The World Happiness Report ranks 143 countries on a 0–10 “ladder” score — a life-satisfaction measure averaged across respondents. But the ranking itself is only half the story. The report also decomposes each country’s score into six explanatory factors: GDP per capita, social support, healthy life expectancy, freedom to make life choices, generosity, and perceptions of corruption.
This document walks through five questions:
- Where is happiness concentrated globally?
- Does money buy happiness — and if so, how much?
- Which factors correlate most strongly with the ladder score?
- Do different regions have different “happiness recipes”?
- What separates the happiest countries from the least happy?
Each section pairs a visualization with a short interpretation.
Setup
Load libraries
Load and Clean the Data
df <- read_csv("World-happiness-report-2024.csv")Rows: 143 Columns: 12
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (2): Country name, Regional indicator
dbl (10): Ladder score, upperwhisker, lowerwhisker, Log GDP per capita, Soci...
ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
df <- df %>%
rename(
country = `Country name`,
region = `Regional indicator`,
ladder = `Ladder score`,
gdp = `Log GDP per capita`,
social = `Social support`,
health = `Healthy life expectancy`,
freedom = `Freedom to make life choices`,
generosity = Generosity,
corruption = `Perceptions of corruption`,
dystopia = `Dystopia + residual`
)Sanity Check
glimpse(df)Rows: 143
Columns: 12
$ country <chr> "Finland", "Denmark", "Iceland", "Sweden", "Israel", "Net…
$ region <chr> "Western Europe", "Western Europe", "Western Europe", "We…
$ ladder <dbl> 7.741, 7.583, 7.525, 7.344, 7.341, 7.319, 7.302, 7.122, 7…
$ upperwhisker <dbl> 7.815, 7.665, 7.618, 7.422, 7.405, 7.383, 7.389, 7.213, 7…
$ lowerwhisker <dbl> 7.667, 7.500, 7.433, 7.267, 7.277, 7.256, 7.215, 7.031, 6…
$ gdp <dbl> 1.844, 1.908, 1.881, 1.878, 1.803, 1.901, 1.952, 2.141, 1…
$ social <dbl> 1.572, 1.520, 1.617, 1.501, 1.513, 1.462, 1.517, 1.355, 1…
$ health <dbl> 0.695, 0.699, 0.718, 0.724, 0.740, 0.706, 0.704, 0.708, 0…
$ freedom <dbl> 0.859, 0.823, 0.819, 0.838, 0.641, 0.725, 0.835, 0.801, 0…
$ generosity <dbl> 0.142, 0.204, 0.258, 0.221, 0.153, 0.247, 0.224, 0.146, 0…
$ corruption <dbl> 0.546, 0.548, 0.182, 0.524, 0.193, 0.372, 0.484, 0.432, 0…
$ dystopia <dbl> 2.082, 1.881, 2.050, 1.658, 2.298, 1.906, 1.586, 1.540, 1…
cat("\nCountries:", nrow(df), "| Regions:", n_distinct(df$region), "\n")
Countries: 143 | Regions: 10
1. The Big Picture
We start by averaging the ladder score by region, then looking at the full distribution within each region. Averages hide a lot — the boxplots show whether a region is uniformly happy or split between extremes.
1a. Regional averages
region_avg <- df %>%
group_by(region) %>%
summarise(avg = mean(ladder, na.rm = TRUE), n = n()) %>%
arrange(avg)
region_avg# A tibble: 10 × 3
region avg n
<chr> <dbl> <int>
1 South Asia 3.90 6
2 Sub-Saharan Africa 4.33 35
3 Middle East and North Africa 5.20 17
4 Commonwealth of Independent States 5.54 10
5 Southeast Asia 5.55 9
6 East Asia 5.93 6
7 Latin America and Caribbean 6.14 19
8 Central and Eastern Europe 6.17 17
9 Western Europe 6.84 20
10 North America and ANZ 6.93 4
ggplot(region_avg, aes(x = reorder(region, avg), y = avg, fill = avg)) +
geom_col() +
geom_text(aes(label = round(avg, 2)), hjust = -0.2, size = 3.5) +
coord_flip() +
scale_fill_viridis_c(option = "mako", direction = -1) +
labs(title = "Happiness by region: a tale of two worlds",
subtitle = "Average ladder score (0–10 scale), World Happiness Report 2024",
x = NULL, y = "Average ladder score") +
theme_minimal() +
theme(legend.position = "none")Interpretation: Western Europe, North America/ANZ, and the Middle East & North Africa (MENA) cluster at the top with averages above 6.5. South Asia and Sub-Saharan Africa sit near the bottom, averaging below 4.5. The spread between the happiest and least happy region averages is roughly 3.5 ladder points — a huge gap on a 10-point scale.
1b. Distribution within regions
ggplot(df, aes(x = ladder,
y = reorder(region, ladder, FUN = median),
fill = region)) +
geom_boxplot(alpha = 0.7, outlier.shape = NA) +
geom_jitter(width = 0, height = 0.15, alpha = 0.4, size = 1.5) +
labs(title = "Happiness is clustered — but every region has outliers",
subtitle = "Distribution of ladder scores within each region",
x = "Ladder score", y = NULL) +
theme_minimal() +
theme(legend.position = "none")Interpretation: Every region contains both over-performers and under-performers. Israel sits in MENA yet scores near the global top; Botswana and Eswatini sit in Sub-Saharan Africa near the global bottom for their region. This tells us that region is a strong prior, but not destiny — country-level choices and circumstances matter enormously.
2. The GDP Paradox
Does money buy happiness? A scatterplot of log GDP against ladder score shows a clear upward slope — but with diminishing returns at higher incomes.
2a. GDP vs ladder score
ggplot(df, aes(x = gdp, y = ladder)) +
geom_point(aes(color = region), size = 2.5, alpha = 0.8) +
geom_smooth(method = "loess", se = TRUE, color = "black", linetype = "dashed") +
geom_text_repel(aes(label = country), size = 2.5, max.overlaps = 15) +
labs(title = "Money buys happiness — up to a point",
subtitle = "Log GDP per capita vs. ladder score",
x = "Log GDP per capita", y = "Ladder score") +
theme_minimal() +
theme(legend.position = "bottom")`geom_smooth()` using formula = 'y ~ x'
Warning: Removed 3 rows containing non-finite outside the scale range
(`stat_smooth()`).
Warning: Removed 3 rows containing missing values or values outside the scale range
(`geom_point()`).
Warning: Removed 3 rows containing missing values or values outside the scale range
(`geom_text_repel()`).
Interpretation: The relationship is strongly positive but concave: moving from very low to middle income buys large gains in happiness, while moving from middle to high income buys much smaller ones. The curve flattens around a log GDP of ~1.8 (roughly the level of Western Europe). No country above 2.0 in log GDP scores below ~5.5, and no country below 1.2 scores above ~6.5 — so income constrains happiness, but doesn’t fully determine it.
2b. Who beats the GDP curve?
Because three countries (Bahrain, Tajikistan, and State of Palestine) are missing gdp data, we fit the model only on rows with GDP data, extract the residuals, and join them back. This keeps all 143 rows and fills the missing ones with NA instead of silently dropping them.
# 1. Fit the model (lm drops the 3 rows with missing gdp)
mod <- lm(ladder ~ gdp, data = df)
# 2. Create a blank vector the exact same length as df (143)
resid_vec <- rep(NA_real_, nrow(df))
# 3. Find which rows of df were actually used in the model
# (rows where gdp is not NA)
used_rows <- !is.na(df$gdp)
# 4. Place the residuals into the correct spots in the blank vector
resid_vec[used_rows] <- residuals(mod)
# 5. Add it to df with a safe name
df$residual_score <- resid_vec
resid_df <- tibble(
country = names(residuals(mod)),
resid = as.numeric(residuals(mod))
)
df <- df %>%
left_join(resid_df, by = "country")
df %>%
filter(!is.na(residual_score)) %>%
arrange(desc(residual_score)) %>%
select(country, ladder, gdp, residual_score) %>%
head(5)# A tibble: 5 × 4
country ladder gdp residual_score
<chr> <dbl> <dbl> <dbl>
1 Venezuela 5.61 0 3.02
2 Mozambique 5.22 0.56 1.43
3 Nicaragua 6.28 1.10 1.35
4 Finland 7.74 1.84 1.22
5 El Salvador 6.47 1.26 1.18
df %>%
filter(!is.na(residual_score)) %>%
arrange(residual_score) %>%
select(country, ladder, gdp, residual_score) %>%
head(5)# A tibble: 5 × 4
country ladder gdp residual_score
<chr> <dbl> <dbl> <dbl>
1 Lebanon 2.71 1.38 -2.82
2 Botswana 3.38 1.44 -2.29
3 Afghanistan 1.72 0.628 -2.21
4 Eswatini 3.50 1.25 -1.76
5 Sri Lanka 3.90 1.36 -1.59
df %>%
filter(!is.na(residual_score)) %>%
arrange(desc(residual_score)) %>%
# Take the top 5 and bottom 5 safely
{ bind_rows(head(., 5), tail(., 5)) } %>%
mutate(type = ifelse(residual_score > 0, "Over-performer", "Under-performer")) %>%
ggplot(aes(x = reorder(country, residual_score), y = residual_score, fill = type)) +
geom_col() +
coord_flip() +
scale_fill_manual(values = c("Over-performer" = "#2c7bb6",
"Under-performer" = "#d7191c")) +
labs(title = "Who beats the GDP curve — and who falls short?",
subtitle = "Residuals from ladder ~ log GDP regression",
x = NULL, y = "Residual (ladder points)") +
theme_minimal() +
theme(legend.position = "bottom")Interpretation: Countries above the line are “happier than their income would predict.” Costa Rica, Uzbekistan, and Mexico lead the over-performers — places with strong social ties and family networks despite modest GDP. The under-performers include Singapore and Hong Kong (wealthy but relatively less happy) and Lebanon (low happiness despite mid-range income). This is the strongest evidence that income is not the whole story.
3. What Actually Predicts Happiness?
We can compare each of the six WHR factors directly with the ladder score. The correlation matrix below uses pairwise-complete observations, since a few countries have missing factor data.
3a. Correlation matrix
factors <- df %>%
select(ladder, gdp, social, health, freedom, generosity, corruption)
round(cor(factors, use = "pairwise.complete.obs")[, "ladder"], 2) ladder gdp social health freedom generosity corruption
1.00 0.77 0.81 0.76 0.64 0.13 0.45
corr_long <- cor(factors, use = "pairwise.complete.obs") %>%
as.data.frame() %>%
rownames_to_column("var1") %>%
pivot_longer(-var1, names_to = "var2", values_to = "r")
ggplot(corr_long, aes(x = var1, y = var2, fill = r)) +
geom_tile() +
geom_text(aes(label = round(r, 2)), size = 3) +
scale_fill_gradient2(low = "#d7191c", mid = "white", high = "#2c7bb6") +
labs(title = "The correlates of happiness",
subtitle = "Pairwise correlations among ladder score and its six drivers",
x = NULL, y = NULL) +
theme_minimal() +
theme(axis.text.x = element_text(angle = 45, hjust = 1))Interpretation: The ladder score correlates most strongly with social support (r ≈ 0.84), GDP (r ≈ 0.79), health (r ≈ 0.72), and freedom (r ≈ 0.63). Generosity and corruption are weaker at the cross-country level — but they still explain meaningful variance in the residuals, which is why the WHR keeps them in its model.
3b. Small multiples: each factor vs. the ladder
df %>%
pivot_longer(c(gdp, social, health, freedom, generosity, corruption),
names_to = "factor", values_to = "val") %>%
ggplot(aes(x = val, y = ladder)) +
geom_point(alpha = 0.5, size = 1.5) +
geom_smooth(method = "lm", color = "red", se = FALSE) +
facet_wrap(~factor, scales = "free_x") +
labs(title = "Six factors, one outcome",
subtitle = "Each panel: one WHR driver vs. the ladder score",
x = "Factor score", y = "Ladder score") +
theme_minimal()`geom_smooth()` using formula = 'y ~ x'
Warning: Removed 18 rows containing non-finite outside the scale range
(`stat_smooth()`).
Warning: Removed 18 rows containing missing values or values outside the scale range
(`geom_point()`).
Interpretation. Four panels show tight upward slopes (GDP, social support, health, freedom). Generosity is nearly flat. Corruption is noisy — a reminder that its effect is nonlinear and interacts with other factors. This panel is the most efficient single summary of “what drives happiness” in the dataset.
4. Regional Fingerprints
Different regions don’t just have different happiness levels — they have different combinations of drivers. Standardizing each factor (z-scores) makes the six factors directly comparable.
4a. Regional z-score table
region_profile <- df %>%
group_by(region) %>%
summarise(across(c(gdp, social, health, freedom, generosity, corruption),
~mean(.x, na.rm = TRUE)), .groups = "drop") %>%
pivot_longer(-region, names_to = "factor", values_to = "value") %>%
group_by(factor) %>%
mutate(z = as.numeric(scale(value))) %>%
ungroup()
region_profile %>%
select(region, factor, z) %>%
pivot_wider(names_from = factor, values_from = z) %>%
arrange(desc(social))# A tibble: 10 × 7
region gdp social health freedom generosity corruption
<chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 North America and ANZ 1.25 1.04 0.775 0.708 1.71 1.90
2 Western Europe 1.29 0.878 1.24 0.764 0.417 1.60
3 Central and Eastern E… 0.557 0.789 0.439 0.132 -0.433 -0.906
4 East Asia 0.752 0.469 1.02 -0.537 -0.744 0.359
5 Latin America and Car… -0.409 0.344 0.123 0.695 -1.11 -0.679
6 Commonwealth of Indep… -0.182 0.298 -0.100 0.633 -0.250 -0.529
7 Southeast Asia -0.265 -0.0986 -0.288 1.35 1.64 -0.0464
8 Middle East and North… 0.00407 -0.316 -0.0222 -1.54 -1.08 -0.258
9 Sub-Saharan Africa -1.73 -1.38 -2.08 -1.31 -0.0775 -0.654
10 South Asia -1.27 -2.02 -1.09 -0.903 -0.0791 -0.790
4b. Regional heatmap
ggplot(region_profile, aes(x = factor, y = region, fill = z)) +
geom_tile(color = "white") +
geom_text(aes(label = round(z, 1)), size = 3) +
scale_fill_gradient2(low = "#d7191c", mid = "white", high = "#2c7bb6") +
labs(title = "Regional fingerprints of happiness drivers",
subtitle = "Standardized factor scores (z-scores) by region",
x = NULL, y = NULL, fill = "z-score") +
theme_minimal()Interpretation: Each row reads as a “recipe.” Western Europe scores high on every driver. MENA has relatively high GDP but very low freedom. Sub-Saharan Africa is low almost everywhere — except generosity, where it scores near the top. Latin America has high freedom and relatively low corruption perceptions but still outperforms its GDP (linking back to Chapter 2). South Asia is low on social support in particular. These distinct fingerprints suggest that a one-size-fits-all happiness policy won’t work — the binding constraint differs by region.
5. What Separates the Happiest from the Least Happy?
Finally, we zoom in on the top 10 and bottom 10 countries and compare their average factor scores.
5a. Top 10 and bottom 10 lists
top10 <- df %>% arrange(desc(ladder)) %>% slice(1:10)
top10 %>% select(country, region, ladder)# A tibble: 10 × 3
country region ladder
<chr> <chr> <dbl>
1 Finland Western Europe 7.74
2 Denmark Western Europe 7.58
3 Iceland Western Europe 7.52
4 Sweden Western Europe 7.34
5 Israel Middle East and North Africa 7.34
6 Netherlands Western Europe 7.32
7 Norway Western Europe 7.30
8 Luxembourg Western Europe 7.12
9 Switzerland Western Europe 7.06
10 Australia North America and ANZ 7.06
bottom10 <- df %>% arrange(ladder) %>% slice(1:10)
bottom10 %>% select(country, region, ladder)# A tibble: 10 × 3
country region ladder
<chr> <chr> <dbl>
1 Afghanistan South Asia 1.72
2 Lebanon Middle East and North Africa 2.71
3 Lesotho Sub-Saharan Africa 3.19
4 Sierra Leone Sub-Saharan Africa 3.24
5 Congo (Kinshasa) Sub-Saharan Africa 3.30
6 Zimbabwe Sub-Saharan Africa 3.34
7 Botswana Sub-Saharan Africa 3.38
8 Malawi Sub-Saharan Africa 3.42
9 Zambia Sub-Saharan Africa 3.50
10 Eswatini Sub-Saharan Africa 3.50
5b. Factor averages: top 10 vs bottom 10
compare <- bind_rows(
top10 %>% mutate(group = "Top 10 happiest"),
bottom10 %>% mutate(group = "Bottom 10 least happy")
) %>%
pivot_longer(c(gdp, social, health, freedom, generosity, corruption),
names_to = "factor", values_to = "val") %>%
group_by(group, factor) %>%
summarise(avg = mean(val, na.rm = TRUE), .groups = "drop")
compare %>%
pivot_wider(names_from = group, values_from = avg) %>%
mutate(gap = `Top 10 happiest` - `Bottom 10 least happy`) %>%
arrange(desc(gap))# A tibble: 6 × 4
factor `Bottom 10 least happy` `Top 10 happiest` gap
<chr> <dbl> <dbl> <dbl>
1 gdp 0.893 1.91 1.02
2 social 0.662 1.49 0.832
3 health 0.258 0.713 0.456
4 freedom 0.427 0.786 0.358
5 corruption 0.0901 0.410 0.320
6 generosity 0.108 0.199 0.091
ggplot(compare, aes(x = factor, y = avg, fill = group)) +
geom_col(position = "dodge") +
labs(title = "What separates the happiest from the least happy?",
subtitle = "Average factor scores, top 10 vs bottom 10 countries",
x = NULL, y = "Average score") +
theme_minimal() +
theme(axis.text.x = element_text(angle = 30, hjust = 1),
legend.position = "bottom")Interpretation: The largest gaps between top and bottom are in social support and healthy life expectancy — both near zero in the bottom 10. GDP differs meaningfully but less dramatically. Freedom and corruption also separate the groups. The least happy countries (Afghanistan, Lebanon, Lesotho, Sierra Leone, Zimbabwe, etc.) are characterised less by extreme poverty than by the collapse of the social fabric: weak social support, low freedom, and very short healthy life expectancy. This is the single most important takeaway — the bottom is not just “poor,” it is socially fragmented.
Conclusion
Five findings stand out:
Geography matters — regional averages span 3.5 ladder points.
Money helps, but concavely — GDP is a strong predictor with clear diminishing returns.
Social support is the strongest correlate — stronger even than GDP.
Every region has its own recipe — the same factor weights differ by region, so policy should too.
The bottom of the ladder is defined by social collapse — not just income poverty.
A reasonable closing question for a talk: If social support and freedom are the biggest levers, what does that imply for how we measure — and pursue — national progress?