library(tidyverse)
library(broom)
library(knitr)
library(lattice)
library(ggplot2)
library(readr)
options(scipen = 999)
This lab explores the Los Angeles Dodgers attendance dataset and builds a marketing response model. The primary question is whether promotions, especially bobbleheads and fireworks, help explain game attendance. The report contains exploratory data analysis, visualizations, regression models and a prediction for a July Saturday game without a promotion.
DodgersData <- read_csv("https://raw.githubusercontent.com/mtpa/mds/refs/heads/master/MDS_Chapter_8/dodgers.csv")
head(DodgersData)
## # A tibble: 6 × 12
## month day attend day_of_week opponent temp skies day_night cap shirt
## <chr> <dbl> <dbl> <chr> <chr> <dbl> <chr> <chr> <chr> <chr>
## 1 APR 10 56000 Tuesday Pirates 67 Clear Day NO NO
## 2 APR 11 29729 Wednesday Pirates 58 Cloudy Night NO NO
## 3 APR 12 28328 Thursday Pirates 57 Cloudy Night NO NO
## 4 APR 13 31601 Friday Padres 54 Cloudy Night NO NO
## 5 APR 14 46549 Saturday Padres 57 Cloudy Night NO NO
## 6 APR 15 38359 Sunday Padres 65 Clear Day NO NO
## # ℹ 2 more variables: fireworks <chr>, bobblehead <chr>
str(DodgersData)
## spc_tbl_ [81 × 12] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
## $ month : chr [1:81] "APR" "APR" "APR" "APR" ...
## $ day : num [1:81] 10 11 12 13 14 15 23 24 25 27 ...
## $ attend : num [1:81] 56000 29729 28328 31601 46549 ...
## $ day_of_week: chr [1:81] "Tuesday" "Wednesday" "Thursday" "Friday" ...
## $ opponent : chr [1:81] "Pirates" "Pirates" "Pirates" "Padres" ...
## $ temp : num [1:81] 67 58 57 54 57 65 60 63 64 66 ...
## $ skies : chr [1:81] "Clear" "Cloudy" "Cloudy" "Cloudy" ...
## $ day_night : chr [1:81] "Day" "Night" "Night" "Night" ...
## $ cap : chr [1:81] "NO" "NO" "NO" "NO" ...
## $ shirt : chr [1:81] "NO" "NO" "NO" "NO" ...
## $ fireworks : chr [1:81] "NO" "NO" "NO" "YES" ...
## $ bobblehead : chr [1:81] "NO" "NO" "NO" "NO" ...
## - attr(*, "spec")=
## .. cols(
## .. month = col_character(),
## .. day = col_double(),
## .. attend = col_double(),
## .. day_of_week = col_character(),
## .. opponent = col_character(),
## .. temp = col_double(),
## .. skies = col_character(),
## .. day_night = col_character(),
## .. cap = col_character(),
## .. shirt = col_character(),
## .. fireworks = col_character(),
## .. bobblehead = col_character()
## .. )
## - attr(*, "problems")=<externalptr>
glimpse(DodgersData)
## Rows: 81
## Columns: 12
## $ month <chr> "APR", "APR", "APR", "APR", "APR", "APR", "APR", "APR", "A…
## $ day <dbl> 10, 11, 12, 13, 14, 15, 23, 24, 25, 27, 28, 29, 7, 8, 9, 1…
## $ attend <dbl> 56000, 29729, 28328, 31601, 46549, 38359, 26376, 44014, 26…
## $ day_of_week <chr> "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", …
## $ opponent <chr> "Pirates", "Pirates", "Pirates", "Padres", "Padres", "Padr…
## $ temp <dbl> 67, 58, 57, 54, 57, 65, 60, 63, 64, 66, 71, 74, 67, 75, 71…
## $ skies <chr> "Clear", "Cloudy", "Cloudy", "Cloudy", "Cloudy", "Clear", …
## $ day_night <chr> "Day", "Night", "Night", "Night", "Night", "Day", "Night",…
## $ cap <chr> "NO", "NO", "NO", "NO", "NO", "NO", "NO", "NO", "NO", "NO"…
## $ shirt <chr> "NO", "NO", "NO", "NO", "NO", "NO", "NO", "NO", "NO", "NO"…
## $ fireworks <chr> "NO", "NO", "NO", "YES", "NO", "NO", "NO", "NO", "NO", "YE…
## $ bobblehead <chr> "NO", "NO", "NO", "NO", "NO", "NO", "NO", "NO", "NO", "NO"…
The Dodgers dataset includes game attendance, month, day of the week, opponent, temperature, weather, day or night game, and promotional variables.
The month and day of week variables need to be converted into factors. This helps R understand that they are categories instead of regular text.
DodgersData$day_of_week <- factor(DodgersData$day_of_week)
DodgersData$month <- factor(DodgersData$month)
levels(DodgersData$day_of_week)
## [1] "Friday" "Monday" "Saturday" "Sunday" "Thursday" "Tuesday"
## [7] "Wednesday"
levels(DodgersData$month)
## [1] "APR" "AUG" "JUL" "JUN" "MAY" "OCT" "SEP"
head(DodgersData, 10)
## # A tibble: 10 × 12
## month day attend day_of_week opponent temp skies day_night cap shirt
## <fct> <dbl> <dbl> <fct> <chr> <dbl> <chr> <chr> <chr> <chr>
## 1 APR 10 56000 Tuesday Pirates 67 Clear Day NO NO
## 2 APR 11 29729 Wednesday Pirates 58 Cloudy Night NO NO
## 3 APR 12 28328 Thursday Pirates 57 Cloudy Night NO NO
## 4 APR 13 31601 Friday Padres 54 Cloudy Night NO NO
## 5 APR 14 46549 Saturday Padres 57 Cloudy Night NO NO
## 6 APR 15 38359 Sunday Padres 65 Clear Day NO NO
## 7 APR 23 26376 Monday Braves 60 Cloudy Night NO NO
## 8 APR 24 44014 Tuesday Braves 63 Cloudy Night NO NO
## 9 APR 25 26345 Wednesday Braves 64 Cloudy Night NO NO
## 10 APR 27 44807 Friday Nationals 66 Clear Night NO NO
## # ℹ 2 more variables: fireworks <chr>, bobblehead <chr>
DodgersData[20, c("temp", "attend", "opponent", "bobblehead")]
## # A tibble: 1 × 4
## temp attend opponent bobblehead
## <dbl> <dbl> <chr> <chr>
## 1 70 47077 Snakes YES
meanattend <- mean(DodgersData$attend)
meanattend
## [1] 41040.07
promotions <- sum(DodgersData$bobblehead == "YES")
promotions
## [1] 11
For EDA, I created a separate dataset called EDAdata.
This lets me convert YES/NO promotion columns into 1/0 values for easier
counting.
EDAdata <- DodgersData
EDAdata$cap <- ifelse(EDAdata$cap == "YES", 1, 0)
EDAdata$shirt <- ifelse(EDAdata$shirt == "YES", 1, 0)
EDAdata$fireworks <- ifelse(EDAdata$fireworks == "YES", 1, 0)
EDAdata$bobblehead <- ifelse(EDAdata$bobblehead == "YES", 1, 0)
promotion_counts <- colSums(EDAdata[, c("cap", "shirt", "fireworks", "bobblehead")])
promotion_counts
## cap shirt fireworks bobblehead
## 2 3 14 11
table(DodgersData$month)
##
## APR AUG JUL JUN MAY OCT SEP
## 12 15 12 9 18 3 12
table(DodgersData$day_of_week)
##
## Friday Monday Saturday Sunday Thursday Tuesday Wednesday
## 13 12 13 13 5 13 12
summary(EDAdata$attend)
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 24312 34493 40284 41040 46588 56000
max(EDAdata$attend)
## [1] 56000
sum(EDAdata$attend >= 56000)
## [1] 2
This summary shows the overall attendance range for the Dodgers games. It also checks how many games reached at least 56,000 fans, which is close to a full stadium.
sum(EDAdata$bobblehead == 1 &
EDAdata$day_of_week == "Tuesday" &
EDAdata$day_night == "Night")
## [1] 6
The results show that the dataset includes several types of promotions, including caps, shirts, fireworks, and bobbleheads. Cap and shirt promotions appear less often, so they may not be as useful for modeling. Fireworks and bobblehead promotions happen more often, so they are better variables to test in the regression model.
ggplot(DodgersData, aes(x = temp, y = attend / 1000, color = fireworks)) +
geom_point(size = 2) +
facet_wrap(day_night ~ skies) +
ggtitle("Dodgers Attendance By Temperature, Time of Game, and Skies") +
theme(plot.title = element_text(lineheight = 3, face = "bold", color = "black", size = 10)) +
xlab("Temperature (Degrees Fahrenheit)") +
ylab("Attendance (Thousands)")
This plot compares attendance by temperature, weather, time of game, and fireworks promotion. It helps show whether hotter or clearer games had different attendance patterns.
ggplot(DodgersData, aes(x = attend / 1000, y = opponent, color = day_night)) +
geom_point(size = 2) +
ggtitle("Dodgers Attendance By Opponent") +
theme(plot.title = element_text(lineheight = 3, face = "bold", color = "black", size = 10)) +
xlab("Attendance (Thousands)") +
ylab("Opponent / Visiting Team")
This strip plot shows attendance by opponent. Some opponents may attract larger crowds than others, and this may also depend on whether the game was played during the day or night.
ggplot(DodgersData, aes(x = month, y = attend)) +
geom_boxplot() +
labs(
title = "Dodgers Attendance by Month",
x = "Month",
y = "Attendance"
)
ggplot(DodgersData, aes(x = day_of_week, y = attend)) +
geom_boxplot() +
labs(
title = "Dodgers Attendance by Day of Week",
x = "Day of Week",
y = "Attendance"
) +
theme(axis.text.x = element_text(angle = 45, hjust = 1))
To advise management if promotions impact attendance, I first created a model using month, day of week, and bobblehead promotion.
my.model <- attend ~ month + day_of_week + bobblehead
my.model.fit <- lm(my.model, data = DodgersData)
print(summary(my.model.fit))
##
## Call:
## lm(formula = my.model, data = DodgersData)
##
## Residuals:
## Min 1Q Median 3Q Max
## -10786.5 -3628.1 -516.1 2230.2 14351.0
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 38792.98 2364.68 16.405 < 0.0000000000000002 ***
## monthAUG 2377.92 2402.91 0.990 0.3259
## monthJUL 2849.83 2578.60 1.105 0.2730
## monthJUN 7163.23 2732.72 2.621 0.0108 *
## monthMAY -2385.62 2291.22 -1.041 0.3015
## monthOCT -662.67 4046.45 -0.164 0.8704
## monthSEP 29.03 2521.25 0.012 0.9908
## day_of_weekMonday -4883.82 2504.65 -1.950 0.0554 .
## day_of_weekSaturday 1488.24 2442.68 0.609 0.5444
## day_of_weekSunday 1840.18 2426.79 0.758 0.4509
## day_of_weekThursday -4108.45 3381.22 -1.215 0.2286
## day_of_weekTuesday 3027.68 2686.43 1.127 0.2638
## day_of_weekWednesday -2423.80 2485.46 -0.975 0.3330
## bobbleheadYES 10714.90 2419.52 4.429 0.0000359 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 6120 on 67 degrees of freedom
## Multiple R-squared: 0.5444, Adjusted R-squared: 0.456
## F-statistic: 6.158 on 13 and 67 DF, p-value: 0.0000002083
The bobblehead coefficient is the main promotion variable in this model. If the coefficient is positive, then bobblehead games are associated with higher attendance compared with games that did not have bobbleheads.
DodgersData$month <- factor(
DodgersData$month,
levels = c("APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT")
)
DodgersData$day_of_week <- factor(
DodgersData$day_of_week,
levels = c("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday")
)
levels(DodgersData$month)
## [1] "APR" "MAY" "JUN" "JUL" "AUG" "SEP" "OCT"
levels(DodgersData$day_of_week)
## [1] "Monday" "Tuesday" "Wednesday" "Thursday" "Friday" "Saturday"
## [7] "Sunday"
my.model2 <- attend ~ month + day_of_week + bobblehead
my.model.fit2 <- lm(my.model2, data = DodgersData)
print(summary(my.model.fit2))
##
## Call:
## lm(formula = my.model2, data = DodgersData)
##
## Residuals:
## Min 1Q Median 3Q Max
## -10786.5 -3628.1 -516.1 2230.2 14351.0
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 33909.16 2521.81 13.446 < 0.0000000000000002 ***
## monthMAY -2385.62 2291.22 -1.041 0.30152
## monthJUN 7163.23 2732.72 2.621 0.01083 *
## monthJUL 2849.83 2578.60 1.105 0.27303
## monthAUG 2377.92 2402.91 0.990 0.32593
## monthSEP 29.03 2521.25 0.012 0.99085
## monthOCT -662.67 4046.45 -0.164 0.87041
## day_of_weekTuesday 7911.49 2702.21 2.928 0.00466 **
## day_of_weekWednesday 2460.02 2514.03 0.979 0.33134
## day_of_weekThursday 775.36 3486.15 0.222 0.82467
## day_of_weekFriday 4883.82 2504.65 1.950 0.05537 .
## day_of_weekSaturday 6372.06 2552.08 2.497 0.01500 *
## day_of_weekSunday 6724.00 2506.72 2.682 0.00920 **
## bobbleheadYES 10714.90 2419.52 4.429 0.0000359 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 6120 on 67 degrees of freedom
## Multiple R-squared: 0.5444, Adjusted R-squared: 0.456
## F-statistic: 6.158 on 13 and 67 DF, p-value: 0.0000002083
tidy(my.model.fit2) %>%
kable(
caption = "Model 1: Attendance Predicted by Month, Day of Week, and Bobblehead",
digits = 3
)
| term | estimate | std.error | statistic | p.value |
|---|---|---|---|---|
| (Intercept) | 33909.162 | 2521.806 | 13.446 | 0.000 |
| monthMAY | -2385.625 | 2291.216 | -1.041 | 0.302 |
| monthJUN | 7163.234 | 2732.721 | 2.621 | 0.011 |
| monthJUL | 2849.828 | 2578.600 | 1.105 | 0.273 |
| monthAUG | 2377.924 | 2402.915 | 0.990 | 0.326 |
| monthSEP | 29.030 | 2521.249 | 0.012 | 0.991 |
| monthOCT | -662.668 | 4046.452 | -0.164 | 0.870 |
| day_of_weekTuesday | 7911.494 | 2702.208 | 2.928 | 0.005 |
| day_of_weekWednesday | 2460.023 | 2514.029 | 0.979 | 0.331 |
| day_of_weekThursday | 775.364 | 3486.154 | 0.222 | 0.825 |
| day_of_weekFriday | 4883.818 | 2504.653 | 1.950 | 0.055 |
| day_of_weekSaturday | 6372.056 | 2552.084 | 2.497 | 0.015 |
| day_of_weekSunday | 6724.003 | 2506.721 | 2.682 | 0.009 |
| bobbleheadYES | 10714.903 | 2419.520 | 4.429 | 0.000 |
glance(my.model.fit2) %>%
select(r.squared, adj.r.squared, sigma, statistic, p.value, AIC, BIC) %>%
kable(
caption = "Model 1 Fit Statistics",
digits = 3
)
| r.squared | adj.r.squared | sigma | statistic | p.value | AIC | BIC |
|---|---|---|---|---|---|---|
| 0.544 | 0.456 | 6120.158 | 6.158 | 0 | 1657.031 | 1692.948 |
I added fireworks to the model to see if it improves the prediction of attendance as a secondary promotional variable.
my.model3 <- attend ~ month + day_of_week + fireworks + bobblehead
my.model.fit3 <- lm(my.model3, data = DodgersData)
print(summary(my.model.fit3))
##
## Call:
## lm(formula = my.model3, data = DodgersData)
##
## Residuals:
## Min 1Q Median 3Q Max
## -9504 -3683 -709 2569 15390
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 34321.68 2418.90 14.189 < 0.0000000000000002 ***
## monthMAY -2492.79 2193.60 -1.136 0.25990
## monthJUN 7062.62 2616.13 2.700 0.00881 **
## monthJUL 1315.38 2534.42 0.519 0.60549
## monthAUG 2377.88 2300.15 1.034 0.30501
## monthSEP -55.37 2413.63 -0.023 0.98177
## monthOCT -502.88 3873.86 -0.130 0.89711
## day_of_weekTuesday 7750.46 2587.35 2.996 0.00386 **
## day_of_weekWednesday 904.16 2476.14 0.365 0.71617
## day_of_weekThursday 309.24 3341.63 0.093 0.92655
## day_of_weekFriday -12386.23 6901.86 -1.795 0.07729 .
## day_of_weekSaturday 6094.17 2445.16 2.492 0.01521 *
## day_of_weekSunday 6577.96 2400.14 2.741 0.00788 **
## fireworksYES 17028.78 6381.63 2.668 0.00958 **
## bobbleheadYES 10995.05 2318.43 4.742 0.0000117 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 5858 on 66 degrees of freedom
## Multiple R-squared: 0.5887, Adjusted R-squared: 0.5015
## F-statistic: 6.749 on 14 and 66 DF, p-value: 0.00000002848
tidy(my.model.fit3) %>%
kable(
caption = "Model 2: Attendance Predicted by Month, Day of Week, Fireworks, and Bobblehead",
digits = 3
)
| term | estimate | std.error | statistic | p.value |
|---|---|---|---|---|
| (Intercept) | 34321.675 | 2418.904 | 14.189 | 0.000 |
| monthMAY | -2492.791 | 2193.598 | -1.136 | 0.260 |
| monthJUN | 7062.618 | 2616.126 | 2.700 | 0.009 |
| monthJUL | 1315.384 | 2534.422 | 0.519 | 0.605 |
| monthAUG | 2377.877 | 2300.152 | 1.034 | 0.305 |
| monthSEP | -55.371 | 2413.633 | -0.023 | 0.982 |
| monthOCT | -502.883 | 3873.864 | -0.130 | 0.897 |
| day_of_weekTuesday | 7750.462 | 2587.349 | 2.996 | 0.004 |
| day_of_weekWednesday | 904.161 | 2476.142 | 0.365 | 0.716 |
| day_of_weekThursday | 309.237 | 3341.635 | 0.093 | 0.927 |
| day_of_weekFriday | -12386.234 | 6901.856 | -1.795 | 0.077 |
| day_of_weekSaturday | 6094.171 | 2445.160 | 2.492 | 0.015 |
| day_of_weekSunday | 6577.963 | 2400.143 | 2.741 | 0.008 |
| fireworksYES | 17028.779 | 6381.631 | 2.668 | 0.010 |
| bobbleheadYES | 10995.053 | 2318.426 | 4.742 | 0.000 |
tidy(my.model.fit3) %>%
filter(term == "fireworksYES") %>%
kable(
caption = "Fireworks Coefficient",
digits = 3
)
| term | estimate | std.error | statistic | p.value |
|---|---|---|---|---|
| fireworksYES | 17028.78 | 6381.631 | 2.668 | 0.01 |
If the fireworks estimate is positive, fireworks games are associated with higher attendance. If the p-value is high, fireworks does not appear to have a strong statistical effect after controlling for month, day of week, and bobblehead promotions.
model_comparison <- bind_rows(
glance(my.model.fit2) %>% mutate(model = "Model 1: Month + Day + Bobblehead"),
glance(my.model.fit3) %>% mutate(model = "Model 2: Added Fireworks")
) %>%
select(model, r.squared, adj.r.squared, AIC, BIC, p.value)
model_comparison %>%
kable(
caption = "Model Comparison",
digits = 3
)
| model | r.squared | adj.r.squared | AIC | BIC | p.value |
|---|---|---|---|---|---|
| Model 1: Month + Day + Bobblehead | 0.544 | 0.456 | 1657.031 | 1692.948 | 0 |
| Model 2: Added Fireworks | 0.589 | 0.502 | 1650.733 | 1689.044 | 0 |
anova(my.model.fit2, my.model.fit3)
## Analysis of Variance Table
##
## Model 1: attend ~ month + day_of_week + bobblehead
## Model 2: attend ~ month + day_of_week + fireworks + bobblehead
## Res.Df RSS Df Sum of Sq F Pr(>F)
## 1 67 2509574563
## 2 66 2265194779 1 244379784 7.1204 0.009581 **
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
The model comparison helps decide whether fireworks improves the model. If adjusted R-squared improves and the fireworks p-value is low, then fireworks matters. If adjusted R-squared does not improve and the p-value is high, then fireworks does not add much value.
Predicted attendance for a Saturday afternoon in July with no promotion.
new_no <- data.frame(
month = factor("JUL", levels = levels(DodgersData$month)),
day_of_week = factor("Saturday", levels = levels(DodgersData$day_of_week)),
bobblehead = factor("NO", levels = c("NO", "YES"))
)
pred_no <- predict(
my.model.fit2,
newdata = new_no,
interval = "prediction"
)
pred_no
## fit lwr upr
## 1 43131.05 29866.75 56395.35
cat("Predicted attendance with no promotion:", round(pred_no[1]), "\n")
## Predicted attendance with no promotion: 43131
The regression results indicate a positive relationship between Dodgers attendance and bobblehead promotions. Fireworks should be thought of as useful only when they have a low p-value, and they improve the model after inclusion. For business, the Dodgers should target their promotions on games that are expected to be weaker attended, as opposed to only doing promotions on games that are expected to sell well.
Los Angeles Dodgers attendance dataset from the MDS Chapter 8 GitHub
repository:
https://github.com/mtpa/mds/tree/master/MDS_Chapter_8
R Documentation for Linear Models:
https://www.rdocumentation.org/packages/stats/versions/3.6.2/topics/lm
Tidyverse Documentation:
https://www.tidyverse.org/