Data generation and analysis modules for standardized questionnaire SUS, System Usability Scale (J. Brooke, 1986).
This notebook is made by Paul Amat, amat-design.com.
Name: SUS (System Usability Scale)
Author(s)-Date: John Brooke, 1986
Principal article: Brooke, John. (1995). SUS: A quick and dirty usability scale. Usability Eval. Ind.. 189.
Link: https://www.researchgate.net/publication/228593520_SUS_A_quick_and_dirty_usability_scale
Other sources:
Bangor, A., Kortum, P., & Miller, J. (2009). Determining what individual SUS scores mean: Adding an adjective rating scale. Journal of usability studies, 4(3), 114-123. http://dl.acm.org/citation.cfm?id=2835589
Sauro, J., & Lewis, J. R. (2012). Quantifying the user experience: Practical statistics for user research. Elsevier. https://www.elsevier.com/books/quantifying-the-user-experience/unknown/978-0-12-802308-2
5-points Likert scales, Strongly disagree to Strongly agree:
Sources: Stuart Cunningham
Themes:
Source: https://uxpajournal.org/revisit-factor-structure-system-usability-scale/
# set number of participants
n <- 200
# initiate SUS variable
SUS <- list()
# initiate items variables
for (i in 1:10){
len <- length(SUS)
SUS[[len+1]] <- NA
names(SUS)[len+1] <- paste("SUS", i, sep = "")
}
prob <- c(1, 1, 1, 10, 10)
# populate SUS
for (i in 1:10) {
if (i==1 | i==3 | i==5 | i==7 | i==9) {
SUS[[i]] <- sample(1:5, n, replace = T, prob = prob)
} else if (i==2 | i==4 | i==6 | i==8 | i==10) {
SUS[[i]] <- sample(5:1, n, replace = T, prob = prob)
}
}
# make it a data frame
SUS <- as.data.frame(SUS)
# display 6 first rows
head(SUS)
## SUS1 SUS2 SUS3 SUS4 SUS5 SUS6 SUS7 SUS8 SUS9 SUS10
## 1 4 2 5 1 4 2 4 1 4 1
## 2 5 5 4 3 4 2 5 2 5 2
## 3 5 1 4 2 3 1 5 2 2 5
## 4 1 2 5 2 5 2 5 1 5 2
## 5 4 2 4 1 4 1 5 1 4 3
## 6 5 1 4 2 4 4 5 1 5 1
# display 6 first rows
knitr::kable(head(SUS), caption = "Raw SUS data (6 first rows)")
| SUS1 | SUS2 | SUS3 | SUS4 | SUS5 | SUS6 | SUS7 | SUS8 | SUS9 | SUS10 |
|---|---|---|---|---|---|---|---|---|---|
| 4 | 2 | 5 | 1 | 4 | 2 | 4 | 1 | 4 | 1 |
| 5 | 5 | 4 | 3 | 4 | 2 | 5 | 2 | 5 | 2 |
| 5 | 1 | 4 | 2 | 3 | 1 | 5 | 2 | 2 | 5 |
| 1 | 2 | 5 | 2 | 5 | 2 | 5 | 1 | 5 | 2 |
| 4 | 2 | 4 | 1 | 4 | 1 | 5 | 1 | 4 | 3 |
| 5 | 1 | 4 | 2 | 4 | 4 | 5 | 1 | 5 | 1 |
Compute total SUS score:
Compute score by themes:
Note: didn’t find official formula to compute learnability and usability. I applied the same protocol but multiplying buy 12.5 for learnability and 3.125 for usability to convert at same range as overall scoring.
Interpretation of SUS scores (grades):
Interpretation of SUS scores (acceptability):
Source: https://measuringu.com/interpret-sus-score/
Predicting NPS from SUS score :
LTR = 1.33 + 0.08(SUS)
Source: https://measuringu.com/nps-sus/
The market average SUS score is 68.
Source: https://measuringu.com/sus/
market.SUS <- 68
SUS.tr <- SUS
# Scale values from 0 to 4
for (i in 1:length(SUS)) {
if (i %% 2 == 0) { #even number
SUS.tr[[i]] <- 5 - SUS[[i]]
} else { #odd
SUS.tr[[i]] <- SUS[[i]] - 1
}
}
# initiate a Score by row variable
SUS.tr$Score <- rep(0, nrow(SUS.tr))
# sum responses by row and multiply it by 2.5
for(i in 1:nrow(SUS.tr)) {
SUS.tr[i, "Score"] <- sum(as.numeric(SUS.tr[i, 1:10])) * 2.5
}
# initiate a Grade by row variable
SUS.tr$Grade <- rep(0, nrow(SUS.tr))
# function to compute grade from score
sus.grade <- function(x) {
if (x >= 91) {
return("A")
} else if (x >= 81) {
return("B")
} else if (x >= 71) {
return("C")
} else if (x >= 61) {
return("D")
} else {
return("F")
}
}
# apply function to each row
for(i in 1:nrow(SUS.tr)) {
SUS.tr[i, "Grade"] <- sus.grade(SUS.tr[i, "Score"])
}
# make this variable a factor and order levels
SUS.tr$Grade <- factor(SUS.tr$Grade, levels = c("F", "D", "C", "B", "A"))
# initiate an Acceptability variable
SUS.tr$Acceptability <- rep(0, nrow(SUS.tr))
# function to compute variability from score
sus.acceptability <- function(x) {
if (x >= 71) {
return("Acceptable")
} else if (x >= 63) {
return("Marginal high")
} else if (x >= 50) {
return("Marginal low")
} else {
return("Not acceptable")
}
}
# apply function for each row
for(i in 1:nrow(SUS.tr)) {
SUS.tr[i, "Acceptability"] <- sus.acceptability(SUS.tr[i, "Score"])
}
# make acceptability a factor and order levels
SUS.tr$Acceptability <- factor(SUS.tr$Acceptability, levels = c("Not acceptable", "Marginal low", "Marginal high", "Acceptable"))
# initiate a Learnability variable
SUS.tr$Learnability <- rep(0, nrow(SUS.tr))
# compute learnability by row
for(i in 1:nrow(SUS.tr)) {
SUS.tr[i, "Learnability"] <- sum(as.numeric(SUS.tr[i, c(4, 10)])) * (100/(4*2))
}
# initiate a Usability variable
SUS.tr$Usability <- rep(0, nrow(SUS.tr))
# compute Usability by row
for(i in 1:nrow(SUS.tr)) {
SUS.tr[i, "Usability"] <- sum(as.numeric(SUS.tr[i, c(1:3, 5:9)])) * (100/(4*8))
}
# initiate a Usability variable
SUS.tr$LTR <- rep(0, nrow(SUS.tr))
# compute Usability by row
for(i in 1:nrow(SUS.tr)) {
SUS.tr[i, "LTR"] <- 1.33 + (0.08 * SUS.tr[i, "Score"])
}
# display 6 first rows
head(SUS.tr[,-c(1:10)])
## Score Grade Acceptability Learnability Usability LTR
## 1 85.0 B Acceptable 100.0 81.250 8.13
## 2 72.5 C Acceptable 62.5 75.000 7.13
## 3 70.0 D Marginal high 37.5 78.125 6.93
## 4 80.0 C Acceptable 75.0 81.250 7.73
## 5 82.5 B Acceptable 75.0 84.375 7.93
## 6 85.0 B Acceptable 87.5 84.375 8.13
# display 6 first rows
knitr::kable(head(SUS.tr[,-c(1:10)]), caption = "Transformed SUS data (6 first rows)")
| Score | Grade | Acceptability | Learnability | Usability | LTR |
|---|---|---|---|---|---|
| 85.0 | B | Acceptable | 100.0 | 81.250 | 8.13 |
| 72.5 | C | Acceptable | 62.5 | 75.000 | 7.13 |
| 70.0 | D | Marginal high | 37.5 | 78.125 | 6.93 |
| 80.0 | C | Acceptable | 75.0 | 81.250 | 7.73 |
| 82.5 | B | Acceptable | 75.0 | 84.375 | 7.93 |
| 85.0 | B | Acceptable | 87.5 | 84.375 | 8.13 |
n.sample <- 10000
# initializing bootstrap
table.S <- numeric(n.sample)
# loop to generate means from original data
for(i in 1:n.sample) {
table.S[i] <- mean(sample(SUS.tr$Score, 10, replace=T))
}
# sort generated means
table.S.sorted <- sort(table.S)
ci1 <- n.sample*0.025
ci2 <- n.sample - (n.sample*0.025)
# catch conf int by selecting heads and tails
IC95.b <- c(table.S.sorted[ci1], table.S.sorted[ci2])
print(paste("Total mean of Score:", round(mean(SUS.tr$Score), 1)))
## [1] "Total mean of Score: 78.5"
print(paste("95% CI for SUS Score mean (bootstrap):", round(IC95.b[1], 1), round(IC95.b[2], 1)))
## [1] "95% CI for SUS Score mean (bootstrap): 73.2 83.2"
# if (!require(ggplot2)) install.packages("ggplot2")
# library(ggplot2)
#
# ggplot(data.frame("x" = table.S), aes(x)) +
# geom_histogram() +
# geom_vline(xintercept = IC95.b[1], linetype = "dashed", colour = "LightCoral") +
# geom_vline(xintercept = IC95.b[2], linetype = "dashed", colour = "LightCoral")
# Calcul de la moyenne:
infere.m <- mean(SUS.tr$Score)
# Calcul de la taille de l’échantillon:
infere.n <- length(SUS.tr$Score)
# find the t-score of the 95th quantile of the Student t distribution with df = n-1
c.95 <- qt(1 - 0.05 / 2, infere.n - 1)
# Calcul de l’écart-type:
infere.sd <- sd(SUS.tr$Score)
# Calcul de l'erreur-type:
infere.sd.error <- infere.sd / sqrt(infere.n)
# Calcul de l’IC à 95%:
IC95.t <- c(infere.m - c.95 * infere.sd.error,
infere.m + c.95 * infere.sd.error)
print(paste("Total mean of Score:", round(mean(SUS.tr$Score), 1)))
## [1] "Total mean of Score: 78.5"
print(paste("95% CI for SUS Score mean (t):", round(IC95.t[1], 1), round(IC95.t[2], 1)))
## [1] "95% CI for SUS Score mean (t): 77.4 79.7"
if (!require(ggplot2)) install.packages("ggplot2")
## Loading required package: ggplot2
library(ggplot2)
# Scores distribution
vlines <- data.frame("Mean" = mean(SUS.tr$Score), "Market" = market.SUS)
CIrect <- data.frame("Low" = IC95.b[1], "High" = IC95.b[2])
limits <- c(0, 100)
ggplot() +
geom_histogram(data = SUS.tr, aes(Score, fill = ..x..), binwidth = 2.5) +
coord_cartesian(xlim = limits) +
scale_fill_gradient(name = "Score", low = "red", high = "green", limits = limits) +
geom_vline(data = vlines, aes(xintercept = Mean), colour = "DodgerBlue") +
geom_text(data = vlines, aes(x = Mean + 2, label="Mean/CI", y = 2.5), colour = "DodgerBlue", angle = 90) +
geom_vline(data = vlines, aes(xintercept = Market), linetype = "dashed", colour = "LightCoral") +
geom_text(data = vlines, aes(x = Market + 2, label = "Market", y = 2), colour = "LightCoral", angle = 90) +
geom_rect(aes(xmin = CIrect$Low, xmax = CIrect$High), ymin = -200, ymax = 200, fill = "DodgerBlue", alpha = 0.15) +
labs(x = "SUS scores", y = "Density", title ="Distribution of SUS scores", subtitle = paste("With market average comparison. N=", nrow(SUS), sep = ""), caption = "95% CI computed by bootstrap\nSource for market average SUS score (68) : https://measuringu.com/sus/")
## Warning: The dot-dot notation (`..x..`) was deprecated in ggplot2 3.4.0.
## ℹ Please use `after_stat(x)` instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
# Interpretation of SUS scores (grades):
#
# - 0-60: F
# - 61-70: D
# - 71-80: C
# - 81-90: B
# - 91-100: A
#
# Interpretation of SUS scores (acceptability):
#
# - 0-50: Not acceptable
# - 51-62: Marginal low
# - 63-70: Marginal high
# - 71-100: Acceptable
#
# The average SUS score is 68.
xmin <- c(0, 61, 71, 81, 91)
xmax <- c(61, 71, 81, 91, 100)
label <- c("F", "D", "C", "B", "A")
ymin <- rep(0.5, 5)
ymax <- rep(1.5, 5)
grades <- data.frame(xmin, xmax, label, ymin, ymax)
xmin <- c(0, 51, 63, 71)
xmax <- c(51, 63, 71, 100)
label <- c("Not acceptable", "Marginal low", "Marginal high", "Acceptable")
ymin <- rep(2, 4)
ymax <- rep(3, 4)
accept <- data.frame(xmin, xmax, label, ymin, ymax)
ggplot() +
coord_cartesian(xlim = c(0, 100), ylim = c(0.25, 3.25)) +
geom_rect(data = grades, mapping = aes(xmin = xmin, xmax = xmax, ymin = ymin, ymax = ymax, fill = label), alpha = 0.40) +
geom_text(data = grades, mapping = aes(x = xmax - 4, y = ymin + 0.25, label = label), alpha = 0.5) +
geom_rect(data = accept, mapping = aes(xmin = xmin, xmax = xmax, ymin = ymin, ymax = ymax, fill = label), alpha = 0.40) +
geom_text(data = accept, mapping = aes(x = xmax - 4, y = ymin + 0.05, label = label), angle = 90, hjust = 0, nudge_y = 0.1, alpha = 0.5) +
scale_fill_brewer(palette = c("RdBu"), direction = -1) +
scale_color_brewer(palette = c("RdBu"), direction = -1) +
geom_vline(data = vlines, aes(xintercept = Mean), colour = "DodgerBlue", size = 0.75) +
geom_text(data = vlines, aes(x = Mean + 2, label="Mean/CI", y = 0), colour = "DodgerBlue", angle = 90, nudge_y = 0.25, hjust = 0) +
geom_vline(data = vlines, aes(xintercept = Market), linetype = "dashed", colour = "LightCoral", size = 0.75) +
geom_text(data = vlines, aes(x = Market + 2, label = "Market", y = 0), colour = "LightCoral", angle = 90, nudge_y = 0.25, hjust = 0) +
geom_rect(aes(xmin = CIrect$Low, xmax = CIrect$High), ymin = -200, ymax = 200, fill = "DodgerBlue", alpha = 0.15) +
theme_minimal() +
theme(axis.text.y = element_blank(), axis.ticks.y = element_blank(), legend.position = "none", panel.grid.major = element_blank(), panel.grid.minor = element_blank()) +
labs(title ="SUS mean score interpretation", x = "Mean score", y = " ", subtitle = paste("With market average comparison. N=", nrow(SUS), sep = ""), caption = "95% CI computed by bootstrap\nSource for market average SUS score (68) : https://measuringu.com/sus/")
## Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
## ℹ Please use `linewidth` instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
print(paste("Total mean score:", round(mean(SUS.tr$Score), 1)))
## [1] "Total mean score: 78.5"
if (mean(SUS.tr$Score) > market.SUS) {
print(paste("Total mean score is above average SUS score for other products (", market.SUS, ").", sep = ""))
} else {
print("Total mean score is below average SUS score for other products.")
}
## [1] "Total mean score is above average SUS score for other products (68)."
print(paste("Grade of total mean score:", sus.grade(mean(SUS.tr$Score))))
## [1] "Grade of total mean score: C"
print(paste("Acceptability of total mean score:", sus.acceptability(mean(SUS.tr$Score))))
## [1] "Acceptability of total mean score: Acceptable"
NPS <- round(mean(SUS.tr$LTR), 0)
print(paste("NPS score prediction:", NPS))
## [1] "NPS score prediction: 8"
NPS.interpret <- NA
if (NPS > 8) {
NPS.interpret <- "Promoters"
} else if (NPS > 6) {
NPS.interpret <- "Passives"
} else {
NPS.interpret <- "Detractors"
}
print(paste("NPS interpretation:", NPS.interpret))
## [1] "NPS interpretation: Passives"
print(paste("Learnability mean score:", round(mean(SUS.tr$Learnability), 1)))
## [1] "Learnability mean score: 78.9"
print(paste("Usability mean score:", round(mean(SUS.tr$Usability), 1)))
## [1] "Usability mean score: 78.4"