The main findings

Sample: 276 unique posts collected from October 22 to October 29, 2026.

Predicted winner: Xavier Becerra

Predicted two-candidate share: Becerra 52.0%; Hilton 48.0%.

Becerra 95% bootstrap interval: 43.3%–60.6%.

Hand-label validation: Pending — complete hand_labels.csv.

The bootstrap interval includes 50%, so the resampling results allow either candidate to lead.

These shares are uncalibrated social-media proxies rather than representative estimates of California voters.

Study purpose and sample

I used public Bluesky posts to make a classroom forecast for the November 3, 2026 California governor election between Xavier Becerra and Steve Hilton. My prediction rule uses each candidate’s share of positively scored posts. I treat that share as a two-candidate vote-share proxy for this assignment, although it has not been calibrated to actual voting.

I used RStudio and the atrrr package, following the earlier Bluesky scraping lab. I searched Becerra and Hilton separately using English-language searches and the same seven-day window for both candidates.

query daily_searches returned_search_rows any_cap_reached
Becerra 7 294 FALSE
Hilton 7 1006 FALSE

Exact AI prompt used

The following is the prompt used to generate the sentiment-analysis approach:

I have a dataframe in R containing public Bluesky posts about the 2026 California governor election between Xavier Becerra and Steve Hilton. Each row represents one Bluesky post. Write R code using tidyverse, tidytext, and the Bing sentiment lexicon from get_sentiments(“bing”). Tokenize each post into individual words, join the words to the Bing lexicon, and calculate the number of positive and negative words in each post. Calculate each post’s net sentiment as positive words minus negative words. Then summarize the results separately for Becerra and Hilton, including the number of posts, positive words, negative words, average net sentiment, and percentage of posts with positive net sentiment. Preserve a unique post ID so that sentiment scores can be joined back to the original posts. Posts with no words found in the Bing lexicon should receive a net sentiment score of 0 rather than being removed. Do not use any sentiment method other than tidytext and the Bing lexicon.

For data cleaning, I also asked AI how to remove posts about Paris Hilton, Hilton Hotels, unrelated Hilton references, duplicate search hits, copied text, spam, repeated authors, sarcasm, and posts whose overall tone does not represent stance toward the candidate.

Preparing the posts for analysis

I removed repeated post URIs, empty text, missing or out-of-window creation times, Hilton Hotel and Paris Hilton references, and search hits without sufficient candidate context.

Cleaning step / outcome Posts
Raw search rows 1300
Repeated URI search hits removed 60
Empty text 49
Hotel / Paris Hilton / other Hilton tourism 120
Missing/out-of-window creation time 140
No candidate name/context in post text 600
Repeated normalized text removed 55
Unique posts retained 276

There were 39 retained posts mentioning both candidates. Such posts contribute an observation to each candidate, which means candidate totals can exceed the number of unique posts.

What the sentiment scores show

Each positive Bing word contributes +1 and each negative Bing word contributes -1. A score of zero is classified as neutral. Posts with no Bing dictionary matches are retained with a score of zero.

counts <- clean %>%
  select(post_id, score_text) %>%
  tidytext::unnest_tokens(word, score_text) %>%
  inner_join(
    tidytext::get_sentiments("bing"),
    by = "word"
  ) %>%
  group_by(post_id) %>%
  summarise(
    positive = sum(sentiment == "positive"),
    negative = sum(sentiment == "negative"),
    .groups = "drop"
  )

scored <- clean %>%
  left_join(
    counts,
    by = "post_id"
  ) %>%
  mutate(
    positive = coalesce(positive, 0L),
    negative = coalesce(negative, 0L),
    net = positive - negative,
    code_label = case_when(
      net > 0 ~ "positive",
      net < 0 ~ "negative",
      TRUE ~ "neutral"
    )
  )
Candidate Posts Positive Negative Neutral
Steve Hilton 152 49 42 61
Xavier Becerra 163 53 37 73
Candidate No dictionary matches Mean net score Positive rate
Steve Hilton 43 0.039 32.2%
Xavier Becerra 49 0.166 32.5%

Checking tone against candidate stance

The file hand_labels.csv contains the randomly selected posts for the manual validation portion of the assignment.

Open that file and enter one of these four labels in the human_label column for every post:

  • positive
  • negative
  • neutral
  • mixed/unclear

Label the stance toward the candidate named in that row, not simply whether the post contains positive or negative words.

## Complete the human_label column in hand_labels.csv, save it, and Knit again to display validation accuracy.
## Confusion table pending completion of hand_labels.csv.

Forecast and uncertainty

I selected positive share as my forecasting rule. Each candidate’s positive-post count is divided by the combined number of positive posts for both candidates.

This produces a classroom vote-share proxy rather than an actual polling estimate.

Predicted winner: Xavier Becerra

candidate predicted_vote_share_proxy bootstrap_low bootstrap_high
Xavier Becerra 52.0% 43.3% 60.6%
Steve Hilton 48.0% 39.4% 56.7%

I used an author-cluster bootstrap with 2,000 resamples. This helps show how much the forecast could change from sampling variability within the collected Bluesky data.

0 bootstrap replicates had no positive posts and were undefined. The confidence interval uses the remaining 2000 valid replicates.

Becerra led in 63.5% of valid bootstrap samples, Hilton led in 32.6%, and the candidates tied in 3.9%.

What this forecast can tell us

This forecast shows which candidate received more positively worded attention in my Bluesky search sample. It should not be interpreted as a scientific election poll. Bluesky users are a self-selected group, and this dataset does not establish that the people posting are California residents, eligible voters, or likely voters.

The Bing dictionary also has difficulty understanding context. Sarcasm can make positive words represent opposition, while negative words can occur in posts written by supporters. Negation, quoted speech, news reporting, and posts mentioning both candidates can also cause sentiment to differ from actual political stance.

More posts do not necessarily mean more support. Criticism, controversy, news coverage, campaign activity, and highly active accounts can all increase a candidate’s post volume. Therefore, this forecast should be interpreted as an analysis of Bluesky conversation rather than a representative forecast of California voters.

Privacy and reproducibility

Only aggregate results are displayed in this report. Usernames, handles, author IDs, post IDs, post text, and post links are excluded from the final report.

package version
atrrr 0.2.0
dplyr 1.2.1
tidyr 1.3.2
stringr 1.6.0
readr 2.2.0
purrr 1.2.2
tidytext 0.4.3
rmarkdown 2.32
knitr 1.52
ggplot2 4.0.3

Method references: atrrr documentation, tidytext sentiment lexicons, and Text Mining with R: Sentiment Analysis.