Second-level agenda-setting theory looks at how the media focus on different sides of a topic. It also looks at how that coverage may influence what people think is important. In my project, I wanted to see whether news coverage focused more on AI taking jobs away or creating new AI jobs.
I predict that the average weekly U.S. news coverage of AI job loss will be higher than coverage of AI jobs during the period studied.
I used RStudio to retrieve daily story counts from MediaCloud’s United States-National news collection from January 5, 2025, through October 17, 2026. I combined the daily counts into weekly totals and created a graph and summary table using an adapted version of the script provided.
The independent variable is the coverage topic, which is divided into two categories: AI Job Loss and AI Jobs. The dependent variable is weekly coverage volume, which is then measured by the number of news stories matching each search.
The AI Job Loss search used terms mentioning AI or artificial intelligence, replacing workers, displacement, layoffs, job losses, unemployment, or threatened jobs.
The AI Jobs search also used terms mentioning AI or artificial intelligence, but with terms related to job creation, hiring, opportunities, demand for skills, or employment growth.
The news stories could match both searches, but the counts measure coverage containing the selected terms rather than whether a story presents AI positively or negatively.
The AI Job Loss search returned 80,894 stories, averaging 869.83 stories per week.
The AI Jobs search returned 67,151 stories, averaging 722.05 stories per week.
The AI Job Loss search also had the higher single-week peak (1,310 stories compared with 1,077 for AI Jobs). It had more week-to-week variation (standard deviation of 184.44 compared with 156.57 for AI Jobs).
Using second-level agenda-setting theory, this suggests that AI Job Loss received more news attention than AI Jobs during the period studied. The two searches use different terms, but some of the stories may have matched both. Reading some of the articles would help me see whether the searches found the coverage I intended to measure.
A possible explanation for the higher coverage of AI job losses could be layoffs at major businesses. Researching the articles might point in that direction.
Overall, these results supported my original hypothesis that AI job losses would receive more news coverage than AI job opportunities.
| topic | Total_Stories | Weekly_Minimum | Weekly_Maximum | Weekly_Median | Weekly_Mean | Weekly_SD |
|---|---|---|---|---|---|---|
| AI Job Loss | 80894 | 458 | 1310 | 866 | 869.83 | 184.44 |
| AI Jobs | 67151 | 328 | 1077 | 711 | 722.05 | 156.57 |
| Descriptive Statistics: Pair Differences | ||||
| count | mean | sd | min | max |
|---|---|---|---|---|
| 93.000 | 147.774 | 90.167 | −94.000 | 352.000 |
| Normality Test (Shapiro-Wilk) | ||
| statistic | p.value | method |
|---|---|---|
| 0.9861 | 0.4343 | Shapiro-Wilk normality test |
| If the P.VALUE is 0.05 or less, the number of pairs is fewer than 40, and the distribution of pair differences shows obvious non-normality or outliers, consider using the Wilcoxon Signed Rank Test results instead of the Paired-Samples t-Test results. | ||
| Paired-Samples t-Test | |||||
| statistic | parameter | p.value | conf.low | conf.high | method |
|---|---|---|---|---|---|
| 15.8050 | 92 | 0.0000 | 129.2046 | 166.3438 | Paired t-test |
| Group Means and SDs (t-Test) | |||
| V1_Mean | V2_Mean | V1_SD | V2_SD |
|---|---|---|---|
| 722.054 | 869.828 | 156.571 | 184.437 |
| Wilcoxon Signed Rank Test | ||
| statistic | p.value | method |
|---|---|---|
| 58.0000 | 0.0000 | Wilcoxon signed rank test with continuity correction |
| Group Means and SDs (Wilcoxon) | |||
| V1_Mean | V2_Mean | V1_SD | V2_SD |
|---|---|---|---|
| 722.054 | 869.828 | 156.571 | 184.437 |
I used a paired-samples t-test to compare weekly news coverage of AI Job Loss and AI Jobs over 93 weeks.
The results showed a statistically significant difference between the two topics, t(92) = 15.805, p < .001. AI Job Loss averaged about 148 more stories per week than AI Jobs.
The standard deviation of the paired differences was 90.17 stories, showing how much the weekly differences varied around the average.
The box plot compares weekly coverage of AI Jobs (V1) and AI Job Loss (V2), showing the distribution of story counts for each topic.
The Shapiro-Wilk test (p = .4343) did not indicate a significant departure from normality in the paired differences.
These results support my hypothesis that AI job losses receive more news coverage than AI job opportunities. However, the results do not explain why the difference exists.
library(tidyverse)
library(httr2)
library(lubridate)
library(plotly)
library(knitr)
library(kableExtra)
library(gt)
library(gtExtras)
library(broom)
mc_key <- Sys.getenv("MEDIACLOUD_KEY")
start_date <- "2025-01-05"
end_date <- "2026-10-17"
loss_query <-
'((AI OR "artificial intelligence")
AND
(replace* OR displacement OR layoff* OR
"job loss" OR "job losses" OR
eliminat* OR unemployment OR
"threatened jobs" OR
"threatens jobs" OR
"threatening jobs"))'
hiring_query <-
'((AI OR "artificial intelligence")
AND
("job creation" OR
"create jobs" OR
"creating jobs" OR
hiring OR
opportunity* OR
"demand for skills" OR
"workforce growth" OR
"employment growth"))'
topics <- c(
"AI Job Loss" = loss_query,
"AI Jobs" = hiring_query
)
mc_counts_over_time <- function(
query, start_date, end_date,
collection_id = 34412234
) {
resp <-
request(
"https://search.mediacloud.org/api/search/count-over-time"
) |>
req_headers(
Authorization = paste("Token", mc_key)
) |>
req_url_query(
q = query,
start = start_date,
end = end_date,
platform = "onlinenews-mediacloud",
cs = collection_id
) |>
req_perform()
results <- resp_body_json(resp)
map_dfr(
results$count_over_time$counts,
as_tibble
) |>
mutate(date = as.Date(date))
}
topic_data <-
imap_dfr(
topics,
function(search_string, topic_name) {
message("Downloading: ", topic_name)
results <- mc_counts_over_time(
query = search_string,
start_date = start_date,
end_date = end_date
)
Sys.sleep(35)
results |>
mutate(topic = topic_name)
}
)
weekly_counts <-
topic_data |>
mutate(
week = floor_date(date, unit = "week")
) |>
group_by(topic, week) |>
summarise(
stories = sum(count),
.groups = "drop"
)
topic_summary <-
weekly_counts |>
group_by(topic) |>
summarise(
Total_Stories = sum(stories),
Weekly_Minimum = min(stories),
Weekly_Maximum = max(stories),
Weekly_Median = median(stories),
Weekly_Mean = round(mean(stories), 2),
Weekly_SD = round(sd(stories), 2),
.groups = "drop"
) |>
arrange(desc(Total_Stories))
Table <-
topic_summary |>
kbl(
caption = "Media Cloud Topic Summary Statistics",
align = "lrrrrrr"
) |>
kable_styling(
full_width = FALSE,
bootstrap_options = c(
"striped", "hover", "condensed"
)
)
Table
Graph <- plot_ly(
data = weekly_counts,
width = 1000,
x = ~week,
y = ~stories,
color = ~topic,
colors = "Set1",
type = "scatter",
mode = "lines+markers"
) |>
plotly::layout(
title = "Weekly U.S. News Coverage: AI Employment",
hovermode = "x unified",
xaxis = list(title = "Week"),
yaxis = list(
title = "Weekly Story Count",
separatethousands = TRUE
)
)
Graph
weekly_counts_wide <-
weekly_counts %>%
mutate(topic = make.names(topic)) %>%
select(week, topic, stories) %>%
pivot_wider(names_from = topic, values_from = stories)
# ============================================================
# Setup: Install and Load Required Packages
# ============================================================
if (!require("tidyverse")) install.packages("tidyverse")
if (!require("plotly")) install.packages("plotly")
if (!require("gt")) install.packages("gt")
if (!require("gtExtras")) install.packages("gtExtras")
if (!require("broom")) install.packages("broom")
library(tidyverse)
library(plotly)
library(gt)
library(gtExtras)
library(broom)
options(scipen = 999)
# ============================================================
# Data Import
# ============================================================
mydata <- weekly_counts_wide
# Specify the two variables involved
mydata$V1 <- mydata$AI.Jobs
mydata$V2 <- mydata$AI.Job.Loss
# ============================================================
# Compute Pair Differences
# ============================================================
mydata$PairDifferences <- mydata$V2 - mydata$V1
# ============================================================
# Interactive Histogram of Pair Differences
# ============================================================
hist_plot <- plot_ly(
data = mydata,
x = ~PairDifferences,
type = "histogram",
marker = list(color = "#1f78b4", line = list(color = "black", width = 1))
) %>%
layout(
title = "Distribution of Pair Differences",
xaxis = list(title = "Pair Differences"),
yaxis = list(title = "Count"),
shapes = list(
list(
type = "line",
x0 = mean(mydata$PairDifferences, na.rm = TRUE),
x1 = mean(mydata$PairDifferences, na.rm = TRUE),
y0 = 0,
y1 = max(table(mydata$PairDifferences)),
line = list(color = "red", dash = "dash")
)
)
)
# ============================================================
# Descriptive Statistics
# ============================================================
desc_stats <- mydata %>%
summarise(
count = n(),
mean = mean(PairDifferences, na.rm = TRUE),
sd = sd(PairDifferences, na.rm = TRUE),
min = min(PairDifferences, na.rm = TRUE),
max = max(PairDifferences, na.rm = TRUE)
)
desc_table <- desc_stats %>%
gt() %>%
gt_theme_538() %>%
tab_header(title = "Descriptive Statistics: Pair Differences") %>%
fmt_number(columns = where(is.numeric), decimals = 3)
# ============================================================
# Normality Test (Shapiro-Wilk)
# ============================================================
shapiro_res <- shapiro.test(mydata$PairDifferences)
shapiro_table <- tidy(shapiro_res) %>%
select(statistic, p.value, method) %>%
gt() %>%
gt_theme_538() %>%
tab_header(title = "Normality Test (Shapiro-Wilk)") %>%
fmt_number(columns = c(statistic, p.value), decimals = 4) %>%
tab_source_note(
source_note = "If the P.VALUE is 0.05 or less, the number of pairs is fewer than 40, and the distribution of pair differences shows obvious non-normality or outliers, consider using the Wilcoxon Signed Rank Test results instead of the Paired-Samples t-Test results."
)
# ============================================================
# Reshape Data for Repeated-Measures Plot
# ============================================================
df_long <- mydata %>%
pivot_longer(cols = c(V1, V2),
names_to = "Measure",
values_to = "Value")
# ============================================================
# Repeated-Measures Boxplot (Interactive, with Means)
# ============================================================
group_means <- df_long %>%
group_by(Measure) %>%
summarise(mean_value = mean(Value), .groups = "drop")
boxplot_measures <- plot_ly() %>%
add_trace(
data = df_long,
x = ~Measure, y = ~Value,
type = "box",
boxpoints = "outliers",
marker = list(color = "red", size = 4),
line = list(color = "black"),
fillcolor = "royalblue",
name = ""
) %>%
add_trace(
data = group_means,
x = ~Measure, y = ~mean_value,
type = "scatter", mode = "markers",
marker = list(
symbol = "diamond", size = 9,
color = "black", line = list(color = "white", width = 1)
),
text = ~paste0("Mean = ", round(mean_value, 2)),
hoverinfo = "text",
name = "Group Mean"
) %>%
layout(
title = list(
text = "<b>Weekly News Coverage: AI Jobs vs. AI Job Loss</b>"
),
xaxis = list(
title = "Topic",
tickvals = c("V1", "V2"),
ticktext = c("AI Jobs", "AI Job Loss")
),
yaxis = list(title = "Value"),
showlegend = FALSE
)
# ============================================================
# Parametric Test (Paired-Samples t-Test)
# ============================================================
t_res <- t.test(mydata$V2, mydata$V1, paired = TRUE)
t_table <- tidy(t_res) %>%
select(statistic, parameter, p.value, conf.low, conf.high, method) %>%
gt() %>%
gt_theme_538() %>%
tab_header(title = "Paired-Samples t-Test") %>%
fmt_number(columns = c(statistic, p.value, conf.low, conf.high), decimals = 4)
t_summary <- mydata %>%
select(V1, V2) %>%
summarise_all(list(Mean = mean, SD = sd)) %>%
gt() %>%
gt_theme_538() %>%
tab_header(title = "Group Means and SDs (t-Test)") %>%
fmt_number(columns = everything(), decimals = 3)
# ============================================================
# Nonparametric Test (Wilcoxon Signed Rank)
# ============================================================
wilcox_res <- wilcox.test(mydata$V1, mydata$V2, paired = TRUE,
exact = FALSE)
wilcox_table <- tidy(wilcox_res) %>%
select(statistic, p.value, method) %>%
gt() %>%
gt_theme_538() %>%
tab_header(title = "Wilcoxon Signed Rank Test") %>%
fmt_number(columns = c(statistic, p.value), decimals = 4)
wilcox_summary <- mydata %>%
select(V1, V2) %>%
summarise_all(list(Mean = mean, SD = sd)) %>%
gt() %>%
gt_theme_538() %>%
tab_header(title = "Group Means and SDs (Wilcoxon)") %>%
fmt_number(columns = everything(), decimals = 3)
# ============================================================
# Results Summary (in specified order)
# ============================================================
hist_plot
desc_table
shapiro_table
boxplot_measures
t_table
t_summary
wilcox_table
wilcox_summary