Plotting Likert-scale survey data with R: what is soup?
Each academic year, I lead my Master’s students in English linguistics on an intellectual journey that traces the evolution from traditional structuralist semantics to contemporary cognitive linguistics. A significant moment of this journey is the exploration of Prototype semantics and how it addresses the conundrums of structuralist semantics. In this post, I show how to handle and plot in R the kind typically obtained in Prototype-semantics experiments, namely Likert-scale data.
Structuralist semantics vs. Prototype semantics
Structuralist semantics (SS) represents meanings in terms of checklists of necessary and sufficient features that must be satisfied. Although this might work well with simple concepts, problems arise with culturally marked ones.
The benefits of Prototype semantics (PS) lie in its ability to provide a more rigorous understanding of how language and categorization work. First, PS allows for the recognition of fuzzy boundaries within categories. Category membership is flexible and graded. With its all-or-nothing approach, structuralist semantics does not allow graded category membership. Second, PS also aligns with the principle of cognitive economy, according to which the human mind stores and processes information efficiently because it allows for the storage of general prototypes of concepts rather than exhaustive lists of necessary and sufficient conditions for each category. Third, PS accommodates variations in how different cultures conceptualize and categorize the world, recognizing that not all languages or cultures categorize concepts in the same way. Lastly, PS is more psychologically plausible than structuralist semantics because it aligns with the idea that human cognition relies on mental representations that are based on prototypes and exemplars rather than strict rules and definitions.
To show the benefits of PS over SS, one excellent case in point is BACHELOR. The structuralist approach, often referred to as componential semantics, is associated with Katz and Fodor (1963), who proposed a method of defining word meanings through a hierarchical organization of concepts, based on a list of semantic primitives. In this framework, BACHELOR is represented as follows:1

Although elegant, this approach fails to consider the centrality or salience of meanings in various contexts, as well as the variability in typicality within a category. In contrast, Prototype Theory, pioneered by cognitive psychologists Mervis and Rosch (1981), Rosch (1978), and Rosch & Mervis (1975), posits that categorization is subject to typicality effects. Not all members of a category have equal status with respect to the prototype of a given category. For example, a 25-year-old unmarried man will be considered a more prototypical bachelor compared to other unmarried men such as the Pope or Superman.
The soup experiment
Last year, I came upon this brilliant Short Stuff piece on YouTube by Scottish comedians Conor Reilly, Tommy Reilly and Malcolm Cumming:
In this video, Tommy grapples with the concept of ‘soup’. While most people might consider soup a straightforward dish, pinpointing its exact definition proves to be a perplexing puzzle for Tommy. His initial attempt to tackle the question, “what makes soup soup?” falls flat, as he approaches it through the lens of structuralist semantics. Frantically attempting to construct a comprehensive checklist of defining features for SOUP, Tommy’s efforts continuously hit roadblocks as he encounters exceptions at every turn. However, everything takes an intriguing turn when an anonymous letter mysteriously appears under his door, bearing the cryptic message, “you’re stirring the wrong pot.” This clever remark serves as a gentle nudge for Tommy to broaden his perspective along the lines of PS, suggesting that understanding soup requires considering the context in which it is cooked, encompassing not just culinary factors but also cultural contexts. Just as Tommy cracks the enigmatic code of soup, a chilling red laser suddenly appears, ominously targeting his forehead. This startling development leaves viewers to speculate whether some mysterious entity, perhaps the government itself, is determined to prevent Tommy from unraveling the ultimate truth about soup.
Obviously, the screenwriters must have taken a semantics course as part of their curriculum! I find this video inspiring for my students as it provides a solid foundation for understanding the benefits of a PS approach over SS.
In October 2023, using Google Forms, I designed a survey to see if, like Tommy, we could find the truth about soup. Here are the instructions:

Eighteen students participated in the experiment. They were instructed to rate 60 different soups on a Likert scale from 1 to 7, representing varying degrees of prototypicality as a ‘soup,’ as detailed in the instructions above. The results of the survey were collected in a spreadsheet (download the anonymized spreadsheet here).
Here is one example of an item they had to rate.

Why a Likert scale?
Likert-scale survey
A Likert scale is a psychometric scale commonly used in surveys and questionnaires to measure people’s attitudes, opinions, or perceptions. The scale is named after its creator, psychologist Rensis Likert. Typically, it involves a series of statements that express various levels of agreement or disagreement with a certain issue. Respondents are asked to indicate their level of agreement with each statement by selecting a point on the scale that reflects their opinion.
Usually, the scale consists of a range of response options, often five or seven, that represent different degrees of agreement or disagreement, typically ranging from “strongly agree” to “strongly disagree.” These response options are often represented as numerical values, with higher numbers indicating stronger agreement or disagreement. Alternatively, they can be represented with labels, such as “strongly disagree,” “disagree,” “neutral,” “agree,” and “strongly agree.” One thing to bear in mind is to keep the number of response options odd (3, 5, 7, etc.). Indeed, an odd number of responses ensures that a middle/neutral option is preserved.

Let me now show you how the Likert data was processed and plotted in R.
Step 1: clear the workspace
rm(list=ls(all=TRUE))
Step 2: install and load the required packages
# Install the packages (run it once)
install.packages("cowplot")
install.packages("dplyr")
install.packages("ggplot2")
install.packages("likert")
install.packages("RColorBrewer")
install.packages("tidyr")
install.packages("xlsx")
# Load the packages (run in each session)
library(cowplot)
library(dplyr)
library(ggplot2)
library(likert)
library(RColorBrewer)
library(tidyr)
library(xlsx)
cowplotoffers a flexible and consistent way to arrange multiple plots into complex arrangements.dplyrshould be known to you if you are familiar with my blog; it provides a consistent set of verbs that help in manipulating data (filtering, selecting specific columns, summarizing data, etc.);ggplot2is a widely used data visualization package that helps in creating all sorts of graphs; it is based on the grammar of graphics and provides a state-of-the-art framework for creating complex plots rather easily;likertprovides functions for handling Likert-scale data;RColorBrewerprovides a set of color palettes for creating attractive and effective plots;tidyrshould, again, be familiar to you; it is designed to help tidy messy data sets. It provides tools for changing the layout of data sets to make them easier to work with. It is particularly useful for data sets where different variables are stored in both rows and columns.xlsxallows you to read data from and writing data to Excel files; you can also interact with Excel files directly from R.
Step 3: load the data with xlsx
data <- read.xlsx("/Users/filepath/PT.experiment.data.xlsx", sheetIndex = 1)
This line reads the data from the first sheet (sheetIndex=1) of the Excel file specified in the given file path (I have used a fake path here). If you have not done it yet, download the data by clicking this link.
Step 4: inspect the data
str(data)
You should see the following:
'data.frame': 117 obs. of 60 variables:
$ Pineapple Gazpacho : num 3 3 5 7 3 2 4 5 7 3 ...
$ Italian wedding soup : num 5 7 6 5 4 1 5 3 6 3 ...
$ Beef and barley soup : num 7 7 7 6 5 2 5 4 5 5 ...
$ Potato Leek Soup : num 3 1 3 2 1 1 1 1 4 1 ...
$ Zuppa Toscana : num 4 5 5 6 5 2 4 5 1 3 ...
$ Chicken Noodle Soup : num 2 5 3 1 4 1 3 1 3 3 ...
$ Asparagus Soup : num 2 1 2 3 1 1 1 1 1 1 ...
$ Porridge : num 7 7 1 7 6 7 7 7 1 6 ...
$ Cream of asparagus soup : num 5 1 2 2 2 1 1 1 1 1 ...
$ Cucumber soup : num 3 7 3 4 2 1 2 6 2 1 ...
$ Avocado soup : num 2 3 3 1 1 1 2 4 2 1 ...
$ Chili : num 7 7 7 7 7 7 7 7 1 7 ...
$ Mulligatawny soup : num 3 1 5 1 1 2 2 3 1 1 ...
$ Sopa de Lima : num 3 7 4 3 3 3 5 4 3 2 ...
$ Shrimp and corn chowder : num 5 7 4 3 6 4 6 7 1 6 ...
$ Hot and sour soup : num 2 6 7 1 2 2 5 3 5 3 ...
$ Gumbo : num 3 4 5 5 6 2 5 5 6 3 ...
$ Gazpacho verde Green Gazpacho : num 1 3 4 3 1 2 2 5 2 1 ...
$ Matzo ball soup : num 4 7 5 6 4 1 5 6 1 3 ...
$ Pumpkin Soup : num 1 1 6 1 1 1 1 1 1 1 ...
$ French onion soup : num 1 7 7 5 3 3 2 3 7 3 ...
$ Gazpacho de Aguacate : num 3 4 5 2 5 2 2 4 1 2 ...
$ Spinach and artichoke soup : num 4 7 3 7 3 1 4 4 3 2 ...
$ Butternut squash soup : num 1 1 4 1 1 1 1 1 1 1 ...
$ Sausage and kale soup : num 6 7 7 7 5 2 5 4 3 4 ...
$ Lentil soup : num 5 6 5 7 2 1 5 2 6 4 ...
$ French pea soup : num 4 5 3 1 3 2 2 1 1 1 ...
$ Gravy : num 7 7 7 7 5 7 7 7 2 7 ...
$ Watercress Soup : num 4 4 4 7 1 1 2 1 7 1 ...
$ Cream of mushroom soup : num 2 7 3 2 3 1 2 1 2 1 ...
$ Beef stew : num 7 7 5 5 6 5 5 7 5 7 ...
$ Watermelon gazpacho : num 3 7 7 7 4 1 4 4 1 2 ...
$ New England clam chowder : num 4 5 3 7 5 3 5 7 5 2 ...
$ Irish potato soup : num 1 3 2 4 1 2 1 1 2 1 ...
$ Tomato soup : num 1 1 3 2 3 1 1 1 1 1 ...
$ Clam chowder : num 4 7 2 5 5 3 4 7 5 2 ...
$ Gazpacho : num 1 5 5 4 2 2 2 4 2 2 ...
$ Split pea soup : num 2 2 2 2 1 2 3 1 2 1 ...
$ Egg drop soup : num 5 7 4 5 3 3 6 4 5 4 ...
$ Acorn squash soup : num 3 7 5 1 1 1 2 1 1 6 ...
$ Carrot ginger soup : num 1 1 3 3 1 1 1 1 1 1 ...
$ Avgolemono Greek Lemon Soup : num 4 7 4 6 5 2 6 4 1 4 ...
$ Ramen : num 5 4 3 1 4 3 4 5 2 5 ...
$ Lobster bisque : num 6 5 2 4 3 2 2 3 4 4 ...
$ Thai Tom Yum soup : num 7 7 4 1 4 2 5 6 6 5 ...
$ Miso soup : num 5 1 4 1 3 1 4 3 4 3 ...
$ Chilled strawberry soup : num 7 7 3 7 5 1 2 6 1 4 ...
$ Minestrone : num 6 7 6 5 6 2 6 5 7 7 ...
$ Okroshka : num 5 7 3 7 3 2 5 5 6 3 ...
$ Vegetable soup : num 2 3 6 1 4 1 4 1 6 1 ...
$ Wonton soup : num 6 3 5 7 3 1 5 5 2 6 ...
$ Tortilla soup : num 7 7 4 7 5 2 7 6 2 7 ...
$ Pumpkin black bean soup : num 4 7 5 2 3 2 5 5 6 3 ...
$ Borscht : num 7 7 6 1 4 1 5 5 1 7 ...
$ Ketchup : num 7 7 6 7 7 7 7 7 1 7 ...
$ Corn and potato chowder : num 3 7 2 4 4 3 4 7 3 3 ...
$ Vichyssoise : num 3 4 2 7 2 1 2 7 1 1 ...
$ Chilled Cucumber Dill Soup : num 4 6 2 1 2 1 2 5 1 1 ...
$ Cabbage soup : num 4 7 6 4 4 1 5 5 7 2 ...
$ Chorba : num 2 7 5 4 3 1 3 5 7 2 ...
Step 5: modify column names
The read.xlsx() function replaces each space with a dot in the column names. Remove the dots using gsub():
colnames(data) <- gsub("\.", " ", colnames(data))
Step 6: prepare Likert scale labels
We define seven labels because our Likert scale contains seven levels.
lbs <- c("Very Good Example of Soup",
"Good Example of Soup", "Moderately Good Example of Soup",
"Neutral (Could be interpreted as a Soup)",
"Moderately Bad Example of Soup",
"Bad Example of Soup",
"Very Bad Example of Soup or Not a Soup at All")
Step 7: data preprocessing
We convert the data to factors, assign custom labels, drop any rows with missing values, and finally convert it back to a data frame.
data <- data %>%
dplyr::mutate_if(is.character, factor) %>%
dplyr::mutate_if(is.numeric, factor, levels = 1:7, labels = lbs) %>%
drop_na() %>% # from tidyr
as.data.frame() # from base R
Step 8: define factor levels
Because we have seven levels in our Likert scale, we define seven factor levels:
factor_levels <- c("Very Good Example of Soup",
"Good Example of Soup",
"Moderately Good Example of Soup",
"Neutral (Could be interpreted as a Soup)",
"Moderately Bad Example of Soup",
"Bad Example of Soup",
"Very Bad Example of Soup or Not a Soup at All")
Step 9: create and customize the Likert plot
This chunk generates the Likert plot using the likert function, sets the color scheme, and adds a title.
survey_p1 <- plot(likert(data), ordered = T, wrap= 60) +
scale_fill_manual(name="",
values = c("red", "#FF6600", "#FF8200", "#D6DCE4", "#44A5FF","#4472C4", "darkblue"),
breaks = factor_levels) +
ylab("") +
ggtitle("What is soup? A prototype-theory experiment")
The first line generates a Likert plot using the data provided. The likert function creates a plot based on the participants’ responses. The ordered = T argument ensures that the plot is ordered. The wrap = 60 argument determines the maximum number of characters per line for the plot. The line with values sets the color scheme for the Likert plot. It manually assigns colors to each level of the Likert scale. The name = "" argument sets an empty legend title, and the values argument specifies the colors to be used. The breaks = factor_levels argument ensures that the color breaks correspond to the levels defined earlier. ylab("") sets the y-axis label of the plot to an empty string, essentially removing the label from the y-axis. ggtitle("What is soup? A prototype-theory experiment") adds a title to the plot.
Step 10: save the plot
We use the save_plot() function from the cowplot package to save the Likert plot as a PDF file in the specified file path (I am using a fake path here):
cowplot::save_plot("/Users/Users/filepath/likert.plot.pdf",
survey_p1,
base_asp = 2,
base_height = 8)
The argument base_asp = 2 sets the aspect ratio of the plot, determining the ratio of space for questions versus space for the plot itself. A value of 2 implies that the plot will have a larger space for the questions compared to the space for the plot. base_height = 8 sets the height of the plot. A value of 8 implies that the plot will have a larger height, which results in a smaller font size.
This is what you should obtain:

The items that best illustrate the SOUP category appear in red in the bottom-left part of the plot. The worst examples appear in blue in the upper-right part of the plot. Neat!
Step 11: export the data
Here, we extract the Likert results and export the original data along with the Likert results to separate sheets in an Excel file.
data.likert <- likert(data)
data.likert.df <- data.likert$results
write.xlsx(as.data.frame(data), file = "/Users/filepath/data.count.xlsx", sheetName ="data with categories", row.names=F, append=FALSE)
write.xlsx(as.data.frame(data.likert.df), file = "/Users/filepath/data.count.xlsx", sheetName ="mean scores", row.names=F, append=TRUE)
data.likert <- likert(data) applies the likert() function to the data. In other words, it converts the input data into a format suitable for Likert analysis. The resulting data.likert is an object that contains the processed Likert data.
data.likert.df <- data.likert$results extracts the summarized results from the data.likert object. It stores these results as a data frame in the variable data.likert.df, which can be used for further analysis or visualization.
write.xlsx(as.data.frame(data), file = "/Users/filepath/data.count.xlsx", sheetName ="data with categories", row.names=F, append=FALSE) writes the original data to an Excel file named “data.count.xlsx” located at the specified file path (here, again the path is fake). The data is written to a sheet named “data with categories”. The row.names=F argument ensures that row names are not included in the data. The append=FALSE argument ensures that the file is not appended if it already exists.
write.xlsx(as.data.frame(data.likert.df), file = "/Users/filepath/data.count.xlsx", sheetName ="mean scores", row.names=F, append=TRUE) writes the summarized Likert results to the same Excel file as before but in a new sheet named “mean scores”. The row.names=F argument ensures that row names are not included in the data. The append=TRUE argument appends the data to the existing file if it already exists, rather than overwriting it.
References
Desagulier G. & Philippe Monneret. 2023. Cognitive Linguistics and a usage-based approach to the study of semantics and pragmatics. In Manuel Díaz-Campos & Sonia Balasch (Eds). The Handbook of Usage-Based Linguistics. Blackwell Publishing. pdf
Katz, Jerrold J & Jerry A Fodor. 1963. The structure of a semantic theory. Language 39(2). 170–210.
Mervis, Carolyn B & Eleanor Rosch. 1981. Categorization of natural objects. Annual review of psychology 32(1). 89–115.
Rosch, Eleanor. 1978. Principles of categorization. In Eleanor Rosch & Barbara B. Lloyd (eds.), Cognition and categorization, 27–48. Hillsdale, N.J.: Lawrence Erlbaum Associates.
Rosch, Eleanor & Carolyn B Mervis. 1975. Family resemblances: studies in the internal structure of categories. Cognitive psychology 7(4). 573–605.
- Two more specific meanings are part of the list described by Katz & Fodor (1963): [who has
the first or lowest academic degree] and [fur seal when without a mate during the breeding time]. [↩]
The text only may be used under licence Creative Commons Attribution Non Commercial 4.0 International. All other elements (illustrations, imported files) are “All rights reserved”, unless otherwise stated.
OpenEdition suggests that you cite this post as follows:
Guillaume Desagulier (October 17, 2023). Plotting Likert-scale survey data with R: what is soup? Around the word. Retrieved May 20, 2026 from https://doi.org/10.58079/n4vd


orcid.org/0000-0003-4895-0788