Does the average price of LEGO sets vary significantly between the 6 LEGO themes with the most sets?
We will be using the lego_population
dataset from openintro.org. The data set has 1304 rows and 14
columns. The 2 columns relevant to our research question are:
set_name (character), which has the retail name of each
set; price (numeric), which is the MSRP of each set; and
theme (character), which is the name of the theme of the
set. A LEGO theme is a product line of LEGO sets that incorporate
characters and ideas from a particular IP. These include original LEGO
themes like LEGO City and Ninjago, and licensed themes like Star Wars
and Marvel. I chose this topic to see whether a LEGO set’s theme has an
impact on its price. Theme was a factor I was not able to explore in my
previous project.
First, let’s load in the data and start cleaning it by selecting only the columns relevant to us.
df <- read.csv('lego_population.csv')
cleaned_df <- df |>
select(set_name, price, theme, item_number)
Next, we will check that the data has loaded in properly. We are
expecting 1304 rows and 3 columns. price should be numeric,
and the other columns should be character-type.
# check number of rows and columns
str(cleaned_df)
## 'data.frame': 1304 obs. of 4 variables:
## $ set_name : chr "Extra Dots - Series 2" "Extra Dots - Series 1" "Creative Blue Bricks" "Creative Green Bricks" ...
## $ price : num 3.99 3.99 4.99 4.99 4.99 4.99 4.99 4.99 4.99 4.99 ...
## $ theme : chr "DOTS" "DOTS" "Classic" "Classic" ...
## $ item_number: int 41916 41908 11006 11007 41901 41902 41903 41911 41912 41917 ...
Everything looks good. Next let’s look at the top and bottom of the dataset to ensure the data were read in properly, things are properly formatted, and that everything is there.
# check top and bottom of data set
head(cleaned_df)
## set_name price theme item_number
## 1 Extra Dots - Series 2 3.99 DOTS 41916
## 2 Extra Dots - Series 1 3.99 DOTS 41908
## 3 Creative Blue Bricks 4.99 Classic 11006
## 4 Creative Green Bricks 4.99 Classic 11007
## 5 Funky Animals Bracelet 4.99 DOTS 41901
## 6 Sparkly Unicorn Bracelet 4.99 DOTS 41902
tail(cleaned_df)
## set_name price theme item_number
## 1299 Aurora's Forest Cottage 39.99 Disney™ 43188
## 1300 SPIKE Prime Set 329.95 LEGO® Education 45678
## 1301 Mario's House & Yoshi 29.99 LEGO® Super Mario™ 71367
## 1302 Toad's Treasure Hunt 69.99 LEGO® Super Mario™ 71368
## 1303 Bowser's Castle Boss Battle 99.99 LEGO® Super Mario™ 71369
## 1304 Propeller Mario Power-Up Pack 9.99 LEGO® Super Mario™ 71371
Great. Now we will check for missing values in any rows.
colSums(is.na(cleaned_df))
## set_name price theme item_number
## 0 239 270 0
There looks to be a significant amount of missing data. Let’s see how many rows we would have to remove if we were to forgo imputation.
sum(is.na(cleaned_df$price) | is.na(cleaned_df$theme))
## [1] 301
Removing 301 observations would remove 23.1% of our data which will
significantly affect our results. We will instead remove the rows with
missing theme information, impute the mean price for themes without any
outliers, and impute the median price for themes with outliers. We will
also add a price_is_imputed column to keep track of which
rows have imputed prices.
imputed_df <- cleaned_df |>
filter(!is.na(theme)) |>
mutate(price_is_imputed = is.na(price)) |>
group_by(theme) |>
mutate(price = ifelse(is.na(price),
ifelse(length(boxplot.stats(price)$out)>0,
median(price, na.rm = TRUE),
mean(price, na.rm = TRUE)), price))
Next, we’ll filter out any sets that are not part of the six most popular themes.
number_of_sets_per_theme <- table(imputed_df$theme)
number_of_sets_per_theme[order(number_of_sets_per_theme, decreasing = TRUE)]
##
## Star Wars™ Friends City
## 119 103 101
## NINJAGO® DUPLO® Marvel
## 78 53 50
## Disney™ BrickHeadz Creator 3-in-1
## 46 43 38
## Technic™ THE LEGO® MOVIE 2™ Harry Potter™
## 38 28 27
## Minecraft™ Classic Jurassic World™
## 26 21 20
## Hidden Side DOTS Speed Champions
## 19 18 18
## LEGO® Super Mario™ Batman™ Creator Expert
## 17 16 15
## Powered UP DC Ideas
## 14 12 12
## Juniors Monkie Kid Architecture
## 12 12 11
## Trolls World Tour LEGO® Frozen 2 Minifigures
## 9 8 8
## Overwatch® Unikitty!™ Xtra
## 8 8 8
## LEGO® Education LEGO® Art LEGO® Brick Sketches™
## 5 4 2
## Minions Powerpuff Girls™ Spider-Man
## 2 2 2
## Stranger Things
## 1
#Top 6 themes are Star Wars™, Friends, City, NINJAGO®, DUPLO®, and Marvel. Filter:
filtered_df <- imputed_df |>
filter(theme %in% c('Star Wars™', 'Friends', 'City', 'NINJAGO®', 'DUPLO®', 'Marvel'))
#Check
table(filtered_df$theme)
##
## City DUPLO® Friends Marvel NINJAGO® Star Wars™
## 101 53 103 50 78 119
Finally, let’s check how much of our final data contains imputed prices. This will be helpful when considering the limitations of our analysis.
sum(filtered_df$price_is_imputed) / length(filtered_df$set_name)
## [1] 0.01587302
~1.6% of rows in our cleaned data contain an imputed price. This is less than 2% of our data, so it is not likely to have a significant effect on our results.
Now we are ready for our ANOVA Test. An ANOVA test makes sense for
this research question because we have a continuous dependent variable
(price) measured across multiple groups
(theme). Again, we picked these six themes in particular
because they had the largest number of sets released during 2018 and
2020. Our dataset only covers sets from these years.
The following is the null hypothesis for our ANOVA test. It states that the means of all groups are equal.
H0: µ1 = µ2 = µ3 = µ4 = µ5 = µ6
The following is the alternative hypothesis for our ANOVA test. It states that at least one group mean differs from the others.
H1: µi \(\neq\) µj
#analysis
model <- aov(filtered_df$price ~ filtered_df$theme)
The ANOVA test has 5 degrees of freedom, meaning that 6 groups were tested. The F value is 4.79, which shows that the differences in mean set price between themes are fairly large relative to the in-group noise. The p-value is 0.000275, which is much less than 0.05. As such, we can reject our null hypothesis and confirm our alternative hypothesis that at least one LEGO set theme’s mean price is significantly different from the others.
Before we run any post-hoc tests, let’s check the assumptions of our ANOVA test. First, we wil check normality using a Q-Q plot.
plot(model, which = 2)
We can see that the residuals of the prices are extremely right-tailed rather than normally distributed. The model violates the normality assumption.
We will check for homogeneity-of-variance by using boxplots to visually check the range of set price by theme.
ggplot(filtered_df, aes(x = theme,
y = price)) +
geom_boxplot() +
labs(
title = "Boxplots of LEGO Set Prices by Theme",
caption = "Source: openintro.org",
x = "LEGO Set Theme",
y = "Price (USD)"
)+
theme_minimal()
We can see a roughly similar range between DUPLO, Friends, and Marvel themes, with NINJAGO and City having a larger, more right-skewed range. Star Wars has a similar IQR to City and NINJAGO, but its many high outliers expand its range much broader than any other theme. It appears that the data does not exhibit homogeny of variance, but a visual check alone is not enough proof.
We will use Levene’s Test to see if there is a statistically significant violation. Levene’s Test is a hypothesis test used to assess whether the variances of a variable are equal across two or more groups.
(Levene’s Test) Null Hypothesis (\(H_0\)): \(\sigma^2_1 = \sigma^2_2 = \ldots = \sigma^2_6\) (all variances are equal)
(Levene’s Test) Alternative Hypothesis (\(H_1\)): \(\sigma^2_i \neq \sigma^2_j\) (at least one variance is not equal to another)
# perform Levene's test
car::leveneTest(price ~ theme, data=filtered_df)
## Warning in leveneTest.default(y = y, group = group, ...): group coerced to
## factor.
## Levene's Test for Homogeneity of Variance (center = median)
## Df F value Pr(>F)
## group 5 3.4899 0.004132 **
## 498
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
The Levene’s Test p-value is 0.004132, which is much less than 0.05 and proves there is a statistically significant difference between LEGO Set theme price ranges. The homogeny-of-variance assumption is violated.
Given these violations, along with the imputed values, results should be interpreted cautiously.
We will use TukeyHSD to determine which themes’ mean set prices vary from each other the most and by how much. The difference in average price between two LEGO set themes is statistically significant if the confidence interval (lwr to upr) does not cross zero or the comparison’s p adj value is less than 0.05.
#see differences between groups and whether they are statistically significant
tukey <- TukeyHSD(model)
tukey
## Tukey multiple comparisons of means
## 95% family-wise confidence level
##
## Fit: aov(formula = filtered_df$price ~ filtered_df$theme)
##
## $`filtered_df$theme`
## diff lwr upr p adj
## DUPLO®-City -14.4156548 -39.0881296 10.256820 0.5513347
## Friends-City -14.9380948 -35.3078945 5.431705 0.2899554
## Marvel-City -7.2518812 -32.4051111 17.901349 0.9628879
## NINJAGO®-City -6.2734196 -28.1999292 15.653090 0.9640830
## Star Wars™-City 14.8015642 -4.8785742 34.481703 0.2626875
## Friends-DUPLO® -0.5224400 -25.1123380 24.067458 0.9999999
## Marvel-DUPLO® 7.1637736 -21.5140805 35.841628 0.9801502
## NINJAGO®-DUPLO® 8.1422351 -17.7518986 34.036369 0.9465175
## Star Wars™-DUPLO® 29.2172190 5.1955165 53.238921 0.0071866
## Marvel-Friends 7.6862136 -17.3860229 32.758450 0.9518826
## NINJAGO®-Friends 8.6646751 -13.1688746 30.498225 0.8664783
## Star Wars™-Friends 29.7396590 10.1631445 49.316173 0.0002423
## NINJAGO®-Marvel 0.9784615 -25.3741498 27.331073 0.9999981
## Star Wars™-Marvel 22.0534454 -2.4617774 46.568668 0.1057468
## Star Wars™-NINJAGO® 21.0749838 -0.1165946 42.266562 0.0522300
#visualize confidence intervals in a plot. Change margin size so that labels for each comparison are fully readable.
par(mar=c(5.1, 8, 4.1, 2.1))
GGTukey.2(tukey)
Our ANOVA test revealed a statistically significant difference between the average set price of the 6 LEGO themes with the most sets, allowing us to reject our null hypothesis that the mean price is equal across the 6 themes. The post-hoc Tukey HSD test identified 2 statistically significant differences: * Star Wars™ v. Friends: The average price of a Star Wars set is ~29.74 dollars higher than that of a Friends set, our most confident result. * Star Wars™ v. DUPLO®: The average price of a Star Wars set is ~29.22 dollars higher than that of a DUPLO set, our second most confident result. These were the two strongest results, with confidence intervals that did not cross zero and p-values that were well below 0.05.
Interestingly, the Tukey test revealed 13 other statistically insignificant differences (e.g. NINJAGO v. Marvel, DUPLO v. City). This suggests that price differences among non-Star Wars themes are comparatively small and could reasonably be caused by random chance rather than a systemic effect. This shows that the Star Wars theme is somewhat of an outlier among LEGO themes rather than provide evidence that licensed sets are more expensive than LEGO-original ones, since Marvel sets did not have the same statistically significant differences from the LEGO-original sets.
This makes sense when considering the sets that make up the Star Wars line. They are often complicated, high-part-count collector’s items aimed at more adult audiences. We can also see this in the boxplot of Star Wars set prices. There are many high outliers that skew the distribution to the right and bring up the theme’s mean price.
To directly answer the research question: There is a statistically significant difference between average LEGO set price across themes, although it is only between Star Wars sets and DUPLO or Friends sets. The differences between average prices among other LEGO themes are not significant.
It is worth reiterating that all results from this test should be interpreted with caution. The data violates both assumptions of normality and homogeneity-of-variance.
Possible avenues for further research include:
comparing prices of LEGO original sets with licensed sets
comparing resale value of different-themed LEGO sets on secondary markets
References: