Data analysis for standardized questionnaire AttrakDiff (M. Hassenzahl, M. Burmester, F. Koller, 2004).
Educational notebook made by Paul Amat.
Name: AttrakDiff
Author(s)-Date: Marc Hassenzahl, Michael Burmester, F. Koller, 2003
Principal article: Hassenzahl, Marc et al. “AttrakDiff: Ein Fragebogen zur Messung wahrgenommener hedonischer und pragmatischer Qualität.” MuC (2003).
Other sources:
French version: Lallemand, C., Koenig, V., Gronier, G., & Martin, R. (2015). Création et validation d’une version française du questionnaire AttrakDiff pour l’évaluation de l’expérience utilisateur des systèmes interactifs. Revue Européenne de Psychologie Appliquée/European Review of Applied Psychology, 65(5), 239-252. Chicago https://www.sciencedirect.com/science/article/abs/pii/S1162908815000651?via%3Dihub
7-points Likert scales, -3 to 3 with a 0 step.
Themes are:
In the passation order:
Inverted items:
Sources: La Grande Ourse
# initialize data frame
AttrakDiff <- list()
# list themes
theme <- c("QP", "QHI", "ATT", "QHS")
# generate 7 named empty columns for each themes
for (i in 1:4){
for (j in 1:7){
len <- length(AttrakDiff)
AttrakDiff[[len+1]] <- NA
names(AttrakDiff)[len+1] <- paste(theme[i], j, sep = "_")
}
}
# set number of participants
n <- 200
# populate with generated data
for (i in 1:length(AttrakDiff)){
if (i %in% c(1:7)){
AttrakDiff[[i]] <- sample(-3:3, n, replace = T, prob = c(1, 1, 1, 2, 3, 6, 4)) #QP
} else if (i %in% c(8:14)){
AttrakDiff[[i]] <- sample(-3:3, n, replace = T, prob = c(1, 3, 6, 2, 2, 1, 1)) #QHI
} else if (i %in% c(9:21)){
AttrakDiff[[i]] <- sample(-3:3, n, replace = T, prob = c(1, 1, 4, 6, 3, 2, 1)) #ATT
} else if (i %in% c(22:28)){
AttrakDiff[[i]] <- sample(-3:3, n, replace = T, prob = c(1, 2, 4, 6, 2, 1, 1)) #QHS
}
}
# list inverted items by name
inverted <- c("QP_1", "ATT_1", "QHS_1", "QP_1", "ATT_1", "QHS_1", "QP_2", "QHI_2",
"QP_3", "ATT_3", "QHI_3", "QP_5", "QHI_6", "ATT_5", "QHS_3", "QHS_4",
"ATT_7", "QHS_7")
# invert items
for (i in 1:length(inverted)){
AttrakDiff[[inverted[i]]] <- AttrakDiff[[inverted[i]]] * -1
}
# make it a data frame
AttrakDiff <- as.data.frame(AttrakDiff)
# reorder columns as the default passation order
AttrakDiff <- AttrakDiff[ , c(
"QP_1", "QHI_1", "ATT_1", "QHS_1", "QP_2", "QHI_2", "ATT_2", "QP_3", "ATT_3", "QP_4", "QHI_3", "QP_5", "QHI_4", "QHI_5", "QHI_6", "QHI_7", "ATT_4", "QHS_2", "ATT_5", "QP_6", "ATT_6", "QHS_3", "QHS_4", "QHS_5", "QHS_6", "ATT_7", "QHS_7", "QP_7"
)]
head(AttrakDiff)
## QP_1 QHI_1 ATT_1 QHS_1 QP_2 QHI_2 ATT_2 QP_3 ATT_3 QP_4 QHI_3 QP_5 QHI_4
## 1 1 -2 -1 1 3 2 -3 1 -1 2 -1 -1 0
## 2 2 -2 0 0 -3 2 -2 -3 0 -2 1 -3 -1
## 3 2 1 3 0 1 1 1 -2 3 3 2 -2 -2
## 4 -1 2 -1 0 0 1 0 -1 -3 1 1 -3 -2
## 5 0 -1 -1 0 0 1 3 3 0 0 2 -3 -1
## 6 1 1 -1 -1 -3 -2 -3 0 0 1 1 -2 0
## QHI_5 QHI_6 QHI_7 ATT_4 QHS_2 ATT_5 QP_6 ATT_6 QHS_3 QHS_4 QHS_5 QHS_6 ATT_7
## 1 -1 1 3 0 1 1 3 1 -1 -3 -1 -1 3
## 2 1 3 -1 1 1 0 0 0 0 1 -1 0 1
## 3 -1 -1 1 0 -1 -2 2 0 0 -3 -1 0 0
## 4 -1 2 -1 0 0 1 -3 1 3 -2 0 0 1
## 5 -1 3 3 0 0 0 2 3 0 0 1 2 1
## 6 2 3 0 2 -1 3 1 0 2 2 -2 -1 3
## QHS_7 QP_7
## 1 1 2
## 2 0 2
## 3 2 -2
## 4 -2 3
## 5 1 3
## 6 -1 1
write.csv(head(AttrakDiff), "AttrakDiff.csv", row.names=FALSE)
# display 6 first rows
knitr::kable(head(AttrakDiff), caption = "Raw AttrakDiff data (6 first rows)")
| QP_1 | QHI_1 | ATT_1 | QHS_1 | QP_2 | QHI_2 | ATT_2 | QP_3 | ATT_3 | QP_4 | QHI_3 | QP_5 | QHI_4 | QHI_5 | QHI_6 | QHI_7 | ATT_4 | QHS_2 | ATT_5 | QP_6 | ATT_6 | QHS_3 | QHS_4 | QHS_5 | QHS_6 | ATT_7 | QHS_7 | QP_7 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | -2 | -1 | 1 | 3 | 2 | -3 | 1 | -1 | 2 | -1 | -1 | 0 | -1 | 1 | 3 | 0 | 1 | 1 | 3 | 1 | -1 | -3 | -1 | -1 | 3 | 1 | 2 |
| 2 | -2 | 0 | 0 | -3 | 2 | -2 | -3 | 0 | -2 | 1 | -3 | -1 | 1 | 3 | -1 | 1 | 1 | 0 | 0 | 0 | 0 | 1 | -1 | 0 | 1 | 0 | 2 |
| 2 | 1 | 3 | 0 | 1 | 1 | 1 | -2 | 3 | 3 | 2 | -2 | -2 | -1 | -1 | 1 | 0 | -1 | -2 | 2 | 0 | 0 | -3 | -1 | 0 | 0 | 2 | -2 |
| -1 | 2 | -1 | 0 | 0 | 1 | 0 | -1 | -3 | 1 | 1 | -3 | -2 | -1 | 2 | -1 | 0 | 0 | 1 | -3 | 1 | 3 | -2 | 0 | 0 | 1 | -2 | 3 |
| 0 | -1 | -1 | 0 | 0 | 1 | 3 | 3 | 0 | 0 | 2 | -3 | -1 | -1 | 3 | 3 | 0 | 0 | 0 | 2 | 3 | 0 | 0 | 1 | 2 | 1 | 1 | 3 |
| 1 | 1 | -1 | -1 | -3 | -2 | -3 | 0 | 0 | 1 | 1 | -2 | 0 | 2 | 3 | 0 | 2 | -1 | 3 | 1 | 0 | 2 | 2 | -2 | -1 | 3 | -1 | 1 |
Sources: La Grande Ourse
# duplicate raw data to transform without lost
AttrakDiff.tr <- AttrakDiff
# transform inverted items
for (i in 1:length(inverted)){
AttrakDiff.tr[[inverted[i]]] <- AttrakDiff.tr[[inverted[i]]] * -1
}
AttrakDiff.tr.margins <- data.frame()
# add a column by themes and compute themes means by row
for (i in 1:length(theme)) {
for (j in 1:nrow(AttrakDiff.tr)) {
AttrakDiff.tr.margins[j, paste("mean", theme[i], sep = "_")] <- mean(
as.numeric(
#select(AttrakDiff.tr, starts_with(theme[i]))[j, ]
AttrakDiff.tr[j, grepl(theme[i], names(AttrakDiff.tr))]
)
)
}
}
# compute QH means by row
for (j in 1:nrow(AttrakDiff.tr.margins)) {
AttrakDiff.tr.margins[j, paste("mean", "QH", sep = "_")] <- mean(
as.numeric(
AttrakDiff.tr[j, grepl("QH", names(AttrakDiff.tr))]
)
)
}
# display 6 first rows
head(AttrakDiff.tr.margins)
## mean_QP mean_QHI mean_ATT mean_QHS mean_QH
## 1 0.7142857 -0.2857143 -0.8571429 0.4285714 0.07142857
## 2 1.5714286 -1.2857143 -0.2857143 -0.1428571 -0.71428571
## 3 1.1428571 -0.4285714 0.4285714 -0.1428571 -0.28571429
## 4 0.5714286 -0.8571429 0.1428571 0.1428571 -0.35714286
## 5 0.7142857 -0.8571429 0.5714286 0.2857143 -0.28571429
## 6 1.2857143 0.1428571 -1.1428571 -1.1428571 -0.50000000
# display 6 first rows
knitr::kable(head(AttrakDiff.tr.margins), caption = "Transformed AttrakDiff data (6 first rows)")
| mean_QP | mean_QHI | mean_ATT | mean_QHS | mean_QH |
|---|---|---|---|---|
| 0.7142857 | -0.2857143 | -0.8571429 | 0.4285714 | 0.0714286 |
| 1.5714286 | -1.2857143 | -0.2857143 | -0.1428571 | -0.7142857 |
| 1.1428571 | -0.4285714 | 0.4285714 | -0.1428571 | -0.2857143 |
| 0.5714286 | -0.8571429 | 0.1428571 | 0.1428571 | -0.3571429 |
| 0.7142857 | -0.8571429 | 0.5714286 | 0.2857143 | -0.2857143 |
| 1.2857143 | 0.1428571 | -1.1428571 | -1.1428571 | -0.5000000 |
Note: normality of distribution may not be assumed for both QP and QH simultaneously. For that reason, I chose the bootstrap method to compute confidence intervals.
# initializing bootstrap for QP
table.QP <- numeric(1000)
# loop to generate means from original data
for(i in 1:1000) {
table.QP[i] <- mean(sample(AttrakDiff.tr.margins$mean_QP, 10, replace=T))
}
# sort generated means
table.QP.sorted <- sort(table.QP)
# catch conf int by selecting heads and tails
QP.ci <- c(table.QP.sorted[25], table.QP.sorted[975])
print(paste("Total mean of QP:", round(mean(AttrakDiff.tr.margins$mean_QP), 1)))
## [1] "Total mean of QP: 1"
print(paste("95% CI for QP mean (bootstrap):", round(QP.ci, 1)[1], round(QP.ci, 1)[2]))
## [1] "95% CI for QP mean (bootstrap): 0.6 1.4"
# initializing bootstrap for QH
table.QH <- numeric(1000)
# loop to generate means from original data
for(i in 1:1000) {
table.QH[i] <- mean(sample(AttrakDiff.tr.margins$mean_QH, 10, replace=T))
}
# sort generated means
table.QH.sorted <- sort(table.QH)
# catch conf int by selecting heads and tails
QH.ci <- c(table.QH.sorted[25], table.QH.sorted[975])
print(paste("Total mean of QH:", round(mean(AttrakDiff.tr.margins$mean_QH), 1)))
## [1] "Total mean of QH: -0.4"
print(paste("95% CI for QH mean (bootstrap):", round(QH.ci, 1)[1], round(QH.ci, 1)[2]))
## [1] "95% CI for QH mean (bootstrap): -0.6 -0.2"
# plotting mean values
y <- c(
mean(AttrakDiff.tr.margins$mean_QP),
mean(AttrakDiff.tr.margins$mean_QHI),
mean(AttrakDiff.tr.margins$mean_ATT),
mean(AttrakDiff.tr.margins$mean_QHS)
)
path <- y
x <- c("Pragmatic qual.", "Identification qual.", "Attractivity", "Stimulation qual.")
meanplot <- data.frame(x, y)
if (!require(ggplot2)) install.packages("ggplot2")
## Loading required package: ggplot2
library(ggplot2)
limits <- c(-3, 3)
ggplot(meanplot, aes(x, y, fill = y)) +
geom_hline(aes(yintercept = 0), size = 0.5, linetype = "dotted", colour = "firebrick") +
geom_dotplot(binaxis = "y", stackdir = "center", binwidth = 0.75, color = "white") +
scale_fill_gradient(name = "Score", low = "red", high = "green", limits = limits) +
coord_cartesian(ylim = limits) +
theme_minimal() +
labs(title ="AttrakDiff mean scores", x = "Themes", y = "Mean scores", subtitle = paste("Scores by themes. N=", nrow(AttrakDiff), sep = ""))
## 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.
# plotting word pairs
itemMean <- c()
itemName <- c()
itemTheme <- c()
for (i in 1:ncol(AttrakDiff.tr)) {
itemMean[i] <- mean(as.numeric(AttrakDiff.tr[, i]))
itemName[i] <- colnames(AttrakDiff.tr)[i]
itemTheme[i] <- sub("_.", "", colnames(AttrakDiff.tr)[i])
}
itemName <- as.factor(itemName)
itemTheme <- as.factor(itemTheme)
AttrakDiff.items <- data.frame(mean = itemMean, name = itemName, theme = itemTheme)
AttrakDiff.items <- AttrakDiff.items[order(AttrakDiff.items$name), ]
ggplot(AttrakDiff.items, aes(mean, name, fill = theme)) +
geom_col(alpha = 0.2) +
geom_dotplot(binaxis = "y", stackdir = "center", binwidth = 0.75, color = "white") +
coord_cartesian(xlim = limits) +
scale_fill_brewer(palette = "Set2") +
geom_vline(aes(xintercept = 0), linetype = "dotted", colour = "firebrick") +
theme_minimal() +
labs(title ="AttrakDiff mean scores", x = "Means", y = "Items", subtitle = paste("Scores by items and themes. N=", nrow(AttrakDiff), sep = ""))
# plotting portfolio results
# Qualité pragmatique = QP
# Qualité hédonique = (QHS + QHI) / 2
# dans mon cas, la donnée existe dèjà sur ma table margins
x <- mean(AttrakDiff.tr.margins$mean_QP)
y <- mean(AttrakDiff.tr.margins$mean_QH)
ci.x.1 <- QP.ci[1]
ci.x.2 <- QP.ci[2]
ci.y.1 <- QH.ci[1]
ci.y.2 <- QH.ci[2]
portfolio <- data.frame(x, y, ci.x.1, ci.x.2, ci.y.1, ci.y.2)
t.x <- c(rep(-2, 3), rep(0, 3), rep(2, 3))
t.y <- rep(c(2, 0, -2), 3)
t.n <- c("too self-oriented", "", "non-desirable", "self-oriented", "neutral", "", "desired", "task-oriented", "too task-oriented")
t.a <- rep(c(0.035, 0), 5)
t.a <- t.a[1:9]
tiles <- data.frame("x" = t.x, "y" = t.y, "label" = t.n, "alpha" = t.a)
ggplot(portfolio, aes()) +
coord_cartesian(xlim = limits, ylim = limits) +
geom_point(aes(x = x, y = y), colour = "DodgerBlue", fill = "DodgerBlue", show.legend = T) +
geom_rect(aes(xmin = ci.x.1, xmax = ci.x.2, ymin = ci.y.1, ymax = ci.y.2), fill = 'DodgerBlue', alpha = 0.25, show.legend = T) +
#geom_vline(aes(xintercept = 0), size = 1, colour = "white") +
#geom_hline(aes(yintercept = 0), size = 1, colour = "white") +
geom_tile(data = tiles, mapping = aes(x = x, y = y, height = 2, width = 2), alpha = t.a) +
geom_text(data = tiles, mapping = aes(x = x, y = y, label = label), alpha = 0.50) +
theme_minimal() +
labs(title ="AttrakDiff Portfolio result", subtitle = paste("with confidence interval. N=", nrow(AttrakDiff), sep = ""), x = "Pragmatic quality", y = "Hedonistic quality", caption = "95% CI computed by bootstrap\nAdapted from : https://lagrandeourse.design/ressources/questionnaire-\nattrakdiff-activites-ux/")
if (x < -1) {
if (y < -1) {
print("Overall judgement: Superfluous")
print("Advice: highly improve both task-oriented and hedonistic qualities")
} else if (y < 1) {
print("Overall judgement: Superfluous-Neutral")
print("Advice: highly improve task-oriented qualities, hedonistic a little")
} else {
print("Overall judgement: Too hedonistic oriented")
print("Advice: highly improve task-oriented qualities")
}
} else if (x < 1) {
if (y < -1) {
print("Overall judgement: Neutral-Superflous")
print("Advice: highly improve hedonistic qualities, task-oriented a little")
} else if (y < 1) {
print("Overall judgement: Neutral")
print("Advice: improve both task-oriented and hedonistic qualities a little")
} else {
print("Overall judgement: Hedonistic oriented")
print("Advice: improve task-oriented a little")
}
} else {
if (y < -1) {
print("Overall judgement: Too task oriented")
print("Advice: highly improve hedonistic qualities")
} else if (y < 1) {
print("Overall judgement: Task oriented")
print("Advice: improve hedonistic qualities a little")
} else {
print("Overall judgement: DesiLightCoral")
print("Advice: Both hedonistic and task-oriented qualities are optimum")
}
}
## [1] "Overall judgement: Task oriented"
## [1] "Advice: improve hedonistic qualities a little"