Salta al contenuto
Note per Studenti Esercizio - Cammini backdoor e insiemi di aggiustamento validi (laboratorio 8)

Esercizio - Cammini backdoor e insiemi di aggiustamento validi (laboratorio 8)

Testo (laboratorio 8, Causal inference - Part B, esercizi 1 e 2 e esempio con il grafo big_csm). Il laboratorio usa la libreria CausalGraphicalModels, che rappresenta un modello causale grafico con nodi e archi orientati e sa elencare i cammini backdoor e gli insiemi di aggiustamento.

  1. Creare la rete causale con quattro nodi X,Y,Z,WX,Y,Z,W e archi X→YX\to Y, Z→XZ\to X, Z→WZ\to W, W→YW\to Y per verificare che il cammino evidenziato in rosso è un cammino backdoor.
  2. Creare la rete con nodi X,Y,A,B,C,DX,Y,A,B,C,D e archi X→AX\to A, A→YA\to Y, X→YX\to Y, B→XB\to X, B→CB\to C, D→CD\to C, D→YD\to Y e trovare tutti gli insiemi che soddisfano il criterio backdoor. Verificare che {B,C,D}\{B,C,D\} sia valido.
  3. (Esempio del laboratorio) Nel grafo con nodi a,b,c,d,e,f,h,x,ya,b,c,d,e,f,h,x,y (figura sotto): quanti cammini backdoor ci sono tra xx e yy? Quali sono gli insiemi di aggiustamento validi? Perché aggiungere hh, ff o cc a un insieme lo rende non valido?

Teoria: criterio backdoor in Interventi e operatore do - criterio backdoor e aggiustamentoOsservare $X=x$ e forzare $X=x$ sono cose diverse: $P(y\mid x)\ne P(y\mid do(x))$ quando c'è un confondente. L'intervento $do(X=x)$ si calcola con la «chirurgia» del grafo (si tolgono le frecce entranti in X) e si traduce in probabilità osservazionali con la formula di aggiustamento $P(y\mid do(x))=\sum_z P(y\mid x,z)P(z)$, dove $Z$ sono i genitori di X, o più in generale un insieme S che soddisfa il criterio backdoor: blocca ogni cammino backdoor (cammino da X a Y che inizia con una freccia entrante in X) e non contiene discendenti di X. Effetto causale medio $ACE=P(y\mid do(x))-P(y\mid do(\neg x))$. Se il confondente non è osservabile ma c'è un mediatore Z con X→Z→Y, vale il front-door $P(y\mid do(x))=\sum_z P(z\mid x)\sum_{x'}P(y\mid x',z)P(x')$. Mediazione: effetto diretto controllato CDE con $do(x),do(z)$. Se due insiemi backdoor danno risultati diversi sui dati, il modello è sbagliato. Argomento del corso da 9 CFU.Interventi e operatore do - criterio backdoor e aggiustamento →, d-separazione in Inferenza causale - modelli causali strutturali e scala della causalitàI dati e la distribuzione congiunta da soli non dicono quali variabili causano quali: lo mostrano il paradosso di Simpson (la medicina funziona in ogni sottogruppo ma sembra peggiorare la popolazione) e il fatto che reti bayesiane diverse (A→B e B→A) descrivono la stessa congiunta. Serve un modello causale: un modello causale strutturale (SCM) ha variabili esogene U, endogene V e funzioni $v_i=f_i(\text{genitori},u_i)$; il suo grafo è la rete causale, e una rete causale è una rete bayesiana in cui le frecce sono cause. Le tre strutture elementari decidono l'indipendenza: catena $A\to B\to C$ e fork $A\leftarrow B\to C$ si bloccano osservando B, il collider $A\to B\leftarrow C$ si blocca se B e i suoi discendenti NON sono osservati (osservarlo crea dipendenza, explaining away). d-separazione: X e Y sono separati da Z se ogni cammino è bloccato; allora $X\perp!!!\perp Y\mid Z$. La scala della causalità (Pearl): associazione $P(y\mid x)$, intervento $P(y\mid do(x))$, controfattuali. Argomento del corso da 9 CFU.Inferenza causale - modelli causali strutturali e scala della causalità →.

Strumenti

CausalGraphicalModels (non installata qui) offre CausalGraphicalModel(nodes=..., edges=...), get_all_backdoor_paths(x, y), get_all_backdoor_adjustment_sets(x, y), is_valid_backdoor_adjustment_set(x, y, S). Per poter verificare i risultati senza la libreria, qui si scrive una piccola funzione equivalente: si enumerano i cammini (frecce in ogni verso), si prendono quelli il cui primo arco entra in xx (cammini backdoor), e si prova ogni sottoinsieme dei nodi restanti.

python
from itertools import combinations

def figli(E, v): return {b for a, b in E if a == v}
def genitori(E, v): return {a for a, b in E if b == v}
def discendenti(E, v):
    out, pila = set(), [v]
    while pila:
        for c in figli(E, pila.pop()):
            if c not in out: out.add(c); pila.append(c)
    return out

def cammini(E, x, y, p=None):
    """tutti i cammini semplici da x a y, con le frecce percorse in ogni verso"""
    p = p or [x]
    if p[-1] == y: yield p; return
    for n in sorted(figli(E, p[-1]) | genitori(E, p[-1])):
        if n not in p: yield from cammini(E, x, y, p + [n])

def bloccato(E, p, S):
    for a, b, c in zip(p, p[1:], p[2:]):
        if (a, b) in E and (c, b) in E:                       # collider: blocca se né b né i suoi discendenti sono in S
            if not ({b} | discendenti(E, b)) & S: return True
        elif b in S: return True                              # catena o fork: blocca se b è in S
    return False

def cammini_backdoor(E, x, y):
    return [p for p in cammini(E, x, y) if (p[1], x) in E]    # il primo arco entra in x

def insiemi_backdoor(E, x, y):
    nodi = sorted({n for e in E for n in e} - {x, y})
    out = []
    for r in range(len(nodi) + 1):
        for S in combinations(nodi, r):
            S = set(S)
            if not S & discendenti(E, x) and all(bloccato(E, p, S) for p in cammini_backdoor(E, x, y)):
                out.append(sorted(S))
    return out

Un insieme SS è valido se (1) blocca tutti i cammini backdoor e (2) non contiene discendenti di xx (not S & discendenti(E, x)): sono le due condizioni del criterio.

Soluzione

1. Un cammino backdoor

Grafico interattivo: Esercizio 1: il cammino backdoor X ← Z → W → Y (in arancione)

python
E1 = {("X", "Y"), ("Z", "X"), ("Z", "W"), ("W", "Y")}
print(cammini_backdoor(E1, "X", "Y"))

Output: [['X', 'Z', 'W', 'Y']]. Ragionamento: i cammini tra XX e YY sono due, X→YX\to Y (diretto, il primo arco esce da XX) e X←Z→W→YX\leftarrow Z\to W\to Y (il primo arco entra in XX): il secondo è l'unico cammino backdoor, coincide con quello rosso del testo e con l'output di get_all_backdoor_paths. È un fork in ZZ seguito da una catena: resta aperto finché non si condiziona su ZZ o su WW.

2. Insiemi che soddisfano il criterio backdoor

Il grafo è quello della lezione: X→A→YX\to A\to Y, X→YX\to Y, B→XB\to X, B→C←DB\to C\leftarrow D, D→YD\to Y.

python
E2 = {("X", "A"), ("A", "Y"), ("X", "Y"), ("B", "X"), ("B", "C"), ("D", "C"), ("D", "Y")}
print(cammini_backdoor(E2, "X", "Y"))
print(insiemi_backdoor(E2, "X", "Y"))
print(bloccato(E2, cammini_backdoor(E2, "X", "Y")[0], {"B", "C", "D"}))

Output:

[['X', 'B', 'C', 'D', 'Y']]
[[], ['B'], ['D'], ['B', 'C'], ['B', 'D'], ['C', 'D'], ['B', 'C', 'D']]
True

Ragionamento. Un solo cammino backdoor: X←B→C←D→YX\leftarrow B\to C\leftarrow D\to Y, con CC collider.

  • {}\{\} è valido: il collider CC, non condizionato e senza discendenti osservati, blocca il cammino.
  • {B}\{B\} e {D}\{D\} sono validi: bloccano il cammino sul fork BB (risp. sul fork DD). Se si condiziona su uno solo dei due, CC resta non condizionato e il collider continua a bloccare.
  • {C}\{C\} non è valido: condizionare sul collider apre il cammino. Per questo compare solo con BB o DD (cioè {B,C},{C,D},{B,C,D}\{B,C\},\{C,D\},\{B,C,D\}), che richiudono il cammino altrove.
  • Qualsiasi insieme con AA è escluso: AA è discendente di XX (regola 2).

Quindi i sette insiemi in uscita sono tutti e soli i validi; {B,C,D}\{B,C,D\} è valido (True: il cammino è bloccato su BB e su DD). Con CausalGraphicalModels si ottengono gli stessi: get_all_backdoor_adjustment_sets('X','Y') restituisce frozenset con {},{B},{D},{B,C},{B,D},{C,D},{B,C,D}\{\},\{B\},\{D\},\{B,C\},\{B,D\},\{C,D\},\{B,C,D\} e is_valid_backdoor_adjustment_set('X','Y',{'B','C','D'}) dà True.

3. L'esempio big_csm

Grafico interattivo: Esempio del laboratorio: due cammini backdoor tra x e y; x ← a → h ← y è bloccato dal collider h, x ← b → d → e → y no

Archi (letti dalla figura): a→xa\to x, b→xb\to x, a→ha\to h, b→db\to d, x→cx\to c, d→ed\to e, c→yc\to y, e→ye\to y, y→hy\to h, y→fy\to f.

python
E3 = {("a", "x"), ("b", "x"), ("a", "h"), ("b", "d"), ("x", "c"), ("d", "e"), ("c", "y"), ("e", "y"), ("y", "h"), ("y", "f")}
print(cammini_backdoor(E3, "x", "y"))
print(len(insiemi_backdoor(E3, "x", "y")), insiemi_backdoor(E3, "x", "y"))

Output:

[['x', 'a', 'h', 'y'], ['x', 'b', 'd', 'e', 'y']]
14 [['b'], ['d'], ['e'], ['a', 'b'], ['a', 'd'], ['a', 'e'], ['b', 'd'], ['b', 'e'], ['d', 'e'], ['a', 'b', 'd'], ['a', 'b', 'e'], ['a', 'd', 'e'], ['b', 'd', 'e'], ['a', 'b', 'd', 'e']]
  • Cammini backdoor: due. x←a→h←yx\leftarrow a\to h\leftarrow y e x←b→d→e→yx\leftarrow b\to d\to e\to y. Il primo ha il collider hh e quindi è bloccato finché non si condiziona su hh (o su un suo discendente); il secondo va bloccato condizionando su uno qualsiasi di bb, dd, ee (fork in bb o catena in dd o ee).
  • Insiemi validi: ogni insieme che contiene almeno uno tra b,d,eb,d,e, eventualmente con aa (aggiungere aa non crea cammini: il cammino x←a→h←yx\leftarrow a\to h\leftarrow y resta bloccato dal collider hh non condizionato). In tutto 2⋅(23−1)=142\cdot(2^3-1)=14. Il vuoto e {a}\{a\} non sono validi: lasciano aperto il cammino per b,d,eb,d,e.
  • Perché hh, ff, cc invalidano un insieme: cc è sul cammino diretto x→c→yx\to c\to y ed è discendente di xx; hh e ff sono discendenti di yy, che a sua volta è discendente di xx, quindi discendenti di xx (regola 2). In più condizionare su hh aprirebbe il collider x←a→h←yx\leftarrow a\to h\leftarrow y, creando un cammino non causale tra xx e yy. Il testo del corso lo dice così: «aggiungere HH, FF o CC creerebbe un nuovo cammino rendendo l'insieme non valido».
  • Quando tutte le variabili sono osservate, l'insieme dei genitori di xx ({a,b}\{a,b\}) è sempre valido, e infatti compare nella lista.

Il corso nota anche che, se non tutte le variabili sono osservate, alcune affermazioni causali non si possono stimare dai dati (nessun insieme valido tra gli osservati).

Versione ripasso

Esercizi su questo argomento

Teoria collegata