library(httr2)
library(jsonlite)
library(dplyr)
library(tidyr)
library(purrr)
library(ggplot2)Four Questions About the Nobel Prize
Working with JSON from the Nobel Prize API in R
The Nobel Prize organization publishes its data through a free public API (version 2.1) at https://api.nobelprize.org/2.1. This report uses both of its main endpoints:
/laureates: one record per laureate (person or organization), with birth details, gender, and every prize they won, including where they worked at the time./nobelPrizes: one record per prize (category and year), with the prize amount and the list of laureates.
No API key is needed.
Getting the data
Both endpoints return at most 1,000 records per request, so this function keeps requesting the next page until a page comes back short.
get_all <- function(endpoint) {
records <- list()
offset <- 0
repeat {
page <- request(paste0("https://api.nobelprize.org/2.1/", endpoint)) |>
req_url_query(offset = offset, limit = 1000) |>
req_perform() |>
resp_body_json()
records <- c(records, page[[endpoint]])
if (length(page[[endpoint]]) < 1000) break
offset <- offset + 1000
}
records
}
laureates_raw <- get_all("laureates")
prizes_raw <- get_all("nobelPrizes")
length(laureates_raw)[1] 1028
length(prizes_raw)[1] 688
Turning the JSON into tidy data frames
The JSON is deeply nested. A laureate holds a list of prizes, each prize holds a list of affiliations, and most text fields hold one value per language (en, se, no). I pulled out the English values and built three tidy tables.
laureates <- tibble(
id = map_chr(laureates_raw, "id"),
type = if_else(map_lgl(laureates_raw, \(x) is.null(x$orgName)),
"person", "organization"),
name = map_chr(laureates_raw, \(x) x$knownName$en %||% x$orgName$en),
gender = map_chr(laureates_raw, "gender", .default = NA_character_),
birth_date = map_chr(laureates_raw, list("birth", "date"), .default = NA_character_),
birth_country = map_chr(laureates_raw, list("birth", "place", "countryNow", "en"),
.default = NA_character_)
) |>
mutate(
birth_year = as.integer(substr(birth_date, 1, 4)),
birth_country = case_when(
birth_country %in% c("Scotland", "Northern Ireland") ~ "United Kingdom",
birth_country == "Faroe Islands (Denmark)" ~ "Denmark",
birth_country == "Guadeloupe, France" ~ "France",
.default = birth_country
)
)
awards <- map(laureates_raw, \(x) {
map(x$nobelPrizes, \(p) tibble(
id = x$id,
award_year = as.integer(p$awardYear),
category = p$category$en,
portion = p$portion,
prize_status = p$prizeStatus,
affiliation = pluck(p, "affiliations", 1, "nameNow", "en",
.default = NA_character_),
affiliation_country = pluck(p, "affiliations", 1, "countryNow", "en",
.default = NA_character_)
)) |>
list_rbind()
}) |>
list_rbind()
prizes <- tibble(
award_year = as.integer(map_chr(prizes_raw, "awardYear")),
category = map_chr(prizes_raw, list("category", "en")),
prize_amount = map_dbl(prizes_raw, "prizeAmount"),
prize_amount_adjusted = map_dbl(prizes_raw, "prizeAmountAdjusted"),
n_laureates = map_int(prizes_raw, \(p) length(p$laureates))
)
glimpse(laureates)Rows: 1,028
Columns: 7
$ id <chr> "745", "102", "779", "259", "1004", "114", "982", "981",…
$ type <chr> "person", "person", "person", "person", "person", "perso…
$ name <chr> "A. Michael Spence", "Aage N. Bohr", "Aaron Ciechanover"…
$ gender <chr> "male", "male", "male", "male", "male", "male", "male", …
$ birth_date <chr> "1943-11-07", "1922-06-19", "1947-10-01", "1926-08-11", …
$ birth_country <chr> "USA", "Denmark", "Israel", "Lithuania", NA, "Pakistan",…
$ birth_year <int> 1943, 1922, 1947, 1926, 1948, 1926, 1961, 1976, 1939, 19…
glimpse(awards)Rows: 1,036
Columns: 7
$ id <chr> "745", "102", "779", "259", "1004", "114", "982", …
$ award_year <int> 2001, 1975, 2004, 1982, 2021, 1979, 2019, 2019, 20…
$ category <chr> "Economic Sciences", "Physics", "Chemistry", "Chem…
$ portion <chr> "1/3", "1/3", "1/3", "1", "1", "1/3", "1/3", "1", …
$ prize_status <chr> "received", "received", "received", "received", "r…
$ affiliation <chr> "Stanford University", "Niels Bohr Institute", "Te…
$ affiliation_country <chr> "USA", "Denmark", "Israel", "United Kingdom", NA, …
glimpse(prizes)Rows: 688
Columns: 5
$ award_year <int> 1901, 1901, 1901, 1901, 1901, 1902, 1902, 1902, …
$ category <chr> "Chemistry", "Literature", "Peace", "Physics", "…
$ prize_amount <dbl> 150782, 150782, 150782, 150782, 150782, 141847, …
$ prize_amount_adjusted <dbl> 10892857, 10892857, 10892857, 10892857, 10892857…
$ n_laureates <int> 1, 1, 2, 1, 1, 1, 1, 2, 2, 1, 1, 1, 1, 3, 1, 1, …
laureateshas one row per laureate.awardshas one row per laureate per prize, so people who won twice, like Marie Curie, have two rows.prizeshas one row per category per year.
A few cleaning choices:
- Organizations have no gender or birth date, so those fields are
NAfor them. - Some birth dates have an unknown month and day (for example
1898-00-00), so I kept only the year. - Many laureates in Peace and Literature have no affiliation, so
affiliationisNAfor them. - Prizes that were never awarded, such as during the World Wars, have zero laureates.
- Birth countries list Scotland and Northern Ireland separately, but affiliations only use “United Kingdom.” I combined them so a Scottish scientist working in Edinburgh isn’t counted as having left home. I did the same for the Faroe Islands (Denmark) and Guadeloupe (France).
Question 2: How old are laureates when they win, and has that changed?
This joins each award to the laureate’s birth year and subtracts. Because some birth dates are missing the month and day, ages can be off by up to one year.
ages <- awards |>
inner_join(laureates |> filter(type == "person"), by = "id") |>
filter(!is.na(birth_year)) |>
mutate(age = award_year - birth_year,
era = if_else(award_year < 1960, "1901-1959", "1960 onward"))
age_by_category <- ages |>
group_by(category, era) |>
summarise(median_age = median(age), .groups = "drop") |>
pivot_wider(names_from = era, values_from = median_age)
knitr::kable(age_by_category)| category | 1901-1959 | 1960 onward |
|---|---|---|
| Chemistry | 50 | 63.0 |
| Economic Sciences | NA | 67.0 |
| Literature | 60 | 69.0 |
| Peace | 68 | 59.0 |
| Physics | 47 | 61.5 |
| Physiology or Medicine | 54 | 61.0 |
ggplot(ages, aes(award_year, age)) +
geom_point(alpha = 0.3, color = "#2a78d6") +
geom_smooth(method = "loess", se = FALSE, color = "#eb6834") +
facet_wrap(~category) +
labs(title = "Age of laureates at the time of their award",
x = "Award year", y = "Age") +
theme_minimal()youngest <- ages |> slice_min(age, n = 1, with_ties = FALSE)
oldest <- ages |> slice_max(age, n = 1, with_ties = FALSE)
went_down <- age_by_category |>
filter(`1960 onward` < `1901-1959`) |>
pull(category)Answer: The median age at the time of the award is 61. Malala Yousafzai is the youngest laureate, winning the Peace prize in 2014 at about 17. John B. Goodenough is the oldest, winning the Chemistry prize in 2019 at about 97. The table compares each category before and after 1960. The median age went up in every category except Peace.
Question 3: When did each category get its first woman, and how has that changed?
This filters the awards to people, joins on gender, and finds the earliest award to a woman in each category.
person_awards <- awards |>
inner_join(laureates |> filter(type == "person"), by = "id")
first_women <- person_awards |>
filter(gender == "female") |>
group_by(category) |>
slice_min(award_year, n = 1, with_ties = FALSE) |>
ungroup() |>
select(category, award_year, name) |>
arrange(award_year)
knitr::kable(first_women, col.names = c("Category", "Year", "First woman laureate"))| Category | Year | First woman laureate |
|---|---|---|
| Physics | 1903 | Marie Curie |
| Peace | 1905 | Bertha von Suttner |
| Literature | 1909 | Selma Lagerlöf |
| Chemistry | 1911 | Marie Curie |
| Physiology or Medicine | 1947 | Gerty Cori |
| Economic Sciences | 2009 | Elinor Ostrom |
women_by_decade <- person_awards |>
mutate(decade = award_year %/% 10 * 10) |>
group_by(decade) |>
summarise(share_women = mean(gender == "female"))
ggplot(women_by_decade, aes(decade, share_women)) +
geom_col(fill = "#2a78d6") +
scale_y_continuous(labels = scales::percent) +
scale_x_continuous(breaks = seq(1900, 2020, 20)) +
labs(title = "Share of Nobel Prizes awarded to women, by decade",
x = NULL, y = NULL) +
theme_minimal()recent <- women_by_decade |> slice_max(decade, n = 1)Answer: Marie Curie was the first woman to win, in Physics in 1903. The last category to award a woman was Economic Sciences, in 2009. Overall, women have received 7.0% of all prizes awarded to people. The share is rising, reaching 20% in the 2020s.
Question 4: Which countries lose the most laureates to other countries?
This compares two fields for every laureate: the country they were born in, and the country of the institution they were working at when they won. Both use today’s country names, so they can be compared directly. A laureate counts as having left if the two countries are different. I counted each person once, using their first prize, and left out laureates with no affiliation (mostly Peace and Literature).
moves <- awards |>
inner_join(laureates |> filter(type == "person"), by = "id") |>
filter(!is.na(birth_country), !is.na(affiliation_country)) |>
group_by(id) |>
slice_min(award_year, n = 1, with_ties = FALSE) |>
ungroup() |>
mutate(left_home = birth_country != affiliation_country)
lost <- moves |>
filter(left_home) |>
count(birth_country, name = "laureates_lost", sort = TRUE)
gained <- moves |>
filter(left_home) |>
count(affiliation_country, name = "laureates_gained", sort = TRUE)
knitr::kable(head(lost, 10), col.names = c("Born in", "Laureates who won while working elsewhere"))| Born in | Laureates who won while working elsewhere |
|---|---|
| United Kingdom | 26 |
| Germany | 24 |
| Poland | 19 |
| Canada | 15 |
| France | 13 |
| Russia | 11 |
| Austria | 10 |
| the Netherlands | 9 |
| Hungary | 8 |
| Italy | 8 |
knitr::kable(head(gained, 10), col.names = c("Working in", "Laureates born elsewhere"))| Working in | Laureates born elsewhere |
|---|---|
| USA | 134 |
| United Kingdom | 28 |
| Germany | 25 |
| Switzerland | 12 |
| France | 11 |
| Canada | 6 |
| Sweden | 4 |
| Russia | 3 |
| Australia | 2 |
| Austria | 2 |
top_lost <- lost |> slice_head(n = 10)
where_they_went <- moves |>
filter(left_home, birth_country %in% top_lost$birth_country) |>
count(birth_country, affiliation_country) |>
group_by(birth_country) |>
slice_max(n, n = 1, with_ties = FALSE) |>
ungroup()
ggplot(top_lost, aes(laureates_lost, reorder(birth_country, laureates_lost))) +
geom_col(fill = "#2a78d6", width = 0.7) +
labs(title = "Countries that lost the most future laureates",
subtitle = "Born there, but working in another country when they won",
x = "Laureates", y = NULL) +
theme_minimal()knitr::kable(where_they_went,
col.names = c("Born in", "Most common destination", "Laureates"))| Born in | Most common destination | Laureates |
|---|---|---|
| Austria | Germany | 4 |
| Canada | USA | 15 |
| France | USA | 8 |
| Germany | USA | 11 |
| Hungary | USA | 4 |
| Italy | USA | 5 |
| Poland | Germany | 9 |
| Russia | USA | 5 |
| United Kingdom | USA | 20 |
| the Netherlands | USA | 4 |
Answer: Of the 757 laureates with both a birth country and an affiliation, 236 (31%) won while working outside the country where they were born. United Kingdom lost the most, with 26 laureates, followed by Germany with 24. USA gained by far the most, with 134 laureates who were born somewhere else. Two things to keep in mind. First, the API records where laureates worked, not their citizenship, so this measures where they did their prize-winning work rather than which passport they held. Second, birth countries use today’s borders. Someone born in Breslau when it was part of Germany counts as born in Poland, which is part of why Poland ranks high.
Source
Data from the Nobel Prize API v2.1 (https://api.nobelprize.org/2.1), published by Nobel Prize Outreach under its terms of use.