PhD Empirical Analysis: Multi-Dimensional Content Analysis of Executive Speeches

Testing the ‘Bifurcation of Authenticity’ (2021–2022 UP Assembly Election Cycle)

This document performs a multi-dimensional content analysis of executive speeches using the modi_speeches.csv dataset.

1. Load Libraries

library(tidyverse)
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.2.1     ✔ readr     2.2.0
✔ forcats   1.0.1     ✔ stringr   1.6.0
✔ ggplot2   4.0.3     ✔ tibble    3.3.1
✔ lubridate 1.9.5     ✔ tidyr     1.3.2
✔ purrr     1.2.2     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(quanteda)
Package version: 4.5.0
Unicode version: 14.0
ICU version: 71.1
Parallel computing: disabled
See https://quanteda.io for tutorials and examples.
library(quanteda.textstats)
library(lubridate)
library(stringr)
library(stm)
stm v1.3.8 successfully loaded. See ?stm for help. 
 Papers, resources, and other materials at structuraltopicmodel.com

2. Data Ingestion

speeches_raw <- read_csv("modi_speeches.csv")
Rows: 988 Columns: 7
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (6): url, title, article_text, images, publish_info, tags
dbl (1): id

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.

3. Date Cleaning & Temporal Windowing (2021–2022 UP Assembly Cycle)

We parse the embedded date strings and align the temporal window to start exactly on November 1, 2021, creating a 130-day pre-election campaign window anchored to the March 10, 2022 counting day.

speeches_clean <- speeches_raw %>%
  filter(!is.na(article_text), !is.na(publish_info)) %>%
  mutate(raw_date_str = str_extract(publish_info, "(?<=\\| ).*(?= \\|)")) %>%
  mutate(parsed_date = parse_date_time(str_trim(raw_date_str), orders = c("B d, Y"))) %>%
  filter(!is.na(parsed_date)) %>%
  mutate(parsed_date = as.Date(parsed_date)) %>%
  mutate(election_date_anchor = as.Date("2022-03-10")) %>%
  mutate(days_to_election = as.numeric(difftime(election_date_anchor, parsed_date, units = "days"))) %>%
  
  # Align temporal window to start exactly on November 1, 2021 (approx. 130 days)
  filter(days_to_election >= 0 & days_to_election <= 365) %>%
  
  # 130-Day Pre-Election Campaign Window (Starts Nov 1, 2021)
  mutate(election_window = ifelse(days_to_election <= 130, "Election_Window (130d)", "Baseline"))

# Safety Check
if (nrow(speeches_clean) == 0) {
  stop("Error: No speeches found in the timeframe. Check dataset dates.")
}

4. Multi-Dimensional Categorical Dictionary Construction

The rhetorical architecture is divided into Pastoral sub-dimensions (Provision, Development, Empowerment, Care/Seva) and Transgressive sub-dimensions (Corrupt Elite, Dynastic Elite, Appeasement/VoteBank, Threat/Security).

speech_corpus <- corpus(speeches_clean, text_field = "article_text")

bifurcation_dict_multidim <- dictionary(list(
  # --- PASTORAL SUB-DIMENSIONS ---
  P1_Provision = c(
    "welfare", "schemes", "beneficiaries", "housing", "electricity", "water", 
    "health", "food", "garib kalyan", "garib", "ration", "ayushman", 
    "sanitation", "toilet", "tap water", "pm awas", "kisan samman", "गरीब", "राशन"
  ),
  P2_Development = c(
    "viksit bharat", "development", "growth", "infrastructure", "progress", 
    "atmanirbhar", "self-reliance", "modernisation", "modernization", 
    "reform", "reforms", "transformation", "innovation", "investment", 
    "manufacturing", "roads", "railways", "airports", "vikas", "विकास", "आत्मनिर्भर"
  ),
  P3_Empowerment = c(
    "nari shakti", "women empowerment", "mahila", "beti", "youth", "yuva", 
    "empowerment", "empower", "entrepreneurship", "startups", "aspirations", 
    "opportunity", "skills", "jobs", "employment", "युवा", "महिला"
  ),
  P4_Care_Seva = c(
    "seva", "sevak", "pradhan sevak", "seva bhav", "sabka saath", "sabka vikas", 
    "sabka vishwas", "sabka prayas", "jan bhagidari", "people's participation", 
    "guarantee", "trust", "dedication", "सेवा", "सबका साथ"
  ),
  
  # --- TRANSGRESSIVE SUB-DIMENSIONS ---
  T1_Corrupt_Elite = c(
    "corruption", "congress", "bhrashtachar", "scam", "scams", "ghotala", "loot", "looted", 
    "black money", "kala dhan", "middlemen", "dalal", "commission", "syndicate", 
    "cut money", "misrule", "irregularities", "extortion", "भ्रष्टाचार", "दलाल", "लूट"
  ),
  T2_Dynastic_Elite = c(
    "parivarvad", "parivarvaad", "parivarwadi", "vanshvad", "vanshvaad", 
    "dynasty", "dynastic", "family politics", "khandan", "royal family", 
    "dynasts", "hereditary politics", "feudal mindset", "परिवारवाद", "वंशवाद"
  ),
  T3_Appeasement_VoteBank = c(
    "appeasement", "tushtikaran", "vote bank", "vote bank politics", 
    "minority appeasement", "discriminatory politics", "communal card",
    "तुष्टिकरण", "वोट बैंक", "हिंदू", "मुसलमान", "समुदाय"
  ),
  T4_Threat_Security = c(
    "infiltrator", "infiltrators", "ghuspeth", "ghuspethiye", "illegal immigrants", 
    "anti-national", "deshdrohi", "rashtradrohi", "tukde tukde", "urban naxal", 
    "separatism", "terrorism", "terrorists", "rioters", "anarchy", "gaddar",
    "जिहादी", "गद्दार", "आतंकवाद", "घुसपैठिए"
  )
))

5. DFM Generation and Lookup

This step tokenizes the corpus, applies the dictionary lookup, and defensively ensures all 8 sub-dimension columns exist in the final dataframe before calculating the composite Pastoral and Transgressive scores.

toks <- tokens(speech_corpus, 
               remove_punct = TRUE, 
               remove_numbers = TRUE, 
               remove_symbols = TRUE, 
               remove_url = TRUE) %>%
  tokens_tolower() %>%
  tokens_remove(stopwords("en"))

dfm_speeches <- dfm(toks)
dfm_lookup_results <- dfm_lookup(dfm_speeches, dictionary = bifurcation_dict_multidim)

# Convert to Dataframe
multidim_df <- convert(dfm_lookup_results, to = "data.frame") %>%
  bind_cols(docvars(speech_corpus))

# Ensure all 8 sub-dimension columns exist, filling with 0 if absent
expected_cols <- c(
  "P1_Provision", "P2_Development", "P3_Empowerment", "P4_Care_Seva",
  "T1_Corrupt_Elite", "T2_Dynastic_Elite", "T3_Appeasement_VoteBank", "T4_Threat_Security"
)

for (col in expected_cols) {
  if (!col %in% names(multidim_df)) {
    multidim_df[[col]] <- 0
  }
}

# Add Composite Scores Safely
multidim_df <- multidim_df %>%
  mutate(
    Total_Pastoral = P1_Provision + P2_Development + P3_Empowerment + P4_Care_Seva,
    Total_Transgressive = T1_Corrupt_Elite + T2_Dynastic_Elite + T3_Appeasement_VoteBank + T4_Threat_Security,
    parsed_date = as.Date(parsed_date)
  )

6. Detailed Sub-Dimensional Summary Table

subdim_summary <- multidim_df %>%
  group_by(election_window) %>%
  summarise(
    N_Speeches = n(),
    # Pastoral Means
    Mean_P1_Provision = round(mean(P1_Provision), 2),
    Mean_P2_Development = round(mean(P2_Development), 2),
    Mean_P3_Empowerment = round(mean(P3_Empowerment), 2),
    Mean_P4_CareSeva = round(mean(P4_Care_Seva), 2),
    Mean_Total_Pastoral = round(mean(Total_Pastoral), 2),
    # Transgressive Means
    Mean_T1_CorruptElite = round(mean(T1_Corrupt_Elite), 2),
    Mean_T2_DynasticElite = round(mean(T2_Dynastic_Elite), 2),
    Mean_T3_Appeasement = round(mean(T3_Appeasement_VoteBank), 2),
    Mean_T4_ThreatSecurity = round(mean(T4_Threat_Security), 2),
    Mean_Total_Transgressive = round(mean(Total_Transgressive), 2)
  )

print("=== SUB-DIMENSIONAL RHETORICAL BREAKDOWN (2021-2022) ===")
[1] "=== SUB-DIMENSIONAL RHETORICAL BREAKDOWN (2021-2022) ==="
print(t(subdim_summary))
                         [,1]       [,2]                    
election_window          "Baseline" "Election_Window (130d)"
N_Speeches               "128"      " 82"                   
Mean_P1_Provision        "11.2"     " 7.7"                  
Mean_P2_Development      "12.39"    "12.65"                 
Mean_P3_Empowerment      "3.77"     "5.27"                  
Mean_P4_CareSeva         "0.84"     "0.94"                  
Mean_Total_Pastoral      "28.20"    "26.55"                 
Mean_T1_CorruptElite     "0.45"     "0.78"                  
Mean_T2_DynasticElite    "0.04"     "0.09"                  
Mean_T3_Appeasement      "0.16"     "0.00"                  
Mean_T4_ThreatSecurity   "0.14"     "0.07"                  
Mean_Total_Transgressive "0.79"     "0.94"                  

7. Multivariate Statistical Testing (Sub-Dimension Regressions)

print("=== REGRESSION RESULTS: SUB-DIMENSIONAL EFFECTS ===")
[1] "=== REGRESSION RESULTS: SUB-DIMENSIONAL EFFECTS ==="
models <- list(
  P1_Provision_Mod = glm(P1_Provision ~ election_window, family = poisson, data = multidim_df),
  P2_Dev_Mod       = glm(P2_Development ~ election_window, family = poisson, data = multidim_df),
  P4_Seva_Mod      = glm(P4_Care_Seva ~ election_window, family = poisson, data = multidim_df),
  T1_Corrupt_Mod   = glm(T1_Corrupt_Elite ~ election_window, family = poisson, data = multidim_df),
  T2_Dynasty_Mod   = glm(T2_Dynastic_Elite ~ election_window, family = poisson, data = multidim_df),
  T3_Appease_Mod   = glm(T3_Appeasement_VoteBank ~ election_window, family = poisson, data = multidim_df),
  T4_Threat_Mod    = glm(T4_Threat_Security ~ election_window, family = poisson, data = multidim_df)
)

model_results <- map_df(names(models), function(name) {
  coefs <- summary(models[[name]])$coefficients
  tibble(
    Dimension = name,
    # Updated to 130d to match the new variable name generated by glm
    Estimate_ElectionWindow = round(coefs["election_windowElection_Window (130d)", "Estimate"], 3),
    Std_Error = round(coefs["election_windowElection_Window (130d)", "Std. Error"], 3),
    P_Value = round(coefs["election_windowElection_Window (130d)", "Pr(>|z|)"], 4)
  )
})

print(model_results)
# A tibble: 7 × 4
  Dimension        Estimate_ElectionWindow Std_Error P_Value
  <chr>                              <dbl>     <dbl>   <dbl>
1 P1_Provision_Mod                  -0.376     0.048  0     
2 P2_Dev_Mod                         0.02      0.04   0.609 
3 P4_Seva_Mod                        0.116     0.149  0.436 
4 T1_Corrupt_Mod                     0.561     0.182  0.0021
5 T2_Dynasty_Mod                     0.782     0.586  0.182 
6 T3_Appease_Mod                   -17.5    1041.     0.987 
7 T4_Threat_Mod                     -0.653     0.471  0.166 

8. Faceted Visualizations (All Sub-Dimensions Over Time)

long_subdims <- multidim_df %>%
  select(parsed_date, P1_Provision, P2_Development, P3_Empowerment, P4_Care_Seva,
         T1_Corrupt_Elite, T2_Dynastic_Elite, T3_Appeasement_VoteBank, T4_Threat_Security) %>%
  pivot_longer(cols = -parsed_date, names_to = "Sub_Dimension", values_to = "Term_Count") %>%
  mutate(Category = ifelse(str_starts(Sub_Dimension, "P"), "Pastoral Sub-Dimensions", "Transgressive Sub-Dimensions"))

ggplot(long_subdims, aes(x = parsed_date, y = Term_Count, color = Sub_Dimension)) +
  geom_smooth(method = "loess", se = FALSE, span = 0.4, linewidth = 0.9) +
  facet_wrap(~ Sub_Dimension, scales = "free_y", ncol = 4) +
  geom_vline(xintercept = as.Date("2021-12-10"), linetype = "dashed", color = "red", alpha = 0.7) +
  scale_x_date(date_breaks = "2 months", date_labels = "%b %y") +
  labs(
    title = "Deconstructed Rhetorical Architecture Across Speeches (2021–2022)",
    subtitle = "Faceted analysis across 8 sub-dimensions. Red line marks start of 90-Day Pre-Election Campaign Window (Dec 10, 2021)",
    x = "Date",
    y = "Average Term Frequency",
    color = "Sub-Dimension"
  ) +
  theme_minimal() +
  theme(
    legend.position = "none",
    strip.text = element_text(face = "bold", size = 9),
    plot.title = element_text(face = "bold", size = 12)
  )
`geom_smooth()` using formula = 'y ~ x'

9. Final Publication Plot: The Bifurcation of Authenticity (2021–2022)

This final visualization truncates strictly to the November 1st, 2021 – March 10th, 2022 timeframe, comparing the composite frequencies of Welfare and Developmental vs. Divisive rhetorical paradigms.

# 1. Truncate strictly to the November 1st, 2021 - March 10th, 2022 window
df_publication <- multidim_df %>% 
  filter(parsed_date >= as.Date("2021-11-01") & parsed_date <= as.Date("2022-03-10"))

final_plot <- ggplot(df_publication, aes(x = parsed_date)) +
  
  geom_point(aes(y = Total_Pastoral), color = "#27AE60", alpha = 0.25, size = 1.5) + 
  geom_point(aes(y = Total_Transgressive), color = "#C0392B", alpha = 0.25, size = 1.5) +
  
  # Updated string names in the geom_smooth aesthetics to match the new legend
  geom_smooth(aes(y = Total_Pastoral, color = "Welfare and Developmental rhetoric"), 
              method = "loess", se = TRUE, span = 0.4, linewidth = 1.2) + 
  geom_smooth(aes(y = Total_Transgressive, color = "Divisive- Exclusion and Attacks"), 
              method = "loess", se = TRUE, span = 0.4, linewidth = 1.2) +
  
  # 90-Day Campaign Window Indicator (Dec 10, 2021)
  geom_vline(xintercept = as.Date("2021-12-10"), linetype = "dotted", color = "#34495E", linewidth = 0.8) + 
  annotate("text", x = as.Date("2021-12-13"), 
           y = 48, 
           label = " 90-Day Campaign Window", angle = 90, hjust = 1, vjust = -0.5, color = "#34495E", fontface = "bold") +
  
  # Updated colors to map to the new legend labels
  scale_color_manual(values = c("Welfare and Developmental rhetoric" = "#27AE60", 
                                "Divisive- Exclusion and Attacks" = "#C0392B")) + 
  
  scale_x_date(date_breaks = "1 month", date_labels = "%b %Y") +
  coord_cartesian(ylim = c(0, 50)) +
  
  # Updated Title, Subtitle, and Legend Key
  labs( 
    title = "Rhetoric Employed by Narendra Modi", 
    subtitle = "Comparing Composite Welfare and Developmental vs. Divisive Terms per Address (UP Assembly Cycle)", 
    x = "Timeline", 
    y = "Composite Term Frequency per Speech", 
    color = "Key:" 
  ) +
  
  theme_minimal(base_size = 14) + 
  theme( 
    legend.position = "bottom", 
    legend.direction = "vertical",
    plot.title = element_text(face = "bold", size = 16), 
    axis.title = element_text(face = "bold"), 
    legend.title = element_text(face = "bold"), 
    legend.text = element_text(size = 11),
    plot.margin = margin(t = 15, r = 15, b = 15, l = 20) 
  )

# 2. Render the plot
print(final_plot)
`geom_smooth()` using formula = 'y ~ x'
`geom_smooth()` using formula = 'y ~ x'

# 3. Save a properly scaled, high-resolution copy
ggsave("Bifurcation_Plot_Fixed.png", plot = final_plot, width = 11, height = 7, dpi = 300)
`geom_smooth()` using formula = 'y ~ x'
`geom_smooth()` using formula = 'y ~ x'