Social science research loves Likert scales, but this type of ordinal data can be fiddly to summarize and visualize. The aptly named 'likert' package provides easy solutions, and I was thrilled when I realised that the plots can even be customized with ggplot2 syntax!
Likert scale data
Probably the most common Likert scale question requires people to state their level of agreement or disagreement with a statement on a symmetric scale. For our analysis, let's simulate a data set from a survey with five of these Likert scale questions. We'll use the base R function sample(), which allows you to take a random sample from a list of elements.
# Response scale labels scale = c("Strongly disagree", "Disagree", "Neutral", "Agree", "Strongly agree") # Simulate data data <- data.frame( q1 = sample(scale, size = 1000, replace = TRUE, prob = c(0.07, 0.11, 0.08, 0.28, 0.46)), q2 = sample(scale, size = 1000, replace = TRUE, prob = c(0.19, 0.12, 0.11, 0.23, 0.35)), q3 = sample(scale, size = 1000, replace = TRUE, prob = c(0.15, 0.25, 0.14, 0.21, 0.25)), q4 = sample(scale, size = 1000, replace = TRUE, prob = c(0.38, 0.18, 0.13, 0.16, 0.15)), q5 = sample(scale, size = 1000, replace = TRUE, prob = c(0.46, 0.26, 0.07, 0.14, 0.07)) )
The first argument to sample() is the list of elements from which to choose. In our case, we specify the following five scale responses:
We also specify the number of samples we want to draw (1000 participants in our imagined survey) and that we want to sample with replacement (i.e., the same response option is chosen more than once). To simulate different levels of agreements for each question, we include different sets of probabilities for selecting each response option (sum = 1).
To use functions from the likert package, our variables need to be ordered factors with levels that correspond to our five scale steps.
# Set factors col_names <- names(data) data[,col_names] <- lapply(data[,col_names], factor, levels = scale)
Side note: Tidy data
All of the above is in base R because the likert package requires a data frame, but I usually prefer to work in the tidyverse. If you already have data in a tibble, you can use the fct() function from forcats to set your factor levels and then convert to a data frame with as.data.frame().
library(tidyverse) # Response scale labels scale = c("Strongly disagree", "Disagree", "Neutral", "Agree", "Strongly agree") data <- tibble %>% # set factors mutate( across( everything(), ~ fct(.x, scale) ) ) %>% # convert to data frame as.data.frame()
At the heart of the likert package is the eponymous likert() function, which makes it easy to summarize survey responses. You can even compare the responses from different groups by adding a grouping variable with the argument grouping = var
library(likert) # Summarize data data.summary <- likert(data) data.summary
The output shows what percentage of responses to each question fall into each response category:
Of course, in this case these percentages reflect the probabilities we specified while creating our simulated data.
Things get more interesting when we move on to visualization with the plot() function from the likert package. I'm a very visual thinker, so plotting always helps me to make better sense of my data. There are many ways of visualizing Likert scale data, and this package provides functions for bar plots, heat plots, and density curves. I will focus on bar plots here, because they are by far the most common and most easily understood. Depending on what we want to achieve with the visualization, the likert package gives us two useful types of bar plots to choose from.
100% stacked bars
plot(data.summary, type = "bar", centered = FALSE)
The common 0 and 100% baselines make it easy to visually compare how many people chose the two end values of the Likert scale (e.g., "Strongly disagree" or "Strongly agree") in response to each question. By default, the proportion of neutral responses are highlighted (in grey) and labelled. The numbers to the left and right of the bars indicate the total proportion of people who gave overall positive ("Agree" or "Strongly agree") or negative ("Disagree" or "Strongly disagree") responses. Even so, it is tricky to compare the neutral, overall positive, and overall negative responses in this plot, as it focuses more on the proportion of responses to each individual question that fall into the different categories (the relationships of the parts to the whole).
Centered bars with split neutrals
plot.likert(data.summary, type = "bar")
In comparison to the 100% bars, this type of plot focuses more on comparing the overall positive and negative responses to the different questions. As before, the proportions of positive, negative, and neutral responses are indicated numerically by default. However, it is difficult to compare individual response categories as none of them share a common baseline.
By default the central response category (in this case, 3 = "Neutral") is split across the center of the plot and highlighted in grey. We can add an argument center = # to the plot() function to change this if we want to treat the central category as either positive or negative, or if our data has no center (e.g., Likert scales without a neutral option, such as those indicating frequency instead of agreement). For example, center = 3.5 would place responses 1–3 left of the center and 4+ to the right. To skip plotting the central response category altogether, add include.center = FALSE.
The likert plot() function comes with some built-in options for customization, such as the legend position; colours of the low, high, and central response categories; and whether or not to label the proportions of the positive, negative, and neutral responses, or all categories separately. Amazingly, the likert plots can also be edited with ggplot2 syntax, which allows you even more flexibility, such as changing the axis and scale labels to include full text questions or item descriptions.
This is the code for the plot at the top of this post:
plot(data.summary, type = "bar", # legend legend.position = "top", # bar colours low.color = "#FDE725FF", high.color = "#21918c", # numerical labels plot.percents = TRUE, plot.percent.high = FALSE, plot.percent.low = FALSE ) + # ggplot2 syntax # change axis label labs(y = "Proportion of total responses") + # insert full-length item labels scale_x_discrete(labels = #line break after 25 characters stringr::str_wrap(questions, 25))
You can download the R script for this post from my GitHub: https://github.com/arndthen/rblog