Analisi Preliminare degli Item: Esercitazione pratica e richiami alla teoria

Test per le organizzazioni - A.A. 2025/2026

Autore/Autrice

Margherita Calderan & Ottavia M. Epifania

Data di Pubblicazione

7 maggio 2026

1 Test di prestazione tipica: il Big Five

Per gli esempi che seguono utilizziamo il file dataB.Rdata, che contiene le risposte al questionario di personalità Big Five (test di prestazione tipica) somministrato a 19.709 partecipanti online. Gli item sono stati valutati su una scala a cinque punti (1 = Completamente in disaccordo, 3 = Né d’accordo né in disaccordo, 5 = Completamente d’accordo), dove il valore NA indica una risposta omessa.

Il questionario misura i Cinque Grandi fattori di personalità secondo il modello di Goldberg (1992): Estroversione (E), Nevroticismo (N), Gradevolezza (A), Coscienziosità (C) e Apertura all’esperienza (O). Ciascuna scala è composta da 10 item, per un totale di 50 item.

Cos’è un test di prestazione tipica?

Nei test di prestazione tipica non esistono risposte giuste o sbagliate: si chiede al rispondente di descrivere come di solito pensa, sente o si comporta.

1.1 Variabili socio-demografiche

Il dataset include, oltre agli item del questionario, cinque variabili socio-demografiche:

Variabile Descrizione Codifica
race Gruppo etnico (menu a tendina) 1 = Multirazziale; 2 = Artico; 3 = Caucasico (Europeo); 4 = Caucasico (Indiano); 5 = Caucasico (Medio Oriente); 6 = Caucasico (Nord Africa); 7 = Australiano indigeno; 8 = Nativo Americano; 9 = Asia nord-orientale; 10 = Pacifico; 11 = Asia sud-orientale; 12 = Africa occidentale/Etiopia; 13 = Altro (0 = mancante)
age Età (inserita come testo; esclusi i partecipanti con età < 13 anni) Variabile continua
engnat Madrelingua inglese? 1 = Sì; 2 = No (0 = mancante)
gender Genere (menu a tendina) 1 = Maschio; 2 = Femmina; 3 = Altro (0 = mancante)
hand Mano dominante 1 = Destra; 2 = Sinistra; 3 = Entrambe (0 = mancante)

1.2 Item del questionario

Di seguito sono riportati gli item suddivisi per scala. Gli item in grassetto sono gli item reverse.

1.2.0.1 Estroversione (E)

Item Testo originale
E1 I am the life of the party.
E2 I don’t talk a lot.
E3 I feel comfortable around people.
E4 I keep in the background.
E5 I start conversations.
E6 I have little to say.
E7 I talk to a lot of different people at parties.
E8 I don’t like to draw attention to myself.
E9 I don’t mind being the center of attention.
E10 I am quiet around strangers.

1.2.0.2 Nevroticismo (N)

Item Testo originale
N1 I get stressed out easily.
N2 I am relaxed most of the time.
N3 I worry about things.
N4 I seldom feel blue.
N5 I am easily disturbed.
N6 I get upset easily.
N7 I change my mood a lot.
N8 I have frequent mood swings.
N9 I get irritated easily.
N10 I often feel blue.

1.2.0.3 Gradevolezza (A)

Item Testo originale
A1 I feel little concern for others.
A2 I am interested in people.
A3 I insult people.
A4 I sympathize with others’ feelings.
A5 I am not interested in other people’s problems.
A6 I have a soft heart.
A7 I am not really interested in others.
A8 I take time out for others.
A9 I feel others’ emotions.
A10 I make people feel at ease.

1.2.0.4 Coscienziosità (C)

Item Testo originale
C1 I am always prepared.
C2 I leave my belongings around.
C3 I pay attention to details.
C4 I make a mess of things.
C5 I get chores done right away.
C6 I often forget to put things back in their proper place.
C7 I like order.
C8 I shirk my duties.
C9 I follow a schedule.
C10 I am exacting in my work.

1.2.0.5 Apertura all’esperienza (O)

Item Testo originale
O1 I have a rich vocabulary.
O2 I have difficulty understanding abstract ideas.
O3 I have a vivid imagination.
O4 I am not interested in abstract ideas.
O5 I have excellent ideas.
O6 I do not have a good imagination.
O7 I am quick to understand things.
O8 I use difficult words.
O9 I spend time reflecting on things.
O10 I am full of ideas.

1.3 Caricamento e preparazione dei dati

# pulisco lo spazio di lavoro
rm(list = ls())

# Carico pacchetti 
# se non ho i pacchetti devo prima lanciare il comando 
# install.packages("nomepacchetto")
library(psych)
library(psychTools)
library(corrplot)
library(ggplot2)
library(tidyr)

# setwd('/percorso/cartella')  # adatta al tuo percorso

# carico il dato in formato R attraverso la funzione load
load("data/dataB.RData")

# visualizzo la struttura: quante righe, quante colonne, che tipo di variabili
str(dataB)
tibble [19,709 × 57] (S3: tbl_df/tbl/data.frame)
 $ race   : num [1:19709] 3 13 1 3 11 13 5 4 5 3 ...
 $ age    : num [1:19709] 53 46 14 19 25 31 20 23 39 18 ...
 $ engnat : num [1:19709] 1 1 2 2 2 1 1 2 1 1 ...
 $ gender : num [1:19709] 1 2 2 2 2 2 2 1 2 2 ...
 $ hand   : num [1:19709] 1 1 1 1 1 1 1 1 3 1 ...
 $ source : num [1:19709] 1 1 1 1 2 2 5 2 4 5 ...
 $ country: chr [1:19709] "US" "US" "PK" "RO" ...
 $ E1     : num [1:19709] 4 2 5 2 3 1 5 4 3 1 ...
 $ E2     : num [1:19709] 2 2 1 5 1 5 1 3 1 4 ...
 $ E3     : num [1:19709] 5 3 1 2 3 2 5 5 5 2 ...
 $ E4     : num [1:19709] 2 3 4 4 3 4 1 3 1 5 ...
 $ E5     : num [1:19709] 5 3 5 3 3 1 5 5 5 2 ...
 $ E6     : num [1:19709] 1 3 1 4 1 3 1 1 1 4 ...
 $ E7     : num [1:19709] 4 1 1 3 3 2 5 4 5 1 ...
 $ E8     : num [1:19709] 3 5 5 4 1 4 4 3 2 4 ...
 $ E9     : num [1:19709] 5 1 5 4 3 1 4 4 5 1 ...
 $ E10    : num [1:19709] 1 5 1 5 5 5 1 3 3 5 ...
 $ N1     : num [1:19709] 1 2 5 5 3 1 2 1 2 5 ...
 $ N2     : num [1:19709] 5 3 1 4 3 5 4 4 4 2 ...
 $ N3     : num [1:19709] 2 4 5 4 3 4 2 4 5 5 ...
 $ N4     : num [1:19709] 5 2 5 2 4 5 4 4 3 2 ...
 $ N5     : num [1:19709] 1 3 5 4 3 1 2 1 3 3 ...
 $ N6     : num [1:19709] 1 4 5 5 3 4 2 1 5 4 ...
 $ N7     : num [1:19709] 1 3 5 5 3 4 3 1 5 3 ...
 $ N8     : num [1:19709] 1 2 5 5 3 1 2 1 4 2 ...
 $ N9     : num [1:19709] 1 2 5 4 3 5 2 1 3 3 ...
 $ N10    : num [1:19709] 1 4 5 5 4 2 2 1 3 4 ...
 $ A1     : num [1:19709] 1 1 5 2 5 2 5 2 1 2 ...
 $ A2     : num [1:19709] 5 3 1 5 5 2 5 5 5 3 ...
 $ A3     : num [1:19709] 1 3 5 4 3 3 1 1 1 1 ...
 $ A4     : num [1:19709] 5 4 5 4 5 4 5 4 5 4 ...
 $ A5     : num [1:19709] 2 4 1 3 1 3 1 3 1 2 ...
 $ A6     : num [1:19709] 3 4 5 5 5 4 5 3 5 4 ...
 $ A7     : num [1:19709] 1 2 1 3 1 3 1 1 1 3 ...
 $ A8     : num [1:19709] 5 3 5 4 5 5 5 3 5 3 ...
 $ A9     : num [1:19709] 4 4 5 4 5 5 4 4 5 3 ...
 $ A10    : num [1:19709] 5 3 5 3 5 3 5 5 4 2 ...
 $ C1     : num [1:19709] 4 4 4 3 3 2 2 4 4 5 ...
 $ C2     : num [1:19709] 1 1 1 3 1 5 4 2 3 2 ...
 $ C3     : num [1:19709] 5 3 5 4 5 4 3 5 5 4 ...
 $ C4     : num [1:19709] 1 2 1 5 3 3 3 1 2 2 ...
 $ C5     : num [1:19709] 5 3 5 1 3 3 3 4 5 3 ...
 $ C6     : num [1:19709] 1 1 1 4 1 4 3 1 2 2 ...
 $ C7     : num [1:19709] 4 5 5 5 1 5 3 4 5 4 ...
 $ C8     : num [1:19709] 1 1 1 4 3 3 3 1 2 2 ...
 $ C9     : num [1:19709] 4 4 5 2 3 5 3 3 4 4 ...
 $ C10    : num [1:19709] 5 4 5 3 3 3 3 5 3 4 ...
 $ O1     : num [1:19709] 4 3 4 4 3 4 3 3 3 4 ...
 $ O2     : num [1:19709] 1 3 5 3 1 2 1 1 3 2 ...
 $ O3     : num [1:19709] 3 3 5 5 1 1 5 5 5 5 ...
 $ O4     : num [1:19709] 1 3 1 2 1 3 1 1 3 2 ...
 $ O5     : num [1:19709] 5 2 5 4 3 3 4 4 5 4 ...
 $ O6     : num [1:19709] 1 3 1 2 1 5 1 1 1 1 ...
 $ O7     : num [1:19709] 4 3 5 5 3 5 4 5 5 4 ...
 $ O8     : num [1:19709] 2 1 5 2 1 4 3 3 3 3 ...
 $ O9     : num [1:19709] 5 3 5 5 5 5 3 2 4 4 ...
 $ O10    : num [1:19709] 5 2 5 5 3 3 4 5 5 4 ...
 - attr(*, "na.action")= 'omit' Named int [1:10] 366 2789 3919 4166 8891 10953 13304 15180 15725 19065
  ..- attr(*, "names")= chr [1:10] "366" "2789" "3919" "4166" ...

Le prime 7 colonne sono le variabili socio-demografiche; dalla colonna 8 in poi ci sono i 50 item del questionario. Li estraiamo in un dataframe separato:

item <- dataB[, 8:ncol(dataB)]

1.4 Struttura dell’intero questionario

Visualizziamo la matrice di correlazione dell’intero questionario:

Codice
R_rec <- cor(item, use = "pairwise.complete.obs")
corrplot(R_rec, type = "lower", tl.cex = 1, mar = c(0, 0, 1, 0))

Matrice di correlazione dell’intero questionario

Cosa notate di strano? Come descrivereste questa matrice?


Codice
# item reverse per tutte le scale
rev_E <- paste("E", seq(2, 10, by = 2), sep = "")
rev_N <- c("N2", "N4")
rev_A <- c("A1", "A3", "A5", "A7")
rev_C <- c("C2", "C4", "C6", "C8")
rev_O <- c("O2", "O4", "O6")
tutti_rev <- c(rev_E, rev_N, rev_A, rev_C, rev_O)

item_rec <- item
item_rec[, tutti_rev] <- 6 - item_rec[, tutti_rev]

# ordino le colonne per scala
ordine <- c(paste("E", 1:10, sep = ""), paste("N", 1:10, sep = ""),
            paste("A", 1:10, sep = ""), paste("C", 1:10, sep = ""),
            paste("O", 1:10, sep = ""))

R_rec <- cor(item_rec[, ordine], use = "pairwise.complete.obs")
corrplot(R_rec, type = "lower", tl.cex = 1, mar = c(0, 0, 1, 0))

Matrice di correlazione dell’intero questionario (tutti gli item reverse ricodificati)

1.5 Analisi della scala di Estroversione

Illustriamo tutti i passaggi dell’analisi preliminare degli item usando la scala di Estroversione come esempio.

Il flusso di analisi che seguiremo è questo:

  1. Estrazione degli item della scala
  2. Analisi dei dati mancanti
  3. Ricodifica degli item reverse
  4. Statistiche descrittive
  5. Distribuzione delle risposte
  6. Matrice di correlazione
  7. Discriminatività (correlazione item-totale corretta)
  8. Ridondanza

1.5.1 Passo 1: Estrazione degli item

# paste() concatena la stringa "E" con i numeri da 1 a 10, senza separatore
E <- paste("E", 1:10, sep = "")
E 
 [1] "E1"  "E2"  "E3"  "E4"  "E5"  "E6"  "E7"  "E8"  "E9"  "E10"
itemE_orig <- as.data.frame(item[, E])  # salvo una copia degli item originali
head(itemE_orig)
  E1 E2 E3 E4 E5 E6 E7 E8 E9 E10
1  4  2  5  2  5  1  4  3  5   1
2  2  2  3  3  3  3  1  5  1   5
3  5  1  1  4  5  1  1  5  5   1
4  2  5  2  4  3  4  3  4  4   5
5  3  1  3  3  3  1  3  1  3   5
6  1  5  2  4  1  3  2  4  1   5

1.5.2 Passo 2: Analisi dei dati mancanti

Prima di qualsiasi analisi è buona prassi verificare quanti dati mancanti ci sono e come sono distribuiti tra gli item.

# numero assoluto di missing per item
colSums(is.na(itemE_orig)) # sommo gli NA per colonna
 E1  E2  E3  E4  E5  E6  E7  E8  E9 E10 
  8   5   3   8   6   4   2   2   2   3 
# missing per item
colMeans(is.na(itemE_orig))
          E1           E2           E3           E4           E5           E6 
0.0004059059 0.0002536912 0.0001522147 0.0004059059 0.0003044294 0.0002029530 
          E7           E8           E9          E10 
0.0001014765 0.0001014765 0.0001014765 0.0001522147 

Nel nostro caso i missing sono più o meno uniformemente distribuiti tra gli item e in percentuale trascurabile. Occorre tuttavia chiedersi perché si sono generati.

Perché il significato dei missing è diverso nei due tipi di test

Nei test di prestazione tipica, un’omissione è ambigua: non sappiamo se il rispondente non ha capito, non voleva rispondere, o si è distratto. Nei test di prestazione massima, invece, un’omissione può essere interpretata come mancata conoscenza (l’item era troppo difficile).

In letteratura si distinguono tre meccanismi che possono generare dati mancanti:

  • MCAR (Missing Completely at Random): la probabilità che un dato sia mancante è del tutto indipendente sia dal costrutto misurato sia da qualsiasi altra variabile osservata. È il caso più favorevole e consente di procedere con la complete-case analysis (analizzare solo i casi completi) senza introdurre distorsioni.
  • MAR (Missing at Random): la probabilità di missing dipende da altre variabili osservate (es. età, genere), ma non dal valore mancante in sé.
  • MNAR (Missing Not at Random): la probabilità di missing dipende dal valore non osservato stesso, il caso più problematico, difficile da rilevare senza informazioni esterne.

Nel nostro caso, con una percentuale di missing molto bassa, qualunque meccanismo sia in atto le conseguenze pratiche sono trascurabili. Procediamo con la complete-case analysis.

1.5.3 Passo 3: Ricodifica degli item reverse

Prima delle statistiche descrittive è necessario ricodificare gli item reverse.

Cosa sono gli item reverse?

Gli item reverse sono item il cui contenuto è formulato nella direzione opposta al costrutto misurato. Nella scala di Estroversione, E1 (“I am the life of the party”) è un item diretto: chi è molto estroverso tende a essere d’accordo (punteggio = 5). Al contrario, E2 (“I don’t talk a lot”) è reverse: chi è molto estroverso tende a essere in disaccordo (punteggio = 1).

Se non li ricodifico prima di calcolare le statistiche, tutte le analisi successive saranno distorte: la media sarà artificialmente bassa per gli item reverse anche in persone con un alto livello del tratto, e le correlazioni tra item della stessa scala risulteranno negative invece che positive.

La formula di ricodifica per una scala 1-5 è:

\[x_{\text{rec}} = k_{\max} - x + k_{\min} = 6 - x\]

Risposta originale Risposta ricodificata
1 (molto in disaccordo) 5 (molto d’accordo)
2 4
3 (neutrale) 3 (neutrale)
4 2
5 (molto d’accordo) 1 (molto in disaccordo)

Il valore neutrale (3) rimane invariato, e i poli si invertono simmetricamente.

Per la scala di Estroversione gli item reverse sono E2, E4, E6, E8, E10 (tutti i pari):

# seq() genera la sequenza 2, 4, 6, 8, 10
Erev <- paste("E", seq(from = 2, to = 10, by = 2), sep = "")
Erev  # verifica
[1] "E2"  "E4"  "E6"  "E8"  "E10"
# Risultato identico a creare un vettore così
Erev <- c("E2","E4","E6","E8","E10")

itemE <- itemE_orig  # lavoro su una copia
itemE[, Erev] <- 5 - itemE[, Erev] + 1  # ricodifica

1.5.4 Passo 4: Statistiche descrittive e difficoltà

describe(itemE)
    vars     n mean   sd median trimmed  mad min max range  skew kurtosis   se
E1     1 19701 2.63 1.23      3    2.57 1.48   1   5     4  0.21    -0.96 0.01
E2     2 19704 3.24 1.31      3    3.30 1.48   1   5     4 -0.21    -1.08 0.01
E3     3 19706 3.42 1.24      4    3.50 1.48   1   5     4 -0.36    -0.88 0.01
E4     4 19701 2.85 1.22      3    2.81 1.48   1   5     4  0.10    -0.94 0.01
E5     5 19703 3.43 1.28      4    3.53 1.48   1   5     4 -0.40    -0.95 0.01
E6     6 19705 3.55 1.24      4    3.66 1.48   1   5     4 -0.54    -0.74 0.01
E7     7 19707 2.87 1.43      3    2.83 1.48   1   5     4  0.12    -1.32 0.01
E8     8 19707 2.62 1.27      3    2.55 1.48   1   5     4  0.28    -1.00 0.01
E9     9 19707 3.09 1.40      3    3.12 1.48   1   5     4 -0.09    -1.28 0.01
E10   10 19706 2.41 1.30      2    2.29 1.48   1   5     4  0.51    -0.94 0.01

Esaminiamo le colonne più rilevanti.

Dati mancanti (colonna n): il numero di casi validi per ciascun item. Se n è inferiore a 19.709, quell’item ha dei missing.

Minimo e massimo (min, max): ci aspettiamo che tutti e cinque i punti della scala siano stati scelti almeno una volta (es. Un item con massimo osservato inferiore a 5 non sfrutta l’intera gamma di risposta).

Media e deviazione standard (mean, sd): ci aspettiamo che la media sia lontana dagli estremi e che la deviazione standard sia sufficientemente ampia. Un item in cui tutti tendono a rispondere allo stesso modo ha deviazione standard molto bassa e discrimina poco tra i rispondenti.

Skewness e curtosi (skew, kurtosis): descrivono la forma della distribuzione. La skewness misura l’asimmetria: un valore positivo indica che la maggior parte delle risposte si concentra nella parte bassa della scala (coda verso destra); un valore negativo indica il contrario. La curtosi misura l’appiattimento: valori positivi code più sottili rispetto alla normale. Come linee guida si usano |sk| < 1 e |ku| < 1 (Muthén & Kaplan, 1985), o più liberalmente |sk| < 2 e |ku| < 7 (Curran, West & Finch, 1996).

1.5.4.1 Difficoltà

Nei test di prestazione tipica la difficoltà si reinterpreta come la tendenza media dell’item \(p_i\).

E’ buona prassi trasformare \(p_i\) in funzione degli estemi della scale, cioè otteniamo un punteggio medio normalizzato rispetto agli estremi teorici della scala:

\[\pi_i = \frac{p_i - k_{\min}}{k_{\max} - k_{\min}}\]

Il risultato è sempre compreso tra 0 e 1, indipendentemente dalla scala usata, il che rende gli item confrontabili anche se provengono da scale diverse. Più \(\pi_i\) è alto, più i rispondenti hanno scelto punteggi alti su quell’item. Valori molto vicini a 0 o a 1 segnalano scarsa variabilità: quasi tutti rispondono allo stesso modo, e l’item discrimina poco.

Attenzione agli item reverse!

Il calcolo di \(\pi_i\) ha senso solo dopo la ricodifica. Prima della ricodifica, un item reverse mostrerebbe \(\pi_i\) artificialmente basso anche per persone con un livello alto del tratto.

Visualizziamo la differenza tra difficoltà prima e dopo la ricodifica:

# difficoltà item prima della trasformazione (itemE_orig)
p_star_orig <- (colMeans(itemE_orig, na.rm = TRUE) - 1) / (5 - 1)

# difficoltà item dopo la trasformazione (itemE)
p_star_rec  <- (colMeans(itemE,      na.rm = TRUE) - 1) / (5 - 1)
Codice
df_pstar <- data.frame(
  Item     = rep(E, 2),
  p_star   = c(p_star_orig, p_star_rec),
  Versione = rep(c("Prima della ricodifica", "Dopo la ricodifica"), each = 10),
  Tipo     = rep(ifelse(E %in% Erev, "Reverse", "Diretto"), 2)
)
df_pstar$Versione <- factor(df_pstar$Versione,
                            levels = c("Prima della ricodifica", "Dopo la ricodifica"))

ggplot(df_pstar, aes(x = Item, y = p_star, fill = Tipo)) +
  geom_col() +
  geom_hline(yintercept = 0.5, linetype = "dashed", color = "gray40") +
  facet_wrap(~Versione) +
  scale_fill_manual(values = c("Diretto" = "steelblue3", "Reverse" = "tomato3")) +
  labs(x = "Item", y = expression(p[i]^"*"), fill = "Tipo di item") +
  ylim(0, 1) +
  theme_bw() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

Difficoltà degli item di Estroversione prima e dopo la ricodifica

Dopo la ricodifica tutti gli item sono nella stessa direzione e i valori di \(\pi_i\) diventano comparabili.

Codice
# tabella riassuntiva della difficoltà dopo la ricodifica
p_star_tab <- data.frame(
  Item   = names(p_star_rec),
  Media  = round(colMeans(itemE, na.rm = TRUE), 2),
  pi = round(p_star_rec, 2),
  Tipo   = ifelse(names(p_star_rec) %in% Erev, "Reverse (ricodificato)", "Diretto")
)
p_star_tab
    Item Media   pi                   Tipo
E1    E1  2.63 0.41                Diretto
E2    E2  3.24 0.56 Reverse (ricodificato)
E3    E3  3.42 0.60                Diretto
E4    E4  2.85 0.46 Reverse (ricodificato)
E5    E5  3.43 0.61                Diretto
E6    E6  3.55 0.64 Reverse (ricodificato)
E7    E7  2.87 0.47                Diretto
E8    E8  2.62 0.41 Reverse (ricodificato)
E9    E9  3.09 0.52                Diretto
E10  E10  2.41 0.35 Reverse (ricodificato)

1.5.5 Passo 5: Distribuzione delle risposte

Calcoliamo le distribuzioni di frequenza per ciascun item della scala di Estroversione. Per gli item Likert questo è particolarmente utile per verificare che tutte le categorie di risposta siano state usate almeno una volta e per individuare eventuali pattern anomali (es. una categoria mai scelta, o una fortemente dominante).

1.5.5.1 Frequenze per un singolo item

Partiamo da E1 per capire la logica, poi vedremo come estendere il calcolo a tutti gli item contemporaneamente.

n <- nrow(dataB)  # numero totale di rispondenti

# frequenze assolute semplici: quante persone hanno scelto ciascun punteggio
fi_E1 <- table(itemE$E1)
fi_E1

   1    2    3    4    5 
4672 4480 5494 3589 1466 
# frequenze relative semplici: proporzione di rispondenti per categoria
pi_E1 <- table(itemE$E1) / n
round(pi_E1, 3)

    1     2     3     4     5 
0.237 0.227 0.279 0.182 0.074 
# frequenze relative cumulate
Pi_E1 <- cumsum(table(itemE$E1) / n)
round(Pi_E1, 3)
    1     2     3     4     5 
0.237 0.464 0.743 0.925 1.000 

1.5.5.2 Frequenze per tutti gli item della scala

Per calcolare le distribuzioni di frequenza di tutti gli item in una volta sola, usiamo la funzione response.frequencies() del pacchetto psych:

# frequenze relative per tutti gli item (una riga per categoria, una colonna per item)
response.frequencies(itemE)
             1         2         3         4          5         miss
E1  0.23714532 0.2273996 0.2788691 0.1821735 0.07441247 0.0004059059
E2  0.12510150 0.1813337 0.2368047 0.2418798 0.21488023 0.0002536912
E3  0.08205623 0.1653811 0.2375926 0.2834670 0.23150310 0.0001522147
E4  0.16192071 0.2455205 0.2789199 0.2098878 0.10375108 0.0004059059
E5  0.09424961 0.1641374 0.2088514 0.2807694 0.25199208 0.0003044294
E6  0.07931997 0.1452931 0.1879218 0.3238264 0.26363867 0.0002029530
E7  0.23463744 0.2141879 0.1815599 0.1880043 0.18161060 0.0001014765
E8  0.23829096 0.2588420 0.2303243 0.1858730 0.08666971 0.0001014765
E9  0.17460801 0.1996245 0.1907444 0.2266200 0.20840311 0.0001014765
E10 0.32817416 0.2487567 0.1851213 0.1563483 0.08159951 0.0001522147

Ogni colonna corrisponde a un item, ogni riga a una categoria di risposta (1–5). Il valore miss nella riga finale indica la proporzione di dati mancanti. Cose a cui prestare attenzione:

  • Categorie mai scelte (valore 0 in una riga): l’item non sfrutta tutta la scala di risposta
  • Categorie quasi mai scelte: la scala potrebbe essere troppo lunga per quel campione
  • Asimmetrie evidenti: quasi tutti scelgono la stessa categoria → l’item discrimina poco
Codice
rf <- round(response.frequencies(itemE)[1:5,-c(6)], 3)
barplot(t(rf), beside = TRUE,
        ylab = "Frequenza relativa", xlab = "Item",
        args.legend = list(title = "Risposta", bty = "n"))

Distribuzione delle risposte – Estroversione (dopo la ricodifica)
Codice
rf <- round(response.frequencies(itemE)[6:10,-c(6)], 3)
barplot(t(rf), beside = TRUE,
        ylab = "Frequenza relativa", xlab = "Item",
        args.legend = list(title = "Risposta", bty = "n"))

Distribuzione delle risposte – Estroversione (dopo la ricodifica)

Il grafico mostra le frequenze relative per ciascuna categoria di risposta, separate per item.

describe(itemE)
    vars     n mean   sd median trimmed  mad min max range  skew kurtosis   se
E1     1 19701 2.63 1.23      3    2.57 1.48   1   5     4  0.21    -0.96 0.01
E2     2 19704 3.24 1.31      3    3.30 1.48   1   5     4 -0.21    -1.08 0.01
E3     3 19706 3.42 1.24      4    3.50 1.48   1   5     4 -0.36    -0.88 0.01
E4     4 19701 2.85 1.22      3    2.81 1.48   1   5     4  0.10    -0.94 0.01
E5     5 19703 3.43 1.28      4    3.53 1.48   1   5     4 -0.40    -0.95 0.01
E6     6 19705 3.55 1.24      4    3.66 1.48   1   5     4 -0.54    -0.74 0.01
E7     7 19707 2.87 1.43      3    2.83 1.48   1   5     4  0.12    -1.32 0.01
E8     8 19707 2.62 1.27      3    2.55 1.48   1   5     4  0.28    -1.00 0.01
E9     9 19707 3.09 1.40      3    3.12 1.48   1   5     4 -0.09    -1.28 0.01
E10   10 19706 2.41 1.30      2    2.29 1.48   1   5     4  0.51    -0.94 0.01

1.5.6 Passo 6: Matrice di correlazione

La matrice di correlazione tra gli item è uno strumento fondamentale per esplorare la struttura interna di una scala. L’idea di fondo è semplice: item che misurano lo stesso costrutto dovrebbero essere positivamente correlati tra loro.

1.5.6.1 Quale coefficiente usare?

La scelta del coefficiente dipende dal numero di categorie di risposta \(k\):

Tipo di item Coefficiente appropriato
Item dicotomici (\(k\) = 2) Correlazione tetracorica
Item Likert con \(k \leq 4\) Correlazione policorica
Item Likert con \(k \geq 5\) Pearson (leggera distorsione) o policorica

La correlazione di Pearson assume che i dati siano su scala continua e distribuiti normalmente. Gli item Likert sono ordinali: applicare Pearson introduce una certa distorsione, più evidente quando \(k \leq 4\) o quando le distribuzioni sono molto asimmetriche.

Dopo la ricodifica, tutte le correlazioni tra item della stessa scala dovrebbero essere positive. Se ne persistono di negative, significa che qualche item reverse non è stato ricodificato.

R_E <- polychoric(itemE)$rho
corrplot(R_E, type = "lower", tl.cex = 0.9, mar = c(0, 0, 1, 0))

Matrice di correlazione – Estroversione (dopo la ricodifica)
R_E <- polychoric(itemE_orig)$rho
corrplot(R_E, type = "lower", tl.cex = 0.9, mar = c(0, 0, 1, 0))

Matrice di correlazione – Estroversione (prima della ricodifica)

1.5.6.2 Correlazioni policoriche

polychoric(itemE)
Call: polychoric(x = itemE)
Polychoric correlations 
    E1   E2   E3   E4   E5   E6   E7   E8   E9   E10 
E1  1.00                                             
E2  0.47 1.00                                        
E3  0.53 0.50 1.00                                   
E4  0.53 0.58 0.54 1.00                              
E5  0.54 0.60 0.65 0.57 1.00                         
E6  0.39 0.63 0.44 0.53 0.54 1.00                    
E7  0.65 0.54 0.65 0.56 0.70 0.46 1.00               
E8  0.41 0.42 0.36 0.50 0.39 0.37 0.39 1.00          
E9  0.51 0.41 0.47 0.50 0.47 0.38 0.49 0.58 1.00     
E10 0.46 0.53 0.54 0.57 0.62 0.48 0.60 0.44 0.43 1.00

 with tau of 
        1       2      3    4
E1  -0.72 -0.0890  0.654 1.44
E2  -1.15 -0.5060  0.109 0.79
E3  -1.39 -0.6826 -0.038 0.73
E4  -0.99 -0.2341  0.486 1.26
E5  -1.32 -0.6483 -0.082 0.67
E6  -1.41 -0.7567 -0.221 0.63
E7  -0.72 -0.1286  0.333 0.91
E8  -0.71 -0.0072  0.605 1.36
E9  -0.94 -0.3207  0.164 0.81
E10 -0.44  0.1940  0.713 1.39

La correlazione tra i punteggi degli item è positiva e moderata. Le soglie associate ad ogni categoria per ogni item sono riportate nella sezione with tau of..

1.5.7 Cosa sono le soglie \(\tau\)?

La correlazione policorica si basa sull’idea che la risposta Likert osservata \(Y_i \in \{1, 2, 3, 4, 5\}\) non sia altro che la versione “tagliata” di una variabile latente continua \(Y_i^*\), distribuita normalmente con media 0 e varianza 1. Questa variabile latente non è mai osservata direttamente: rappresenta il livello del tratto sottostante (es. estroversione) di ogni rispondente.

Le soglie \(\tau\) sono i valori sulla scala di \(Y_i^*\) che separano le categorie di risposta:

\[Y_i = k \quad \text{se} \quad \tau_{k-1} \leq Y_i^* < \tau_k, \quad k = 1, \ldots, K\]

Con \(K = 5\) categorie ci sono 4 soglie: \(\tau_1 < \tau_2 < \tau_3 < \tau_4\).

Codice
# soglie stimate da polychoric() per E1
tau <- c(-0.72, -0.0891, 0.654, 1.44)

x <- seq(-5, 5, length.out = 1000)
df_curve <- data.frame(x = x, y = dnorm(x))

breaks <- c(-Inf, tau, Inf)
labels_cat <- paste("Risposta", 1:5)
colors <- c("#dbeafe", "#bfdbfe", "#c4b5fd", "#a5b4fc", "#818cf8")

df_areas <- do.call(rbind, lapply(1:5, function(k) {
  xs <- seq(max(-3.5, breaks[k]), min(3.5, breaks[k+1]), length.out = 200)
  data.frame(x = xs, y = dnorm(xs), categoria = labels_cat[k])
}))
df_areas$categoria <- factor(df_areas$categoria, levels = labels_cat)

df_tau <- data.frame(
  x   = tau,
  y   = dnorm(tau),
  lab = paste0("\u03c4", 1:4, " = ", tau)
)

ggplot() +
  geom_area(data = df_areas,
            aes(x = x, y = y, fill = categoria),
            position = "identity", alpha = 0.85) +
  geom_line(data = df_curve, aes(x = x, y = y),
            color = "#1e3a8a", linewidth = 1) +
  geom_vline(data = df_tau, aes(xintercept = x),
             color = "#C2410C", linetype = "dashed", linewidth = 0.8) +
  annotate("text",
           x = c(-1.6, -0.5, 0.1, 0.8, 1.7),
           y = c(0.05, 0.13, 0.19, 0.13, 0.05),
           label = 1:5,
           size = 5, fontface = "bold", color = "black") +
  scale_fill_manual(values = colors, name = NULL) +
  scale_x_continuous(breaks = c(-3, -2, tau, 2, 3),
                     labels = c("-3", "-2", as.character(tau), "2", "3")) +
  labs(
    x = expression(Y^"*" ~ "(tratto latente, scala normale standardizzata)"),
    y = "Densità"
  ) +
  theme_bw(base_size = 18) +
  theme(
    legend.position  = "none",
    panel.grid.minor = element_blank(),
    axis.text.x      = element_text(size = 9)
  )

Variabile latente Y* e soglie τ per un item con scala 1–5. Ogni area colorata corrisponde a una categoria di risposta.
# with tau of
#  E1  -0.72 -0.089  0.654 1.44

Ogni riga riporta le 4 soglie di un item sulla scala normale standardizzata. Come si vede nel grafico:

  • \(\tau < 0\): la soglia è sotto la media della distribuzione normale
  • \(\tau > 0\): la soglia è sopra la media
  • La distanza tra soglie consecutive indica quanto è “larga” quella categoria: soglie vicine indicano che quella categoria copre un intervallo ristretto del tratto, e quindi discrimina poco tra i rispondenti

Da dove vengono questi valori? (in pratica)

Le soglie \(\tau\) sono stimate a partire dalle frequenze cumulate osservate nel campione. Per ogni item si calcola la proporzione di rispondenti con risposta \(\leq k\)

# Frequenze relative osservate per E1 
# (proporzione per ciascuna categoria)
response.frequencies(itemE)[1, ]
           1            2            3            4            5         miss 
0.2371453226 0.2273996244 0.2788690929 0.1821734937 0.0744124664 0.0004059059 
# Le frequenze cumulate (attraverso la funzione cumsum, 
# prendo le prime 4)
freq_cum <- cumsum(response.frequencies(itemE)[1, ])[1:4]
freq_cum
        1         2         3         4 
0.2371453 0.4645449 0.7434140 0.9255875 

…e quella proporzione viene tradotta nel quantile della normale standard che le corrisponde: il punto su \(Y^*\) al di sotto del quale ricade esattamente quella quota della distribuzione.

qnorm(freq_cum)  
          1           2           3           4 
-0.71551537 -0.08898995  0.65390670  1.44369449 

La soglia \(\tau_k\) è il quantile della normale standard corrispondente alla proporzione di risposte \(\leq\) k nel campione.

pnorm() e qnorm()

pnorm(): Dato un valore \(q\), restituisce l’area a sinistra sotto la curva, cioè \(P(X \leq q)\).

pnorm(0) # probabilità di ottenere un valore minore o uguale a 0
[1] 0.5

qnorm(): Dato un livello di probabilità cumulata \(p\), restituisce il quantile corrispondente, cioè il valore \(x\) tale che \(P(X \leq x) = p\).

qnorm(0.5) #quantile corrispondete alla probabilità cumulata 0.5
[1] 0
Esercizio 1

Per l’item E1, le soglie stimate sono: \(\tau_1 = -0.72\), \(\tau_2 = -0.089\), \(\tau_3 = 0.654\), \(\tau_4 = 1.44\).

Quale proporzione di rispondenti ci si aspetta che abbia scelto la categoria 3 (cioè tra \(\tau_2\) e \(\tau_3\))? Qual’è la probabiltà di rispondere 3?

Suggerimento: usa pnorm() su entrambe le soglie.

# output desiderato: 0.2789429

Esercizio 2

Immagina un item con scala 1–5 e le seguenti frequenze relative osservate nel campione:

Categoria 1 2 3 4 5
frequenza relativa 0.08 0.34 0.32 0.25 0.01
  1. Calcola le frequenze cumulate.
  2. Applica qnorm() per ottenere le 4 soglie \(\tau\).
# frequenze relative
freq_rel  <- c(0.08, 0.34, 0.32, 0.25, 0.01)

# calcolo frequenze cumulate

# calcolo i tau


# output desiderato: -1.4050716 -0.2018935  0.6433454  2.3263479

1.5.8 Passo 7: Discriminatività

La discriminatività misura quanto un item riesce a distinguere persone con livelli alti da persone con livelli bassi del tratto. La misura standard è la correlazione item-totale corretta: la correlazione tra il punteggio all’item e il punteggio totale della scala calcolato escludendo quell’item dal totale.

Si dice “corretta” perché se includessimo l’item nel totale la correlazione sarebbe artificialmente gonfiata: un numero è sempre correlato con una somma di cui fa parte.

Assume che il tratto misurato sia unidimensionale, e dipende dal campione. Maggiore è l’eterogeneità dei livelli di abilità delle persone, maggiore è la capacità discriminatività degli item.

alpha_E <- psych::alpha(itemE)
round(alpha_E$item.stats[, c("raw.r", "r.drop")], 2)
    raw.r r.drop
E1   0.70   0.63
E2   0.73   0.65
E3   0.72   0.65
E4   0.75   0.68
E5   0.78   0.71
E6   0.66   0.57
E7   0.78   0.70
E8   0.62   0.52
E9   0.67   0.58
E10  0.72   0.64

Se un item ha una correlazione item-totale corretta bassa o negativa, significa che non “lavora” nella stessa direzione del test e, quindi, discrimina male il costrutto.

alpha_E <- psych::alpha(itemE_orig)
Some items ( E2 E4 E6 E8 E10 ) were negatively correlated with the first principal component and 
probably should be reversed.  
To do this, run the function again with the 'check.keys=TRUE' option
round(alpha_E$item.stats[, c("raw.r", "r.drop")], 2)
    raw.r r.drop
E1   0.38   0.03
E2   0.22  -0.15
E3   0.37   0.02
E4   0.15  -0.19
E5   0.29  -0.08
E6   0.27  -0.09
E7   0.40  -0.01
E8   0.20  -0.16
E9   0.30  -0.10
E10  0.13  -0.24

1.5.9 Passo 8: Ridondanza

Due item sono ridondanti se correlano troppo tra loro (\(r > .70\)): stanno misurando sostanzialmente la stessa cosa e uno dei due potrebbe essere rimosso senza perdita di informazione rilevante.

Codice
# estraggo il triangolo superiore e ordino per correlazione decrescente
corr_E <- data.frame(
  item1 = rownames(R_E)[row(R_E)[upper.tri(R_E)]],
  item2 = colnames(R_E)[col(R_E)[upper.tri(R_E)]],
  r     = round(R_E[upper.tri(R_E)], 2)
)
corr_E <- corr_E[order(-corr_E$r), ]
head(corr_E, 10)
   item1 item2    r
20    E5    E7 0.70
9     E3    E5 0.65
16    E1    E7 0.65
18    E3    E7 0.65
12    E2    E6 0.63
5     E2    E4 0.58
40    E4   E10 0.57
7     E1    E5 0.54
2     E1    E3 0.53
14    E4    E6 0.53

Esercizio: Nevroticismo

Applica la stessa procedura alla scala di Nevroticismo (item reverse: N2, N4):

  1. Estrai gli item N1–N10
  2. Analizza i dati mancanti
  3. Ricodifica gli item reverse
  4. Calcola statistiche descrittive e la difficoltà
  5. Visualizza la distribuzione delle risposte (opzionale)
  6. Calcola e visualizza la matrice di correlazione
  7. Valuta la discriminatività con alpha()
  8. Verifica se ci sono coppie di item ridondanti (\(r > .70\))

2 Test di prestazione massima: abilità cognitiva

Per gli esempi che seguono utilizziamo il dataset iqitems, incluso nel pacchetto psychTools e liberamente disponibile in R. Il dataset contiene le risposte grezze a 16 item a scelta multipla di abilità cognitiva somministrati online nel progetto SAPA (Synthetic Aperture Personality Assessment) da agosto a settembre 2012. Il campione comprende 1.525 partecipanti.

Gli item sono stati selezionati da un pool di 80 item sviluppati per costruire misure open-source di abilità cognitiva (Condon & Revelle, 2014). Ogni item prevede tra 6 e 8 alternative di risposta, di cui una sola corretta; le risposte degli altri sono i distrattori. Il valore 0 indica una risposta omessa o non valida.

Cos’è un test di prestazione massima?

Nei test di prestazione massima esiste una risposta corretta e una o più risposte sbagliate (i distrattori). Si chiede al rispondente di fornire la migliore prestazione possibile: non si descrive come ci si sente o come ci si comporta, ma si risolve un problema. Esempi classici sono i test di intelligenza, i test attitudinali e gli esami scolastici.

2.1 Item del questionario

I 16 item misurano quattro abilità cognitive di primo ordine, che condividono un fattore generale (g):

Sottoscala Item Descrizione
Ragionamento verbale reason.4, reason.16, reason.17, reason.19 Domande di ragionamento di base
Serie di lettere letter.7, letter.33, letter.34, letter.58 Serie alfanumeriche
Matrici matrix.45, matrix.46, matrix.47, matrix.55 Completamento di matrici visive
Rotazioni spaziali rotate.3, rotate.4, rotate.6, rotate.8 Rotazione mentale di figure

2.2 Caricamento e preparazione dei dati

# puliamo lo spazio di lavoro
rm(list = ls())

# carico pacchetti
library(psych)
library(psychTools)
library(ggplot2)
library(tidyr)
library(corrplot)

#carico dati
data(iqitems)

# definisco le chiavi di risposta (i.e corrette)
iq.keys <- c(4, 4, 4, 6,   # ragionamento verbale
             6, 3, 4, 4,   # serie di lettere
             5, 2, 2, 4,   # matrici
             3, 2, 6, 7)   # rotazioni spaziali

# struttura del dataset: 1525 righe, 16 colonne
str(iqitems)
'data.frame':   1525 obs. of  16 variables:
 $ reason.4 : int  3 3 3 4 0 4 4 1 4 4 ...
 $ reason.16: int  3 3 4 0 4 4 4 4 4 4 ...
 $ reason.17: int  6 4 4 6 4 4 4 5 2 4 ...
 $ reason.19: int  3 4 2 1 5 6 6 2 6 6 ...
 $ letter.7 : int  5 6 6 5 5 6 6 4 5 2 ...
 $ letter.33: int  3 2 5 1 3 3 3 1 3 1 ...
 $ letter.34: int  5 4 3 4 3 4 4 1 4 4 ...
 $ letter.58: int  2 2 3 1 0 4 4 2 4 3 ...
 $ matrix.45: int  4 4 5 2 5 5 5 4 5 5 ...
 $ matrix.46: int  3 1 2 0 2 2 2 2 5 2 ...
 $ matrix.47: int  4 4 4 6 4 2 2 2 4 6 ...
 $ matrix.55: int  4 6 6 2 6 2 2 3 3 6 ...
 $ rotate.3 : int  5 6 8 1 5 3 3 5 6 8 ...
 $ rotate.4 : int  6 5 8 7 7 2 2 2 8 0 ...
 $ rotate.6 : int  5 6 8 1 1 6 6 1 8 4 ...
 $ rotate.8 : int  5 1 8 6 6 4 7 1 2 8 ...

I valori in ogni cella rappresentano l’alternativa scelta dal rispondente (1, 2, 3, …); 0 = risposta omessa o non valida. La differenza fondamentale rispetto a un questionario di personalità (es. Big Five) è che qui ogni cella non contiene un punteggio Likert, ma il codice dell’alternativa scelta in un item a scelta multipla.

2.2.1 Gestione dei dati mancanti

Prima di procedere con le analisi, è buona prassi verificare la presenza di dati mancanti. Distinguiamo due tipi:

  • Valore 0: risposta omessa o non valida, registrata esplicitamente;
  • NA: dato assente per problemi di acquisizione o codifica.
# numero di risposte omesse (valore = 0) per item
apply(iqitems, 2, function(x) sum(x == 0, na.rm = TRUE))
 reason.4 reason.16 reason.17 reason.19  letter.7 letter.33 letter.34 letter.58 
       81        61        83        67        83        85        68        87 
matrix.45 matrix.46 matrix.47 matrix.55  rotate.3  rotate.4  rotate.6  rotate.8 
       65        54        58        65        67        63        67        64 
# numero di NA veri per item
apply(iqitems, 2, function(x) sum(is.na(x)))
 reason.4 reason.16 reason.17 reason.19  letter.7 letter.33 letter.34 letter.58 
        2         1         2         2         1         2         2         0 
matrix.45 matrix.46 matrix.47 matrix.55  rotate.3  rotate.4  rotate.6  rotate.8 
        2         1         2         1         2         2         2         1 
# quante righe rimangono con dati completi?
nrow(iqitems[complete.cases(iqitems), ])
[1] 1523

Vediamo i dati dei casi incompleti:

iqitems[!complete.cases(iqitems), ]
    reason.4 reason.16 reason.17 reason.19 letter.7 letter.33 letter.34
77        NA         4        NA        NA       NA        NA        NA
155       NA        NA        NA        NA        6        NA        NA
    letter.58 matrix.45 matrix.46 matrix.47 matrix.55 rotate.3 rotate.4
77          1        NA        NA        NA         5       NA       NA
155         4        NA         2        NA        NA       NA       NA
    rotate.6 rotate.8
77        NA        8
155       NA       NA

Questi soggetti presentano molti NA, probabilmente per problemi di codifica. È opportuno rimuoverli con na.omit():

iqitems <- na.omit(iqitems)
Perché il significato dei missing è diverso nei due tipi di test

Nei test di prestazione tipica, un’omissione è ambigua: il rispondente potrebbe non aver voluto rispondere, non aver capito l’item, o essersi distratto.

Nei test di prestazione massima, invece, un’omissione ha spesso un significato più preciso: l’item era probabilmente troppo difficile (o il tempo era scaduto). Per questo motivo, in molti contesti i missing vengono trattati come risposte errate (valore = 0) prima di calcolare le statistiche.


2.3 Analisi preliminare item matrici e rotazioni

Illustriamo tutti i passaggi dell’analisi preliminare degli item usando gli item Matrix e Rotation.

Il flusso di analisi che seguiremo è questo:

  1. Estrazione degli item della scala
  2. Statistiche descrittive e difficoltà
  3. Analisi dei distrattori
  4. Matrice di correlazione (Ridondanza)
  5. Discriminatività (correlazione item-totale corretta)

2.3.1 Passo 1: Estrazione degli item

names(iqitems)
 [1] "reason.4"  "reason.16" "reason.17" "reason.19" "letter.7"  "letter.33"
 [7] "letter.34" "letter.58" "matrix.45" "matrix.46" "matrix.47" "matrix.55"
[13] "rotate.3"  "rotate.4"  "rotate.6"  "rotate.8" 
# matrici (col. 9-12) + rotazioni spaziali (col. 13-16)
item <- iqitems[, 9:16]
names(item)
[1] "matrix.45" "matrix.46" "matrix.47" "matrix.55" "rotate.3"  "rotate.4" 
[7] "rotate.6"  "rotate.8" 
# chiavi di risposta corrette per questi 8 item
key <- iq.keys[9:16]
key
[1] 5 2 2 4 3 2 6 7

2.3.2 Passo 2: Statistiche descrittive e difficoltà

Quando si analizzano item di un test di prestazione massima, la prima caratteristica da esaminare è la difficoltà, definita come la proporzione di rispondenti che ha risposto correttamente. Per calcolarla, occorre prima eseguire lo scoring: ogni risposta grezza viene convertita in 0 (errata) o 1 (corretta).

La difficoltà non è una qualità assoluta

La difficoltà degli item dipende dal campione di rispondenti considerato: item “difficili” in un campione di bassa abilità possono risultare “facili” in un campione esperto. Questo sottolinea l’importanza della rappresentatività del campione rispetto alla popolazione target nella fase di validazione di una misura.

Un modo manuale per ottenere lo scoring è tramite ifelse():

# scoring manuale per singolo item
# se il valore è uguale a ... allora assegna 1, altrimenti 0
item$matrix.45 <- ifelse(item$matrix.45 == 5, 1, 0)
item$matrix.46 <- ifelse(item$matrix.46 == 2, 1, 0)

# metodo più compatto, applicato a tutte le righe
t(apply(item, 1, function(x) ifelse(x == key, 1, 0)))

Il pacchetto psych automatizza questa trasformazione tramite score.multiple.choice():

# key = risposta giuata (chiave di risposta)
score.multiple.choice(key = key, data = item, score = FALSE)
# visualizza le prime righe 
head(score.multiple.choice(key = key, data = item, score = FALSE))
   matrix.45 matrix.46 matrix.47 matrix.55 rotate.3 rotate.4 rotate.6 rotate.8
5          0         0         0         1        0        0        0        0
6          0         0         0         0        0        0        1        0
7          1         1         0         0        0        0        0        0
8          0         0         0         0        0        0        0        0
9          1         1         0         0        0        0        0        0
10         1         1         1         0        1        1        1        0

Con score = TRUE otteniamo alcune statistiche per ciascun item:

item.stats <- score.multiple.choice(key = key, data = item, score = TRUE)$item.stats
item.stats
          key    0    1    2    3    4    5    6    7    8 miss    r    n mean
matrix.45   5 0.04 0.01 0.06 0.14 0.18 0.53 0.04 0.00 0.00    0 0.56 1523 0.53
matrix.46   2 0.04 0.12 0.55 0.07 0.11 0.06 0.05 0.00 0.00    0 0.55 1523 0.55
matrix.47   2 0.04 0.05 0.61 0.07 0.11 0.06 0.06 0.00 0.00    0 0.56 1523 0.61
matrix.55   4 0.04 0.02 0.18 0.14 0.37 0.07 0.18 0.00 0.00    0 0.52 1523 0.37
rotate.3    3 0.04 0.03 0.04 0.19 0.22 0.15 0.05 0.12 0.15    0 0.63 1523 0.19
rotate.4    2 0.04 0.03 0.21 0.05 0.18 0.04 0.04 0.25 0.15    0 0.65 1523 0.21
rotate.6    6 0.04 0.22 0.02 0.05 0.14 0.05 0.30 0.04 0.14    0 0.64 1523 0.30
rotate.8    7 0.04 0.03 0.21 0.07 0.16 0.05 0.13 0.19 0.13    0 0.60 1523 0.19
            sd
matrix.45 0.50
matrix.46 0.50
matrix.47 0.49
matrix.55 0.48
rotate.3  0.40
rotate.4  0.41
rotate.6  0.46
rotate.8  0.39
Come interpretare la tabella

Le colonne principali sono:

Colonna Significato
key Risposta corretta (chiave)
mean Difficoltà: proporzione di rispondenti corretti (0 = nessuno corretto, 1 = tutti corretti)
sd Deviazione standard delle difficoltà
n numero di unità statistiche
0 ... 8 frequenze relative

2.3.3 Passo 3: Analisi dei distrattori

Per ciascun item vogliamo sapere:

  1. Quanti rispondenti hanno scelto ciascuna alternativa?
  2. I distrattori sono funzionanti? Un distrattore scelto da pochissimi (o da nessuno) può essere revisionato o rimosso.
  3. Le alternative sono scelte in modo relativamente distribuito? Idelamente i distrattori dovrebbero essere scelti uniformemente.

2.3.3.1 Frequenze delle alternative: grafici per item

Visualizziamo la distribuzione di frequenza per ogni item:

Codice
# salvo item originali (risposte grezze) prima dello scoring
item_raw <- iqitems[, 9:16]

par(mfrow = c(2, 4), mar = c(4, 4, 3, 1))

for (i in seq_along(names(item_raw))) {
  nome <- names(item_raw)[i]
  freq  <- table(item_raw[[i]]) / nrow(item_raw)

  bp <- barplot(
    freq,
    main  = nome,
    ylab  = "Proporzione",
    xlab  = "Alternativa",
    col   = ifelse(names(freq) == key[i], "#2196F3", "#BDBDBD"),
    ylim  = c(0, max(freq) * 1.2)
  )
  }

La barra blu rappresenta la risposta corretta; le barre grigie sono i distrattori.


2.3.4 Passo 4: Matrice di correlazione

È utile esaminare le correlazioni tra item per individuare possibili ridondanze e verificare la coerenza interna della batteria. Per item dicotomici, tuttavia, la correlazione di Pearson non è la scelta più appropriata: le risposte 0/1 non sono variabili continue, e applicare Pearson significherebbe ignorare la natura del dato.

2.3.4.1 Correlazione tetracorica

La stessa logica della correlazione policorica si applica al caso più semplice in cui le variabili sono dicotomiche (0/1): con una sola categoria di confine, rimane un’unica soglia \(\tau_i\).

L’assunzione è che ogni risposta osservata \(Y_i\) sia la realizzazione di una variabile latente continua \(Y_i^*\), distribuita normalmente, osservabile solo attraverso il superamento o meno di una soglia:

\[ Y_i = \begin{cases} 1 & \text{se } Y_i^* \geq \tau_i \\ 0 & \text{se } Y_i^* < \tau_i \end{cases} \]

dove \(\tau_i\) è una soglia fissa (incognita). La correlazione tetracorica stima la correlazione tra le variabili latenti sottostanti:

\[ r_{\text{tet}_{Y_i Y_j}} = r_{Y_i^* Y_j^*} \]

La regione arancione rappresenta i rispondenti il cui livello latente supera la soglia \(\tau_i\) (rispondono 1); la regione blu chi non la supera (risponde 0). La soglia \(\tau_i\) determina la probabilità di rispondere 1: più è alta (spostata a destra), minore è la proporzione di risposte corrette (ie., più difficile è l’item).

Codice
tau <- 0.5

x <- seq(-4, 4, length.out = 500)
y <- dnorm(x)

plot(x, y, type = "l", lwd = 2,
     xlab = expression(Y[i]^"*"),
     ylab = "Densità",
     main = "Dalla variabile latente alla risposta osservata",
     axes = FALSE, frame.plot = FALSE)
axis(1); axis(2)

x_left <- x[x < tau]
polygon(c(x_left, rev(x_left)),
        c(dnorm(x_left), rep(0, length(x_left))),
        col = "#d9e8f5", border = NA)

x_right <- x[x >= tau]
polygon(c(x_right, rev(x_right)),
        c(dnorm(x_right), rep(0, length(x_right))),
        col = "#f4a582", border = NA)

abline(v = tau, lty = 2, lwd = 1.8, col = "gray30")
mtext(expression(tau[i]), side = 1, at = tau,
      line = 0.4, cex = 1.1, col = "gray30")
text(-1.8, 0.15, expression(Y[i] == 0), cex = 1.1, col = "#2166ac")
text( 1.8, 0.15, expression(Y[i] == 1), cex = 1.1, col = "#b2182b")

p1 <- round(1 - pnorm(tau), 2)
p0 <- round(pnorm(tau), 2)
text(-1.8, 0.10, paste0("P = ", p0), cex = 0.9, col = "#2166ac")
text( 1.8, 0.10, paste0("P = ", p1), cex = 0.9, col = "#b2182b")
lines(x, y, lwd = 2)

L’area arancione corrisponde esattamente a \(1 - \Phi(\tau_i)\), cioè la proporzione attesa di risposte 1 (lo stesso pnorm() che abbiamo usato per interpretare le soglie nella sezione precedente).

Binomiale (\(p = 1 - \Phi(\tau_i)\))

Possiamo confermarlo empiricamente: se simuliamo 10.000 rispondenti da un processo binomiale con probabilità di successo \(p = 1 - \Phi(\tau_i)\), la proporzione di 1 ottenuta converge all’area arancione.

tau <- 0.5

# P(Y = 1) teorica
p_corretto <- 1 - pnorm(tau)
p_corretto
[1] 0.3085375
# Verifica empirica: proporzione di 1 su 10.000 simulazioni
mean(rbinom(10000, size = 1, prob = p_corretto))
[1] 0.301

2.3.4.2 Approssimazione della correlazione tetracorica

Il punto di partenza è la tabella di contingenza \(2 \times 2\) tra due item \(Y_i\) e \(Y_j\):

\(Y_j = 0\) \(Y_j = 1\)
\(Y_i = 0\) \(n_{00}\) \(n_{01}\) \(n_{0\bullet} = n_{00} + n_{01}\)
\(Y_i = 1\) \(n_{10}\) \(n_{11}\) \(n_{1\bullet} = n_{10} + n_{11}\)
\(n_{\bullet 0} = n_{00} + n_{10}\) \(n_{\bullet 1} = n_{01} + n_{11}\) \(n\)

A partire da qui, la correlazione tetracorica si può approssimare come:

\[ r_{\text{tet}_{Y_i, Y_j}} \approx \cos\!\left(\frac{\pi}{1 + \sqrt{\dfrac{\hat{\pi}_{00}\,\hat{\pi}_{11}}{\hat{\pi}_{10}\,\hat{\pi}_{01}}}}\right) \]

dove \(\pi\) è il pi greco e il termine sotto radice è il rapporto tra le celle concordanti (\(\hat{\pi}_{00}\,\hat{\pi}_{11}\), stessa risposta su entrambi gli item) e discordanti (\(\hat{\pi}_{10}\,\hat{\pi}_{01}\), risposte opposte). Più questo rapporto è grande, più i due item tendono a co-occorrere, e quindi più alta è la correlazione.

Esempio numerico:

Supponiamo di avere \(n = 100\) rispondenti e la seguente tabella di contingenza osservata:

\(Y_j = 0\) \(Y_j = 1\) Totale
\(Y_i = 0\) 30 20 50
\(Y_i = 1\) 10 40 50
Totale 40 60 100

Le proporzioni di cella sono:

\[ \hat{\pi}_{00} = 0.30, \quad \hat{\pi}_{01} = 0.20, \quad \hat{\pi}_{10} = 0.10, \quad \hat{\pi}_{11} = 0.40 \]

Sostituendo nella formula:

\[ r_{\text{tet}} \approx \cos\!\left(\frac{\pi}{1 + \sqrt{\dfrac{0.30 \times 0.40}{0.10 \times 0.20}}}\right) = \cos\!\left(\frac{\pi}{1 + \sqrt{6}}\right) = \cos\!\left(\frac{\pi}{3.449}\right) \approx 0.61 \]

I due item mostrano dunque una correlazione tetracorica di circa 0.61, coerentemente con un’associazione positiva tra le variabili latenti sottostanti.

Esercizio: 1

Supponiamo di avere \(n = 100\) rispondenti e la seguente tabella di contingenza osservata:

\(Y_j = 0\) \(Y_j = 1\) Totale
\(Y_i = 0\) 10 40 50
\(Y_i = 1\) 40 10 50
Totale 50 50 100

Cosa puoi dire della correlazione tra queste due variabili?

2.3.4.3 Calcolo in R

# scoring 
item_scored <- score.multiple.choice(key = key, data = item, score = FALSE)

# visualizzo prime righe
head(item_scored)
   matrix.45 matrix.46 matrix.47 matrix.55 rotate.3 rotate.4 rotate.6 rotate.8
5          0         0         0         1        0        0        0        0
6          0         0         0         0        0        0        1        0
7          1         1         0         0        0        0        0        0
8          0         0         0         0        0        0        0        0
9          1         1         0         0        0        0        0        0
10         1         1         1         0        1        1        1        0
# matrice di correlazione tetracorica
cor_matrix <- tetrachoric(item_scored)$rho
# Viasualizzo
corrplot(cor_matrix,
         method = "color",
         type = "lower",
         addCoef.col = "black",
         number.cex  = 0.7,
         tl.col = "black",
         tl.cex = 0.85,
         mar  = c(0, 0, 1, 0))

L’output di tetrachoric() riporta la stima di \(\rho\) e i valori di \(\tau\) (le soglie) per ciascun item. Valori di \(\tau\) negativi indicano che la soglia latente è al di sotto della media della distribuzione, cioè che l’item è relativamente facile.

tetrachoric(item_scored)
Call: tetrachoric(x = item_scored)
tetrachoric correlation 
          mt.45 mt.46 mt.47 mt.55 rtt.3 rtt.4 rtt.6 rtt.8
matrix.45 1.00                                           
matrix.46 0.51  1.00                                     
matrix.47 0.40  0.38  1.00                               
matrix.55 0.35  0.25  0.36  1.00                         
rotate.3  0.31  0.29  0.41  0.33  1.00                   
rotate.4  0.33  0.32  0.40  0.32  0.77  1.00             
rotate.6  0.27  0.35  0.34  0.30  0.67  0.69  1.00       
rotate.8  0.31  0.29  0.34  0.34  0.67  0.68  0.67  1.00 

 with tau of 
matrix.45 matrix.46 matrix.47 matrix.55  rotate.3  rotate.4  rotate.6  rotate.8 
   -0.065    -0.125    -0.290     0.321     0.864     0.797     0.526     0.896 

Cosa potete dire su questo output?

Relazione tra: matrix.45 e matrix.46

draw.tetra(0.51, -0.065, -0.125)

Relazione tra: matrix.46 e rotate.8

draw.tetra(0.29, -0.125, 0.896)

Relazione inventata, con correlazione negativa:

draw.tetra(-0.29, 0.896, 0.896)


2.3.5 Passo 5: Discriminatività (correlazione item-totale corretta)

La discriminatività misura quanto bene un singolo item distingue tra rispondenti con abilità alta e bassa. L’indice più usato è la correlazione item-totale corretta: correlazione tra il punteggio di ciascun item e il punteggio totale calcolato escludendo quell’item (per evitare correlazioni spurie).

Correlazione punto-biseriale

Nel caso di item dicotomici, questa misura viene calcolata tramite la correlazione punto-biseriale (\(r_{pb}\)), che è la correlazione tra una variabile binaria (la risposta 0/1 al singolo item) e una variabile continua (il punteggio totale). La formula è:

\[ r_{pb} = \frac{\bar{X}_1 - \bar{X}_0}{s_X} \sqrt{\frac{n_1 \, n_0}{n^2}} \]

dove:

  • \(\bar{X}_1\) è il punteggio medio totale dei rispondenti che hanno risposto correttamente all’item (risposta = 1)
  • \(\bar{X}_0\) è il punteggio medio totale dei rispondenti che hanno risposto in modo errato (risposta = 0)
  • \(s_X\) è la deviazione standard del punteggio totale
  • \(n_1\) e \(n_0\) sono rispettivamente il numero di rispondenti con risposta 1 e 0
  • \(n = n_1 + n_0\) è il totale dei rispondenti

Intuitivamente, un item discrimina bene se chi lo risponde correttamente tende ad avere anche un punteggio totale alto: in questo caso \(\bar{X}_1 \gg \bar{X}_0\) e \(r_{pb}\) sarà elevata.

# prendo gli item relativi a matrix
df <- as.data.frame(item_scored[,1:4])

# estraggo osservazioni sottogruppi
g1 <- df[df$matrix.45 == 1, ]
g0 <- df[df$matrix.45 == 0, ]

# tolgo la prima colonna (matrix.45)
g1 <- g1[,-c(1)] 
g0 <- g0[,-c(1)] 

# computo il punteggio totale
# somma gli 1 sulle per riga
# mean applica la media 
m1 <- mean(rowSums(g1))
m0 <- mean(rowSums(g0))

# ottengo la deviazione standard del totale 
# escludendo però "matrix.45"
df <- df[,-c(1)] 
sdtot <- sd(rowSums(df))

# mi serve il numero di soggetti che hanno 
# risposto corrett vs. no
n1 <- nrow(g1)
n2 <- nrow(g0)
N <- n1+n2 # il totale

# la formula
((m1-m0)/sdtot)*sqrt((n1*n2)/N^2)
[1] 0.3987347

Verifico che il valore sia simile a quanto ottengo attraverso la funzione psych::alpha:

alpha_output <- psych::alpha(item_scored[,1:4])

alpha_output$item.stats[ , c("r.drop")]
[1] 0.3988657 0.3561506 0.3466763 0.2772232

In generale, si considerano accettabili valori superiori a .20–.30. Item con valori molto bassi o negativi sono problematici: non discriminano tra rispondenti capaci e non capaci, e potrebbero essere formulati in modo ambiguo, avere una chiave di risposta errata, o misurare qualcosa di diverso dal resto del test.

Esercizio

Computa la correlazione item-totale corretta per matrix.46, seguendo lo script scritto sopra.

# prendo gli item relativi a matrix
df <- as.data.frame(item_scored[,1:4])

##

##

Recap correlazioni
Coefficiente Variabile 1 Variabile 2
Point-biserial (\(r_{pb}\)) dicotomica continua
Tetracorica (\(r_{\text{tet}}\)) dicotomica (latente continua) dicotomica (latente continua)
Policorica (\(r_{\text{poly}}\)) politomica (latente continua) politomica (latente continua)

2.4 Riferimenti