This analysis examined the most common bigrams, or two-word phrases, found in news headlines mentioning Donald Trump and compared their use between left-leaning (Democratic) and right-leaning (Republican) news outlets. Common stop words and several custom phrases listed in Step 12 were removed to improve the quality of the analysis and focus the results on more meaningful topics. Because the headlines in the dataset already referenced Trump, Trump-related phrases were also excluded to focus the results on broader topics, events, and individuals rather than repeated references to the President himself. I also removed “abdul el” because it referred to the same individual as “el sayed,” keeping only the more frequently occurring phrase. 

The results showed that the two groups were often discussing many of the same topics. While the rankings differed, there was substantial overlap between the most common phrases used by both. Common phrases such as “white house,” “supreme court,” “iran war,” and “9 11” appeared among the most frequent bigrams in both groups, suggesting that both sides were covering many of the same major political stories during the period represented in the dataset. 

Beyond those similarities, several differences were noticeable. One phrase that stood out among left-leaning outlets was “Natalie Harp,” likely reflecting coverage of comments by Senator Jon Ossoff that received significant media attention during a 2026 midterm rally. On the right-leaning side, phrases such as “senate race,” “senate primary,” and “midterm convention” appeared more frequently, suggesting a stronger emphasis on election-related coverage overall. 

The ridgeline plots showed how the frequency of top bigrams changed over time. Because the dataset only covered August through October 2026, many phrases appeared for only a short period before declining. For example, the bigram “9 11” showed noticeable activity around September but then dropped to zero, suggesting that coverage decreased once the anniversary had passed. Similar patterns appeared with several other bigrams, which experienced brief spikes before returning little or no coverage. 

AI was used throughout this project to help adapt the provided R script to the headline dataset, create the bigram analysis, generate the frequency and ridgeline visualizations, troubleshoot coding errors, and improve the script’s overall organization. The generated code and results were reviewed, modified, and tested before being included in the final report to ensure they accurately reflected the given data. 

CODE:

############################################################
# A Bigram Analysis between left-leaning outlets v.s
# right-leaning outlets of Trump Headlines
############################################################

# Step 1: Install and Load Packages ----

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

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

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

# Step 2: Import the Data ----

headlines <- readRDS(
  "Trump_story_headlines.rds"
)

# Step 3: 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"
)

# Step 4: Create Left- and Right-Leaning Headlines Dataset ----

headlines_lr <- headlines %>%
  inner_join(
    source_ideology,
    by = "media_name"
  )

# Step 5: Create Bigrams ----

bigrams <- headlines_lr %>%
  unnest_tokens(
    bigram,
    title,
    token = "ngrams",
    n = 2
  )

# Step 6: Separate Bigram Words ----

bigrams_separated <- bigrams %>%
  separate(
    bigram,
    into = c("word1", "word2"),
    sep = " "
  )

# Step 7: Load Stop Words ----

data("stop_words")

# Step 8: Remove Stop Words ----

bigrams_filtered <- bigrams_separated %>%
  filter(
    !word1 %in% stop_words$word,
    !word2 %in% stop_words$word
  )

# Step 9: Count Bigrams by Ideology ----

bigram_counts <- bigrams_filtered %>%
  unite(
    bigram,
    word1,
    word2,
    sep = " "
  ) %>%
  count(
    ideology,
    bigram,
    sort = TRUE
  )

# Step 10: Examine Most Common Bigrams ----

head(
  bigram_counts,
  20
)
#Step 11: Create Top Bigram Dataset ----
#Select the 15 most common bigrams from each ideological group.

top_bigrams <- bigram_counts %>%
  group_by(ideology) %>%
  slice_max(
    order_by = n,
    n = 15
  ) %>%
  ungroup()
#View(top_bigrams)

# Step 12: Define Custom Words/Phrases to Remove ----

custom_bigrams <- tibble(
  bigram = c(
    "op ed",
    "abdul el",
    "executive news",
    "guardian view",
    "news summary",
    "donald trump",
    "trump admin",
    "trump administration",
    "fox news",
    "watch live",
    "president trump",
    "trump signs",
    "short cuts",
    "dem rep",
    "politics live",
    "morning mail"
  )
)
# Step 13: Remove Custom Bigrams ----

bigrams_final <- bigram_counts %>%
  anti_join(
    custom_bigrams,
    by = "bigram"
  )
# Step 14: Examine Final Bigram Counts ----

top_bigrams <- bigrams_final %>%
  group_by(ideology) %>%
  slice_max(
    order_by = n,
    n = 15
  ) %>%
  ungroup()

#View(top_bigrams)

# Step 15: Create Bigram Comparison Dataset ----

top_bigrams <- bigrams_final %>%
  group_by(ideology) %>%
  slice_max(
    order_by = n,
    n = 15
  ) %>%
  ungroup()

# Step 16: Create Left vs. Right Bigram Graph ----

bigram_graph <- top_bigrams %>%
  mutate(
    bigram = reorder_within(
      bigram,
      n,
      ideology
    )
  ) %>%
  ggplot(
    aes(
      x = n,
      y = bigram,
      fill = ideology
    )
  ) +
  geom_col(
    show.legend = FALSE
  ) +
  scale_y_reordered() +
  facet_wrap(
    ~ ideology,
    scales = "free"
  ) +
  labs(
    title = "Most Common Bigrams in Left- and Right-Leaning Trump Headlines",
    x = "Frequency",
    y = "Bigram"
  ) +
  theme_minimal()

bigram_graph

#Step 17: Create Bigram Dataset for Left-Leaning Outlets ----

left_bigrams <- bigrams_filtered %>%
  unite(
    bigram,
    word1,
    word2,
    sep = " "
  ) %>%
  filter(
    ideology == "Left"
  )
#Step 18: Identify Top Left-Leaning Bigrams ----

top_left_bigrams <- left_bigrams %>%
  count(
    bigram,
    sort = TRUE
  ) %>%
  anti_join(
    custom_bigrams,
    by = "bigram"
  ) %>%
  slice_max(
    n,
    n = 15
  )

#Step 19: Create Left-Leaning Ridgeline Dataset ----

left_ridge_data <- left_bigrams %>%
  semi_join(
    top_left_bigrams,
    by = "bigram"
  ) %>%
  mutate(
    month = lubridate::floor_date(
      publish_date,
      unit = "month"
    )
  ) %>%
  count(
    month,
    bigram
  )

#Step 20: Create Left-Leaning Ridgeline Plot ----

left_ridgeline_plot <- ggplot(
  left_ridge_data,
  aes(
    x = month,
    y = reorder(bigram, n, max),
    height = n,
    group = bigram
  )
) +
  geom_density_ridges(
    stat = "identity",
    fill = "blue",
    alpha = .7
  ) +
  labs(
    title = "Frequency of Top Left-Leaning Headline Bigrams Over Time",
    x = "Month",
    y = "Bigram"
  ) +
  theme_minimal()

left_ridgeline_plot

# Step 21:  Create Bigram Dataset for Right-Leaning Outlets ----

right_bigrams <- bigrams_filtered %>%
  unite(
    bigram,
    word1,
    word2,
    sep = " "
  ) %>%
  filter(
    ideology == "Right"
  )

# Step 22: Identify Top Right-Leaning Bigrams ----

top_right_bigrams <- right_bigrams %>%
  count(
    bigram,
    sort = TRUE
  ) %>%
  anti_join(
    custom_bigrams,
    by = "bigram"
  ) %>%
  slice_max(
    n,
    n = 15
  )

# Step 23: Create Right-Leaning Ridgeline Dataset ----

right_ridge_data <- right_bigrams %>%
  semi_join(
    top_right_bigrams,
    by = "bigram"
  ) %>%
  mutate(
    month = lubridate::floor_date(
      publish_date,
      unit = "month"
    )
  ) %>%
  count(
    month,
    bigram
  )
# Step 24: Create Right-Leaning Ridgeline Plot ----

right_ridgeline_plot <- ggplot(
  right_ridge_data,
  aes(
    x = month,
    y = reorder(bigram, n, max),
    height = n,
    group = bigram
  )
) +
  geom_density_ridges(
    stat = "identity",
    fill = "red",
    alpha = .7
  ) +
  labs(
    title = "Frequency of Top Right-Leaning Headline Bigrams Over Time",
    x = "Month",
    y = "Bigram"
  ) +
  theme_minimal()

right_ridgeline_plot

## Step 25: Display Final Visualizations ----

bigram_graph

left_ridgeline_plot

right_ridgeline_plot