Python · Happiness & GDP
GDP & Life Satisfaction Analysis
What's the Pricetag?
Life evaluation × log GDP. r = 0.836. 1,927 country–years.
Analyst · Python · public data
This is exploratory analysis: after joining happiness scores to World Bank GDP, I look at the distribution and the correlation. Life evaluation moves with log GDP (r = 0.836) on 1,927 country–years — related, not a switch.
The question
Does reported life evaluation actually move with GDP per capita — or is a one-year chart just a pretty coincidence?
A single-year scatter is easy. A 2011–2024 panel is two schemas, two gather methods, and country names that do not match. After the join: 1,927 country–years, r = 0.836 on log GDP. Related, not a switch.
Method (the wrangling is the work)
- Gather — Happiness Report figure 2.1 (2011–2025) and World Bank GDP per capita (2011–2024), written under
data_wrangling/raw/. - Assess — Country labels, missing years, duplicate keys, GDP spanning orders of magnitude.
- Clean — Harmonize names, keep country–years present in both, drop unusable GDP/happiness pairs.
- Store —
cleaned_happiness_gdp_by_country_year.csv(1,927 × 4). - EDA — Scatter on log GDP, yearly mean life evaluation, heatmap of mean happiness by GDP quartile × year.
Python · tidy the World Bank file
GDP arrives wide (one column per year). Happiness is already country–year. The work is melt, fix names that do not match, inner-join, then drop pairs you cannot plot. Runnable copy: python/happiness_gdp.py.
import pandas as pd
import numpy as np
happy = pd.read_csv("data_wrangling/raw/happiness_2011_2025.csv")
gdp = pd.read_csv("data_wrangling/raw/gdp_per_capita.csv")
gdp_long = gdp.melt(
id_vars=["Country Name", "Country Code"],
value_vars=[str(y) for y in range(2011, 2025)],
var_name="year",
value_name="gdp_per_capita",
)
gdp_long["year"] = gdp_long["year"].astype(int)
# World Bank labels ≠ Happiness Report labels
aliases = {
"Czechia": "Czech Republic",
"Korea, Rep.": "South Korea",
"Russian Federation": "Russia",
"Egypt, Arab Rep.": "Egypt",
"Iran, Islamic Rep.": "Iran",
"Slovak Republic": "Slovakia",
"Turkiye": "Turkey",
"Viet Nam": "Vietnam",
"Hong Kong SAR, China": "Hong Kong",
"Kyrgyz Republic": "Kyrgyzstan",
"Lao PDR": "Laos",
"Congo, Dem. Rep.": "Congo (Kinshasa)",
"Congo, Rep.": "Congo (Brazzaville)",
}
gdp_long["country"] = gdp_long["Country Name"].replace(aliases)
happy["country"] = happy["country"].replace(aliases)
panel = happy.merge(
gdp_long[["country", "year", "gdp_per_capita"]],
on=["country", "year"],
how="inner",
)
panel = panel.dropna(subset=["life_evaluation", "gdp_per_capita"])
panel = panel[panel["gdp_per_capita"] > 0]
panel[["country", "year", "life_evaluation", "gdp_per_capita"]].to_csv(
"cleaned_happiness_gdp_by_country_year.csv",
index=False,
)
# 1,927 × 4
Python · the number on the thumbnail
Incomes are skewed, so the honest x-axis is log GDP. Quartiles keep the heatmap from turning into a rainbow of every dollar.
panel["log_gdp"] = np.log(panel["gdp_per_capita"])
r = panel["life_evaluation"].corr(panel["log_gdp"])
# r = 0.836
panel["gdp_quartile"] = pd.qcut(
panel["gdp_per_capita"], 4, labels=["Q1", "Q2", "Q3", "Q4"]
)
by_year = (
panel.groupby("year")["life_evaluation"]
.agg(mean_life="mean", n_countries="count")
.round(2)
)
heat = panel.pivot_table(
index="gdp_quartile",
columns="year",
values="life_evaluation",
aggfunc="mean",
)
# seaborn: scatter(log_gdp, life_evaluation) + heatmap(heat)
Findings
The scatter is a positive association: higher GDP per capita, higher life evaluation. Log scale is the honest x-axis because incomes are skewed. Correlation with log GDP is 0.836 — moderately strong, with plenty of spread at the same income. Money is related here. It is not a perfect predictor.
Mean life evaluation in the merged sample is fairly stable from 2011–2024 (about 5.36 to 5.59). Small wiggles can be sample composition (which countries have both files that year) as well as a true global shift. No dramatic long-run jump.
Mean life evaluation by year (merged sample)
| 2011 | 5.38 · 153 countries |
|---|---|
| 2016 | 5.36 · 151 |
| 2020 | 5.54 · 146 |
| 2021 | 5.56 · 143 |
| 2024 | 5.59 · 144 |
The quartile heatmap (notebook) stays the same story over time: the highest GDP quartile stays happier than the lowest. Happiness and GDP move together in the panel, not only in one cross-section.
So what
If you are briefing a non-economist: richer country–years in this merge report higher life evaluation, the relationship is clearer on log income, and average happiness did not leap across this decade. Do not sell GDP as a happiness switch.
Limitations
- Inner join on country–year drops places missing either file.
- Yearly means mix true change with who is in the sample.
- Life evaluation is a 3-year average in the source; GDP is current US$.
- Association, not causation — institutions, health, and inequality sit in the residual.