1. Data Collection

Public Bluesky posts were scraped with the atrrr package using four queries: “Becerra”, “Xavier Becerra”, “Steve Hilton”, and “Hilton governor” (200 posts per query, pulled October 29, 2026). All results are reported in aggregate only. No usernames are shown.

2. Cleaning Audit

Posts remaining after each cleaning step
step remaining removed
Raw scrape 806 NA
Remove empty/NA text 789 17
Remove duplicate text 731 58
Remove non-English posts 729 2
Text names the candidate 596 133
Remove hotel / Paris Hilton noise 593 3
Require political context 551 42
Remove sports Becerra noise 550 1
Remove posts mentioning both candidates 361 189
Remove posts under 3 words 359 2

Other issues noted: near-duplicate copy-pasted posts, a few accounts posting repeatedly, non-English posts, wrong-person matches (a boxer named Becerra, Newsom posts), and all posts coming from roughly one day.

3. AI Prompt Used (exact text)

I am analyzing public Bluesky posts to forecast the 2026 California governor’s race (Xavier Becerra, Democrat, vs. Steve Hilton, Republican). I have a cleaned CSV called “Governor_clean.csv” with 359 rows and these columns: text, author_handle, indexed_at, created_at, like_count, repost_count, reply_count, candidate (“Becerra” or “Hilton”), query, and text_clean (post text with URLs and @mentions removed). Each row is one post about one candidate.

Write R code that scores each post’s sentiment with tidytext and the Bing lexicon, then summarizes the results by candidate.

Method: add a post_id, tokenize text_clean into words, inner join with the Bing lexicon, and count positive and negative words per post. Net score = positive words minus negative words. Label each post positive (net > 0), negative (net < 0), or neutral (net = 0 or no lexicon words). Then summarize by candidate: number of posts, number of positive/negative/neutral posts, positive share (positive / (positive + negative)), mean net score, and volume share (candidate posts / all posts).

Constraints: use the pipe %>% (not |>), use function() instead of backslash lambdas, and make the code copy-paste friendly. Output only aggregate results and never print usernames. Flag any words in the Bing lexicon that could be misleading in a political context (for example “trump” is coded positive) and remove them with a custom list. Keep the code beginner-friendly with short comments. Save the scored posts to “Governor_scored.csv” without the author_handle column.

4. Sentiment Results

Sentiment summary by candidate (Bing lexicon)
candidate posts positive negative neutral positive_share mean_net volume_share
Becerra 137 38 32 67 0.543 0.131 0.382
Hilton 222 60 68 94 0.469 -0.135 0.618

5. Validation: Code vs. Labels

A random sample of 20 posts (seed 123) was labeled for stance toward the named candidate. These labels were generated with AI assistance (Claude), not by a human coder, and were then compared with the lexicon’s labels.

Stance label (rows) vs. lexicon label (columns), counts
my_label code_label n
negative negative 4
negative neutral 3
negative positive 1
neutral negative 1
neutral neutral 2
neutral positive 3
positive negative 1
positive neutral 3
positive positive 2

Validation accuracy: 8 of 20 (40%). The lexicon often scored posts as neutral when positive and negative words cancelled out or when no lexicon words appeared. It also misread tone and policy vocabulary.

6. Prediction and Bootstrap

Rule: positive share = positive / (positive + negative) for each candidate. Predicted Becerra vote share = Becerra positive share / (Becerra + Hilton positive shares).

The interval crosses 50%, so sampling noise alone could flip this result. The forecast is a lean toward Becerra, not a confident call.

7. Reflection: Limits of the Forecast

Who is on Bluesky. Bluesky users are not a random sample of California voters. The platform skews younger, more online, and more left-leaning, so a Democrat doing better than a Republican here says little about the electorate. The sample is also small (359 posts) and covers about one day, and a handful of heavy posters can account for many posts.

Sarcasm and tone vs. stance. The lexicon counts words and does not understand meaning. In validation it matched my stance labels only 40% of the time. Sarcasm, criticism that uses positive words, and neutral news headlines with loaded words all confuse it. The bootstrap measures sampling noise only, so it cannot capture this measurement error, and the true uncertainty is larger than the interval suggests.

Does more posts mean more support? No. Hilton had more posts (62% of volume) but more negative sentiment, because people post about candidates they criticize, mock, or find newsworthy. Volume measures attention, not approval, and under a volume-share rule the forecast would flip to Hilton. Different prediction rules give different winners, which shows how fragile social-media forecasts are.