required_packages <- c("dplyr", "ggplot2", "knitr", "maps", "tidyr")
missing_packages <- required_packages[!vapply(required_packages, requireNamespace, logical(1), quietly = TRUE)]
if (length(missing_packages) > 0) {
stop(
"Install the missing packages before knitting: ",
paste(missing_packages, collapse = ", ")
)
}
library(dplyr)
library(ggplot2)
library(knitr)
library(maps)
library(tidyr)
knitr::opts_chunk$set(
echo = TRUE,
message = FALSE,
warning = FALSE,
fig.width = 11,
fig.height = 7,
fig.align = "center"
)
dir.create("visuals", showWarnings = FALSE, recursive = TRUE)
dir.create("data/processed", showWarnings = FALSE, recursive = TRUE)
theme_set(theme_minimal(base_size = 12))
1. Project
overview
1.1 Background
Childhood and adolescent obesity is a major public-health challenge
because it affects physical and mental health, quality of life and later
noncommunicable-disease risk. The sixth round of the WHO
European Childhood Obesity Surveillance Initiative (COSI) found
substantial differences between countries and renewed concern about the
lack of progress after the COVID-19 period. The 2021/2022 Health
Behaviour in School-aged Children (HBSC) report additionally shows
that weight status, diet and physical activity are socially
patterned.
The 18th European Public Health Conference 2025 abstract supplement
sharpened the policy context for this project. Its childhood-obesity
contributions highlighted four connected challenges: interventions
limited to one behaviour or setting; unequal food and activity
environments; weak protection from unhealthy food marketing; and the
need for standardized, measurement-based surveillance. The report
therefore treats obesity as a population and systems problem rather than
an individual failure.
The present dataset can quantify differences by country, sex
and time. It cannot directly measure family affluence, food
marketing, school meals or policy implementation. Those determinants are
used to interpret the findings and define future data extensions, not as
causes tested in this analysis.
1.2 Primary research
question
How large and persistent are inequalities in modelled obesity
prevalence among children and adolescents aged 5–19 years across the WHO
European Region from 1990 to 2022, considering country, sex, change over
time and uncertainty?
1.3 Secondary
research questions
- Which countries have the highest latest prevalence, and how wide are
their confidence intervals?
- Which countries experienced the largest absolute and annualized
changes between their first and latest estimates?
- Did the spread between lower- and higher-prevalence countries narrow
or widen over time?
- How large is the male–female prevalence gap in each country, and did
the average absolute gap change over time?
- How should the patterns be interpreted alongside current European
concerns about socioeconomic disadvantage, unhealthy food environments,
physical inactivity and food marketing?
The first four questions are answered quantitatively. The fifth is a
contextual interpretation and does not claim causal effects.
1.4 Objectives
- Validate and clean the WHO-derived dataset.
- Describe its geographic and temporal coverage.
- Compare the latest country estimates with confidence intervals.
- Quantify absolute, relative and annualized country-level
change.
- Measure between-country inequality using the interquartile range and
P90–P10 gap.
- Quantify the direction and magnitude of sex differences over
time.
- Separate observed inequalities from unmeasured explanations and
policy determinants.
- Export an analysis-ready file for Tableau.
1.5 Unit of analysis
and terminology
Each row represents one country–year–sex–age-group
estimate. Results are ecological country-level estimates and must not be
interpreted as individual-level risk.
In this report, inequality means a measurable
difference between countries or sex categories.
Inequity implies that a difference is avoidable and
unfair. Establishing inequity requires information on social position
and structural determinants that is not present in the core dataset.
1.6 Analytical
framework
analytical_framework <- data.frame(
Domain = c(
"Current burden", "Long-term change", "Geographic inequality",
"Sex inequality", "Estimate uncertainty", "Systems context"
),
Measure = c(
"Latest prevalence and rank", "Absolute, relative and annualized change",
"IQR and P90–P10 gap", "Male minus female gap and absolute gap",
"95% confidence interval width", "Narrative triangulation only"
),
Interpretation = c(
"Where modelled prevalence is highest", "Direction and speed of change",
"Whether country distributions converge or diverge",
"Direction and magnitude of sex difference",
"Precision of each modelled estimate",
"Food, activity, school, community and policy environment"
)
)
kable(analytical_framework, caption = "Conference-informed analytical framework")
Conference-informed analytical framework
| Current burden |
Latest prevalence and rank |
Where modelled prevalence is highest |
| Long-term change |
Absolute, relative and annualized change |
Direction and speed of change |
| Geographic inequality |
IQR and P90–P10 gap |
Whether country distributions converge or diverge |
| Sex inequality |
Male minus female gap and absolute gap |
Direction and magnitude of sex difference |
| Estimate uncertainty |
95% confidence interval width |
Precision of each modelled estimate |
| Systems context |
Narrative triangulation only |
Food, activity, school, community and policy
environment |
2. Data source and
variables
The raw file is stored at
data/raw/childhood_obesity.csv. It contains modelled
estimates from the WHO indicator Prevalence of obesity among
children and adolescents aged 5–19 years. The source URL is
retained in every row for provenance.
data_dictionary <- data.frame(
Variable = c(
"Country", "Country_code", "Year", "Sex", "Age_group",
"Obesity_prevalence", "Lower_CI", "Upper_CI", "Source"
),
Meaning = c(
"Country name", "ISO alpha-3 country code", "Estimate year",
"Female, Male or Total", "WHO age-group code",
"Modelled obesity prevalence (%)", "Lower confidence limit (%)",
"Upper confidence limit (%)", "WHO indicator URL"
)
)
kable(data_dictionary, caption = "Data dictionary")
Data dictionary
| Country |
Country name |
| Country_code |
ISO alpha-3 country code |
| Year |
Estimate year |
| Sex |
Female, Male or Total |
| Age_group |
WHO age-group code |
| Obesity_prevalence |
Modelled obesity prevalence (%) |
| Lower_CI |
Lower confidence limit (%) |
| Upper_CI |
Upper confidence limit (%) |
| Source |
WHO indicator URL |
3. Import and
validation
3.1 Read the CSV
Using a relative path makes the project reproducible on another
computer after cloning the repository.
data_path <- "/Users/test/Desktop/Research with Codex/childhood_obesity.csv"
if (!file.exists(data_path)) {
stop("Data file not found: ", data_path)
}
obesity_raw <- read.csv(
data_path,
na.strings = c("", "NA"),
check.names = FALSE,
stringsAsFactors = FALSE
)
required_columns <- c(
"Country", "Country_code", "Year", "Sex", "Age_group",
"Obesity_prevalence", "Lower_CI", "Upper_CI", "Source"
)
missing_columns <- setdiff(required_columns, names(obesity_raw))
if (length(missing_columns) > 0) {
stop("Missing required columns: ", paste(missing_columns, collapse = ", "))
}
3.2 Clean and derive
analysis variables
This step converts numeric fields, creates readable labels and
removes rows that cannot contribute to the analysis. The original raw
CSV remains unchanged.
obesity <- obesity_raw |>
transmute(
Country = trimws(Country),
Country_code = toupper(trimws(Country_code)),
Year = as.integer(Year),
Sex = trimws(Sex),
Age_group = trimws(Age_group),
Age_group_label = recode(
Age_group,
"Y5T9" = "5–9 years",
"Y10T19" = "10–19 years",
"Y5T19" = "5–19 years",
.default = Age_group
),
Obesity_prevalence = as.numeric(Obesity_prevalence),
Lower_CI = as.numeric(Lower_CI),
Upper_CI = as.numeric(Upper_CI),
Source = trimws(Source)
) |>
filter(
Country != "",
!is.na(Year),
!is.na(Obesity_prevalence)
) |>
arrange(Country, Year, Sex, Age_group)
kable(
head(obesity, 10),
digits = 2,
caption = "First ten cleaned observations"
)
4. Data-quality
assessment
4.1 Coverage
summary
coverage_summary <- data.frame(
Measure = c(
"Rows", "Countries", "First year", "Latest year",
"Sex categories", "Age groups", "Duplicate keys"
),
Value = c(
nrow(obesity),
n_distinct(obesity$Country),
min(obesity$Year),
max(obesity$Year),
paste(sort(unique(obesity$Sex)), collapse = ", "),
paste(sort(unique(obesity$Age_group_label)), collapse = ", "),
obesity |>
count(Country, Year, Sex, Age_group) |>
filter(n > 1) |>
nrow()
)
)
kable(coverage_summary, caption = "Dataset coverage and uniqueness checks")
Dataset coverage and uniqueness checks
| Rows |
15444 |
| Countries |
52 |
| First year |
1990 |
| Latest year |
2022 |
| Sex categories |
Female, Male, Total |
| Age groups |
10–19 years, 5–19 years, 5–9 years |
| Duplicate keys |
0 |
4.2 Missingness and
logical checks
Confidence limits should contain the point estimate. Any violations
are counted so that they are visible rather than silently ignored.
quality_checks <- data.frame(
Check = c(
"Missing prevalence", "Missing lower confidence limit",
"Missing upper confidence limit", "Prevalence outside 0–100%",
"Lower limit above estimate", "Upper limit below estimate"
),
Count = c(
sum(is.na(obesity$Obesity_prevalence)),
sum(is.na(obesity$Lower_CI)),
sum(is.na(obesity$Upper_CI)),
sum(obesity$Obesity_prevalence < 0 | obesity$Obesity_prevalence > 100, na.rm = TRUE),
sum(obesity$Lower_CI > obesity$Obesity_prevalence, na.rm = TRUE),
sum(obesity$Upper_CI < obesity$Obesity_prevalence, na.rm = TRUE)
)
)
kable(quality_checks, caption = "Missingness and logical validation")
Missingness and logical validation
| Missing prevalence |
0 |
| Missing lower confidence limit |
0 |
| Missing upper confidence limit |
0 |
| Prevalence outside 0–100% |
0 |
| Lower limit above estimate |
0 |
| Upper limit below estimate |
0 |
5. Descriptive
analysis
5.1 Define the latest
comparable population
For a fair country comparison, the analysis uses the latest year
shared by the data and restricts results to total-sex estimates for ages
5–19 years.
latest_year <- max(obesity$Year, na.rm = TRUE)
latest_total <- obesity |>
filter(
Year == latest_year,
Sex == "Total",
Age_group == "Y5T19"
) |>
mutate(
Burden_quartile = factor(
ntile(Obesity_prevalence, 4),
levels = 1:4,
labels = c("Low", "Moderate", "High", "Very high")
)
)
latest_summary <- latest_total |>
summarise(
Year = latest_year,
Countries = n_distinct(Country),
Mean = mean(Obesity_prevalence, na.rm = TRUE),
Median = median(Obesity_prevalence, na.rm = TRUE),
Minimum = min(Obesity_prevalence, na.rm = TRUE),
Maximum = max(Obesity_prevalence, na.rm = TRUE)
)
kable(latest_summary, digits = 1, caption = "Latest comparable prevalence summary")
Latest comparable prevalence summary
| 2022 |
52 |
8.4 |
8.6 |
1.6 |
14.8 |
5.2
Highest-prevalence countries
highest_prevalence <- latest_total |>
slice_max(Obesity_prevalence, n = 15, with_ties = FALSE) |>
arrange(desc(Obesity_prevalence))
kable(
highest_prevalence |>
select(Country, Year, Obesity_prevalence, Lower_CI, Upper_CI, Burden_quartile),
digits = 1,
caption = "Fifteen highest latest estimates, total population aged 5–19"
)
Fifteen highest latest estimates, total population aged
5–19
| Hungary |
2022 |
14.8 |
12.4 |
17.4 |
Very high |
| Cyprus |
2022 |
14.1 |
10.5 |
18.0 |
Very high |
| Finland |
2022 |
12.6 |
10.1 |
15.5 |
Very high |
| Malta |
2022 |
12.1 |
8.8 |
15.8 |
Very high |
| Bulgaria |
2022 |
12.1 |
9.5 |
14.7 |
Very high |
| Bosnia and Herzegovina |
2022 |
12.0 |
6.7 |
18.6 |
Very high |
| Türkiye |
2022 |
11.7 |
7.9 |
16.0 |
Very high |
| Greece |
2022 |
11.6 |
9.2 |
14.1 |
Very high |
| North Macedonia |
2022 |
11.3 |
8.2 |
15.1 |
Very high |
| United Kingdom of Great Britain and Northern
Ireland |
2022 |
11.3 |
9.4 |
13.4 |
Very high |
| Andorra |
2022 |
11.2 |
3.9 |
21.3 |
Very high |
| Serbia |
2022 |
10.9 |
8.2 |
13.9 |
Very high |
| Slovenia |
2022 |
10.8 |
9.8 |
12.0 |
Very high |
| Estonia |
2022 |
10.7 |
8.9 |
12.6 |
High |
| Georgia |
2022 |
10.6 |
7.3 |
14.7 |
High |
ranking_plot <- highest_prevalence |>
mutate(Country = reorder(Country, Obesity_prevalence)) |>
ggplot(aes(x = Obesity_prevalence, y = Country)) +
geom_col(fill = "#C94C4C", width = 0.72) +
geom_errorbar(
aes(xmin = Lower_CI, xmax = Upper_CI),
orientation = "y",
width = 0.22,
colour = "#333333"
) +
labs(
title = paste("Highest modelled obesity prevalence in", latest_year),
subtitle = "Total population aged 5–19; lines show confidence intervals",
x = "Obesity prevalence (%)",
y = NULL,
caption = "Source: WHO"
)
ranking_plot

ggsave("visuals/latest_obesity_prevalence.png", ranking_plot, width = 11, height = 7, dpi = 200)
6. Inequality analysis
by sex
The sex gap is defined as male prevalence minus female prevalence.
Positive values indicate a higher estimate among males; negative values
indicate a higher estimate among females.
sex_gap_all <- obesity |>
filter(
Age_group == "Y5T19",
Sex %in% c("Female", "Male")
) |>
select(Country, Country_code, Year, Sex, Obesity_prevalence) |>
pivot_wider(names_from = Sex, values_from = Obesity_prevalence) |>
mutate(
Sex_gap = Male - Female,
Absolute_sex_gap = abs(Sex_gap)
) |>
filter(!is.na(Sex_gap)) |>
arrange(Country, Year)
sex_gap <- sex_gap_all |>
filter(Year == latest_year) |>
arrange(desc(abs(Sex_gap)))
kable(
head(sex_gap, 15),
digits = 1,
caption = "Largest absolute sex gaps in the latest year"
)
Largest absolute sex gaps in the latest year
| Serbia |
SRB |
2022 |
7.4 |
14.3 |
6.9 |
6.9 |
| Montenegro |
MNE |
2022 |
5.2 |
12.0 |
6.8 |
6.8 |
| Georgia |
GEO |
2022 |
7.6 |
13.4 |
5.8 |
5.8 |
| Cyprus |
CYP |
2022 |
11.2 |
16.8 |
5.7 |
5.7 |
| Finland |
FIN |
2022 |
10.0 |
15.2 |
5.3 |
5.3 |
| Estonia |
EST |
2022 |
8.1 |
13.2 |
5.1 |
5.1 |
| Greece |
GRC |
2022 |
9.0 |
14.0 |
5.0 |
5.0 |
| Bosnia and Herzegovina |
BIH |
2022 |
9.5 |
14.5 |
4.9 |
4.9 |
| Hungary |
HUN |
2022 |
12.3 |
17.2 |
4.9 |
4.9 |
| Malta |
MLT |
2022 |
9.6 |
14.4 |
4.8 |
4.8 |
| Romania |
ROU |
2022 |
6.5 |
11.1 |
4.6 |
4.6 |
| Slovenia |
SVN |
2022 |
8.5 |
13.1 |
4.6 |
4.6 |
| Sweden |
SWE |
2022 |
7.8 |
12.3 |
4.5 |
4.5 |
| Italy |
ITA |
2022 |
7.4 |
11.7 |
4.3 |
4.3 |
| North Macedonia |
MKD |
2022 |
9.1 |
13.4 |
4.3 |
4.3 |
sex_gap_plot <- sex_gap |>
slice_max(abs(Sex_gap), n = 15, with_ties = FALSE) |>
mutate(Country = reorder(Country, Sex_gap)) |>
ggplot(aes(x = Sex_gap, y = Country, fill = Sex_gap > 0)) +
geom_col(show.legend = FALSE) +
geom_vline(xintercept = 0, colour = "#333333") +
scale_fill_manual(values = c("TRUE" = "#3B78A8", "FALSE" = "#D9795B")) +
labs(
title = paste("Largest sex differences in", latest_year),
subtitle = "Male prevalence minus female prevalence, ages 5–19",
x = "Percentage-point difference",
y = NULL
)
sex_gap_plot

ggsave("visuals/latest_sex_gap.png", sex_gap_plot, width = 11, height = 7, dpi = 200)
6.1 Change in the
regional sex gap
The mean absolute gap describes the typical size of the country-level
difference regardless of which sex has the higher estimate. The mean
signed gap retains direction.
sex_gap_trend <- sex_gap_all |>
group_by(Year) |>
summarise(
Mean_signed_gap = mean(Sex_gap, na.rm = TRUE),
Mean_absolute_gap = mean(Absolute_sex_gap, na.rm = TRUE),
Countries = n_distinct(Country),
.groups = "drop"
)
sex_gap_trend_plot <- sex_gap_trend |>
select(Year, `Signed gap` = Mean_signed_gap, `Absolute gap` = Mean_absolute_gap) |>
pivot_longer(-Year, names_to = "Measure", values_to = "Gap") |>
ggplot(aes(Year, Gap, colour = Measure)) +
geom_hline(yintercept = 0, colour = "grey65") +
geom_line(linewidth = 1) +
scale_colour_manual(values = c("Signed gap" = "#3B78A8", "Absolute gap" = "#C94C4C")) +
labs(
title = "Country-level sex inequality over time",
subtitle = "Unweighted mean across countries, ages 5–19",
x = "Year", y = "Percentage-point gap", colour = NULL
)
sex_gap_trend_plot

ggsave("visuals/sex_gap_trend.png", sex_gap_trend_plot, width = 11, height = 7, dpi = 200)
7. Time trends and
between-country inequality
7.1 Regional
descriptive trend
The regional trend is an unweighted mean of available country
estimates. It describes the average country trajectory and is not a
population-weighted European prevalence estimate.
regional_trend <- obesity |>
filter(Age_group == "Y5T19", Sex %in% c("Female", "Male", "Total")) |>
group_by(Year, Sex) |>
summarise(
Mean_prevalence = mean(Obesity_prevalence, na.rm = TRUE),
Countries = n_distinct(Country),
.groups = "drop"
)
trend_plot <- ggplot(
regional_trend,
aes(x = Year, y = Mean_prevalence, colour = Sex)
) +
geom_line(linewidth = 1) +
scale_colour_manual(values = c("Female" = "#D979A7", "Male" = "#3B78A8", "Total" = "#333333")) +
labs(
title = "Mean country-level obesity prevalence over time",
subtitle = "Ages 5–19; unweighted mean across available countries",
x = "Year",
y = "Mean modelled prevalence (%)",
colour = "Sex"
)
trend_plot

ggsave("visuals/regional_obesity_trend.png", trend_plot, width = 11, height = 7, dpi = 200)
7.2 Country-level
change
Absolute change is the latest minus the first estimate. Annualized
change divides that difference by the number of elapsed years, allowing
countries with different observation spans to be compared. Relative
percentage change is reported but should be interpreted cautiously when
baseline prevalence is low.
country_change <- obesity |>
filter(Age_group == "Y5T19", Sex == "Total") |>
group_by(Country, Country_code) |>
arrange(Year, .by_group = TRUE) |>
summarise(
Start_year = first(Year),
End_year = last(Year),
Start_prevalence = first(Obesity_prevalence),
End_prevalence = last(Obesity_prevalence),
Absolute_change = End_prevalence - Start_prevalence,
Relative_change_percent = 100 * Absolute_change / Start_prevalence,
Annualized_change = Absolute_change / (End_year - Start_year),
.groups = "drop"
) |>
arrange(desc(Absolute_change))
kable(
head(country_change, 15), digits = 2,
caption = "Countries with the largest absolute increase over the available period"
)
Countries with the largest absolute increase over the available
period
| Hungary |
HUN |
1990 |
2022 |
3.56 |
14.78 |
11.23 |
315.83 |
0.35 |
| Finland |
FIN |
1990 |
2022 |
3.07 |
12.65 |
9.58 |
312.38 |
0.30 |
| Türkiye |
TUR |
1990 |
2022 |
2.61 |
11.73 |
9.12 |
349.46 |
0.29 |
| Estonia |
EST |
1990 |
2022 |
2.38 |
10.69 |
8.32 |
350.10 |
0.26 |
| Bosnia and Herzegovina |
BIH |
1990 |
2022 |
3.73 |
12.05 |
8.31 |
222.73 |
0.26 |
| Bulgaria |
BGR |
1990 |
2022 |
3.99 |
12.06 |
8.07 |
202.50 |
0.25 |
| Romania |
ROU |
1990 |
2022 |
1.09 |
8.84 |
7.75 |
712.39 |
0.24 |
| Cyprus |
CYP |
1990 |
2022 |
6.77 |
14.07 |
7.30 |
107.70 |
0.23 |
| Slovakia |
SVK |
1990 |
2022 |
2.49 |
9.70 |
7.21 |
289.05 |
0.23 |
| North Macedonia |
MKD |
1990 |
2022 |
4.20 |
11.32 |
7.11 |
169.21 |
0.22 |
| Austria |
AUT |
1990 |
2022 |
3.58 |
10.57 |
6.98 |
194.82 |
0.22 |
| United Kingdom of Great Britain and Northern
Ireland |
GBR |
1990 |
2022 |
4.48 |
11.31 |
6.83 |
152.50 |
0.21 |
| Slovenia |
SVN |
1990 |
2022 |
4.09 |
10.84 |
6.75 |
165.02 |
0.21 |
| Georgia |
GEO |
1990 |
2022 |
3.94 |
10.59 |
6.66 |
169.06 |
0.21 |
| Serbia |
SRB |
1990 |
2022 |
4.45 |
10.94 |
6.48 |
145.55 |
0.20 |
country_change_plot <- country_change |>
mutate(Country = reorder(Country, Absolute_change)) |>
ggplot(aes(Absolute_change, Country, fill = Absolute_change > 0)) +
geom_col(show.legend = FALSE) +
geom_vline(xintercept = 0, colour = "#333333") +
scale_fill_manual(values = c("TRUE" = "#C94C4C", "FALSE" = "#3B78A8")) +
labs(
title = "Change in modelled obesity prevalence by country",
subtitle = "First to latest available estimate; total population aged 5–19",
x = "Absolute change (percentage points)", y = NULL
)
country_change_plot

ggsave("visuals/country_change.png", country_change_plot, width = 11, height = 10, dpi = 200)
7.3 Did countries
converge or diverge?
The P90–P10 gap is the difference between the 90th and 10th
percentiles of country prevalence. The interquartile range (IQR)
captures the middle half of countries and is less influenced by
extremes. Both are absolute inequality measures.
country_inequality <- obesity |>
filter(Age_group == "Y5T19", Sex == "Total") |>
group_by(Year) |>
summarise(
P10 = quantile(Obesity_prevalence, 0.10, na.rm = TRUE),
P90 = quantile(Obesity_prevalence, 0.90, na.rm = TRUE),
P90_P10_gap = P90 - P10,
IQR = IQR(Obesity_prevalence, na.rm = TRUE),
Countries = n_distinct(Country),
.groups = "drop"
)
inequality_plot <- country_inequality |>
select(Year, `P90–P10 gap` = P90_P10_gap, IQR) |>
pivot_longer(-Year, names_to = "Measure", values_to = "Value") |>
ggplot(aes(Year, Value, colour = Measure)) +
geom_line(linewidth = 1) +
scale_colour_manual(values = c("P90–P10 gap" = "#7A3E9D", "IQR" = "#D9795B")) +
labs(
title = "Between-country inequality in obesity prevalence",
subtitle = "Total population aged 5–19; unweighted country distribution",
x = "Year", y = "Absolute spread (percentage points)", colour = NULL
)
inequality_plot

ggsave("visuals/between_country_inequality.png", inequality_plot, width = 11, height = 7, dpi = 200)
8. Uncertainty and
ranking caution
Confidence-interval width is used as a transparent indicator of
estimate precision. It is not a data-quality score. Countries with
overlapping intervals should not be described as definitively different
based on rank alone.
latest_uncertainty <- latest_total |>
mutate(CI_width = Upper_CI - Lower_CI) |>
arrange(desc(CI_width))
kable(
latest_uncertainty |>
select(Country, Obesity_prevalence, Lower_CI, Upper_CI, CI_width) |>
head(15),
digits = 1,
caption = "Latest estimates with the widest confidence intervals"
)
Latest estimates with the widest confidence intervals
| Andorra |
11.2 |
3.9 |
21.3 |
17.4 |
| Iceland |
8.7 |
2.8 |
17.8 |
15.1 |
| Luxembourg |
7.4 |
2.2 |
15.7 |
13.5 |
| Azerbaijan |
8.6 |
3.5 |
16.1 |
12.6 |
| Bosnia and Herzegovina |
12.0 |
6.7 |
18.6 |
11.9 |
| Belarus |
7.9 |
3.3 |
14.2 |
10.8 |
| Uzbekistan |
6.4 |
2.3 |
12.8 |
10.4 |
| Türkiye |
11.7 |
7.9 |
16.0 |
8.2 |
| Cyprus |
14.1 |
10.5 |
18.0 |
7.5 |
| Greenland |
4.7 |
1.8 |
9.2 |
7.5 |
| Georgia |
10.6 |
7.3 |
14.7 |
7.4 |
| Malta |
12.1 |
8.8 |
15.8 |
6.9 |
| North Macedonia |
11.3 |
8.2 |
15.1 |
6.9 |
| Armenia |
9.6 |
6.7 |
13.0 |
6.4 |
| Germany |
8.5 |
5.7 |
11.9 |
6.3 |
9. Geographic
patterns
Maps help reveal geographic clustering that is difficult to see in a
ranked table. The map frame covers Europe and central Asia. Greenland
remains in the analytical dataset but falls outside this focused
display.
Country names in the WHO file are matched to the names used by the
maps package through one explicit lookup table. This avoids
fragile automatic text matching and creates a reusable
Map_region field for Tableau.
9.1 Latest obesity
prevalence across the Region
prevalence_map <- ggplot(
prevalence_map_data,
aes(long, lat, group = group, fill = Obesity_prevalence)
) +
geom_polygon(colour = "white", linewidth = 0.18) +
coord_quickmap(xlim = c(-30, 90), ylim = c(25, 75), expand = FALSE) +
scale_fill_viridis_c(
option = "C",
direction = -1,
na.value = "#E6E6E6",
name = "Prevalence (%)"
) +
labs(
title = paste("Childhood and adolescent obesity prevalence across Europe in", latest_year),
subtitle = "WHO modelled estimates for ages 5–19, total population",
x = NULL,
y = NULL,
caption = "Darker colours indicate higher prevalence; grey indicates no matched estimate. Source: WHO"
) +
theme_void(base_size = 12) +
theme(
legend.position = "right",
plot.title = element_text(face = "bold"),
plot.caption = element_text(hjust = 0)
)
prevalence_map

ggsave(
"visuals/latest_obesity_prevalence_map.png",
prevalence_map,
width = 12,
height = 7,
dpi = 220,
bg = "white"
)
How to read the map: darker countries have higher
modelled prevalence in 2022; grey countries are outside the matched
analytical data.
Easy interpretation: Hungary has the highest point
estimate at 14.8%, followed by Cyprus and Finland. The map helps
identify geographic patterning, but colour differences do not by
themselves prove statistically significant or causal differences.
9.2 Geographic
pattern of the sex gap
max_absolute_sex_gap <- max(abs(sex_gap$Sex_gap), na.rm = TRUE)
sex_gap_map <- ggplot(
sex_gap_map_data,
aes(long, lat, group = group, fill = Sex_gap)
) +
geom_polygon(colour = "white", linewidth = 0.18) +
coord_quickmap(xlim = c(-30, 90), ylim = c(25, 75), expand = FALSE) +
scale_fill_gradient2(
low = "#3B78A8",
mid = "#F7F7F7",
high = "#C94C4C",
midpoint = 0,
limits = c(-max_absolute_sex_gap, max_absolute_sex_gap),
na.value = "#E6E6E6",
name = "Male − female\n(percentage points)"
) +
labs(
title = paste("Sex differences in obesity prevalence across Europe in", latest_year),
subtitle = "Ages 5–19; red indicates higher prevalence among males and blue among females",
x = NULL,
y = NULL,
caption = "Near-white values indicate a small sex gap; grey indicates no matched estimate. Source: WHO"
) +
theme_void(base_size = 12) +
theme(
legend.position = "right",
plot.title = element_text(face = "bold"),
plot.caption = element_text(hjust = 0)
)
sex_gap_map

ggsave(
"visuals/latest_sex_gap_map.png",
sex_gap_map,
width = 12,
height = 7,
dpi = 220,
bg = "white"
)
How to read the map: red means the male estimate is
higher, blue means the female estimate is higher, and colours close to
white indicate a small difference.
Easy interpretation: Serbia has the largest absolute
sex gap (6.9 percentage points). This is a descriptive difference
between modelled estimates; it does not explain whether biology,
behaviour, social conditions or data inputs produced the gap.
10. Tableau-ready
export
The export contains cleaned fields plus latest-year flags, quartiles
and sex-gap values. These derived variables simplify Tableau dashboard
construction.
tableau_export <- obesity |>
left_join(map_name_lookup, by = "Country") |>
left_join(
sex_gap |>
select(Country_code, Sex_gap),
by = "Country_code"
) |>
left_join(
latest_total |>
select(Country_code, Burden_quartile),
by = "Country_code"
) |>
left_join(
country_change |>
select(Country_code, Absolute_change, Relative_change_percent, Annualized_change),
by = "Country_code"
) |>
mutate(
CI_width = Upper_CI - Lower_CI,
Latest_year = latest_year,
Is_latest_year = Year == latest_year,
Latest_burden_quartile = ifelse(
Year == latest_year & Sex == "Total" & Age_group == "Y5T19",
as.character(Burden_quartile),
NA_character_
)
) |>
select(-Burden_quartile)
write.csv(
tableau_export,
"data/processed/tableau_childhood_obesity.csv",
row.names = FALSE,
na = ""
)
kable(
head(tableau_export, 8),
digits = 2,
caption = "Preview of the Tableau-ready dataset"
)
11. Interpretation and
public-health relevance
This is a surveillance and inequality-screening analysis. A high or
rapidly increasing estimate identifies where closer investigation may be
warranted; it does not identify why prevalence is high. Confidence
intervals and changes in the country distribution are more informative
than a league table alone.
The conference report and WHO evidence suggest a practical
interpretation pathway:
- Surveillance: compare modelled patterns with
standardized measured systems such as COSI and national child-growth
registers.
- Equity diagnosis: add HBSC family-affluence, diet
and physical-activity indicators before making claims about
socioeconomic inequity.
- Systems response: examine school meals, active
travel, physical education, food retail and digital marketing together
rather than treating behaviour as an isolated choice.
- Policy audit: link country trends to comparable
policy-implementation data before evaluating marketing restrictions or
other interventions.
These steps convert descriptive findings into hypotheses for further
study; they are not causal conclusions from the current data.
12. Limitations
- The values are modelled estimates rather than a single harmonised
survey.
- Country comparisons may reflect differences in source data and
uncertainty.
- The regional trend is not population weighted.
- Country-level associations cannot be applied to individuals.
- Prevalence alone does not measure access to prevention or
treatment.
- Statistical overlap between confidence intervals should be
considered before describing differences as meaningful.
- The data end in 2022 and therefore do not measure subsequent changes
reported by COSI 2022–2024.
- Sex is represented as Female, Male and Total; gender identity and
intersectional inequalities cannot be assessed.
- P90–P10 and IQR summarize between-country dispersion but do not
measure within-country social inequality.
- No correction is made for multiple comparisons, and ranks are
descriptive.
13. Recommended
next-stage data linkage
To investigate mechanisms while maintaining a defensible design, a
second phase should link the WHO prevalence series to country-year
indicators from:
- HBSC: family affluence, diet, physical activity and
self-reported weight status;
- COSI: standardized measured prevalence, family
behaviours and school environments for children aged 6–9;
- WHO policy sources: implementation of
food-marketing restrictions, school food policies and nutrition actions;
and
- Eurostat or World Bank: material deprivation,
income inequality and urbanization.
The primary extension question would be: Are country-level
obesity trends associated with socioeconomic conditions and policy
environments after accounting for time and repeated observations? A
panel-data design with pre-specified confounders would be required.
Associations would still be ecological and should not be interpreted as
individual causal effects.
14. Reproducibility
checklist
- Keep the repository folder structure unchanged.
- Do not edit the raw CSV manually.
- Install the packages listed in
required_packages.
- Open
childhood-obesity-analysis.Rmd in RStudio.
- Set the repository root as the working project folder.
- Click Knit to create the HTML report.
- Confirm that
visuals/ and data/processed/
are regenerated.