74  Dal GLM a un modello processuale per dati binari

Obiettivi del capitolo
  • Mostrare come un Probabilistic Programming Language (Stan) permetta di andare oltre un GLM classico.
  • Costruire un modello processuale per scelte binarie (es. sì/no, A/B) in psicologia.
  • Eseguire prior predictive check, stima bayesiana e posterior predictive check in R (con cmdstanr e brms opzionale).
Preparazione del Notebook
here::here("code", "_common.R") |> 
  source()

# Load packages
if (!requireNamespace("pacman")) install.packages("pacman")
pacman::p_load(brms, cmdstanr, posterior, brms, bayestestR, insight, conflicted)

conflicts_prefer(posterior::mad)

Introduzione

La regressione logistica, introdotta nei capitoli precedenti, è uno strumento fondamentale per analizzare variabili dicotomiche \(y \in {0,1}\) (ad esempio: scelta sì/no, risposta corretta/errata, accettazione/rifiuto) in funzione di predittori osservabili \(\mathbf{x}\). Tuttavia, questo approccio presenta limiti importanti quando vogliamo descrivere processi psicologici dinamici, nei quali la risposta osservata è soltanto la manifestazione finale di meccanismi interni che si sviluppano nel tempo.

Ad esempio, in un questionario online possiamo usare la regressione logistica per prevedere se uno studente risponde correttamente o erroneamente in base alla difficoltà della domanda. Ma in un compito cognitivo ripetuto, la risposta dello stesso studente al trial 10 dipende anche da come è andato il trial 9 (feedback positivo/negativo, fatica accumulata, ecc.). Il modello statico non “vede” questa dipendenza, mentre un modello processuale la incorpora.

In molti contesti cognitivi e comportamentali, le risposte binarie riflettono infatti:

  • processi accumulativi, come l’integrazione di evidenza nel decision making,
  • dinamiche temporali interne, ad esempio legate all’apprendimento, all’adattamento o all’affaticamento,
  • interazioni stato-dipendenti, come variazioni momentanee di motivazione o attenzione.

Il modello logistico standard assume invece:

  • indipendenza tra le osservazioni,
  • effetti costanti nel tempo,
  • un unico meccanismo generativo omogeneo.

Queste ipotesi sono spesso poco realistiche nei dati psicologici longitudinali o sperimentali, dove:

  • le risposte sono influenzate dalla storia precedente (history dependence),
  • gli stati latenti del soggetto evolvono lungo traiettorie individuali,
  • la stessa soglia decisionale può cambiare nel corso del tempo.

Per catturare meglio questi aspetti servono modelli più sofisticati, capaci di rappresentare la dipendenza temporale tra le osservazioni, l’eterogeneità intra-individuale e la natura incrementale dei processi cognitivi sottostanti. Questa transizione da una rappresentazione statica alla modellazione dinamica è cruciale in psicologia: il comportamento osservato non va visto come un evento isolato, ma come l’istantanea di un sistema complesso in continua evoluzione.

Immaginiamo uno studente che affronta un test a scelta multipla. Con un modello logistico classico possiamo prevedere se risponderà correttamente in base alla difficoltà della domanda. Ma se lo stesso studente sta svolgendo una lunga serie di prove, la sua risposta alla domanda 10 dipende anche da come è andata la domanda 9: un errore può ridurre la fiducia, un successo può aumentarla. In più, col passare del tempo, possono intervenire affaticamento o distrazione. Il modello statico logit non cattura nulla di tutto ciò. Per includere questi aspetti servono modelli che riconoscano che ogni osservazione porta memoria del passato.

L’obiettivo di questo capitolo è mostrare come, con Stan, sia possibile esplicitare il meccanismo generativo delle risposte, superando i limiti della regressione logistica classica e introducendo la nozione di processi dinamici autoregressivi che meglio riflettono la natura temporale dei fenomeni psicologici.

74.1 Dal modello statico al modello processuale

74.1.1 La regressione logistica classica

Nel modello di regressione logistica (GLM logit) la probabilità di osservare una risposta positiva è:

\[ \Pr(y_i=1 \mid \mathbf{x}_i) = \operatorname{logit}^{-1}\!\left(\alpha + \mathbf{x}_i^\top \boldsymbol\beta\right). \]

Qui:

  • \(\alpha\) è l’intercetta, che rappresenta la tendenza di base a rispondere 1;
  • \(\boldsymbol\beta\) descrive come i predittori osservati influenzano questa probabilità.

Un modo intuitivo per interpretare la formula è introdurre una variabile latente continua \(u_i\), che possiamo pensare come la propensione interna dell’individuo a rispondere “1”:

\[ u_i = \alpha + \mathbf{x}_i^\top \boldsymbol\beta + \varepsilon_i, \qquad \varepsilon_i \sim \text{Logistic}(0,1). \]

La regola di decisione è semplice:

  • se \(u_i > 0\) allora osserviamo \(y_i=1\),
  • se \(u_i \leq 0\) allora osserviamo \(y_i=0\).

In altre parole, immaginiamo che l’individuo abbia una soglia fissa: quando la propensione supera questa soglia, la risposta osservata diventa positiva.

Possiamo pensare a \(u_i\) come a un “serbatoio di propensione”: se il livello supera la soglia, si osserva una risposta positiva. Nei modelli statici il serbatoio si svuota e si riempie indipendentemente a ogni trial; nei modelli dinamici, invece, il livello attuale dipende anche da quanto era pieno al trial precedente.

Nella regressione logistica classica, questa soglia è sempre costante nel tempo e uguale per tutti i trial. Ma nei processi psicologici reali ciò non è sempre realistico: la soglia decisionale (o l’intensità della propensione) può cambiare da un momento all’altro, ad esempio per effetto di apprendimento, fatica o variazioni di motivazione.

Ed è proprio da qui che nasce la necessità di estendere il modello logit statico a un modello dinamico, in cui la variabile latente \(u\) (e talvolta anche la soglia) possa variare nel tempo e riflettere la natura evolutiva dei processi psicologici.

74.2 Oltre il GLM: dinamica temporale

Nella regressione logistica classica abbiamo visto che ogni risposta osservata \(y_i\) può essere pensata come il risultato di una propensione latente \(u_i\), confrontata con una soglia fissa. Questa impostazione funziona bene se consideriamo le osservazioni come indipendenti e isolate.

Ma in psicologia le cose vanno spesso diversamente:

  • negli esperimenti con prove ripetute, le decisioni prese oggi sono influenzate da quelle appena fatte;
  • nelle misurazioni longitudinali (EMA), lo stato emotivo o motivazionale di un momento dipende in parte da quello precedente;
  • nei compiti di apprendimento, l’esperienza accumulata modifica gradualmente la propensione a scegliere un’opzione rispetto a un’altra.

In tutti questi casi, è naturale immaginare che la variabile latente \(u_{i,t}\) non nasca da zero a ogni prova, ma porti memoria del passato.

74.3 Il modello AR(1)

Per rendere esplicita la dipendenza dal passato, usiamo un modello autoregressivo di ordine 1 (AR(1); Chatfield & Xing (2019)):

\[ \begin{aligned} u_{i,t} &= \alpha_i + \mathbf{x}_{i,t}^\top \boldsymbol\beta + \phi \, u_{i,t-1} + \eta_{i,t}, & \eta_{i,t} \sim \mathcal{N}(0,\sigma_u), \\[6pt] y_{i,t} \mid u_{i,t} &\sim \text{Bernoulli}\!\left(\operatorname{logit}^{-1}(u_{i,t})\right). \end{aligned} \]

Possiamo immaginare \(u_{i,t}\) come il livello di un “serbatoio di propensione”: se questo valore supera la soglia implicita dello 0 sulla scala logit, la risposta osservata è positiva \((y_{i,t}=1)\). La novità rispetto al modello statico è che il livello attuale \(u_{i,t}\) dipende anche da quello precedente \(u_{i,t-1}\), attraverso il termine \(\phi u_{i,t-1}\).

Significati dei parametri del modello AR(1):

  • \(\alpha_i\) (intercetta soggetto-specifica): propensione media di un individuo (es. uno studente molto ansioso potrebbe avere più probabilità di rispondere “no”).

  • \(\beta\) (effetto dei predittori): effetto di variabili osservabili (es. domande più facili aumentano la probabilità di risposta corretta).

  • \(\phi\) (persistenza dinamica): quanta parte dello stato passato sopravvive:

    • se \(\phi=0\), nessuna memoria: ogni risposta è “indipendente”,
    • se \(\phi>0\), inerzia: un successo ieri aumenta la probabilità di successo oggi,
    • se \(\phi<0\), alternanza: un successo ieri rende più probabile un errore oggi (pattern a zig-zag).
  • \(\sigma_u\) (variabilità del processo): irregolarità: se grande, le traiettorie diventano rumorose (es. risposte altalenanti per distrazioni).

Un piccolo schema concettuale aiuta a visualizzare:

u(t-1)  ──▶  u(t)  ──▶  y(t)
   │
   └─────────── φ ───────────┘

Per esempio:

  • \(\alpha_i\): uno studente particolarmente ansioso potrebbe avere un’alta probabilità di rispondere “no” a prescindere dalla domanda;
  • \(\beta\): se la domanda è facile (\(x=1\)), aumenta la probabilità di risposta corretta;
  • \(\phi\): se lo studente ha risposto correttamente ieri, oggi sarà più probabile che risponda ancora correttamente;
  • \(\sigma_u\): cattura la variabilità inspiegata, come distrazioni improvvise.

74.3.1 Che cos’è \(u_{i,t}\)?

  • \(u_{i,t}\) non è osservato: è uno stato latente.
  • Possiamo pensarlo come il “livello di propensione” di un individuo in un certo istante \(t\).
  • L’osservazione \(y_{i,t}\) (corretto/errato, sì/no, 1/0) nasce da questo stato: se \(u_{i,t}\) è alto, la probabilità di risposta positiva è alta; se è basso, è bassa.
  • Nei modelli Bayesiani o di stato latente, i valori di \(u_{i,t}\) non si calcolano direttamente dai dati ma vengono stimati/inferiti dal modello. In pratica, otteniamo una distribuzione a posteriori su ciascun \(u_{i,t}\), non un singolo valore deterministico.

74.3.2 E il ruolo di \(\phi\)?

Il coefficiente \(\phi\) funziona come un “peso di memoria”:

  • Se \(\phi = 0\), il passato non conta: \(u\_{i,t}\) dipende solo dai predittori e dal rumore.
  • Se \(\phi > 0\), c’è inerzia: lo stato precedente influenza positivamente quello attuale.
  • Se \(\phi < 0\), c’è compensazione o alternanza: uno stato alto ieri spinge verso uno basso oggi.

Formalmente, \(\phi\) è un coefficiente di regressione come \(\alpha\) e \(\beta\), ma agisce sulla variabile latente del tempo precedente, quindi introduce dipendenza temporale.

74.3.3 Come “si trovano” i valori di \(u_{i,t-1}\)?

  • All’inizio (al tempo \(t=1\)), bisogna specificare una condizione iniziale per \(u_{i,0}\), ad esempio assumendo \(u_{i,0} \sim \mathcal{N}(0,\sigma_0)\).
  • Per i tempi successivi, ogni \(u_{i,t}\) viene costruito ricorsivamente dal precedente: il modello stesso definisce la sequenza degli stati latenti.
  • In fase di stima (ad esempio con Stan), si usano i dati osservati \(y_{i,t}\) per inferire a posteriori quali valori plausibili di \(u_{i,t}\) rendono il modello coerente con le risposte osservate.

Riassumendo:

  • \(u_{i,t}\): stato latente, stimato dal modello, non osservato.
  • \(\phi\): coefficiente che regola quanto lo stato passato influenza quello presente.
  • \(\alpha_i\), \(\beta\): intercetta e predittori osservati, come in una regressione logistica.
  • \(\sigma_u\): variabilità residua del processo latente.

74.3.4 Perché è importante?

Con il modello AR(1) facciamo un passo oltre la regressione logistica classica. La probabilità di risposta non è più determinata solo dai fattori esterni osservati, ma anche dagli stati interni accumulati nel tempo. In altre parole, il comportamento osservato non nasce “da zero” a ogni prova: porta con sé una traccia del passato.

Esempi concreti aiutano a capirlo:

  • Compito di apprendimento: se un partecipante ha appena ricevuto un rinforzo positivo, la sua propensione a ripetere la stessa scelta sarà più alta al trial successivo.
  • Diario EMA: un umore negativo oggi aumenta la probabilità di trovarsi in uno stato simile anche domani, a meno che un evento esterno intervenga a interrompere la continuità.

La lezione fondamentale è questa: le scelte non sono indipendenti, ma intrecciate con la storia recente dell’individuo. Ed è proprio questa “memoria del passato” che rende i modelli dinamici strumenti più realistici e potenti per descrivere processi psicologici rispetto ai modelli statici.

Per fare un esempio semplice, consideriamo un modello continuo con un solo predittore continuo. La dipendenza temporale la collochiamo nei residui, con una struttura AR(1). In questo modo evitiamo il problema di introdurre variabili latenti non osservabili come \(u\).

Per il soggetto \(i\) al tempo \(t\):

\[ \begin{aligned} y_{i,t} &= \alpha_i + \beta\,x_{i,t} + e_{i,t},\\ e_{i,t} &= \phi\, e_{i,t-1} + \eta_{i,t}, \qquad \eta_{i,t}\sim\mathcal N(0,\sigma_\eta^2). \end{aligned} \]

dove:

  • \(y_{i,t}\) è la risposta osservata (continua),
  • \(x_{i,t}\) è il predittore osservato (continuo),
  • \(e_{i,t}\) è l’errore con memoria AR(1),
  • \(\phi\) è l’autocorrelazione a lag 1 dei residui,
  • \(\sigma_\eta\) controlla l’ampiezza del rumore “nuovo” che entra a ogni passo.

In altre parole, il valore osservato \(y_{i,t}\) è composto da:

  • una parte sistematicamente spiegata dal predittore \(x_{i,t}\), ponderata da \(\beta\),
  • una parte casuale, \(e_{i,t}\), che però non è indipendente: conserva memoria del residuo precedente (\(\phi e_{i,t-1}\)). Se ieri il modello ha sovrastimato, è probabile che anche oggi rimanga un residuo positivo; lo stesso vale per una sottostima.

Nota su \(e_{i,0}\). Per generare una serie “stazionaria”, inizializziamo il primo residuo dalla distribuzione stazionaria:

\[ e_{i,0} \sim \mathcal N\!\left(0,\; \frac{\sigma_\eta^2}{1-\phi^2}\right). \]

# ---- Funzione di simulazione ----
simulate_reg_ar1 <- function(alpha, beta = 0.8, phi = 0.7, sigma_eta = 1.0,
                             T_ = 60, x_sd = 1, seed = 123) {
  set.seed(seed)
  n_subj <- length(alpha)
  rec <- vector("list", n_subj)

  # Varianza stazionaria dei residui AR(1)
  sigma_e2 <- sigma_eta^2 / (1 - phi^2)

  for (i in seq_len(n_subj)) {
    e_prev <- rnorm(1, mean = 0, sd = sqrt(sigma_e2))  # e_{i,0}
    rows <- vector("list", T_)
    for (t in seq_len(T_)) {
      x_t <- rnorm(1, 0, x_sd)             # predittore osservato
      eta <- rnorm(1, 0, sigma_eta)        # innovazione
      e_t <- phi * e_prev + eta            # residuo AR(1)
      y_t <- alpha[i] + beta * x_t + e_t   # risposta continua
      rows[[t]] <- data.frame(
        subject = i, time = t,
        x = x_t, e = e_t, y = y_t
      )
      e_prev <- e_t
    }
    rec[[i]] <- dplyr::bind_rows(rows)
  }
  dplyr::bind_rows(rec)
}

# ---- Parametri e simulazione ----
alpha <- c(-1, 0, 1)   # intercette soggetto-specifiche
beta  <- 0.8           # effetto del predittore x
phi   <- 0.7           # autocorrelazione a lag 1 dei residui
sigma_eta <- 1.0       # deviazione standard innovazione
T_    <- 60            # lunghezza serie temporale

df <- simulate_reg_ar1(alpha, beta, phi, sigma_eta, T_)

# ---- Grafico didattico ----
df <- df %>%
  group_by(subject) %>%
  mutate(x_scaled = (x - mean(x)) / sd(x) * sd(y) + mean(y)) %>%
  ungroup()

ggplot(df, aes(time)) +
  geom_line(aes(y = y), linewidth = 0.9) +
  geom_line(aes(y = x_scaled), linetype = "dashed") +
  geom_hline(aes(yintercept = ave_y),
             data = df %>% group_by(subject) %>% summarise(ave_y = mean(y)),
             linewidth = 0.3, color = "grey40") +
  facet_wrap(~ subject, ncol = 1,
             labeller = as_labeller(function(s) {
               i <- as.integer(s)
               paste0("Soggetto ", s, " (alpha = ", alpha[i], ")")
             })) +
  labs(title = "Regressione con residui AR(1): y(t) e x(t) riscalato",
       subtitle = paste0("phi = ", phi, ", sigma_eta = ", sigma_eta, ", beta = ", beta),
       y = "y(t)  (x in tratteggio, riscalato)", x = "Tempo")

  1. Struttura del modello

    • La parte regressiva è \(\alpha_i + \beta x_{i,t}\).
    • La memoria sta nei residui: \(e_{i,t} = \phi e_{i,t-1} + \eta_{i,t}\).
    • Così il predittore \(x_{i,t}\) è esogeno e \(\phi\) ha un significato chiaro: è l’autocorrelazione tra residui consecutivi.
  2. Grafico a pannelli

    • Linea piena = \(y_{i,t}\).
    • Tratteggio = \(x_{i,t}\), riscalato per confrontarlo visivamente con \(y\).
    • Si vede che \(y\) segue \(x\), ma non cambia bruscamente: la presenza di \(\phi=0.7\) rende la traiettoria più “liscia” grazie alla memoria nei residui.
# ---- Verifica dell'AR(1) sui residui (soggetto 1) ----
df1 <- df %>% filter(subject == 1)
ols_fit <- lm(y ~ x, data = df1)
resid_ols <- resid(ols_fit)

par(mfrow = c(1, 2))
plot(df1$time, resid_ols, type = "l", main = "Residui OLS (soggetto 1)",
     xlab = "Tempo", ylab = "Residuo")
acf(resid_ols, main = "ACF residui OLS (soggetto 1)")

par(mfrow = c(1, 1))
  1. ACF dei residui OLS

Nel modello di regressione lineare semplice \(y \sim x\) si assume che i residui siano indipendenti. Nel nostro esempio, però, i residui hanno memoria AR(1): se ieri erano positivi, oggi tendono a esserlo di nuovo.

L’ACF (autocorrelation function) dei residui ci permette di vedere questo effetto:

  • misura quanto i residui in tempi diversi sono correlati,
  • il valore a lag 1 (tra residui consecutivi) è chiaramente positivo e vicino a \(\phi\).

Quindi:

  • il modello lineare classico che assume residui indipendenti non descrive bene i dati,
  • occorre un modello che includa la memoria, come l’AR(1) sui residui o un modello equivalente in stato-spazio.

74.4 Dall’AR(1) all’AR(K)

Il modello AR(1) ci ha mostrato che lo stato latente \(u_{i,t}\) non nasce mai da zero, ma porta con sé una traccia del passato immediato. Tuttavia, in molti processi psicologici questa “memoria a un passo” può essere troppo corta.

Pensiamo a situazioni in cui:

  • l’effetto di un’esperienza non si esaurisce al trial successivo ma dura più a lungo,
  • l’umore di oggi non dipende solo da quello di ieri, ma anche da quello di due o tre giorni fa,
  • l’apprendimento si accumula su una coda di feedback estesa.

In questi casi conviene estendere il modello ad un processo autoregressivo di ordine \(K\) (AR(K)):

\[ u_{i,t} = \alpha_i + \mathbf{x}_{i,t}^\top \boldsymbol\beta + \phi_1 u_{i,t-1} + \phi_2 u_{i,t-2} + \dots + \phi_K u_{i,t-K} + \eta_{i,t}, \qquad \eta_{i,t} \sim \mathcal{N}(0,\sigma_u). \]

74.4.1 Interpretazione psicologica

  • \(K=1\) (AR(1)) → memoria cortissima: il presente dipende solo dallo stato immediatamente precedente (es. l’effetto diretto di un feedback appena ricevuto).
  • \(K=2\) (AR(2)) → memoria breve: lo stato attuale risente degli ultimi due passi (es. l’umore influenzato dagli ultimi due giorni consecutivi).
  • \(K \geq 3\) → memoria più lunga: utile per processi cumulativi o ciclici (es. oscillazioni tra fasi di alta e bassa motivazione).

In altre parole, aumentando \(K\) allarghiamo la “finestra temporale” che il modello utilizza per spiegare il presente.

74.4.2 Perché è utile?

L’estensione ad AR(K) consente di modellare una gamma più ricca di dinamiche:

  • inerzia semplice (AR(1)),
  • effetti ritardati, che emergono dopo due o più step,
  • oscillazioni regolari o pattern ciclici (catturabili già con un AR(2) o AR(3)).

Così il modello diventa più flessibile e aderente alla complessità dei processi psicologici reali, nei quali la memoria del passato non ha sempre la stessa profondità, ma può essere breve, prolungata o ciclica.

Quando usare modelli AR in psicologia?

  • Nei compiti decisionali con prove ripetute, quando sospettiamo che non solo l’ultima esperienza ma anche quelle precedenti influenzino la scelta.
  • Negli studi EMA, quando l’umore o la motivazione di oggi risentono di più giorni consecutivi.
  • In generale, in tutti i casi in cui la sequenza temporale porta informazioni importanti che sarebbe un errore trattare come semplice rumore.

74.5 Simulazione dati

Prima di stimare il modello su dati reali, conviene costruire un dataset simulato (AR(1) logit a livello latente). In questo modo possiamo verificare se il modello riesce a recuperare parametri noti e comprendere meglio il suo funzionamento.

Immaginiamo \(I=100\) soggetti, ciascuno con \(T=30\) prove, e un predittore binario \(x_{i,t}\) (ad esempio: tipo di stimolo, 0 = neutro, 1 = emozionale). Lo stato latente \(u_{i,t}\) evolve come un AR(1) sulla scala logit.

set.seed(123)

I   <- 100   # numero di soggetti
Tt  <- 30    # numero di trial per soggetto
N   <- I*Tt  # osservazioni totali

# Parametri "veri" usati per generare i dati
alpha_mu    <- 0.0   # intercetta media
alpha_sigma <- 0.7   # variabilità tra-soggetti
beta_true   <- 0.6   # effetto del predittore
phi_true    <- 0.5   # persistenza dinamica
sigma_u     <- 0.6   # rumore di processo

# Intercette soggetto-specifiche
alpha_i <- rnorm(I, alpha_mu, alpha_sigma)

# Predittore binario (0/1) random
x <- rbinom(N, 1, 0.5)

# Costruzione dataset
df <- tibble::tibble(
  id = rep(1:I, each = Tt),
  t  = rep(1:Tt, times = I),
  x  = x
)

# Stato latente e risposta
u <- numeric(N)
y <- integer(N)

for (i in 1:I) {
  a  <- alpha_i[i]
  ui <- numeric(Tt)
  for (tt in 1:Tt) {
    idx <- (i-1)*Tt + tt
    mean_ut <- a + beta_true*df$x[idx] + ifelse(tt == 1, 0, phi_true*ui[tt-1])
    ui[tt]  <- rnorm(1, mean_ut, sigma_u)        # stato latente
    p       <- 1/(1 + exp(-ui[tt]))              # probabilità risposta
    y[idx]  <- rbinom(1, 1, p)                   # risposta binaria
  }
  u[((i-1)*Tt+1):(i*Tt)] <- ui
}

df$u_lat <- u
df$y     <- y

Il modello ha bisogno di sapere quale osservazione viene prima nello stesso soggetto. Se non stiamo attenti, potremmo collegare l’ultimo trial del soggetto \(i\) con il primo del soggetto \(i+1\), il che è sbagliato. Per evitare errori, creiamo un indice prev che punta al trial precedente solo dello stesso soggetto. Se non esiste (primo trial), mettiamo 0.

df <- df[order(df$id, df$t), ]
prev <- integer(nrow(df))
for (i in unique(df$id)) {
  idx  <- which(df$id == i)
  prev[idx] <- c(0, head(idx, -1))  # 0 = nessun precedente
}
stopifnot(all(df$t[prev[df$t>1]] == df$t[df$t>1]-1))

Esempio: se il soggetto 5 ha 30 trial, per il trial 12 prev punterà al trial 11, mentre per il trial 1 avrà valore 0.

74.6 Tabella-ponte: dall’algebra a Stan

Per tradurre il modello matematico in Stan, costruiamo una “mappa” dei concetti.

Concetto Simbolo Stan Nota
Numero osservazioni \(N\) int<lower=1> N;
Numero soggetti \(I\) int<lower=1> I;
Soggetto per trial array[N] int<lower=1,upper=I> id;
Trial precedente (stesso soggetto) array[N] int<lower=0,upper=N> prev; 0 se non esiste
Predittori \(\mathbf{x}_{i,t}\) array[N] int x; Estendibile a matrice
Risposta \(y_{i,t}\) array[N] int y; Bernoulli
Stato latente \(u_{i,t}\) vector[N] u;
Intercetta soggetto \(\alpha_i\) vector[I] alpha; (non centrato)
Persistenza \(\phi\) real<lower=-0.99,upper=0.99> phi; vincolo stazionarietà
Rumore di processo \(\sigma_u\) real<lower=0> sigma_u; deviazione standard

74.7 Modello Stan

Ora traduciamo il modello AR(1) logit in Stan. L’idea è di rappresentare esplicitamente tre parti del processo:

  1. Intercette soggetto-specifiche (\(\alpha_i\)), stimate in forma non centrata per migliorare la mescolanza della catena.

  2. Evoluzione dello stato latente \(u_{i,t}\):

    • al primo trial di ciascun soggetto, \(u_{i,1}\) dipende solo dall’intercetta, dai predittori e dal rumore;
    • nei trial successivi, \(u_{i,t}\) dipende anche dal valore precedente \(u_{i,t-1}\), con peso \(\phi\).
  3. Likelihood: la risposta osservata \(y_{i,t}\) segue una Bernoulli logit con parametro \(u_{i,t}\).

Inoltre, nel blocco generated quantities calcoliamo:

  • y_rep = repliche simulate, utili per i posterior predictive check;
  • log_lik = contributi della verosimiglianza, necessari per il calcolo di LOO/WAIC.

Formalmente, il modello implementato è:

\[ \begin{aligned} u_{i,1} &\sim \mathcal{N}(\alpha_i + \mathbf{x}_{i,1}^\top \beta, \sigma_u), \\ u_{i,t} &\sim \mathcal{N}(\alpha_i + \mathbf{x}_{i,t}^\top \beta + \phi u_{i,t-1}, \sigma_u) \quad (t>1), \\ y_{i,t} \mid u_{i,t} &\sim \text{Bernoulli}\!\left(\operatorname{logit}^{-1}(u_{i,t})\right). \end{aligned} \]

stan_code <- '
data{
  int<lower=1> N;
  int<lower=1> I;
  array[N] int<lower=1,upper=I> id;
  array[N] int<lower=0,upper=1> x;    // estendibile a vettore
  array[N] int<lower=0,upper=1> y;
  array[N] int<lower=0,upper=N> prev; // 0 se non esiste trial precedente dello stesso soggetto
}
parameters{
  vector[I] alpha_raw;
  real      alpha_mu;
  real<lower=0> alpha_sigma;

  real beta;
  real<lower=-0.99, upper=0.99> phi;
  real<lower=0> sigma_u;

  vector[N] eps; // innovazioni standard N(0,1)
}
transformed parameters{
  vector[I] alpha = alpha_mu + alpha_sigma * alpha_raw;
  vector[N] u;
  for (n in 1:N){
    real mean_u = alpha[id[n]] + beta * x[n];
    if (prev[n] == 0) {
      // opzionale: condizione stazionaria per il primo trial
      real sd1 = sigma_u / sqrt(1 - square(phi));
      u[n] = mean_u + sd1 * eps[n];
    } else {
      u[n] = mean_u + phi * u[prev[n]] + sigma_u * eps[n];
    }
  }
}
model{
  // Priori più informative (vedi §2)
  alpha_raw   ~ normal(0, 1);
  alpha_mu    ~ normal(0, 0.5);
  alpha_sigma ~ normal(0, 0.5);   // <lower=0> già impone metà-normale
  beta        ~ normal(0, 0.5);
  phi         ~ normal(0, 0.4);   // bounds già imposti
  sigma_u     ~ normal(0, 0.5);   // metà-normale su sd

  eps ~ normal(0,1);              // innovazioni standard
  y ~ bernoulli_logit(u);
}
generated quantities{
  array[N] int y_rep;
  vector[N] log_lik;
  for (n in 1:N){
    y_rep[n] = bernoulli_logit_rng(u[n]);
    log_lik[n] = bernoulli_logit_lpmf(y[n] | u[n]);
  }
}
'
stan_file <- write_stan_file(stan_code)

Focalizziamoci sul blocco di codice che costruisce la variabile latente dinamica \(u_n\) su cui poi si basa la likelihood logistica dei dati osservati \(y_n\).

Struttura della likelihood. Il modello assume che la risposta osservata \(y_n \in \{0,1\}\) derivi da una regressione logistica:

\[ y_n \sim \text{Bernoulli}\!\left(\operatorname{logit}^{-1}(u_n)\right), \]

dove \(u_n\) è il predittore lineare dinamico che evolve nel tempo con memoria AR(1). In pratica, invece di avere un predittore statico \(u_n = \alpha_i + \beta x_n\), qui aggiungiamo una dipendenza dal passato: lo stato latente corrente dipende anche da quello precedente.

Costruzione di \(u_n\) nel codice:

for (n in 1:N){
  real mean_u = alpha[id[n]] + beta * x[n];
  if (prev[n] == 0) {
    // primo trial del soggetto
    real sd1 = sigma_u / sqrt(1 - square(phi));
    u[n] = mean_u + sd1 * eps[n];
  } else {
    // trial successivi
    u[n] = mean_u + phi * u[prev[n]] + sigma_u * eps[n];
  }
}
  • Linea 1. Calcoliamo il contributo sistematico del soggetto e del predittore:

    \[ \texttt{mean\_u} = \alpha_{id[n]} + \beta x_n. \]

    Qui \(\alpha_{id[n]}\) è l’intercetta specifica del soggetto, mentre \(\beta x_n\) è l’effetto del predittore osservato.

  • Caso prev[n]==0. È il primo trial di quel soggetto. Non abbiamo uno stato precedente a cui agganciarci, quindi inizializziamo \(u_n\) assumendo la condizione stazionaria del processo AR(1):

    \[ u_n \sim \mathcal N\!\left(\texttt{mean\_u}, \; \frac{\sigma_u^2}{1-\phi^2}\right). \]

    Questo è implementato come mean_u + sd1 * eps[n], dove eps[n] ~ Normal(0,1) e sd1 = sigma_u / sqrt{1 - phi^2}.

  • Caso prev[n]!=0. È un trial successivo. In questo caso \(u_n\) dipende dal valore precedente \(u_{prev[n]}\):

    \[ u_n = \texttt{mean\_u} + \phi \, u_{prev[n]} + \sigma_u \, \varepsilon_n, \quad \varepsilon_n \sim \mathcal N(0,1). \]

    Qui \(\phi\) è il coefficiente AR(1) che controlla quanto del passato sopravvive nel presente.

Intuizione:

  • \(\alpha_i\): propensione media del soggetto.
  • \(\beta x_n\): effetto del predittore osservato al trial \(n\).
  • \(\phi u_{prev[n]}\): memoria: se ieri \(u\) era alto, oggi tenderà a restare alto (se \(\phi>0\)).
  • \(\sigma_u \varepsilon_n\): rumore nuovo che introduce variabilità tra un trial e l’altro.

Il risultato finale è che la probabilità di risposta positiva è:

\[ \Pr(y_n=1) = \operatorname{logit}^{-1}(u_n), \]

e l’intera likelihood del modello è:

\[ p(y \mid \alpha, \beta, \phi, \sigma_u) = \prod_{n=1}^N \text{Bernoulli}\!\left(y_n \,\middle|\, \operatorname{logit}^{-1}(u_n)\right), \]

dove ciascun \(u_n\) è costruito ricorsivamente come sopra.

Generazione dei dati di input:

stan_dat <- list(
  N   = nrow(df),
  I   = length(unique(df$id)),
  id  = as.integer(df$id),
  x   = as.array(as.integer(df$x)),
  y   = as.array(as.integer(df$y)),
  prev = as.array(prev)
)

Compilazione del modello:

mod <- cmdstanr::cmdstan_model(stan_file)

Campionamento:

fit <- mod$sample(
  data = stan_dat,
  seed = 2024,
  chains = 4, parallel_chains = 4,
  iter_warmup = 1500, iter_sampling = 1500,
  adapt_delta = 0.99,
  max_treedepth = 15
)

Riassunto dei parametri chiave:

fit$summary(c("alpha_mu","alpha_sigma","beta","phi","sigma_u"))
#> # A tibble: 5 × 10
#>   variable     mean median    sd   mad    q5   q95  rhat ess_bulk ess_tail
#>   <chr>       <dbl>  <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>    <dbl>    <dbl>
#> 1 alpha_mu     0.10   0.10  0.10  0.09 -0.05  0.27  1.00  1146.17  1890.42
#> 2 alpha_sigma  0.69   0.68  0.12  0.11  0.52  0.89  1.00   936.52  2046.04
#> 3 beta         0.61   0.61  0.09  0.09  0.46  0.77  1.00  2591.21  4164.98
#> 4 phi          0.49   0.50  0.07  0.07  0.37  0.60  1.00  1219.00  2733.87
#> 5 sigma_u      0.69   0.69  0.19  0.18  0.39  1.00  1.00   381.85   561.85

Lettura dei risultati.

Confrontiamo i valori stimati con quelli usati nella simulazione:

  • \(\alpha_\mu\) (vero = 0.0) → stimato ≈ 0.10. L’intercetta media è molto vicina al valore vero, con intervallo che comprende lo 0. La stima è quindi ben calibrata.

  • \(\alpha_\sigma\) (vero = 0.7) → stimato ≈ 0.69. La variabilità tra soggetti è recuperata quasi perfettamente. Questo mostra che il modello distingue bene la propensione media dei soggetti dalle loro differenze individuali.

  • \(\beta\) (vero = 0.6) → stimato ≈ 0.61. L’effetto del predittore viene stimato con grande precisione, centrato sul valore vero.

  • \(\phi\) (vero = 0.5) → stimato ≈ 0.49. Anche il parametro di persistenza dinamica è correttamente recuperato: la memoria del passato è catturata in linea con i dati generati.

  • \(\sigma_u\) (vero = 0.6) → stimato ≈ 0.69. Il rumore di processo è leggermente sovrastimato, ma rimane molto vicino al valore usato nella simulazione.

In sintesi, il modello MCMC recupera in modo accurato tutti i parametri simulati. Le diagnostiche (Rhat ≈ 1, ESS elevati, nessuna divergenza) confermano che la catena ha esplorato bene lo spazio dei parametri.

74.7.1 Diagnostica e Posterior Predictive Check

Un passo fondamentale è confrontare i dati osservati con quelli simulati dal modello (y_rep). Se il modello è adeguato, le distribuzioni delle repliche devono sovrapporsi a quella dei dati reali.

yrep_draws <- fit$draws("y_rep")

# Converte in data.frame e poi in matrice
yrep_df  <- as_draws_df(yrep_draws)
yrep_mat <- as.matrix(yrep_df[, grepl("^y_rep", names(yrep_df))])

# proporzione osservata
prop_obs <- mean(stan_dat$y)

# proporzioni replicate (una per draw)
prop_rep <- rowMeans(yrep_mat)

ppc_dens_overlay(y = stan_dat$y, yrep = yrep_mat[1:100, ])

Il posterior predictive check mostra che la distribuzione dei dati simulati dal modello (y_rep) si sovrappone bene a quella dei dati osservati (y).

  • La densità osservata (linea nera) cade quasi sempre all’interno del ventaglio di densità replicate (linee colorate).
  • Questo significa che il modello riesce a generare dati che “assomigliano” a quelli reali, un segnale che la struttura autoregressiva AR(1) e i parametri stimati catturano i meccanismi principali del processo.
  • Se vedessimo sistematiche discrepanze (ad esempio code troppo corte o una distribuzione spostata), sarebbe un campanello d’allarme che il modello è mal specificato o che mancano variabili importanti.

In sintesi: un buon PPC non prova che il modello sia vero, ma aumenta la fiducia che sia plausibile e che descriva i dati in modo coerente con le ipotesi teoriche.

74.8 Confronto con GLMM logit (via brm)

Per confronto abbiamo stimato lo stesso dataset con un GLM logit semplice in brm:

dat <- tibble(
  y = as.numeric(stan_dat$y),
  x = as.numeric(stan_dat$x),
  id = as.numeric(stan_dat$id)
)
# ATTENZIONE: brms ignora la dipendenza seriale e l'eterogeneità nei trials!
fit_glmer <- brm(
  y ~ x + (x | id),
  data = dat,
  family = bernoulli(link = "logit"),
  prior = c(prior(normal(0, 1), class = "b")),
  chains = 2, iter = 2000, seed = 123,
  backend = "cmdstanr"
)
summary(fit_glmer)
#>  Family: bernoulli 
#>   Links: mu = logit 
#> Formula: y ~ x + (x | id) 
#>    Data: dat (Number of observations: 3000) 
#>   Draws: 2 chains, each with iter = 2000; warmup = 1000; thin = 1;
#>          total post-warmup draws = 2000
#> 
#> Multilevel Hyperparameters:
#> ~id (Number of levels: 100) 
#>                  Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS
#> sd(Intercept)        1.20      0.11     1.00     1.44 1.00      608
#> sd(x)                0.13      0.10     0.00     0.37 1.00      552
#> cor(Intercept,x)    -0.09      0.55    -0.95     0.92 1.00     2199
#>                  Tail_ESS
#> sd(Intercept)        1062
#> sd(x)                 821
#> cor(Intercept,x)     1438
#> 
#> Regression Coefficients:
#>           Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> Intercept     0.42      0.13     0.17     0.68 1.01      347      630
#> x             0.50      0.09     0.32     0.69 1.00     2547     1579
#> 
#> Draws were sampled using sample(hmc). For each parameter, Bulk_ESS
#> and Tail_ESS are effective sample size measures, and Rhat is the potential
#> scale reduction factor on split chains (at convergence, Rhat = 1).

74.8.1 Interpretazione dei risultati brm

Il modello multilevel logit con slope/intercetta casuali, \(y \sim x + (x \mid id)\), stima:

  • Intercept = 0.42 (SE ≈ 0.13): log-odds media di risposta 1 quando \(x=0\), considerando la variabilità tra soggetti.
  • \(x = 0.50\) (SE ≈ 0.09; 95% CI [0.32, 0.69]): effetto medio del predittore sulla log-odds, con pooling parziale tra soggetti.

Eterogeneità tra soggetti:

  • sd(Intercept) ≈ 1.20: forte variabilità individuale nella propensione di base.
  • sd(x) ≈ 0.13: variabilità più contenuta nell’effetto di \(x\).
  • cor(Intercept, x) ≈ −0.09 con IC molto ampio: nessuna evidenza di relazione sistematica tra tendenza di base e sensibilità al predittore.

Il modello, quindi, distingue l’effetto medio di \(x\) dalla notevole eterogeneità individuale, ma non rappresenta esplicitamente la dipendenza seriale tra prove.

74.8.2 Confronto con Stan (modello processuale AR(1))

Il modello AR(1) in Stan, stimato sugli stessi dati simulati, recupera accuratamente i valori veri:

  • \(\beta \approx 0.61\) (vero = 0.60),
  • \(\phi \approx 0.49\) (vero = 0.50),
  • \(\alpha_\sigma \approx 0.69\) (vero = 0.70),
  • \(\sigma_u \approx 0.69\) (vero = 0.60).

La differenza principale riguarda l’effetto di \(x\):

  • Nel GLMM multilevel, \(\hat\beta \approx 0.50\),
  • Nel modello processuale AR(1), \(\hat\beta \approx 0.61\), perfettamente in linea con il valore vero.

Questo accade perché il modello brm controlla l’eterogeneità individuale ma non rappresenta la dinamica temporale: la memoria del passato (\(\phi\)) resta non modellata e una parte della dipendenza seriale viene assorbita nelle stime delle varianze casuali o nell’effetto medio del predittore.

Il modello in Stan, invece, separa esplicitamente il contributo del predittore (\(\beta\)) dalla persistenza dinamica (\(\phi\)) e dal rumore di processo (\(\sigma_u\)), producendo stime più fedeli al meccanismo generativo.

74.8.3 Messaggio chiave

  • Il GLMM con slope/intercette casuali cattura bene l’eterogeneità tra soggetti, ma non la dipendenza temporale.
  • Il modello AR(1) in Stan, introducendo la dinamica degli stati latenti, fornisce un effetto di \(x\) più vicino al valore vero e una rappresentazione più realistica del processo psicologico sottostante.

Riflessioni conclusive

In questo capitolo abbiamo visto come estendere la regressione logistica a un quadro dinamico AR, capace di descrivere processi psicologici che evolvono nel tempo. Abbiamo imparato che:

  • modellare la dipendenza temporale è essenziale quando lavoriamo con dati sequenziali,
  • Stan consente di implementare questi modelli in modo flessibile,
  • i modelli statici (come il GLMM logit) rischiano di dare stime distorte se la dinamica interna non è presa in considerazione.

Nei capitoli successivi potremo estendere questa logica ad altri processi cognitivi e decisionali, mostrando come la modellazione dinamica sia una chiave potente per passare dalla descrizione statistica alla spiegazione psicologica.

Questo passaggio dal GLMM al modello processuale mostra bene come i metodi bayesiani con Stan non servano solo a stimare parametri in modo diverso, ma a formulare nuove domande psicologiche, come per esempio: quanta memoria hanno i processi decisionali? Nei prossimi capitoli vedremo come ampliare questa prospettiva a modelli più complessi, in grado di catturare interazioni tra stati latenti e feedback ambientali.

Informazioni sull’ambiente di sviluppo

sessionInfo()
#> R version 4.5.1 (2025-06-13)
#> Platform: aarch64-apple-darwin20
#> Running under: macOS Sequoia 15.6.1
#> 
#> Matrix products: default
#> BLAS:   /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRblas.0.dylib 
#> LAPACK: /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.1
#> 
#> locale:
#> [1] C/UTF-8/C/C/C/C
#> 
#> time zone: Europe/Zagreb
#> tzcode source: internal
#> 
#> attached base packages:
#> [1] stats     graphics  grDevices utils     datasets  methods   base     
#> 
#> other attached packages:
#>  [1] insight_1.3.1         bayestestR_0.16.1     cmdstanr_0.9.0       
#>  [4] pillar_1.11.0         tinytable_0.11.0      patchwork_1.3.1      
#>  [7] ggdist_3.3.3          tidybayes_3.0.7       bayesplot_1.13.0     
#> [10] ggplot2_3.5.2         reliabilitydiag_0.2.1 priorsense_1.1.0     
#> [13] posterior_1.6.1       loo_2.8.0             rstan_2.32.7         
#> [16] StanHeaders_2.32.10   brms_2.22.0           Rcpp_1.1.0           
#> [19] sessioninfo_1.2.3     conflicted_1.2.0      janitor_2.2.1        
#> [22] matrixStats_1.5.0     modelr_0.1.11         tibble_3.3.0         
#> [25] dplyr_1.1.4           tidyr_1.3.1           rio_1.2.3            
#> [28] here_1.0.1           
#> 
#> loaded via a namespace (and not attached):
#>  [1] gridExtra_2.3        inline_0.3.21        sandwich_3.1-1      
#>  [4] rlang_1.1.6          magrittr_2.0.3       multcomp_1.4-28     
#>  [7] snakecase_0.11.1     compiler_4.5.1       reshape2_1.4.4      
#> [10] systemfonts_1.2.3    vctrs_0.6.5          stringr_1.5.1       
#> [13] pkgconfig_2.0.3      arrayhelpers_1.1-0   fastmap_1.2.0       
#> [16] backports_1.5.0      labeling_0.4.3       utf8_1.2.6          
#> [19] rmarkdown_2.29       ps_1.9.1             ragg_1.4.0          
#> [22] purrr_1.1.0          xfun_0.52            cachem_1.1.0        
#> [25] jsonlite_2.0.0       broom_1.0.9          parallel_4.5.1      
#> [28] R6_2.6.1             stringi_1.8.7        RColorBrewer_1.1-3  
#> [31] lubridate_1.9.4      estimability_1.5.1   knitr_1.50          
#> [34] zoo_1.8-14           pacman_0.5.1         Matrix_1.7-3        
#> [37] splines_4.5.1        timechange_0.3.0     tidyselect_1.2.1    
#> [40] abind_1.4-8          yaml_2.3.10          codetools_0.2-20    
#> [43] curl_6.4.0           processx_3.8.6       pkgbuild_1.4.8      
#> [46] plyr_1.8.9           lattice_0.22-7       withr_3.0.2         
#> [49] bridgesampling_1.1-2 coda_0.19-4.1        evaluate_1.0.4      
#> [52] survival_3.8-3       RcppParallel_5.1.10  tensorA_0.36.2.1    
#> [55] checkmate_2.3.2      stats4_4.5.1         distributional_0.5.0
#> [58] generics_0.1.4       rprojroot_2.1.0      rstantools_2.4.0    
#> [61] scales_1.4.0         xtable_1.8-4         glue_1.8.0          
#> [64] emmeans_1.11.2       tools_4.5.1          data.table_1.17.8   
#> [67] mvtnorm_1.3-3        grid_4.5.1           QuickJSR_1.8.0      
#> [70] colorspace_2.1-1     nlme_3.1-168         cli_3.6.5           
#> [73] textshaping_1.0.1    svUnit_1.0.6         Brobdingnag_1.2-9   
#> [76] V8_6.0.5             gtable_0.3.6         digest_0.6.37       
#> [79] TH.data_1.1-3        htmlwidgets_1.6.4    farver_2.1.2        
#> [82] memoise_2.0.1        htmltools_0.5.8.1    lifecycle_1.0.4     
#> [85] MASS_7.3-65

Bibliografia

Chatfield, C., & Xing, H. (2019). The analysis of time series: an introduction with R. Chapman; hall/CRC.