The results show that left- and right-leaning news outlets covered some of the same topics related to Donald Trump. Headlines mentioning the White House, Iran war, and Trump administration appeared consistently over several weeks. However, differences became more noticeable when looking at headlines about specific individuals other than Donald Trump.

To conduct this analysis,  I broke the headlines into two-word phrases, known as bigrams, removed common stop words, and identified the 15 most frequently appearing phrases for each group.

The bar graph showed that “white house” was the most common bigram for both groups, appearing about 123 times in left-leaning headlines and about 92 times in right-leaning headlines. Other shared phrases included “supreme court” and “Iran war.”

The ridgeline plots showed how coverage changed over time. For example, “Natalie Harp” experienced a sharp spike in left-leaning headlines during August, while “El Sayed” appeared more consistently in right-leaning headlines. Another interesting finding was a spike in the phrase “9 11” around early September in both groups, possibly reflecting coverage surrounding the anniversary of the September 11 terrorist attacks. I used AI to help adapt the congressional text-mining scripts to this dataset, troubleshoot graphing issues, and help interpret the results. The graphs reveal differences in headline frequency and timing.

CODE

if (!require(tidyverse)) {
  install.packages("tidyverse")
  library(tidyverse)
}

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 source ideology lookup table ----

source_ideology <- tribble(
  ~media_name, ~ideology,
  
  # Left
  "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
  "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"
)

headlines_lr <- headlines %>%
  inner_join(
    source_ideology,
    by = "media_name"
  )
# Check the structure of the filtered dataset
glimpse(headlines_lr)

# Display the column names
names(headlines_lr)

# Count headlines by ideology
headlines_lr %>%
  count(ideology)

# Preview the first six rows
head(headlines_lr)
# STEP 2: Install and load packages

packages <- c(
  "tidytext",
  "tidylo",
  "ggplot2",
  "dplyr",
  "tidyr",
  "stringr",
  "lubridate",
  "ggridges"
)

for (pkg in packages) {
  if (!requireNamespace(pkg, quietly = TRUE)) {
    install.packages(pkg)
  }
  library(pkg, character.only = TRUE)
}

# STEP 3: Create bigrams from headlines

n_size <- 2

bigrams <- headlines_lr %>%
  mutate(
    WeekStart = floor_date(
      as.Date(publish_date),
      unit = "week"
    )
  ) %>%
  unnest_tokens(
    bigram,
    title,
    token = "ngrams",
    n = n_size
  )

# STEP 4: Remove low-information phrases

max_stopword_pct <- 50

bigrams_clean <- bigrams %>%
  mutate(
    words = str_split(bigram, " ")
  ) %>%
  rowwise() %>%
  mutate(
    total_words = length(words),
    stop_words_in_phrase =
      sum(words %in% stop_words$word),
    stop_word_pct =
      stop_words_in_phrase / total_words * 100
  ) %>%
  ungroup() %>%
  filter(
    stop_word_pct < max_stopword_pct
  ) %>%
  select(-words, -total_words,
         -stop_words_in_phrase, -stop_word_pct)

# STEP 5: Count bigrams by ideology

bigram_counts <- bigrams_clean %>%
  count(
    ideology,
    bigram,
    sort = TRUE
  )

# STEP 6: Find the top 15 bigrams
# for each political orientation

top_bigrams <- bigram_counts %>%
  group_by(ideology) %>%
  slice_max(
    n,
    n = 15,
    with_ties = FALSE
  ) %>%
  ungroup()

# View the results

print(top_bigrams, n = 30)
# STEP 7: Create the bigram comparison graph

library(tidytext)

bigram_graph <- top_bigrams %>%
  mutate(
    bigram = reorder_within(bigram, n, ideology)
  ) %>%
  ggplot(aes(x = n, y = bigram, fill = ideology)) +
  geom_col() +
  facet_wrap(~ideology, ncol = 1, scales = "free_y") +
  scale_y_reordered() +
  scale_fill_manual(
    values = c("Left" = "steelblue",
               "Right" = "firebrick")
  ) +
  labs(
    title = "Top 15 Bigrams in Trump-Related News Headlines",
    subtitle = "Left-Leaning vs. Right-Leaning News Outlets",
    x = "Number of Appearances",
    y = "Bigram",
    fill = "Ideology"
  ) +
  theme_minimal()

print(bigram_graph)
# STEP 8: Count top bigrams by week

weekly_bigram_counts <- bigrams_clean %>%
  semi_join(
    top_bigrams,
    by = c("ideology", "bigram")
  ) %>%
  count(
    ideology,
    bigram,
    WeekStart,
    name = "weekly_count"
  )

# Create a complete list of weeks
all_weeks <- seq(
  min(bigrams_clean$WeekStart, na.rm = TRUE),
  max(bigrams_clean$WeekStart, na.rm = TRUE),
  by = "week"
)

# Fill missing weeks with zero

weekly_bigram_counts <- weekly_bigram_counts %>%
  group_by(ideology, bigram) %>%
  complete(
    WeekStart = all_weeks,
    fill = list(weekly_count = 0)
  ) %>%
  ungroup()
# STEP 9: Left-leaning outlet ridgeline plot

library(ggridges)

left_weekly <- weekly_bigram_counts %>%
  filter(ideology == "Left") %>%
  left_join(
    top_bigrams %>%
      filter(ideology == "Left") %>%
      select(bigram, n),
    by = "bigram"
  ) %>%
  mutate(
    bigram = forcats::fct_reorder(bigram, n)
  )

left_ridgeline <- ggplot(
  left_weekly,
  aes(
    x = WeekStart,
    y = weekly_count,
    height = weekly_count,
    group = bigram
  )
) +
  geom_ridgeline(
    fill = "steelblue",
    color = "steelblue",
    alpha = 0.6,
    scale = 0.9
  ) +
  facet_wrap(
    ~bigram,
    ncol = 1,
    scales = "free_y"
  ) +
  scale_x_date(date_labels = "%b %d") +
  labs(
    title = "Top Bigrams Over Time: Left-Leaning News Outlets",
    x = "Week",
    y = "Weekly Frequency"
  ) +
  theme_minimal()

print(left_ridgeline)
# STEP 10: Right-leaning outlet ridgeline plot

right_weekly <- weekly_bigram_counts %>%
  filter(ideology == "Right") %>%
  left_join(
    top_bigrams %>%
      filter(ideology == "Right") %>%
      select(bigram, n),
    by = "bigram"
  ) %>%
  mutate(
    bigram = forcats::fct_reorder(bigram, n)
  )

right_ridgeline <- ggplot(
  right_weekly,
  aes(
    x = WeekStart,
    y = weekly_count,
    height = weekly_count,
    group = bigram
  )
) +
  geom_ridgeline(
    fill = "firebrick",
    color = "firebrick",
    alpha = 0.6,
    scale = 0.9
  ) +
  facet_wrap(
    ~bigram,
    ncol = 1,
    scales = "free_y"
  ) +
  scale_x_date(date_labels = "%b %d") +
  labs(
    title = "Top Bigrams Over Time: Right-Leaning News Outlets",
    x = "Week",
    y = "Weekly Frequency"
  ) +
  theme_minimal()

print(right_ridgeline)
# STEP 11: Fix left-leaning ridgeline plot

left_ridgeline <- ggplot(
  left_weekly,
  aes(
    x = WeekStart,
    y = bigram,
    height = weekly_count,
    group = bigram
  )
) +
  geom_ridgeline(
    fill = "steelblue",
    color = "steelblue",
    alpha = 0.6,
    scale = 0.05
  ) +
  scale_x_date(date_labels = "%b %d") +
  labs(
    title = "Top Bigrams Over Time: Left-Leaning News Outlets",
    x = "Week",
    y = "Bigram"
  ) +
  theme_minimal()

print(left_ridgeline)


# STEP 12: Fix right-leaning ridgeline plot

right_ridgeline <- ggplot(
  right_weekly,
  aes(
    x = WeekStart,
    y = bigram,
    height = weekly_count,
    group = bigram
  )
) +
  geom_ridgeline(
    fill = "firebrick",
    color = "firebrick",
    alpha = 0.6,
    scale = 0.05
  ) +
  scale_x_date(date_labels = "%b %d") +
  labs(
    title = "Top Bigrams Over Time: Right-Leaning News Outlets",
    x = "Week",
    y = "Bigram"
  ) +
  theme_minimal()

print(right_ridgeline)