This report examines differences in the language used by left-leaning and right-leaning news outlets when reporting on Donald Trump. Using news headlines collected through Media Cloud, I identify the most frequently occurring bigrams, which are pairs of consecutive words, in each group of outlets. I also examine how the appearance of these bigrams changes over time. These comparisons can help show which topics and phrases receive attention from different types of news organizations.Here’s the code:
# Install required packages if needed
packages <- c(
"tidyverse",
"tidytext",
"lubridate",
"ggridges",
"scales"
)
for (package in packages) {
if (!requireNamespace(package, quietly = TRUE)) {
install.packages(package)
}
}
library(tidyverse)
library(tidytext)
library(lubridate)
library(ggridges)
library(scales)
# Download the dataset
download.file(
url = "https://github.com/drkblake/Data/raw/refs/heads/main/Trump_story_headlines.rds",
destfile = "Trump_story_headlines.rds",
mode = "wb"
)
headlines <- readRDS("Trump_story_headlines.rds")
# Create the source ideology lookup table
source_ideology <- tribble(
~media_name, ~ideology,
# Left-leaning outlets
"Talking Points Memo", "Left",
"Daily Kos", "Left",
"The New Replublic", "Left",
"Mother Jones", "Left",
"thenation.com", "Left",
"Raw Story", "Left",
"salon.com", "Left",
"Jezebel", "Left",
"slate.com", "Left",
"Truthout", "Left",
"colorlines.com", "Left",
"newsone.com", "Left",
"National Memo", "Left",
"TheRealNews.com", "Left",
"huffpost.com", "Left",
"AlterNet", "Left",
"vox.com", "Left",
"MintPress News", "Left",
"Mic", "Left",
"theintercept.com", "Left",
"theguardian.com", "Left",
"The Root", "Left",
# Right-leaning outlets
"Hot Air", "Right",
"RedState", "Right",
"foxnews.com", "Right",
"National Review", "Right",
"NewsBusters", "Right",
"Daily Caller", "Right",
"PJ Media", "Right",
"breitbart.com", "Right",
"Townhall", "Right",
"CNS News", "Right",
"American Spectator", "Right",
"TheBlaze", "Right",
"newsmax.com", "Right",
"patriotpost.us", "Right",
"Daily Signal", "Right",
"One America News Network", "Right",
"Fox Business", "Right",
"shop.americanfreepress.net", "Right",
"National Catholic Register", "Right"
)
# Filter the dataset and classify each outlet
headlines_lr <- headlines %>%
inner_join(source_ideology, by = "media_name")
# Identify the headline and publication-date columns
# This makes the script work with common Media Cloud column names.
headline_candidates <- c("title", "headline", "story_title")
date_candidates <- c(
"publish_date", "published_date", "date",
"publish_date_utc", "published_at"
)
headline_col <- intersect(headline_candidates, names(headlines_lr))[1]
date_col <- intersect(date_candidates, names(headlines_lr))[1]
if (is.na(headline_col) || is.na(date_col)) {
stop(
paste(
"Could not identify the headline or publication-date column.",
"Available columns:",
paste(names(headlines_lr), collapse = ", ")
)
)
}
# Prepare clean headlines and dates
headlines_clean <- headlines_lr %>%
transmute(
media_name,
ideology,
headline = as.character(.data[[headline_col]]),
published = suppressWarnings(
parse_date_time(
as.character(.data[[date_col]]),
orders = c(
"ymd HMS", "ymd HM", "ymd",
"mdy HMS", "mdy HM", "mdy",
"dmy HMS", "dmy HM", "dmy"
)
)
)
) %>%
filter(
!is.na(headline),
headline != "",
!is.na(published)
) %>%
mutate(
published = as.Date(published),
month = floor_date(published, unit = "month")
)
# Show the number of headlines in each group
headlines_clean %>%
count(ideology, name = "number_of_headlines") %>%
knitr::kable(
caption = "Number of Headlines by Outlet Ideology"
)
| ideology | number_of_headlines |
|---|---|
| Left | 3787 |
| Right | 3357 |
A bigram consists of two consecutive words. For example, the phrase “Donald Trump” is a bigram. This analysis uses tokenization to divide each headline into bigrams, removes pairs containing common stop words, and counts the remaining pairs by outlet ideology. The ten most frequent bigrams in each group are then selected for comparison.Here’s the code:
# Tokenize the headlines into bigrams
bigrams <- headlines_clean %>%
unnest_tokens(
output = bigram,
input = headline,
token = "ngrams",
n = 2
) %>%
separate(
bigram,
into = c("word1", "word2"),
sep = " ",
remove = FALSE
) %>%
anti_join(stop_words, by = c("word1" = "word")) %>%
anti_join(stop_words, by = c("word2" = "word")) %>%
filter(
str_detect(bigram, "[a-z]"),
!str_detect(word1, "^\\d+$"),
!str_detect(word2, "^\\d+$")
)
# Count bigrams separately for left- and right-leaning outlets
bigram_counts <- bigrams %>%
count(ideology, bigram, sort = TRUE, name = "frequency")
# Select the ten most common bigrams for each ideology
top_bigrams <- bigram_counts %>%
group_by(ideology) %>%
slice_max(
order_by = frequency,
n = 10,
with_ties = FALSE
) %>%
ungroup()
# Display the most frequent bigrams
top_bigrams %>%
arrange(ideology, desc(frequency)) %>%
group_by(ideology) %>%
mutate(rank = row_number()) %>%
ungroup() %>%
select(ideology, rank, bigram, frequency) %>%
knitr::kable(
caption = "Ten Most Frequent Bigrams by Outlet Ideology"
)
| ideology | rank | bigram | frequency |
|---|---|---|---|
| Left | 1 | white house | 123 |
| Left | 2 | supreme court | 79 |
| Left | 3 | op ed | 68 |
| Left | 4 | donald trump | 66 |
| Left | 5 | iran war | 53 |
| Left | 6 | trump administration | 44 |
| Left | 7 | fox news | 40 |
| Left | 8 | natalie harp | 38 |
| Left | 9 | abdul el | 34 |
| Left | 10 | el sayed | 34 |
| Right | 1 | white house | 92 |
| Right | 2 | el sayed | 58 |
| Right | 3 | supreme court | 46 |
| Right | 4 | iran war | 42 |
| Right | 5 | donald trump | 40 |
| Right | 6 | executive news | 40 |
| Right | 7 | news summary | 40 |
| Right | 8 | trump admin | 34 |
| Right | 9 | president trump | 27 |
| Right | 10 | lake america | 25 |
This graph compares the most frequent bigrams in each group. The bars represent the number of times each phrase appeared in the analyzed headlines.Here’s the code:
top_bigrams %>%
mutate(
bigram = reorder_within(bigram, frequency, ideology)
) %>%
ggplot(
aes(
x = frequency,
y = bigram,
fill = ideology
)
) +
geom_col(show.legend = FALSE) +
facet_wrap(
~ ideology,
scales = "free_y"
) +
scale_y_reordered() +
scale_fill_manual(
values = c("Left" = "steelblue", "Right" = "firebrick")
) +
labs(
title = "Most Frequent Bigrams in Political News Headlines",
subtitle = "Comparison of left-leaning and right-leaning news outlets",
x = "Number of appearances",
y = "Bigram"
) +
theme_minimal(base_size = 12) +
theme(
plot.title = element_text(face = "bold"),
strip.text = element_text(face = "bold")
)
The following ridgeline plot examines when the top bigrams used by left-leaning outlets appeared in published headlines. Each ridge represents the distribution of headline publication dates associated with a particular phrase.Here’s the code:
left_top <- top_bigrams %>%
filter(ideology == "Left") %>%
select(bigram)
left_timeline <- bigrams %>%
semi_join(left_top, by = "bigram") %>%
filter(ideology == "Left") %>%
mutate(
date_num = as.numeric(published)
)
ggplot(
left_timeline,
aes(
x = date_num,
y = reorder(bigram, bigram, FUN = function(x) length(x))
)
) +
geom_density_ridges(
fill = "steelblue",
color = "white",
alpha = 0.85,
scale = 2,
rel_min_height = 0.01
) +
scale_x_continuous(
labels = function(x) {
format(as.Date(x, origin = "1970-01-01"), "%b %Y")
},
breaks = pretty(left_timeline$date_num, n = 8)
) +
labs(
title = "When Left-Leaning Headlines Used Their Top Bigrams",
subtitle = "Distribution of publication dates for the ten most frequent bigrams",
x = "Publication date",
y = "Bigram"
) +
theme_minimal(base_size = 11) +
theme(
plot.title = element_text(face = "bold"),
axis.text.x = element_text(angle = 45, hjust = 1)
)
This plot uses the same method as the left-leaning plot, allowing the timing of frequently used phrases to be compared across outlet groups. Here’s the code:
right_top <- top_bigrams %>%
filter(ideology == "Right") %>%
select(bigram)
right_timeline <- bigrams %>%
semi_join(right_top, by = "bigram") %>%
filter(ideology == "Right") %>%
mutate(
date_num = as.numeric(published)
)
ggplot(
right_timeline,
aes(
x = date_num,
y = reorder(bigram, bigram, FUN = function(x) length(x))
)
) +
geom_density_ridges(
fill = "firebrick",
color = "white",
alpha = 0.85,
scale = 2,
rel_min_height = 0.01
) +
scale_x_continuous(
labels = function(x) {
format(as.Date(x, origin = "1970-01-01"), "%b %Y")
},
breaks = pretty(right_timeline$date_num, n = 8)
) +
labs(
title = "When Right-Leaning Headlines Used Their Top Bigrams",
subtitle = "Distribution of publication dates for the ten most frequent bigrams",
x = "Publication date",
y = "Bigram"
) +
theme_minimal(base_size = 11) +
theme(
plot.title = element_text(face = "bold"),
axis.text.x = element_text(angle = 45, hjust = 1)
)
The comparison graph shows the most common two-word phrases in headlines from left-leaning and right-leaning outlets. The results can help identify similarities and differences in the language used by each group when reporting on Donald Trump. Phrases that appear frequently may reflect recurring political topics, public figures, policy debates or events receiving attention during the period covered by the dataset.
The ridgeline plots provide another perspective by showing the distribution of publication dates for the most frequent bigrams. Concentrations around particular dates can suggest periods when certain phrases received greater attention. Comparing the two groups may reveal whether they emphasized similar topics at the same time or focused on different subjects.
One limitation is that frequently appearing phrases do not automatically reveal the overall political position or accuracy of a news outlet. Headlines are short and may simplify complicated events. Removing common stop words also affects which phrases appear in the results. In addition, the number of headlines published by each outlet may influence the frequency counts.
I used AI to help adapt the n-gram analysis workflow to the headline dataset, organize the code into reusable steps, and create visualizations for comparing political news coverage. I also used the output to explore how word choices differed between outlet groups and how the timing of those phrases could provide context. The graphs should be interpreted alongside the original headlines rather than as definitive evidence of bias. Overall, this analysis demonstrates how text mining can help identify patterns in political news coverage while also showing the importance of considering the limits of automated analysis.