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)Assignment 10B
2026-11-10
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:
# httr2 requests the data; jsonlite reads it; tidyverse helps clean and plot it.
library(httr2)
library(jsonlite)
library(tidyverse)
library(knitr)
library(scales)blue_color <- "#0072B2"
teal_color <- "#009E73"
theme_set(theme_minimal(base_size = 12) + theme(plot.title.position = "plot"))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.
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")| check | result |
|---|---|
| Source used | Live Nobel Prize API |
| HTTP status | 200 |
| Prize records | 688 |
| API version | 2.1 |
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.
# 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
}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")| 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 |
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")| 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 |
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.
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")| 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.
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.
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")| 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 |
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)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.
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.
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")| 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 |
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))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.
I counted prize records that had no laureates listed in the API and compared those counts by category.
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")| 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.
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.
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")| 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.
prize_id from the category and award year to connect the two tables.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.
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.