Research Question:

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.

Data Analysis

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.

Null and Alternative Hypotheses

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.

Normality + Homogeneity-of-Variance checks

Normality

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.

Homogeny-of-variance

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.

Post-hoc test

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)

Final Results

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:

References:

Brickset API

ggplot TukeyHSD

Levene’s Test for homogeny-of-variance

Population of LEGO sets from years 2018-2020