Show code
library(tidyverse)
library(httr2)How has New York Times coverage of artificial intelligence changed from 2016 to 2026, and which sections cover it most?
In our Project 3 analysis, LLM / Gen AI skills appeared in about 1 in 4 data job postings. This document checks whether the news shows the same rise.
| API | Article Search API (NYT Developer Network) |
| Endpoint | https://api.nytimes.com/svc/search/v2/articlesearch.json |
| What it returns | Article metadata from 1851 to today: headline, abstract, publication date, section, byline, keywords |
| Results per page | 10 articles |
| Rate limit | 5 requests per minute, 500 per day |
library(tidyverse)
library(httr2)The key is stored in my .Renviron file as NYT_API_KEY (set with usethis::edit_r_environ()), so it never appears in this document. The key is sent as a URL parameter, so request objects and URLs are never printed.
api_key <- Sys.getenv("NYT_API_KEY")
if (api_key == "") stop("NYT_API_KEY is not set. Add it to .Renviron and restart R.")Every request goes through one function. NYT allows about one request every 12 seconds, so the function waits 12 seconds before each call. If NYT still answers “429 Too Many Requests”, it waits 60 seconds and tries again (up to 5 tries).
nyt_search <- function(...) {
Sys.sleep(12) # NYT asks for ~12 seconds between calls
request("https://api.nytimes.com/svc/search/v2/articlesearch.json") |>
req_url_query(..., `api-key` = api_key) |>
req_retry(max_tries = 5, backoff = \(attempt) 60) |> # on a 429, wait 60s and try again
req_perform() |>
resp_body_json()
}| Parameter | Value |
|---|---|
q |
"artificial intelligence" (exact phrase) |
begin_date / end_date |
January 1 to December 31 of each year, 2016 to 2026 |
Each response includes response$metadata$hits, the total number of matching articles, so only the first page is needed for each year (11 requests).
count_hits <- function(q = NULL, begin, end) {
nyt_search(q = q, begin_date = begin, end_date = end)$response$metadata$hits
}
ai_yearly <- tibble(year = 2016:2026) |>
mutate(ai_articles = map_int(year, \(y) count_hits(q = '"artificial intelligence"',
begin = paste0(y, "0101"),
end = paste0(y, "1231"))))
ai_yearly| year | ai_articles |
|---|---|
| 2016 | 325 |
| 2017 | 462 |
| 2018 | 639 |
| 2019 | 665 |
| 2020 | 417 |
| 2021 | 417 |
| 2022 | 425 |
| 2023 | 1529 |
| 2024 | 1739 |
| 2025 | 2371 |
| 2026 | 3266 |
To compare years fairly, AI articles are divided by everything the Times published that year. A request with no search term returns every article, but the API caps hits at 10,000, and a full year of the Times is far above that (every year came back as exactly 10,000). One month is under the cap (January 2025: 4,612), so totals are counted month by month and added up.
That is 130 requests, about 30 minutes at NYT’s rate limit, so this code was run once (November 1, 2026) and the result was saved to GitHub. The chunk below shows the code but is not re-run when the document is rendered.
monthly <- expand_grid(year = 2016:2026, month = 1:12) |>
mutate(start = make_date(year, month, 1),
end = ceiling_date(start, "month") - days(1)) |>
filter(start <= today()) |> # skip future months
mutate(all_articles = map2_int(
start, end,
\(s, e) count_hits(begin = format(s, "%Y%m%d"), end = format(e, "%Y%m%d")),
.progress = TRUE
))
write_csv(monthly, "nyt_monthly_totals.csv")monthly <- read_csv(
"https://raw.githubusercontent.com/dillonleeper/DATA-607/main/assignments/week09/nyt_monthly_totals.csv",
show_col_types = FALSE
)
all_yearly <- monthly |>
group_by(year) |>
summarise(all_articles = sum(all_articles))yearly <- ai_yearly |>
left_join(all_yearly, by = "year") |>
mutate(share_pct = ai_articles / all_articles * 100)
yearly |>
mutate(share_pct = round(share_pct, 2),
across(c(ai_articles, all_articles), \(x) format(x, big.mark = ","))) |>
rename(Year = year, `AI articles` = ai_articles,
`All articles` = all_articles, `AI share (%)` = share_pct)| Year | AI articles | All articles | AI share (%) |
|---|---|---|---|
| 2016 | 325 | 68,897 | 0.47 |
| 2017 | 462 | 59,543 | 0.78 |
| 2018 | 639 | 57,565 | 1.11 |
| 2019 | 665 | 54,212 | 1.23 |
| 2020 | 417 | 62,462 | 0.67 |
| 2021 | 417 | 64,100 | 0.65 |
| 2022 | 425 | 58,683 | 0.72 |
| 2023 | 1,529 | 53,686 | 2.85 |
| 2024 | 1,739 | 54,552 | 3.19 |
| 2025 | 2,371 | 50,417 | 4.70 |
| 2026 | 3,266 | 42,999 | 7.60 |
ggplot(yearly, aes(x = year, y = share_pct)) +
geom_vline(xintercept = 2022.9, linetype = "dashed", colour = "grey60") +
annotate("text", x = 2022.8, y = max(yearly$share_pct) * 0.9,
label = "ChatGPT released\n(Nov 2022)", hjust = 1, size = 3.2, colour = "grey40") +
geom_line(colour = "#2c5f8a", linewidth = 1) +
geom_point(colour = "#2c5f8a", size = 2.5) +
geom_label(aes(label = paste0(round(share_pct, 1), "%")), vjust = -0.6, size = 3.2,
label.size = 0, fill = "white") +
scale_x_continuous(breaks = 2016:2026, labels = \(x) if_else(x == 2026, "2026*", as.character(x))) +
scale_y_continuous(labels = \(x) paste0(x, "%"), expand = expansion(mult = c(0.05, 0.15))) +
labs(x = NULL, y = "Share of all articles",
title = "NYT articles mentioning \"artificial intelligence\"",
caption = "*2026 runs through October") +
theme_minimal(base_size = 12) +
theme(panel.grid.minor = element_blank())Note: AI coverage grew slowly from 0.5% of articles in 2016 to 1.2% in 2019, then fell back to about 0.7% in 2020 to 2022, possibly because other news (the pandemic, the 2020 election) took up more space. After ChatGPT’s release in November 2022, the share grew 3.9 times in one year, to 2.8% in 2023, and reached 7.6% so far in 2026, about 1 in 13 articles. Meanwhile the Times published fewer articles overall, so raw counts alone would understate how much bigger a share AI has become.
| Parameter | Value |
|---|---|
q |
"artificial intelligence" |
begin_date / end_date |
20260101 to 20261231 |
sort |
newest |
page |
0 to 9 (10 articles per page, 100 articles) |
get_page <- function(page) {
nyt_search(q = '"artificial intelligence"',
begin_date = "20260101", end_date = "20261231",
sort = "newest", page = page)$response$docs
}
docs <- map(0:9, get_page) |> list_flatten()
length(docs)[1] 100
Each article is a nested list. headline and byline are lists with several fields, and keywords is a list of lists (one per keyword, each with a name, value, and rank). Each article becomes one row:
articles <- map(docs, \(d) tibble(
id = d$`_id`,
pub_date = as_date(ymd_hms(d$pub_date)), # "2025-10-31T16:45:06Z" -> date
headline = d$headline$main, # main headline only
section = d$section_name,
byline = d$byline$original,
keywords = paste(map_chr(d$keywords, "value"), collapse = ", ") # all keywords in one column
)) |>
list_rbind() |>
mutate(section = if_else(is.na(section) | section == "", "Unknown", section)) |>
distinct(id, .keep_all = TRUE) # in case an article appears on two pages
glimpse(articles)Rows: 100
Columns: 6
$ id <chr> "nyt://article/0b3054b8-4404-5cad-b6f8-3e99d899e3e2", "nyt://…
$ pub_date <date> 2026-11-01, 2026-11-01, 2026-11-01, 2026-11-01, 2026-11-01, …
$ headline <chr> "Binance Reports $3.64 Billion in Outflows as It Resumes Norm…
$ section <chr> "Business", "U.S.", "Opinion", "Opinion", "U.S.", "U.S.", "U.…
$ byline <chr> "By Kate Conger", "", "", "By Toni Sims", "By Matt Zdun and T…
$ keywords <chr> "Virtual Currency, Robberies and Thefts, Cyberattacks and Hac…
articles |>
select(pub_date, section, headline) |>
head(10)| pub_date | section | headline |
|---|---|---|
| 2026-11-01 | Business | Binance Reports $3.64 Billion in Outflows as It Resumes Normal Transactions |
| 2026-11-01 | U.S. | Does the Needle use A.I.? |
| 2026-11-01 | Opinion | Using Denver as a Model to Help the Homeless |
| 2026-11-01 | Opinion | Could A.I. Be Conscious? It’s Definitely Possible. |
| 2026-11-01 | U.S. | Meet the Megadonors Pumping $2 Billion Into the 2026 Midterms |
| 2026-11-01 | U.S. | Culture War Tensions Roil Beverly Hills School Board Race |
| 2026-11-01 | U.S. | Threats, Gaffes and Men’s Posteriors: Trump’s Talk Gets Wilder |
| 2026-11-01 | Arts | Museums Turn to A.I. to Create Personalized Tour Experiences |
| 2026-10-31 | Technology | How Binance’s $6.3 Billion Hack Went Down |
| 2026-10-31 | Business | The New Hot Job in Law Doesn’t Require a J.D. |
section_counts <- articles |> count(section, sort = TRUE)
section_counts |>
slice_max(n, n = 8, with_ties = FALSE) |>
ggplot(aes(x = n, y = fct_reorder(section, n))) +
geom_col(fill = "#2c5f8a") +
geom_text(aes(label = n), hjust = -0.3, size = 3.2) +
scale_x_continuous(expand = expansion(mult = c(0, 0.1))) +
labs(x = "Articles", y = NULL,
title = "Where AI coverage appears",
subtitle = paste0(nrow(articles), " most recent 2026 articles, ",
format(min(articles$pub_date), "%b %d"), " to ",
format(max(articles$pub_date), "%b %d"))) +
theme_minimal(base_size = 12) +
theme(panel.grid.major.y = element_blank())Note: Business has the most AI articles (30 of 100), ahead of Technology (17). AI is covered mostly as a business story, not only as technology news.
headline$main and byline$original are kept. keywords (a list of lists) is collapsed into one comma-separated column. multimedia and other unused fields are dropped.pub_date is text with a time and time zone, converted to a date.id in case one appears on two pages.Claude (Anthropic) helped write and debug the code and organize the text. I ran each request myself, checked the results, and found the 10,000 cap in the output.
Anthropic. (2026). Claude [Large language model]. https://claude.ai/. Accessed November 1, 2026.