Rationale

Framing theory explains how the media influences public understanding of an issue by emphasizing certain details, perspectives, and social circumstances over others. In crime reporting, news organizations may include information about an individual’s race, family background, community, neighborhood, education and socioeconomic circumstances. The inclusion or omission of these details can shape how audiences interpret individuals and the events surrounding a crime.

This project uses framing theory to compare how U.S. news coverage presents social-context information in stories involving Black and White individuals. Specifically, the analysis compares Black Social-Context coverage white White Social-Context coverage. Using MediaCloud.org’s U.S. news archive, the project examines stories that connect racial identifiers with crime-related terms and social-context keywords, including family, community, poverty, inequality, discrimination, education, and mental health.

By comparing the weekly volume of stories matching each search query, this project examines whether news coverage gives greater prominence to social-context information associated with Black individuals or White individuals. The findings may help identify differences in how frequently these social-context frames appear in crime reporting and provide insight into how race and background are represented in U.S. news coverage.

Hypothesis

The average weekly coverage volume of Black Social-Context coverage will differ from the average weekly volume of White Social-Context coverage in U.S. news reporting.

Variables & method

This project used framing theory to compare Black Social-Context and White-Social Context frames in U.S. crime coverage. The independent variable was racial category, measured as either Black or White. The dependent variable was weekly coverage volume, measured by the number of stories matching each MediaCloud.org search query per week.

The queries combined racial identifiers (Black or African American;White or Caucasian) with crime-related terms and social-context keywords, including family, community, poverty, inequality, discrimination, education and mental health. The analysis examined U.S. news stories collected between April 5, 2025 and October 17, 2026.

Descriptive statistics, including total stories, weekly minimums, maximums, medians, and means were used to compare coverage of the two frames. A time-series graph was also created to show how coverage changed over time and which frame received greater prominence in news reporting.

Results & discussion

The results show a noticeable difference in U.S. news coverage between the Black Social-Context Frame and the White Social-Context Frame from April5, 2025 through October 17, 2026. The White Social-Context Frame received 137,000 total stories, compared to 48,437 stories for the Black Social-Context Frame.

The weekly averages also showed a significant difference. The White frame average 1,702.47 stories per week, while the Black frame averaged 597.99. As shown in the line graph, the White frame consistently received more coverage throughout the time period, even though both frames experienced weekly increases and decreases.

Overall, these findings suggest that social context involving White individuals received considerably more news coverage than social context involving Black individuals, supporting the hypothesis. However, the results measure the amount of coverage rather than how positively or negatively each group was portrayed.

Media Cloud Topic Summary Statistics
topic Total_Stories Weekly_Minimum Weekly_Maximum Weekly_Median Weekly_Mean Weekly_SD
White Social Context Frame 137901 135 3135 1661 1702.48 375.28
Black Social Context Frame 48438 36 866 588 598.00 114.01

Paired-Samples t-Test

The analysis used a paired-samples t-test to assess whether the average difference between weekly Black Social Context Frame (V1) AND White Social Context Frame (V2) story counts was statistically different from zero. The analysis included 81 paired observations. This box plot summarizes the distributions of weekly U.S. news coverage involving Black Social Context Framing (V1) and White Social Context Framing (V2).

The Pair Differences

According to the results, stories involving the White Social Context Frame outnumbered stories involving the Black Social Context Frame by an average of 1,104.48 stories per week, with a standard deviation of 322.46 stories. The average weekly coverage for the Black Social Context Frame was 597.99 stories, compared to 1,702.47 stories for the White Social Context Frame.

The box plots also show a noticeable difference between the distributions. The median weekly count for Black Social Context Framing was 588 stories, while the median for White Social Context Framing was 1,661 stories. This indicates that White Social Context Framing consistently appeared more frequently throughout the study period.

The paired differences showed a non-normal distribution (p < .001), according to a Shapiro-Wilk normality test. Since the p-value was below 0.05, this indicates that the differences between the weekly Black and White Social Context Frame story counts were not normally distributed.

Descriptive Statistics: Pair Differences
count mean sd min max
81.000 1,104.481 322.453 99.000 2,550.000
Normality Test (Shapiro-Wilk)
statistic p.value method
0.9101 0.0000 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 t-test results

The paired-samples t-test found that the average difference between Black Social Context Framing and White Social Context Framing was statistically significantly different from zero.

The t value was 30.83, with an associated p-value of less than .001. The results indicate a statistically significant difference between the two types of social-context framing. White Social Context Framing appeared substantially more frequently in weekly U.S. news coverage then Black Social Context Framing.

Paired-Samples t-Test
statistic parameter p.value conf.low conf.high method
30.8272 80 0.0000 1,033.1812 1,175.7818 Paired t-test
Group Means and SDs (t-Test)
V1_Mean V2_Mean V1_SD V2_SD
598.000 1,702.481 114.012 375.283

Discussion

These findings suggest that U.S. news coverage does not give equal attention to Black and White social-context framing when reporting on crime-related topics. White Social Context Framing appeared more frequently than Black Social Context Framing through the study period. One possible explanation is that news organizations may differ in how frequently they connect crime-related stories involving Black and White individuals to broader social circumstances, such as family, community, poverty, inequality, discrimination, and education.

These differences raises questions about how news organizations present race and social circumstances when reporting on crime. The way media stories emphasize certain details can influence how audiences understand these issues.

Overall, the results show a noticeable difference in the frequency of the two framing categories. Further examination of the actual news stories could help determine whether differences in coverage also reflect differences in how race, social circumstances, and crime are presented to audiences.

Code

Here is the code that produced the plot

############################################################
# STEP 1: Install and Load Required Packages
############################################################

required_packages <- c(
  "tidyverse",
  "httr2",
  "lubridate",
  "plotly",
  "knitr",
  "kableExtra"
)

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

library(tidyverse)
library(httr2)
library(lubridate)
library(plotly)
library(knitr)
library(kableExtra)

############################################################
# STEP 2: Verify Media Cloud API Key
############################################################

if (!nzchar(Sys.getenv("MEDIACLOUD_KEY"))) {
  stop(
    "No Media Cloud API key was found.\n",
    "Run the setup section once,\n",
    "then restart R and try again."
  )
}

mc_key <- Sys.getenv("MEDIACLOUD_KEY")

############################################################
# STEP 3: Define the Date Range
############################################################

start_date <- "2025-04-05"
end_date <- "2026-10-17"

############################################################
# STEP 4: Define Search Queries
############################################################

black_social_context_query <-
  '((Black OR African-American) AND
  (friends OR family OR background OR relationships OR bullying) AND
   (crime OR criminal OR suspect OR defendant OR arrest* OR charge* OR shooting OR homicide)
   AND
   (family OR community OR neighborhood OR poverty OR inequality OR discrimination OR education OR school OR upbringing OR "mental health" OR trauma OR social))'

white_social_context_query <-
  '((White OR Caucasian)
  AND
    (crime OR criminal OR suspect OR defendant OR arrest* OR charge* OR shooting OR homicide)
    AND
     (family OR community OR neighborhood OR poverty OR inequality OR discrimination OR education OR school OR upbringing OR "mental health" OR trauma OR social))'


############################################################
# STEP 5: Create Topic Definitions
############################################################

topics <- c(
  "Black Social Context Frame" = black_social_context_query,
  "White Social Context Frame" = white_social_context_query
)

############################################################
# STEP 6: Create a Function to Retrieve Daily
# Story Counts
############################################################

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 |>
    resp_body_json()
  
  map_dfr(
    results$count_over_time$counts,
    as_tibble
  ) |>
    mutate(
      date = as.Date(date)
    )
}

############################################################
# STEP 7: Download Daily Counts for All Topics
############################################################

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
        )
    }
  )

############################################################
# STEP 8: Examine the Downloaded Daily Data
############################################################

head(topic_data)

glimpse(topic_data)

############################################################
# STEP 9: Convert Daily Counts to Weekly Counts
############################################################

weekly_counts <-
  topic_data |>
  mutate(
    week = floor_date(
      date,
      unit = "week"
    )
  ) |>
  group_by(
    topic,
    week
  ) |>
  summarise(
    stories = sum(count),
    .groups = "drop"
  )

############################################################
# STEP 10: Examine Weekly Counts
############################################################

weekly_counts

############################################################
# STEP 11: Summarize Coverage by Topic
############################################################

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

############################################################
# STEP 12: Create an Interactive Plotly Graph
############################################################

weekly_counts <- weekly_counts |>
  mutate(
    stories_formatted = formatC(
      stories,
      format = "d",
      big.mark = ","
    )
  )

Graph <- plot_ly(
  data = weekly_counts,
  
  x = ~week,
  y = ~stories,
  
  color = ~topic,
  colors = "Set1",
  
  type = "scatter",
  mode = "lines+markers",
  
  customdata = ~topic,
  
  text = ~stories_formatted,
  
  hovertemplate =
    paste(
      "%{customdata}
",
      "Week: %{x|%Y-%m-%d}
",
      "Stories: %{text}
",
      ""
    )
) |>
  layout(
    title =
      "Weekly U.S. Coverage Volume",
    
    hovermode = "x unified",
    
    xaxis = list(
      title = "Week"
    ),
    
    yaxis = list(
      title = "Weekly Story Count",
      separatethousands = TRUE
    )
  )

Graph

############################################################ 
# Reshape the data
############################################################ 

weekly_counts_wide <-
  weekly_counts %>%
  mutate(
    topic = make.names(topic)
  ) %>%
  select(week, topic, stories) %>%
  pivot_wider(
    names_from  = topic,
    values_from = stories
  )

# ============================================================
#  Paired-samples t-test code
#
#  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$Black.Social.Context.Frame
mydata$V2 <- mydata$White.Social.Context.Frame

# ============================================================
#  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 = "Boxplot of Repeated Measures (V1 vs V2) with Means",
    xaxis = list(title = "Measure"),
    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