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)
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.
gtrendsRThe 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.
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)
| 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 |
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).
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)
| 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.
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)
| 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"))
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.
gtrendsR can rate-limit; attaching Excel protects
reproducibility.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.
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/