library(dplyr)
library(tidyr)
library(ggplot2)
library(forcats)
# In the participant pack: data_file <- "comparisons_all.csv"
comp <- readr::read_csv(data_file, show_col_types = FALSE)
dim(comp)[1] 4076 32
R practical · Tuesday 14:00–14:45 · FRB-CESAB training course
| Time | What | Section |
|---|---|---|
| 14:00 | Framing (slides) | — |
| 14:08 | Load the data, count studies | 1 · 2 |
| 14:13 | Exercise: your first evidence map | 3 |
| 14:25 | Exercise (pairs): add a third dimension (organisms, or a bubble map of evidence volume × reliability) | 4 |
| 14:35 | Share your maps | 5 |
| 14:42 | Key messages | 6 |
| later | Bonus: where and when? | 7 |
Before the session, run 00_install_and_check.R from the course kit (it installs and checks every package of the week).
Open the RStudio project of the course kit, meta_analysis_course.Rproj, then the script td_evidence_map.R: it contains every exercise below with blanks (___) to fill in.
library(dplyr)
library(tidyr)
library(ggplot2)
library(forcats)
# In the participant pack: data_file <- "comparisons_all.csv"
comp <- readr::read_csv(data_file, show_col_types = FALSE)
dim(comp)[1] 4076 32
The data come from a published systematic map on diversified farming, biodiversity and yield (Jones et al. 2021). Each row is one comparison between a diversified system (the intervention) and a simpler one (the comparator), for one biodiversity outcome. The columns you need today:
| Column | Codebook meaning | Examples |
|---|---|---|
study_id |
Study (article) identifier — we count these | 1033 |
year, country |
Publication year, country of the study site | 2008, Mexico |
practice |
Diversification practice (intervention) | Agroforestry, Intercropping |
comparator_system |
Simplified system it is compared with (comparator) | Monoculture, Natural |
taxa_class, functional_group |
Which organisms were sampled | Insect · Pests, Natural enemies |
b_measure |
Biodiversity metric | Abundance, Species richness |
validity_biodiv |
Critical appraisal coded by Jones et al.: 1 all criteria met, 0 not, NA could not be appraised |
1 |
Every axis of the maps you draw today is a column of a codebook. If a category was not coded, it cannot be mapped. You will see that the published data dictionary lists categories (“cover crop”) that are absent from the data, and that the data contain categories (“Associated plant species”) absent from the dictionary: coding choices shape the map.
Maps usually start from a broad question (PECO, or PCC — Population, Concept, Context — for scoping reviews), whereas a meta-analysis needs a focused PICO/PECO question.
comp |>
group_by(year) |>
summarise(rows = n(), studies = n_distinct(study_id)) |>
arrange(desc(rows)) |>
head(5)# A tibble: 5 × 3
year rows studies
<dbl> <int> <int>
1 1992 360 2
2 2013 351 16
3 2012 331 24
4 2003 302 9
5 2007 259 10
In 1992, 360 rows come from only 2 studies. A study can report dozens of comparisons (several taxa, sites, dates). Counting rows would tell you that 1992 was the richest year in the literature — it was not.
Describe an evidence base in number of studies: n_distinct(study_id). Rows are comparisons; they are not independent, which is why on Wednesday we will fit multilevel models.
Question: which combinations of practice × comparator have been studied, and which have not?
Step 1. Count the number of studies for each combination, and keep the combinations with zero studies: they are the gaps.
map_data_1 <- comp |>
distinct(___, practice, comparator_system) |> # one line per study x combination
count(practice, comparator_system, name = "n_studies") |>
complete(___, ___, fill = list(n_studies = 0)) # add the empty combinationsStep 2. Draw the heat map: one tile per combination, the number of studies written in each tile, and a sequential colour scale (light = few studies, dark = many).
ggplot(map_data_1, aes(x = ___, y = ___, fill = n_studies)) +
geom_tile(colour = "white") +
geom_text(aes(label = ___)) +
scale_fill_gradient(low = "#F4F7F8", high = "#045C82") +
labs(x = "Comparator", y = "Diversification practice", fill = "Studies")map_data_1 <- comp |>
distinct(study_id, practice, comparator_system) |>
count(practice, comparator_system, name = "n_studies") |>
complete(practice, comparator_system, fill = list(n_studies = 0)) |>
mutate(practice = fct_reorder(practice, n_studies, .fun = sum))
ggplot(map_data_1, aes(x = comparator_system, y = practice, fill = n_studies)) +
geom_tile(colour = "white", linewidth = 1) +
geom_text(aes(label = ifelse(n_studies == 0, "gap", n_studies),
colour = n_studies > 30), size = 4.5) +
scale_fill_gradient(low = "#F4F7F8", high = "#045C82") +
scale_colour_manual(values = c(`TRUE` = "white", `FALSE` = "#1F2A33"), guide = "none") +
labs(x = "Comparator", y = "Diversification practice", fill = "Studies") +
theme_minimal(base_size = 14) +
theme(panel.grid = element_blank())Three choices make this map honest:
distinct(study_id, ...) → we count studies, not rows;complete() → empty cells are shown: the gap is a result;Two variables tell you what was compared. The value of a map often comes from crossing a third one. Choose one option with your neighbour.
map_data_2 <- comp |>
filter(comparator_system %in% c("Monoculture", "Natural")) |>
distinct(study_id, practice, functional_group, comparator_system) |>
count(practice, functional_group, comparator_system, name = "n_studies") |>
complete(practice, functional_group, comparator_system, fill = list(n_studies = 0))
ggplot(map_data_2, aes(x = ___, y = ___, fill = n_studies)) +
geom_tile(colour = "white") +
geom_text(aes(label = n_studies), size = 3) +
scale_fill_gradient(low = "#F4F7F8", high = "#045C82") +
facet_wrap(~ ___) +
theme(axis.text.x = element_text(angle = 45, hjust = 1))map_data_2 <- comp |>
filter(comparator_system %in% c("Monoculture", "Natural")) |>
distinct(study_id, practice, functional_group, comparator_system) |>
count(practice, functional_group, comparator_system, name = "n_studies") |>
complete(practice, functional_group, comparator_system, fill = list(n_studies = 0))
ggplot(map_data_2, aes(x = functional_group, y = practice, fill = n_studies)) +
geom_tile(colour = "white", linewidth = 0.8) +
geom_text(aes(label = n_studies), size = 3) +
scale_fill_gradient(low = "#F4F7F8", high = "#045C82") +
facet_wrap(~ comparator_system) +
labs(x = NULL, y = NULL, fill = "Studies",
title = "Studies per practice and functional group, by comparator") +
theme_minimal(base_size = 12) +
theme(panel.grid = element_blank(),
axis.text.x = element_text(angle = 45, hjust = 1))Zeros are shown: some rows (e.g. cultivar mixtures vs natural habitat) are entirely empty because the combination makes no sense — a coding artefact of complete(), not a gap. Against monocultures, pests (62 studies) and natural enemies (52) dominate; pollinators (16) and weeds (3) are much less studied — although both matter for yield.
Size of the bubble = number of studies; colour = share of studies whose comparisons meet all validity criteria (critical appraisal coded by Jones et al.). First compute a validity score per study, otherwise big studies dominate the share.
map_data_3 <- comp |>
group_by(practice, comparator_system, study_id) |> # 1. one line per study
summarise(valid = mean(validity_biodiv == 1, na.rm = TRUE), # share of its valid comparisons
.groups = "drop") |>
group_by(practice, comparator_system) |> # 2. then per cell
summarise(n_studies = n_distinct(___),
share_valid = mean(valid, na.rm = TRUE),
share_na = mean(is.na(valid)), # studies that could not be appraised
.groups = "drop")
ggplot(map_data_3, aes(comparator_system, practice)) +
geom_point(aes(size = ___, colour = ___)) +
scale_size_area(max_size = 18) +
scale_colour_viridis_c(direction = -1, labels = scales::percent) # colour-blind safemap_data_3 <- comp |>
group_by(practice, comparator_system, study_id) |>
summarise(valid = mean(validity_biodiv == 1, na.rm = TRUE), .groups = "drop") |>
mutate(valid = ifelse(is.nan(valid), NA, valid)) |>
group_by(practice, comparator_system) |>
summarise(n_studies = n_distinct(study_id),
share_valid = mean(valid, na.rm = TRUE),
share_na = mean(is.na(valid)),
.groups = "drop") |>
mutate(practice = fct_reorder(practice, n_studies, .fun = sum),
lab = paste0(n_studies, ifelse(share_na > 0, paste0(" (", round(100 * share_na), "% NA)"), "")))
ggplot(map_data_3, aes(comparator_system, practice)) +
geom_point(aes(size = n_studies, colour = share_valid)) +
geom_text(aes(label = lab), size = 3.3, colour = "#1F2A33", nudge_x = 0.38) +
scale_size_area(max_size = 20, guide = "none") +
scale_colour_viridis_c(direction = -1, na.value = "grey80",
labels = scales::percent, limits = c(0, 1),
name = "Mean share of valid\ncomparisons per study") +
labs(x = "Comparator", y = NULL,
caption = "Bubble area and number = studies; (% NA) = studies that could not be appraised. Grey = none appraised.") +
theme_minimal(base_size = 13)A big dark bubble = abundant evidence that passes the appraisal; a big yellow bubble = abundant but questionable; a small bubble = fragile, whatever its colour. Grey bubbles and high % NA: validity could not be appraised — itself a finding. scale_size_area() makes the bubble area, not its radius, proportional to the number of studies (otherwise big numbers look even bigger). The viridis palette stays readable for colour-blind readers — avoid red–green scales.
For those who finish early, or to do after the session.
The country names of the data and of the map must match exactly. Find the countries that would silently disappear from the map, then fix their names.
world <- map_data("world") # from the {maps} package, via ggplot2
setdiff(unique(comp$country), unique(world$region))[1] "Viet Nam" "United Kingdom"
[3] "United States of America"
by_country <- comp |>
mutate(region = recode(country,
"United States of America" = "USA",
"United Kingdom" = "UK",
"Viet Nam" = "Vietnam")) |>
group_by(region) |>
summarise(n_studies = n_distinct(study_id))
world |>
left_join(by_country, by = "region") |>
ggplot(aes(long, lat, group = group, fill = n_studies)) +
geom_polygon(colour = "white", linewidth = 0.1) +
scale_fill_gradient(low = "#C9E7E7", high = "#045C82", na.value = "grey90",
name = "Studies") +
coord_quickmap(ylim = c(-55, 80)) +
theme_void(base_size = 13) +
theme(legend.position = "bottom")Without the recoding, the United States — the country with the most studies (52) — would be drawn in grey, as if there were no evidence at all. Always check joins with setdiff() or anti_join().
comp |>
distinct(study_id, year, practice) |>
count(practice, year) |>
group_by(practice) |>
arrange(year) |>
mutate(cumulative = ___(n)) |>
ggplot(aes(year, cumulative, colour = practice)) +
geom_step(linewidth = 1)comp |>
distinct(study_id, year, practice) |>
count(practice, year) |>
group_by(practice) |>
arrange(year) |>
mutate(cumulative = cumsum(n)) |>
ggplot(aes(year, cumulative, colour = practice)) +
geom_step(linewidth = 1) +
labs(x = "Publication year", y = "Cumulative number of studies", colour = NULL) +
theme_minimal(base_size = 13)eviatlas (interactive evidence atlas), PRISMA2020 (flow diagrams — the screening records of Jones et al. are in the course folder donnees/derivees/screening_records.csv), UpSet plots for multi-category variables.