library(tidyverse)
library(lubridate)
library(scales)
library(writexl)
library(knitr)
library(kableExtra)

# gtrendsR is used for live pulls; the knitted report reads a saved extract so
# Google rate limits (HTTP 429) do not break reproducibility.
# library(gtrendsR)

1. Introduction and Exercise Choice

This Lab 3 report completes Exercise 2 — Seasonal Campaign Planning using Google Trends data pulled with the gtrendsR package (Massicotte & Eddelbuettel, 2025; Xu, 2026a).

Seasonal product: "Halloween costume"
Geography: United States
Window: approximately 3 years (2023-08-01 to 2026-08-01)

Google Trends returns a relative search-interest index (0–100) within the selected window and geography — 100 marks the peak week in this extract, not absolute search volume (Xu, 2026a). That makes Trends ideal for spotting seasonality and planning campaign timing, which is the marketing goal of this exercise.

I chose Exercise 2 over the brand-tracker and NewsAPI options because it maps cleanly to a campaign-planning decision: when to launch paid/organic media for a highly seasonal category, with an Excel deliverable and a concrete 2–4 week pre-peak recommendation.

2. Data Collection with gtrendsR

2.1 Live pull syntax (reproducible method)

The code below is the collection method used for this lab. For knitting reliability, the remainder of the report loads the saved Excel/CSV extract produced by this pull.

library(gtrendsR)

# Explicit date range (~3 years). Prefer this over "today 3-y" with some
# gtrendsR / anytime versions that fail to parse relative time strings.
res <- gtrends(
  keyword = "Halloween costume",
  geo = "US",
  time = "2023-08-01 2026-08-01",
  onlyInterest = TRUE
)

interest_raw <- res$interest_over_time

Note on rate limits: Google occasionally returns HTTP 429. If that happens, wait a minute and re-run the pull, or knit from the attached Excel file already saved in this project folder.

2.2 Load scraped data and export Excel

interest <- read_csv("halloween_costume_interest.csv", show_col_types = FALSE) %>%
  mutate(
    date = as.Date(date),
    hits = as.numeric(hits),
    year = year(date),
    month = month(date),
    month_name = month(date, label = TRUE, abbr = TRUE)
  )

monthly_avg <- interest %>%
  group_by(month, month_name) %>%
  summarise(
    avg_interest = mean(hits, na.rm = TRUE),
    max_interest = max(hits, na.rm = TRUE),
    median_interest = median(hits, na.rm = TRUE),
    n_obs = n(),
    .groups = "drop"
  ) %>%
  arrange(month)

weekly_avg <- interest %>%
  mutate(week_of_year = isoweek(date)) %>%
  group_by(week_of_year) %>%
  summarise(
    avg_interest = mean(hits, na.rm = TRUE),
    max_interest = max(hits, na.rm = TRUE),
    n_obs = n(),
    .groups = "drop"
  ) %>%
  arrange(desc(avg_interest))

# Requirement: attach scraped data as an Excel spreadsheet
write_xlsx(
  list(
    interest_over_time = interest %>%
      select(date, hits, keyword, geo, year, month, month_name),
    monthly_average = monthly_avg,
    weekly_average = weekly_avg
  ),
  path = "halloween_costume_gtrends.xlsx"
)

cat(
  "Excel saved: halloween_costume_gtrends.xlsx\n",
  "Rows (weekly interest):", nrow(interest), "\n",
  "Date range:", as.character(min(interest$date)), "to",
  as.character(max(interest$date)), "\n"
)
## Excel saved: halloween_costume_gtrends.xlsx
##  Rows (weekly interest): 157 
##  Date range: 2023-07-30 to 2026-07-26
interest %>%
  slice_head(n = 8) %>%
  select(date, hits, keyword, geo) %>%
  kable(caption = "Sample of scraped Google Trends weekly interest") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Sample of scraped Google Trends weekly interest
date hits keyword geo
2023-07-30 6 Halloween costume US
2023-08-06 7 Halloween costume US
2023-08-13 9 Halloween costume US
2023-08-20 10 Halloween costume US
2023-08-27 13 Halloween costume US
2023-09-03 18 Halloween costume US
2023-09-10 21 Halloween costume US
2023-09-17 26 Halloween costume US

3. Plot the Seasonal Pattern

peak_overall <- interest %>% slice_max(hits, n = 1, with_ties = FALSE)

ggplot(interest, aes(x = date, y = hits)) +
  geom_line(color = "#1f4e79", linewidth = 0.9) +
  geom_point(
    data = peak_overall,
    aes(x = date, y = hits),
    color = "#c0392b",
    size = 3
  ) +
  annotate(
    "text",
    x = peak_overall$date,
    y = peak_overall$hits + 6,
    label = paste0("Peak: ", peak_overall$date, " (hits = ", peak_overall$hits, ")"),
    color = "#c0392b",
    size = 3.3,
    fontface = "bold"
  ) +
  scale_y_continuous(limits = c(0, 110), breaks = seq(0, 100, 20)) +
  labs(
    title = "U.S. Search Interest: 'Halloween costume'",
    subtitle = "Weekly Google Trends index (0–100), ~3-year window",
    x = NULL,
    y = "Search Interest Index (0–100)",
    caption = "Source: Google Trends via gtrendsR | Geography: US"
  ) +
  theme_minimal(base_size = 13) +
  theme(
    plot.title = element_text(face = "bold"),
    panel.grid.minor = element_blank()
  )

Interpretation: Interest stays near the floor for most of the year, begins rising in late summer, accelerates through September, and spikes in October — then collapses immediately after Halloween. That shape is textbook seasonality for costume demand and is exactly what campaign planners should exploit (Choi & Varian, 2012; Xu, 2026a).

4. Average Interest by Month

ggplot(monthly_avg, aes(x = month_name, y = avg_interest)) +
  geom_col(fill = "#1f4e79", width = 0.75) +
  geom_text(
    aes(label = round(avg_interest, 1)),
    vjust = -0.4,
    size = 3.2
  ) +
  scale_y_continuous(expand = expansion(mult = c(0, 0.12))) +
  labs(
    title = "Average Search Interest by Month",
    subtitle = "Mean weekly Google Trends hits aggregated by calendar month",
    x = "Month",
    y = "Average Interest Index",
    caption = "Source: Google Trends via gtrendsR | Geography: US"
  ) +
  theme_minimal(base_size = 13) +
  theme(plot.title = element_text(face = "bold"))

monthly_avg %>%
  mutate(across(where(is.numeric), ~ round(.x, 2))) %>%
  kable(caption = "Average / max / median search interest by month") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Average / max / median search interest by month
month month_name avg_interest max_interest median_interest n_obs
1 Jan 1.00 1 1 12
2 Feb 1.50 3 1 12
3 Mar 1.20 2 1 15
4 Apr 1.58 3 1 12
5 May 1.69 3 2 13
6 Jun 2.29 3 2 14
7 Jul 4.54 6 4 13
8 Aug 10.62 18 10 13
9 Sep 26.62 41 25 13
10 Oct 63.00 100 56 13
11 Nov 3.38 13 2 13
12 Dec 1.00 1 1 14
peak_month <- monthly_avg %>% slice_max(avg_interest, n = 1)
peak_week <- weekly_avg %>% slice_head(n = 1)

cat(
  "Highest average month:", as.character(peak_month$month_name),
  "| avg interest =", round(peak_month$avg_interest, 1), "\n",
  "Highest average ISO week:", peak_week$week_of_year,
  "| avg interest =", round(peak_week$avg_interest, 1), "\n",
  "Single highest week in sample:", as.character(peak_overall$date),
  "| hits =", peak_overall$hits, "\n"
)
## Highest average month: Oct | avg interest = 63 
##  Highest average ISO week: 43 | avg interest = 77.3 
##  Single highest week in sample: 2025-10-26 | hits = 100

Finding: October dominates average interest (~63), far above September (~27) and every other month. Within the year, ISO weeks 42–43 (late October) are the strongest average weeks.

5. Campaign Launch Recommendation (2–4 Weeks Before Peak)

Marketing media works best when it builds awareness before category demand peaks, so inventory, creative, and paid spend are already live when search intent surges (Choi & Varian, 2012).

# Peak weeks average ~42–43; recommend launch 2–4 weeks earlier (weeks 39–41)
# Rough calendar mapping for a typical year:
#   Week 39 ≈ late September
#   Week 40–41 ≈ early–mid October
#   Week 42–43 ≈ late October (peak)

recommend <- tibble(
  planning_item = c(
    "Peak demand window (search)",
    "Recommended campaign launch window",
    "Primary media push",
    "Wind-down"
  ),
  timing = c(
    "Late October (ISO weeks ~42–43; near Oct 25–31)",
    "Late September through early/mid October (ISO weeks ~39–41) — i.e., 2–4 weeks before peak",
    "First two weeks of October (capture rising intent before costumes sell out)",
    "After Oct 31 — interest collapses; shift remaining budget to clearance / next year learning"
  )
)

recommend %>%
  kable(caption = "Recommended seasonal campaign calendar for Halloween costumes") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = TRUE)
Recommended seasonal campaign calendar for Halloween costumes
planning_item timing
Peak demand window (search) Late October (ISO weeks ~42–43; near Oct 25–31)
Recommended campaign launch window Late September through early/mid October (ISO weeks ~39–41) — i.e., 2–4 weeks before peak
Primary media push First two weeks of October (capture rising intent before costumes sell out)
Wind-down After Oct 31 — interest collapses; shift remaining budget to clearance / next year learning
# Shade recommended launch window using month proxies for readability
ggplot(interest, aes(x = date, y = hits)) +
  geom_rect(
    aes(
      xmin = as.Date(paste0(year(date), "-09-25")),
      xmax = as.Date(paste0(year(date), "-10-15")),
      ymin = -Inf,
      ymax = Inf
    ),
    fill = "#f1c40f",
    alpha = 0.08,
    inherit.aes = FALSE
  ) +
  geom_line(color = "#1f4e79", linewidth = 0.9) +
  labs(
    title = "Recommended Launch Window Overlay",
    subtitle = "Gold bands ≈ late Sep–mid Oct each year (2–4 weeks before late-October peak)",
    x = NULL,
    y = "Search Interest Index (0–100)",
    caption = "Source: Google Trends via gtrendsR | Geography: US"
  ) +
  theme_minimal(base_size = 13) +
  theme(plot.title = element_text(face = "bold"))

Business recommendation (summary)

Launch the Halloween costume campaign in the late September to early/mid-October window (about 2–4 weeks before the late-October search peak). That timing rides the September ramp, wins consideration before peak weeks 42–43, and avoids wasting spend after November 1 when interest collapses.

6. Limitations

  1. Relative index: Hits are normalized within the query window; they are not raw search counts (Xu, 2026a).
  2. Category vs. brand: “Halloween costume” measures category demand, not a specific retailer. Brand keywords would be needed for share-of-search work (Exercise 1 style).
  3. External shocks: Weather, viral costumes, or retailer promotions can shift week-to-week peaks.
  4. API fragility: Google Trends / gtrendsR can rate-limit; attaching Excel protects reproducibility.

7. Peer Review Reminder

After publishing to RPubs, respond to two peers’ Lab 3 posts with specific suggestions (e.g., keyword choice, window length, control brand, or campaign-timing logic).

BlueSky reflection is optional and not part of official grading for this lab.

References

Choi, H., & Varian, H. (2012). Predicting the present with Google Trends. Economic Record, 88(s1), 2–9.

Massicotte, P., & Eddelbuettel, D. (2025). gtrendsR: Perform and display Google Trends queries [R package]. https://CRAN.R-project.org/package=gtrendsR

Xu, Z. (2026a). Tutorial and Case Study — Data Scraping using gtrendsR and quantmod, DiD, and Causal Inference. MSBA 580 lecture materials.

Xu, Z. (2026b). Mining the News: Real-time Sentiment and Text Analysis of Headlines with NewsAPI. MSBA 580 lecture materials. https://newsapi.org/

Silge, J., & Robinson, D. (2017). Text Mining with R: A Tidy Approach. O’Reilly. https://www.tidytextmining.com/