Introduction

The purpose of this project is to investigate whether public conversations on Bluesky can provide insight into the California gubernatorial election. The analysis focuses on two candidates: Xavier Becerra, a Democrat, and Steve Hilton, a Republican.

Public Bluesky posts were collected using the R package atrrr. The posts were cleaned and analyzed using the Bing sentiment lexicon from the tidytext package.

The project uses sentiment analysis to compare positive, negative, and neutral posts about each candidate. A bootstrap analysis was also conducted to measure sampling variability in the results.

Data Collection

Bluesky posts were collected using the atrrr package in R.

Two separate searches were conducted: - Xavier Becerra - Steve Hilton

The initial collection produced 58 posts: - 20 posts about Xavier Becerra. - 38 posts about Steve Hilton.

The two datasets were combined into one dataset and labeled according to candidate.

Data Cleaning

The collected data was cleaned before analysis.

The cleaning process included: - Removing posts with missing or empty text. - Removing irrelevant references to Hilton Hotels and Paris Hilton. - Removing duplicate posts.

After cleaning, 54 posts remained: - 19 posts about Xavier Becerra. - 35 posts about Steve Hilton.

Four posts were removed during cleaning.

Additional limitations include possible spam, reposts, automated accounts, and posts that mention a candidate without actually discussing their campaign.

AI Prompt

The following prompt was used with ChatGPT to generate the sentiment analysis code:

“I am completing a social-media analytics project using public Bluesky posts about two California gubernatorial candidates: Xavier Becerra (Democrat) and Steve Hilton (Republican).

My dataset is a CSV file containing the following columns:

Please write R code using the tidyverse and tidytext packages to analyze the sentiment of these posts.

Requirements:

  1. Import the cleaned CSV dataset into R.
  2. Use unnest_tokens() to tokenize the post text into individual words.
  3. Use the Bing sentiment lexicon from tidytext (get_sentiments(‘bing’)) to identify positive and negative words.
  4. Calculate a sentiment score for each post by subtracting the number of negative words from the number of positive words.
  5. Classify posts with positive scores as positive, negative scores as negative, and zero scores as neutral.
  6. Summarize the number and percentage of positive, negative, and neutral posts for each candidate.
  7. Calculate the overall net sentiment for each candidate.
  8. Create visualizations comparing sentiment between the two candidates.
  9. Include clear comments explaining each step of the R code.

Please make the code beginner-friendly and ensure that it can run in RStudio. Do not invent data or assume that the sentiment results indicate actual voter preferences. Explain the limitations of using a word-based sentiment lexicon to measure political support.”

Sentiment Analysis Method

The Bing sentiment lexicon was used to identify positive and negative words in each post.

Each positive word received a score of +1, and each negative word received a score of -1.

The scores were added together to create a sentiment score for each post.

Posts were classified as: - Positive: score greater than zero. - Negative: score less than zero. - Neutral: score equal to zero.

Sentiment Analysis Results

The analysis produced the following results:

Candidate Positive Negative Neutral
Xavier Becerra 8 (42.1%) 2 (10.5%) 9 (47.4%)
Steve Hilton 13 (37.1%) 10 (28.6%) 12 (34.3%)

Becerra had a higher percentage of positive posts than Hilton. Becerra also had a lower percentage of negative posts.

Hilton received more total positive posts, but this was partly because more posts about Hilton were collected.

Human Validation

A random sample of 20 posts was selected for manual review.

Each post was manually classified according to whether the author supported, opposed, or expressed a neutral stance toward the candidate.

The manual classifications were compared with the computer-generated sentiment classifications.

Human validation accuracy: [human audit completed.csv]

This comparison is important because positive or negative language does not always represent political support or opposition.

Election Prediction

The prediction rule selected for this project was positive-post share.

The positive-post share was calculated by dividing the number of positive posts about each candidate by the total number of positive posts about both candidates.

There were 21 positive posts in total.

Candidate Positive posts Positive-post share
Xavier Becerra 8 38.10%
Steve Hilton 13 61.90%

Based on this prediction rule, Steve Hilton is the predicted winner of the social-media analysis, with a positive-post share of approximately 61.90%.

Xavier Becerra received approximately 38.10%.

These percentages represent positive-post shares, not actual predicted election vote shares.

Bootstrap Analysis

A bootstrap analysis with 1,000 resamples was conducted to measure sampling variability.

The posts were resampled separately for each candidate, and the positive-post shares were recalculated for each sample.

The results were:

Candidate Average share Lower 95% Upper 95%
Xavier Becerra 38.32% 21.42% 55.00%
Steve Hilton 61.68% 45.00% 78.58%

The bootstrap results suggest that Hilton had a higher average positive-post share in the resampled datasets.

However, the intervals are relatively wide, indicating substantial sampling variability.

The bootstrap measures uncertainty from sampling the collected posts. It does not account for all possible biases in the original Bluesky data.

Reflection and Limitations

There are several limitations to using Bluesky posts to predict an election.

First, Bluesky users may not represent the overall California voting population. Certain age groups, political communities, and demographic groups may be more active on the platform than others.

Second, sentiment analysis cannot always understand sarcasm, humor, political context, or tone. A negative-sounding post does not necessarily mean that the author opposes the candidate being discussed.

Third, having more posts about a candidate does not necessarily mean that candidate has more support. People may discuss a candidate because they disagree with them, criticize their policies, or respond to controversial news.

Another limitation is the relatively small sample size. Only 54 cleaned posts were collected, which is not enough to represent the opinions of millions of California voters.

The human validation process is also important because it demonstrates that automated sentiment scores do not always accurately identify political stance.

Finally, the bootstrap analysis measures sampling variability but cannot eliminate selection bias or make the sample representative of actual voters.

Overall, Bluesky sentiment analysis provides an interesting snapshot of online political conversations, but it should not be treated as a reliable replacement for representative election polling.

Conclusion

Based on the positive-post-share prediction rule, Steve Hilton received the larger share of positive Bluesky posts, with approximately 61.90%, compared with 38.10% for Xavier Becerra.

The bootstrap analysis also showed a higher average positive-post share for Hilton.

However, these findings should be interpreted cautiously because social-media activity does not necessarily reflect actual voter preferences or election outcomes.

Sentiment Analysis Graph

library(ggplot2)

graph_data <- data.frame(
  candidate = c(
    "Becerra", "Becerra", "Becerra",
    "Hilton", "Hilton", "Hilton"
  ),
  sentiment = c(
    "Positive", "Negative", "Neutral",
    "Positive", "Negative", "Neutral"
  ),
  percentage = c(
    42.1, 10.5, 47.4,
    37.1, 28.6, 34.3
  )
)

ggplot(graph_data,
       aes(x = candidate,
           y = percentage,
           fill = sentiment)) +
  geom_col(position = "dodge") +
  labs(
    title = "Bluesky Sentiment by Candidate",
    x = "Candidate",
    y = "Percentage of Posts",
    fill = "Sentiment"
  ) +
  theme_minimal()

library(dplyr)
## 
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
library(stringr)

audit <- read.csv("human audit completed.csv")

audit <- audit %>%
  mutate(
    human_label = str_to_title(str_trim(human_label)),
    computer_label = case_when(
      sentiment == "Positive" ~ "Support",
      sentiment == "Negative" ~ "Oppose",
      sentiment == "Neutral" ~ "Neutral"
    )
  )

accuracy <- mean(
  audit$human_label == audit$computer_label
)

cat("Human validation accuracy:",
    round(accuracy * 100, 1), "%")
## Human validation accuracy: 40 %
table(
  Human = audit$human_label,
  Computer = audit$computer_label
)
##          Computer
## Human     Neutral Oppose Support
##   Neutral       2      1       2
##   Oppose        3      3       1
##   Support       4      1       3

Step 4. Save and publish to RPubs

1. Save your document

  Name it `california_governor_analysis.Rmd`.
</box>

2. Click Knit

  Choose Knit to HTML. Check that your report generates without errors.
</box>

3. Publish

  Click Publish in RStudio and select RPubs. Sign in or create an RPubs account if prompted.
</box>

4. Copy your link

  Open your published report and copy its URL for your class discussion.
</box>

You can visit .

One last reminder: Include your actual post-collection dates and make sure the data was collected before the election outcome was known if you’re presenting this as a forecast. Publish aggregate results only, without usernames or identifying information.

Where we stand

  • Completed Data collection and cleaning
  • Completed Sentiment analysis
  • Completed Bootstrap
  • Still needed Actual human validation accuracy
  • Still needed Knit and publish to RPubs