library(tidyverse)
library(lubridate)
library(slider)Empirical Analysis — Phase 2: The Transgressive Periphery
This document performs a temporal analysis of human-annotated hate speech using the Election_tweet_2021.csv dataset.
1. Load Libraries
2. Data Ingestion & Snowflake Decoding
We read the academic dataset (from Jafri et al., 2023). Column 1 contains the Tweet ID (read as a double to preserve the 64-bit integer), and Column 2 contains the Labels (0 = Non-Hate, 1 = Hate).
Methodology: Since Twitter developer terms prohibit sharing raw text/dates, we reverse-engineer the exact timestamp directly from the Tweet ID using the Snowflake Algorithm. The formula is: Timestamp (ms) = floor(Tweet_ID / 2^22) + Twitter Epoch (1288834974657).
social_raw <- read_csv("Election_tweet_2021.csv",
col_types = cols(`Tweet Id` = col_double(), Labels = col_integer()))
social_temporal <- social_raw %>%
rename(tweet_id = `Tweet Id`, label = Labels) %>%
mutate(
# Snowflake decoding to recover exact dates
timestamp_ms = floor(tweet_id / 4194304) + 1288834974657,
parsed_date = as.POSIXct(timestamp_ms / 1000, origin = "1970-01-01", tz = "UTC"),
parsed_date = as.Date(parsed_date)
)3. Temporal Windowing (UP Election 2021-2022)
We anchor the timeline to March 10, 2022 (Counting Day for the 2022 UP Assembly Elections) and establish a 90-day pre-election campaign window starting December 10, 2021, compared against the preceding baseline.
ELECTION_ANCHOR <- as.Date("2022-03-10")
social_clean <- social_temporal %>%
filter(parsed_date <= ELECTION_ANCHOR) %>%
mutate(
days_to_election = as.numeric(difftime(ELECTION_ANCHOR, parsed_date, units = "days")),
# 90-Day Pre-Election Campaign Window vs. Baseline (Starts Dec 10, 2021)
election_window = ifelse(days_to_election <= 90, "Election_Window (90d)", "Baseline")
)
print("=== TEMPORAL DISTRIBUTION OF TWEETS ===")[1] "=== TEMPORAL DISTRIBUTION OF TWEETS ==="
print(table(social_clean$election_window))
Baseline Election_Window (90d)
299 11158
4. Daily Aggregation & Rolling Averages
We aggregate the posts daily, fill missing days with zeroes to prevent line breaks in the visualization, and apply 7-day rolling means for smoothed tracking of hate speech volume.
daily_periphery <- social_clean %>%
group_by(parsed_date, election_window) %>%
summarise(
Total_Posts = n(),
Hate_Posts = sum(label == 1),
Non_Hate_Posts = sum(label == 0),
.groups = "drop"
) %>%
# Fill missing days with 0 to prevent line breaks in the ggplot
complete(
parsed_date = seq(min(parsed_date), max(parsed_date), by = "day"),
fill = list(Total_Posts = 0, Hate_Posts = 0, Non_Hate_Posts = 0)
) %>%
arrange(parsed_date) %>%
mutate(
# Re-derive election_window for filled rows
days_to_election = as.numeric(difftime(ELECTION_ANCHOR, parsed_date, units = "days")),
election_window = ifelse(days_to_election <= 90, "Election_Window (90d)", "Baseline"),
# Apply 7-Day Rolling Means for smoothed visualization
Roll_Hate = slide_dbl(Hate_Posts, mean, .before = 6, .after = 0),
Roll_Total = slide_dbl(Total_Posts, mean, .before = 6, .after = 0)
)5. Poisson GLM Regression
This model tests whether the 90-day campaign window predicts an explosion in hate speech. An offset controls for the fact that total tweeting volume also generally increases during campaigns. An Incidence Rate Ratio (IRR) significantly greater than 1 indicates that hate speech volume disproportionately surged in the digital periphery during the final 90 days.
print("=== REGRESSION RESULTS: FRANCHISED TRANSGRESSION ON SOCIAL MEDIA ===")[1] "=== REGRESSION RESULTS: FRANCHISED TRANSGRESSION ON SOCIAL MEDIA ==="
daily_periphery_reg <- daily_periphery %>% mutate(log_posts = log(Total_Posts + 1))
hate_model <- glm(Hate_Posts ~ election_window + offset(log_posts),
family = poisson, data = daily_periphery_reg)
coefs <- summary(hate_model)$coefficients
results <- tibble(
Metric = "Human-Annotated Hate Speech",
Estimate = round(coefs["election_windowElection_Window (90d)", "Estimate"], 4),
P_Value = round(coefs["election_windowElection_Window (90d)", "Pr(>|z|)"], 6),
IRR = round(exp(coefs["election_windowElection_Window (90d)", "Estimate"]), 3)
)
print(results)# A tibble: 1 × 4
Metric Estimate P_Value IRR
<chr> <dbl> <dbl> <dbl>
1 Human-Annotated Hate Speech -0.654 0.000004 0.52
6. Publication Plot — The Surge of the Periphery
final_phase2_plot <- ggplot(daily_periphery, aes(x = parsed_date)) +
# Smoothed Line & Ribbon
geom_line(aes(y = Roll_Hate), color = "#C0392B", linewidth = 1.1) +
geom_ribbon(aes(ymin = 0, ymax = Roll_Hate), fill = "#C0392B", alpha = 0.2) +
# 90-Day Campaign Window Indicator
geom_vline(xintercept = as.Date("2021-12-10"), linetype = "dashed", color = "black", linewidth = 0.7) +
annotate("rect", xmin = as.Date("2021-12-10"), xmax = ELECTION_ANCHOR, ymin = -Inf, ymax = Inf,
alpha = 0.05, fill = "red") +
# Text Annotation (Increased to size = 4 for better visibility)
annotate("text", x = as.Date("2021-12-12"),
y = max(daily_periphery$Roll_Hate, na.rm = TRUE) * 0.9,
label = "90-Day \nCampaign Window",
angle = 0, hjust = 0, color = "black", fontface = "bold",
size = 4) +
# Date Formatting
scale_x_date(date_breaks = "1 month", date_labels = "%b %Y") +
labs(
title = "The Surge of Peripheral Hate Speech",
subtitle = "7-day rolling mean of annotated divisive tweets (2021–2022 UP Election Cycle)",
x = "Timeline",
y = "Daily Hate Speech Volume (7-Day Mean)"
) +
# Base theme size
theme_minimal(base_size = 14) +
theme(
plot.title = element_text(face = "bold", size = 16),
plot.subtitle = element_text(size = 13),
axis.title = element_text(face = "bold", size = 13),
axis.text = element_text(size = 11),
plot.margin = margin(t = 10, r = 10, b = 10, l = 15)
)
print(final_phase2_plot)# Save the plot
ggsave("Phase2_Peripheral_Surge.png", plot = final_phase2_plot, width = 8, height = 5, dpi = 300)