Data analysis for standardized questionnaire AttrakDiff (M. Hassenzahl, M. Burmester, F. Koller, 2004).

Educational notebook made by Paul Amat.

About

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).

Link: https://www.semanticscholar.org/paper/AttrakDiff%3A-Ein-Fragebogen-zur-Messung-hedonischer-Hassenzahl-Burmester/869f95148302f61e4ca02ec11fcffea7839e9712

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

Items

7-points Likert scales, -3 to 3 with a 0 step.

Themes are:

  • QP = Qualités pragmatiques ;
  • QHS = Qualités hédoniques stimulation,
  • QHI = Qualité hédonique identification,
  • ATT = Attractivité globale.

In the passation order:

  1. QP_1: Humain, Technique
  2. QHI_1: M’isole, Me sociabilise
  3. ATT_1: Plaisant, Déplaisant
  4. QHS_1: Original, Convientionnel
  5. QP_2: Simple, Compliqué
  6. QHI_2: Professionnel, Amateur
  7. ATT_2: Laid, Beau
  8. QP_3: Pratique, Pas pratique
  9. ATT_3: Agréable, Désagréable
  10. QP_4: Fastidieux, Efficace
  11. QHI_3: De bon goût, De mauvais goût
  12. QP_5: Prévisible, Imprévisible
  13. QHI_4: Bas de gamme, Haut de gamme
  14. QHI_5: M’exclut, M’intègre
  15. QHI_6: Me rapproche des autres, Me sépare des autres
  16. QHI_7: Non-présentable, Présentable
  17. ATT_4: Rebutant, Attirant
  18. QHS_2: Sans imagination, Créatif
  19. ATT_5: Bon, Mauvais
  20. QP_6: Confus, Clair
  21. ATT_6: Repoussant, Attrayant
  22. QHS_3: Audacieux, Prudent
  23. QHS_4: Novateur, Conservateur
  24. QHS_5: Ennuyeux, Captivant
  25. QHS_6: Peu exigeant, Exigeant
  26. ATT_7: Motivant, Décourageant
  27. QHS_7: Nouveau, Commun
  28. QP_7: Incontrôlable, Maîtrisable

Inverted items:

  1. QP_1
  2. ATT_1
  3. QHS_1
  4. QP_2
  5. QHI_2
  6. QP_3
  7. ATT_3
  8. QHI_3
  9. QP_5
  10. QHI_6
  11. ATT_5
  12. QHS_3
  13. QHS_4
  14. ATT_7
  15. QHS_7

Sources: La Grande Ourse

Generate

# 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)")
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

Transform

  • attention aux items inversés
  • regrouper les items par dimension (7 items dans chacune) ;
  • calculer la moyenne obtenue pour chaque dimension.

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)")
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

Infere

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"

Visualize

# 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/")

Conclusion

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"