1 Scope and use

Keyword: starbucks.

This report converts Starbucks_Bluesky_Analysis.xlsx into executable R Markdown. The 10 supplied summaries, engagement counts, and sentiment labels are embedded below, so the sample analysis runs without Excel files or internet access. Install rmarkdown and knitr once, open this file in RStudio, and click Knit.

The sample has no documented collection dates, post timestamps, reply counts, or random sampling procedure. It is not a complete Day 1 or Day 3 collection. The summaries and technology-related claims have not been independently verified. Results below describe only the supplied records.

To add the workbook’s completed collection data later, place the Excel file beside this Rmd, install readxl, and set workbook in the YAML above to "Starbucks_Bluesky_Analysis.xlsx". The comparison reads the Collections tab. This script analyzes data; it does not collect new Bluesky posts.

2 Supplied post data

Original sentiment labels are preserved. Review whether each label expresses sentiment toward Starbucks. In particular, the positive label for the post comparing someone’s appearance to the Starbucks logo needs full-text context.

posts <- data.frame(
  handle = c(
    "@pattymo.com",
    "@clarajeffery.bsky.social",
    "@otsumamiboy.blacksky.app",
    "@hypervisible.blacksky.app",
    "@dixonij.bsky.social",
    "@carlquintanilla.bsky.social",
    "@sbworkersunited.org",
    "@jamiedcville.bsky.social",
    "@hhaelines.bsky.social",
    "@paleofuture.bsky.social"
  ),
  text = c(
    "Me looking at the shitty little brownie behind the Starbucks display case",
    "never enters a Starbucks again (AI recognition reaction)",
    "Separate from the nefarious Price Profiling purposes of Starbucks...",
    "Invasive surveillance for modest convenience...",
    "The best time to stop going to Starbucks was yesterday.",
    "STARBUCKS CEO: We are also exploring experiences that recognize...",
    "Listen to Angie Nixon: Stand with workers and BOYCOTT STARBUCKS!",
    "I'm gonna boycott Starbucks even more now.",
    "she kind of looks like the starbucks logo",
    "Remember the creepy shopping scene from Minority Report?"
  ),
  likes = c(
    514,
    315,
    318,
    240,
    224,
    201,
    186,
    117,
    100,
    79
  ),
  reposts = c(
    9,
    65,
    38,
    93,
    59,
    98,
    47,
    1,
    10,
    29
  ),
  sentiment = c(
    "Neutral",
    "Negative",
    "Negative",
    "Negative",
    "Negative",
    "Neutral",
    "Negative",
    "Negative",
    "Positive",
    "Negative"
  ),
  stringsAsFactors = FALSE
)

# Engagement follows the assignment's definition; replies are excluded.
posts$engagement <- posts$likes + posts$reposts
stopifnot(nrow(posts) == 10L,
          all(posts$sentiment %in% c("Negative", "Neutral", "Positive")))
knitr::kable(posts, caption = "Ten supplied Starbucks records")
Ten supplied Starbucks records
handle text likes reposts sentiment engagement
@pattymo.com Me looking at the shitty little brownie behind the Starbucks display case 514 9 Neutral 523
@clarajeffery.bsky.social never enters a Starbucks again (AI recognition reaction) 315 65 Negative 380
@otsumamiboy.blacksky.app Separate from the nefarious Price Profiling purposes of Starbucks… 318 38 Negative 356
@hypervisible.blacksky.app Invasive surveillance for modest convenience… 240 93 Negative 333
@dixonij.bsky.social The best time to stop going to Starbucks was yesterday. 224 59 Negative 283
@carlquintanilla.bsky.social STARBUCKS CEO: We are also exploring experiences that recognize… 201 98 Neutral 299
@sbworkersunited.org Listen to Angie Nixon: Stand with workers and BOYCOTT STARBUCKS! 186 47 Negative 233
@jamiedcville.bsky.social I’m gonna boycott Starbucks even more now. 117 1 Negative 118
@hhaelines.bsky.social she kind of looks like the starbucks logo 100 10 Positive 110
@paleofuture.bsky.social Remember the creepy shopping scene from Minority Report? 79 29 Negative 108

3 Engagement

# Original row order breaks any engagement ties consistently.
ranked_posts <- posts[order(-posts$engagement, seq_len(nrow(posts))), ]
top_five <- head(ranked_posts, 5)
top_five <- data.frame(
  rank = seq_len(nrow(top_five)),
  top_five[, c("handle", "likes", "reposts", "engagement", "sentiment")],
  row.names = NULL
)
knitr::kable(top_five, caption = "Top five among the supplied records")
Top five among the supplied records
rank handle likes reposts engagement sentiment
1 @pattymo.com 514 9 523 Neutral
2 @clarajeffery.bsky.social 315 65 380 Negative
3 @otsumamiboy.blacksky.app 318 38 356 Negative
4 @hypervisible.blacksky.app 240 93 333 Negative
5 @carlquintanilla.bsky.social 201 98 299 Neutral

The highest-engagement supplied post is humorous. Several other leading summaries discuss privacy and recognition technology. This small sample does not establish that humor generally spreads faster than news or that engagement implies agreement with a post.

4 Manual sentiment

sentiment_levels <- c("Negative", "Neutral", "Positive")
sentiment_counts <- table(factor(posts$sentiment, levels = sentiment_levels))
sentiment_summary <- data.frame(
  sentiment = sentiment_levels,
  count = as.integer(sentiment_counts)
)
sentiment_summary$share <- sentiment_summary$count / sum(sentiment_summary$count)
sentiment_display <- sentiment_summary
sentiment_display$share <- sprintf("%.0f%%", 100 * sentiment_display$share)
knitr::kable(sentiment_display,
             col.names = c("Sentiment", "Posts", "Percentage"),
             caption = "Supplied manual labels; random selection is undocumented")
Supplied manual labels; random selection is undocumented
Sentiment Posts Percentage
Negative 7 70%
Neutral 2 20%
Positive 1 10%
Sentiment labels in the supplied 10-post sample

Sentiment labels in the supplied 10-post sample

Under the supplied labels, 7 of 10 records are negative (70%). This is a description of the supplied sample, not an estimate of all Starbucks customers’ opinions.

5 Themes and participating accounts

The supplied summaries suggest three topics for review against the full posts:

author_counts <- sort(table(posts$handle), decreasing = TRUE)
author_summary <- data.frame(handle = names(author_counts),
                             posts = as.integer(author_counts))
knitr::kable(author_summary, caption = "Author counts in the supplied sample")
Author counts in the supplied sample
handle posts
@carlquintanilla.bsky.social 1
@clarajeffery.bsky.social 1
@dixonij.bsky.social 1
@hhaelines.bsky.social 1
@hypervisible.blacksky.app 1
@jamiedcville.bsky.social 1
@otsumamiboy.blacksky.app 1
@paleofuture.bsky.social 1
@pattymo.com 1
@sbworkersunited.org 1

Each handle appears once in the supplied sample. Account dominance in the full conversation cannot be assessed without the complete collections.

6 Optional Day 1 and Day 3 comparison

This section uses only actual rows entered in the workbook’s Collections tab. Blank reserved rows are excluded. Missing collections and unreviewed sentiment remain unavailable rather than being treated as zero sentiment.

collections <- NULL
if (!nzchar(params$workbook)) {
  cat("No collection workbook selected. Day 1 and Day 3 results are unavailable.")
} else {
  if (!file.exists(params$workbook)) {
    stop("Workbook not found. Place it beside this Rmd or provide its full path.")
  }
  if (!requireNamespace("readxl", quietly = TRUE)) {
    stop("Install readxl first: install.packages('readxl')")
  }
  collections <- as.data.frame(readxl::read_excel(
    params$workbook, sheet = "Collections", skip = 5,
    na = c("", "NA"), .name_repair = "minimal"
  ))
  required <- c("Collection", "Author handle", "Manual sentiment")
  if (!all(required %in% names(collections))) {
    stop("Collections must include: Collection, Author handle, Manual sentiment.")
  }
  keep <- !is.na(collections[["Author handle"]]) &
          nzchar(trimws(collections[["Author handle"]]))
  collections <- collections[keep, , drop = FALSE]
  collections[["Collection"]] <- trimws(collections[["Collection"]])
  collections[["Manual sentiment"]] <- tolower(trimws(collections[["Manual sentiment"]]))
  unexpected_days <- setdiff(unique(collections[["Collection"]]), c("Day 1", "Day 3"))
  if (length(unexpected_days)) stop("Use Collection labels Day 1 or Day 3.")
  labels <- collections[["Manual sentiment"]]
  if (any(!is.na(labels) & nzchar(labels) &
          !labels %in% c("negative", "neutral", "positive"))) {
    stop("Manual sentiment must be blank, Negative, Neutral, or Positive.")
  }
}
## No collection workbook selected. Day 1 and Day 3 results are unavailable.
if (!is.null(collections)) {
  summarize_day <- function(day) {
    d <- collections[collections[["Collection"]] == day, , drop = FALSE]
    tags <- d[["Manual sentiment"]]
    labeled <- tags %in% c("negative", "neutral", "positive")
    counts <- vapply(c("negative", "neutral", "positive"),
                     function(x) sum(tags == x, na.rm = TRUE), numeric(1))
    shares <- if (sum(labeled) > 0) counts / sum(labeled) else rep(NA_real_, 3)
    c(nrow(d), sum(labeled), shares)
  }
  first <- summarize_day("Day 1")
  second <- summarize_day("Day 3")
  delta <- if (first[1] > 0 && second[1] > 0) second - first else rep(NA_real_, 5)
  fmt <- function(x, change = FALSE) {
    result <- character(5)
    result[1:2] <- ifelse(is.na(x[1:2]), "Unavailable", sprintf("%.0f", x[1:2]))
    result[3:5] <- ifelse(is.na(x[3:5]), "Unavailable",
                          sprintf(if (change) "%.1f pp" else "%.1f%%", 100 * x[3:5]))
    result
  }
  comparison <- data.frame(
    Metric = c("Posts entered", "Sentiment-labeled posts", "Negative share",
               "Neutral share", "Positive share"),
    `Day 1` = fmt(first), `Day 3` = fmt(second),
    Change = fmt(delta, TRUE), check.names = FALSE
  )
  print(knitr::kable(comparison, caption = "Day 3 minus Day 1; pp = percentage points"))
  if (first[1] < 100 || second[1] < 100) {
    cat("\n\nAt least one collection has fewer than the required 100 posts.\n")
  }
  if (any(c(first[2], second[2]) < 10 | c(first[2], second[2]) > 20)) {
    cat("\n\nUse a documented random sample of 10–20 manually labeled posts per collection.\n")
  }
}

Collect both datasets with the same keyword and comparable settings. Record collection dates, timezone, post timestamps, and reply counts. Preserve IDs or URLs to identify repeated posts. Two fixed-size 100-post pulls alone cannot establish a percentage change in conversation volume; compare equivalent time windows. Different post ages also affect accumulated engagement.

7 Assignment completion

Use Excel for the required analysis evidence and screenshot. The R report provides a reproducible companion to that work. Before submission, add:

No Day 3 findings, collection dates, or external research conclusions have been invented for this report.