NobelPrize.org API

Assignment 10B

Andre Thomson

2026-11-10

1 API and questions

I used the Nobel Prize API’s nobelPrizes endpoint for this assignment. The response contains prize records, with laureates listed inside each record. I wanted to practice working with nested JSON and see what I could learn once I separated the information into tables.

API endpoint: https://api.nobelprize.org/2.1/nobelPrizes?limit=1000

I looked at four questions:

  1. Which categories have the most laureate entries?
  2. How has the average number of laureates per awarded prize changed by decade?
  3. Which categories have the most prize records without a listed laureate?
  4. Which laureates appear in more than one Nobel Prize record?

2 Packages

Show code
# httr2 requests the data; jsonlite reads it; tidyverse helps clean and plot it.
library(httr2)
library(jsonlite)
library(tidyverse)
library(knitr)
library(scales)
Show code
blue_color <- "#0072B2"
teal_color <- "#009E73"
theme_set(theme_minimal(base_size = 12) + theme(plot.title.position = "plot"))

3 Retrieve the JSON

I request the live API first. If that fails, the code looks for a saved JSON file in the data folder. That file is not included in this QMD. I render and the report shows which source was used.

Show code
api_url <- "https://api.nobelprize.org/2.1/nobelPrizes"
snapshot_path <- "data/nobel_prizes_snapshot.json"

api_result <- tryCatch({
  response <- request(api_url) |>
    req_url_query(limit = 1000) |>
    req_timeout(seconds = 20) |>
    req_perform()
  list(json = resp_body_string(response), status = resp_status(response))
}, error = function(e) NULL)

if (is.null(api_result)) {
  if (!file.exists(snapshot_path)) {
    stop("API request failed and data/nobel_prizes_snapshot.json is missing. Save a real API snapshot or restore connectivity.")
  }
  raw_json <- paste(readLines(snapshot_path, warn = FALSE), collapse = "\n")
  data_source_used <- "Local API snapshot"
  request_status <- NA_integer_
} else {
  raw_json <- api_result$json
  data_source_used <- "Live Nobel Prize API"
  request_status <- api_result$status
}

nobel_raw <- fromJSON(raw_json, simplifyVector = FALSE)
stopifnot(is.list(nobel_raw$nobelPrizes), length(nobel_raw$nobelPrizes) > 0)
if (length(nobel_raw$nobelPrizes) >= 1000) {
  warning("The API returned 1000 prize records. Check pagination before treating these results as complete.")
}

request_check <- tibble(
  check = c("Source used", "HTTP status", "Prize records", "API version"),
  result = c(data_source_used,
             if (is.na(request_status)) "Not available" else as.character(request_status),
             as.character(length(nobel_raw$nobelPrizes)), "2.1")
)
kable(request_check, caption = "API retrieval check")
API retrieval check
check result
Source used Live Nobel Prize API
HTTP status 200
Prize records 688
API version 2.1

4 Turn the nested JSON into two tables

I made one table for prizes and another for the laureates listed under each prize. The prize_id field connects them when I need information from both tables.

Show code
# Names can appear under different fields for individuals and organizations.
get_english <- function(x) {
  if (is.null(x) || is.null(x$en)) return(NA_character_)
  x$en
}
Show code
prizes_clean <- map_dfr(nobel_raw$nobelPrizes, function(prize) {
  category_name <- get_english(prize$category)
  award_year <- as.integer(prize$awardYear)
  tibble(
    prize_id = paste(category_name, award_year, sep = "-"),
    award_year = award_year,
    category = category_name,
    date_awarded = if (is.null(prize$dateAwarded)) NA_character_ else prize$dateAwarded,
    laureate_count = if (is.null(prize$laureates)) 0L else length(prize$laureates)
  )
})
stopifnot(!anyDuplicated(prizes_clean$prize_id))
stopifnot(!anyNA(prizes_clean$category), !anyNA(prizes_clean$award_year))
kable(head(prizes_clean, 8), caption = "First eight rows of the prize table")
First eight rows of the prize table
prize_id award_year category date_awarded laureate_count
Chemistry-1901 1901 Chemistry 1901-11-12 1
Literature-1901 1901 Literature 1901-11-14 1
Peace-1901 1901 Peace 1901-12-10 2
Physics-1901 1901 Physics 1901-11-12 1
Physiology or Medicine-1901 1901 Physiology or Medicine 1901-10-30 1
Chemistry-1902 1902 Chemistry 1902-11-11 1
Literature-1902 1902 Literature 1902-11-13 1
Peace-1902 1902 Peace 1902-12-10 2
Show code
laureates_clean <- map_dfr(nobel_raw$nobelPrizes, function(prize) {
  if (is.null(prize$laureates) || length(prize$laureates) == 0) return(tibble())
  category_name <- get_english(prize$category)
  award_year <- as.integer(prize$awardYear)
  prize_id <- paste(category_name, award_year, sep = "-")
  map_dfr(prize$laureates, function(laureate) {
    laureate_name <- coalesce(get_english(laureate$knownName),
                              get_english(laureate$orgName),
                              get_english(laureate$fullName))
    tibble(prize_id = prize_id,
           laureate_id = if (is.null(laureate$id)) NA_character_ else as.character(laureate$id),
           laureate_name = laureate_name,
           portion = if (is.null(laureate$portion)) NA_character_ else as.character(laureate$portion))
  })
})
stopifnot(nrow(laureates_clean) == sum(prizes_clean$laureate_count))
stopifnot(all(laureates_clean$prize_id %in% prizes_clean$prize_id))
kable(head(laureates_clean, 8), caption = "First eight rows of the laureate table")
First eight rows of the laureate table
prize_id laureate_id laureate_name portion
Chemistry-1901 160 Jacobus H. van ’t Hoff 1
Literature-1901 569 Sully Prudhomme 1
Peace-1901 462 Henry Dunant 1/2
Peace-1901 463 Frédéric Passy 1/2
Physics-1901 1 Wilhelm Conrad Röntgen 1
Physiology or Medicine-1901 293 Emil von Behring 1
Chemistry-1902 161 Emil Fischer 1
Literature-1902 571 Theodor Mommsen 1

5 Data-quality checks

Before answering the questions, I checked the row counts, duplicate prize IDs, missing names, and years covered by the data. These checks help me catch obvious problems, but they do not replace a full review of the API records.

Show code
data_quality <- tibble(
  check = c("Prize rows", "Duplicate prize IDs", "Laureate rows",
            "Missing laureate names", "Prize records with no listed laureate",
            "First award year", "Last award year"),
  result = c(nrow(prizes_clean), sum(duplicated(prizes_clean$prize_id)),
             nrow(laureates_clean), sum(is.na(laureates_clean$laureate_name)),
             sum(prizes_clean$laureate_count == 0),
             min(prizes_clean$award_year), max(prizes_clean$award_year))
)
kable(data_quality, caption = "Data checks before analysis")
Data checks before analysis
check result
Prize rows 688
Duplicate prize IDs 0
Laureate rows 1036
Missing laureate names 0
Prize records with no listed laureate 49
First award year 1901
Last award year 2026

I kept the records with no listed laureate because they are needed for Question 3. A blank laureate list tells me what is missing from the API response, not necessarily why it is missing.

6 Question 1: Which categories have the most laureate entries?

I counted the laureate entries in each category and ranked the categories from highest to lowest. I also counted the prize records to show how many awards those entries came from.

Show code
category_totals <- prizes_clean |>
  group_by(category) |>
  summarise(prize_records = n(), laureate_entries = sum(laureate_count), .groups = "drop") |>
  arrange(desc(laureate_entries))
kable(category_totals,
      col.names = c("Category", "Prize records", "Laureate entries"),
      caption = "Prize records and laureate entries by category")
Prize records and laureate entries by category
Category Prize records Laureate entries
Physiology or Medicine 126 235
Physics 126 231
Chemistry 126 202
Peace 126 144
Literature 126 123
Economic Sciences 58 101
Show code
ggplot(category_totals,
       aes(x = laureate_entries, y = fct_reorder(category, laureate_entries))) +
  geom_col(fill = blue_color, width = 0.7) +
  geom_text(aes(label = comma(laureate_entries)), hjust = -0.15, size = 3.6) +
  scale_x_continuous(expand = expansion(mult = c(0, 0.12))) +
  labs(title = "Laureate entries by category", x = "Laureate entries", y = NULL)

Horizontal bar chart of laureate entries by Nobel Prize category.

Answer: Physiology or Medicine has the most laureate entries, with 235. I am counting entries under prizes, not unique laureates, so someone who won more than once is counted for each prize.

7 Question 2: How has the average number of laureates changed by decade?

I calculated the average number of laureates per prize for each decade. I left out prize records with no listed laureate because including them as zero would lower the averages.

Show code
decade_summary <- prizes_clean |>
  filter(laureate_count > 0) |>
  mutate(decade = (award_year %/% 10) * 10) |>
  group_by(decade) |>
  summarise(awarded_prizes = n(), average_laureates = mean(laureate_count),
            .groups = "drop") |>
  arrange(decade)
kable(decade_summary, digits = 2,
      col.names = c("Decade", "Prize records with laureates", "Average laureates per prize"),
      caption = "Average listed laureates per prize by decade")
Average listed laureates per prize by decade
Decade Prize records with laureates Average laureates per prize
1900 45 1.27
1910 36 1.11
1920 44 1.23
1930 44 1.27
1940 32 1.34
1950 48 1.50
1960 49 1.61
1970 59 1.76
1980 60 1.62
1990 60 1.73
2000 60 2.05
2010 60 2.02
2020 42 2.05
Show code
ggplot(decade_summary, aes(x = decade, y = average_laureates)) +
  geom_line(color = teal_color, linewidth = 1) +
  geom_point(color = teal_color, size = 2.5) +
  scale_x_continuous(breaks = decade_summary$decade) +
  labs(title = "Average laureates per prize by decade",
       subtitle = "The 2020s are not a complete decade",
       x = "Decade", y = "Average laureates per prize") +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

Line chart of average listed laureates per prize by decade.

Answer: The average went from 1.27 laureates per prize in the 1900s to 2.05 in the 2020s. That is an increase overall, although the average does not move in the same direction every decade. The highest average is 2.05, in the 2000s. I can describe the pattern from these numbers, but I cannot tell from this data alone why it changed. The 2020s are still incomplete.

8 Question 3: Which categories have the most records without a listed laureate?

I counted prize records that had no laureates listed in the API and compared those counts by category.

Show code
unawarded_by_category <- prizes_clean |>
  filter(laureate_count == 0) |>
  count(category, name = "records_without_laureate", sort = TRUE)
kable(unawarded_by_category,
      col.names = c("Category", "Records without a listed laureate"),
      caption = "Prize records with no listed laureate")
Prize records with no listed laureate
Category Records without a listed laureate
Peace 19
Physiology or Medicine 9
Chemistry 8
Literature 7
Physics 6

Answer: Peace has the most records without a listed laureate, with 19. I kept these rows in my analysis. A missing name in the API does not tell me why no laureate is listed.

9 Question 4: Which laureates appear in more than one prize record?

I joined the laureate and prize tables using prize_id, then counted how many different prize records were linked to each laureate ID. This also gave me the years and categories for their awards.

Show code
laureate_awards <- laureates_clean |>
  inner_join(prizes_clean |> select(prize_id, award_year, category),
             by = "prize_id", relationship = "many-to-one")
repeat_laureates <- laureate_awards |>
  filter(!is.na(laureate_id)) |>
  group_by(laureate_id) |>
  summarise(laureate_name = first(na.omit(laureate_name), default = NA_character_),
            prize_records = n_distinct(prize_id),
            awards = paste(unique(paste(award_year, category)), collapse = "; "),
            .groups = "drop") |>
  filter(prize_records > 1) |>
  arrange(desc(prize_records), laureate_name)
kable(repeat_laureates |> select(laureate_name, prize_records, awards),
      col.names = c("Laureate", "Prize records", "Years and categories"),
      caption = "Laureates appearing in more than one prize record")
Laureates appearing in more than one prize record
Laureate Prize records Years and categories
International Committee of the Red Cross 3 1917 Peace; 1944 Peace; 1963 Peace
Frederick Sanger 2 1958 Chemistry; 1980 Chemistry
John Bardeen 2 1956 Physics; 1972 Physics
K. Barry Sharpless 2 2001 Chemistry; 2022 Chemistry
Linus Pauling 2 1954 Chemistry; 1962 Peace
Marie Curie 2 1903 Physics; 1911 Chemistry
Office of the United Nations High Commissioner for Refugees 2 1954 Peace; 1981 Peace

Answer: I found 7 laureates who appear in more than one prize record. For example, International Committee of the Red Cross, Frederick Sanger, John Bardeen, among others. The table shows their award years and categories. I matched records by laureate ID instead of name because the ID is more reliable when names vary.

10 Data-cleaning decisions

11 Limitations

The API may change when NobelPrize.org updates its records. The 2020s are not a complete decade. Laureate entries are not the same as unique laureates, and an empty laureate list does not by itself explain why a record is empty. These results describe what is in the API response, not the historical reasons behind the patterns.

12 Sources

13 AI use

I used ChatGPT to revise the wording of my answers and suggest code and data-quality checks. I used VS Code to work with the Quarto file.