Converted from
California_Governor_2026_Sentiment_Analysis.xlsx. All four
worksheets are preserved below. Tables use the workbook’s saved formula
results. The original R scripts are included as R chunks with
eval=FALSE to preserve the reported results and allow
knitting without downloading lexicons. To run those scripts, install
tidyverse and tidytext and change their chunk
options to eval=TRUE. The Bing lexicon may require a
first-use download.
Data Source: Bluesky Social Media | Candidates: Xavier Becerra (D) vs. Steve Hilton (R)
| Metric | Xavier Becerra (D) | Steve Hilton (R) | Difference / Lead |
|---|---|---|---|
| Predicted Vote / Sentiment Share | 64.2% | 35.8% | 28.4% |
| Manual Validation Accuracy | 60.0% | 60.0% | N/A |
| Bootstrap 95% CI Lower Bound | 11.7% | N/A | N/A |
| Bootstrap 95% CI Upper Bound | 43.3% | N/A | N/A |
| Bootstrap p-value (H0: Lead <= 0) | 0 | N/A | Significant (p < 0.001) |
AI Sentiment Accuracy: The Bing dictionary achieved 60% accuracy on manual validation, struggling with political slang (‘hack’), sarcasm, and jargon (‘landslide’).
Statistical Significance: The 2,000-replicate bootstrap test confirmed Becerra’s sentiment lead is statistically significant (p < 0.001, 95% CI: [11.7%, 43.3%]).
Demographic Bias: Bluesky users skew left-leaning and highly engaged, creating sample selection bias relative to the California voting electorate.
Volume vs. Votes: Post volume reflects vocal active users and news bots rather than voting representative counts.
Conversion note: The executive summary’s 64.2% / 35.8% figures and 28.4 percentage point lead are retained as supplied. The bootstrap input table instead gives within-candidate positive shares of 45/70 (64.3%) and 22/60 (36.7%), a difference of 27.6 percentage points. These are different quantities; the workbook does not reconcile them. The reported p-value of 0 is an empirical bootstrap tail fraction, not proof that the true probability is exactly zero. Claims and sample descriptions are reproduced from the workbook, not independently verified.
“Write an R script using the tidyverse and tidytext packages to perform sentiment analysis on text data related to two political candidates: Xavier Becerra and Steve Hilton. 1. Create a mock dataset containing example text/posts for each candidate. 2. Tokenize the text into individual words using unnest_tokens(). 3. Remove standard stop words using anti_join(stop_words). 4. Inner join with the bing sentiment lexicon from tidytext to categorize words as either ‘positive’ or ‘negative’. 5. Calculate total positive, negative, and net sentiment scores for each candidate, and summarize the final counts in a clean output.”
# Load required libraries
library(tidyverse)
library(tidytext)
# 1. Mock text data
candidate_data <- tibble(
candidate = c(rep("Xavier Becerra", 3), rep("Steve Hilton", 3)),
doc_id = 1:6,
text = c(
"Becerra highlights steady leadership, effective progress, and strong support for public health initiatives.",
"Supporters praise his administrative experience, though critics express concern over bureaucratic delays.",
"A clean campaign focusing on stability, affordable housing, and expanded opportunity for working families.",
"Hilton calls for aggressive action to fix government failure, waste, and rising costs.",
"Enthusiastic crowds welcome his energetic proposals, but opponents label his tax plans risky and divisive.",
"A bold campaign promising disruption, lower taxes, and reform to tackle state corruption."
)
)
# 2, 3, & 4. Tokenize, remove stop words, and join with Bing lexicon
sentiment_scored <- candidate_data %>%
unnest_tokens(word, text) %>%
anti_join(stop_words, by = "word") %>%
inner_join(get_sentiments("bing"), by = "word")
# 5. Summary
candidate_sentiment_summary <- sentiment_scored %>%
count(candidate, sentiment) %>%
pivot_wider(names_from = sentiment, values_from = n, values_fill = 0) %>%
mutate(net_sentiment = positive - negative)
print(candidate_sentiment_summary)
Manual Verification of AI Sentiment Scoring (15 Bluesky Sample Posts)
| Post # | Post Content (Anonymized) | Manual Score | Bing Lexicon Score | Match? | Discrepancy / Error Cause |
|---|---|---|---|---|---|
| 1 | PPIC poll showing Xavier Becerra 61% vs Steve Hilton 37%. | 0 | 0 | Yes | Fact/Poll numbers containing no sentiment words. |
| 2 | Steve Hilton is misotic. Vote Xavier Becerra. | -1 | 0 | No | Lexicon missed uncommon word ‘misotic’. |
| 3 | Why Xavier Becerra Is the Clear Choice Over Steve Hilton for California Governor. | 1 | 1 | Yes | Word ‘clear’ mapped to positive sentiment. |
| 4 | Hilton Makes Final Push Against California’s Dem Machine. | 0 | -1 | No | Word ‘against’ scored negative in neutral context. |
| 5 | Vote Steve Hilton for California governor over establishment hack Xavier Becerra for real change. | 1 | 0 | No | Words ‘hack’ (-1) and ‘change’ (+1) canceled out; missed endorsement context. |
| 6 | Steve Hilton storms Southern California as Xavier Becerra lays low… | 0 | -1 | No | Word ‘storms’ scored negative; descriptive phrasing misinterpreted. |
| 7 | Steve Hilton rips Xavier Becerra’s proposed mileage tax: ‘They never stop gouging you’ | -1 | -1 | Yes | Words ‘rips’ and ‘gouging’ correctly scored negative. |
| 8 | Xavier Becerra Headed for Landslide in California. | 1 | 0 | No | Lexicon did not score ‘landslide’ as political positive. |
| 9 | I’m voting for Xavier fucking Becerra… Anything to deny Steve Hilton a seat. | -1 | -1 | Yes | Expressed opposition (‘deny’) correctly captured. |
| 10 | Repulsive ad features AI Xavier Becerra. | -1 | -1 | Yes | Word ‘repulsive’ accurately flagged negative. |
| 11 | Proposes abolishing DMV… Polls have him down 20+ points. | -1 | 0 | No | Missed contextual negativity of election trajectory. |
| 12 | Becerra highlights steady leadership, effective progress… | 1 | 1 | Yes | Explicit positive words (‘steady’, ‘effective’). |
| 13 | Opponents label his tax plans risky and divisive. | -1 | -1 | Yes | Explicit negative terms (‘risky’, ‘divisive’). |
| 14 | A clean campaign focusing on stability and affordable housing. | 1 | 1 | Yes | Explicit positive terms (‘clean’, ‘stability’). |
| 15 | A bold campaign promising disruption, lower taxes, and reform… | 1 | 1 | Yes | Words ‘bold’, ‘promising’, ‘reform’ scored positive. |
Total Posts: 15
Accuracy Rate: 60%
manual_scores <- c(0, -1, 1, 0, 1, 0, -1, 1, -1, -1, -1, 1, -1, 1, 1)
bing_scores <- c(0, 0, 1, -1, 0, -1, -1, 0, -1, -1, 0, 1, -1, 1, 1)
matches <- manual_scores == bing_scores
data.frame(total_posts = length(matches), matched_posts = sum(matches),
accuracy = mean(matches))
## total_posts matched_posts accuracy
## 1 15 9 0.6
Bootstrap Significance Simulation (2,000 Replicates)
| Candidate | Positive Sentiment Posts | Negative Sentiment Posts | Total Scored Posts | Positive Share |
|---|---|---|---|---|
| Xavier Becerra (D) | 45 | 25 | 70 | 64.3% |
| Steve Hilton (R) | 22 | 38 | 60 | 36.7% |
library(tidyverse)
set.seed(2026)
# Create observed post sentiment dataset
bs_data <- tibble(
candidate = c(rep("Becerra", 70), rep("Hilton", 60)),
positive = c(rep(1, 45), rep(0, 25), rep(1, 22), rep(0, 38))
)
# Bootstrap 2,000 replicates
boot_diffs <- replicate(2000, {
s <- bs_data %>% group_by(candidate) %>% sample_frac(1, replace = TRUE) %>% summarize(r = mean(positive))
sr[scandidate == "Becerra"] - sr[scandidate == "Hilton"]
})
# Results
ci_lower <- quantile(boot_diffs, 0.025)
ci_upper <- quantile(boot_diffs, 0.975)
p_val <- mean(boot_diffs <= 0)
cat("Mean Difference:", mean(boot_diffs), "\n95% CI: [", ci_lower, ",", ci_upper, "]\np-value:", p_val)