Open In Colab

Metodi di sintesi della distribuzione a posteriori#

L’obiettivo di questo capitolo è analizzare in dettaglio il flusso di lavoro all’interno del contesto dell’inferenza bayesiana, una volta che sia stata ottenuta la stima della distribuzione a posteriori mediante l’utilizzo della tecnica MCMC.

Preparazione del Notebook#

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import pymc as pm
import pymc.sampling_jax
import arviz as az
import scipy.stats as stats
import warnings

warnings.filterwarnings("ignore", category=UserWarning)
warnings.filterwarnings("ignore", category=FutureWarning)
warnings.filterwarnings("ignore", category=Warning)
/Users/corrado/opt/anaconda3/envs/pymc_env/lib/python3.11/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
  from .autonotebook import tqdm as notebook_tqdm
%config InlineBackend.figure_format = 'retina'
RANDOM_SEED = 42
rng = np.random.default_rng(RANDOM_SEED)
az.style.use("arviz-darkgrid")
sns.set_theme(palette="colorblind")

Sintesi della Distribuzione a Posteriori#

Uno dei fattori principali che ha contribuito alla crescente popolarità dei metodi bayesiani nelle scienze sociali è stata la (ri)scoperta di algoritmi numerici capaci di stimare le distribuzioni a posteriori dei parametri del modello a partire dai dati osservati. Prima di tali sviluppi, era praticamente impossibile ottenere misure riassuntive delle distribuzioni a posteriori, specialmente per modelli complessi con un elevato numero di parametri.

Gli algoritmi numerici che esamineremo in questo capitolo fanno uso dell’integrazione Monte Carlo attraverso catene di Markov, meglio conosciuta come campionamento Markov chain Monte Carlo (MCMC). Questa tecnica ha rivoluzionato la capacità di effettuare inferenze bayesiane, rendendo più accessibile e gestibile l’analisi di modelli complessi.

Campionamento con PyMC#

A titolo di esempio, useremo ancora una volta il set di dati “moma_sample”, che è stato analizzato nel capitolo precedente. Iniziamo replicando il processo di campionamento PyMC descritto in precedenza.

y = 14
ntrials = 100

alpha_prior = 4
beta_prior = 6

with pm.Model() as bb_model:
    theta = pm.Beta("theta", alpha=alpha_prior, beta=beta_prior)
    obs = pm.Binomial("obs", p=theta, n=ntrials, observed=y)
with bb_model:
    idata = pm.sampling_jax.sample_numpyro_nuts()
Hide code cell output
Compiling...
Compilation time = 0:00:00.531103
Sampling...
  0%|                                                                                  | 0/2000 [00:00<?, ?it/s]
Compiling.. :   0%|                                                                    | 0/2000 [00:00<?, ?it/s]

  0%|                                                                                  | 0/2000 [00:00<?, ?it/s]


Compiling.. :   0%|                                                                    | 0/2000 [00:00<?, ?it/s]


  0%|                                                                                  | 0/2000 [00:00<?, ?it/s]


Compiling.. :   0%|                                                                    | 0/2000 [00:00<?, ?it/s]


  0%|                                                                                  | 0/2000 [00:00<?, ?it/s]


Compiling.. :   0%|                                                                    | 0/2000 [00:00<?, ?it/s]

Running chain 0:   0%|                                                                 | 0/2000 [00:01<?, ?it/s]

Running chain 3:   0%|                                                                 | 0/2000 [00:01<?, ?it/s]


Running chain 2:   0%|                                                                 | 0/2000 [00:01<?, ?it/s]


Running chain 1:   0%|                                                                 | 0/2000 [00:01<?, ?it/s]

Running chain 0: 100%|████████████████████████████████████████████████████| 2000/2000 [00:01<00:00, 1858.52it/s]
Running chain 1: 100%|████████████████████████████████████████████████████| 2000/2000 [00:01<00:00, 1860.85it/s]
Running chain 2: 100%|████████████████████████████████████████████████████| 2000/2000 [00:01<00:00, 1863.13it/s]
Running chain 3: 100%|████████████████████████████████████████████████████| 2000/2000 [00:01<00:00, 1865.08it/s]
Sampling time = 0:00:01.293610
Transforming variables...
Transformation time = 0:00:00.053729

La figura seguente rappresenta la distribuzione a posteriori di \(\theta\) ottenuta mediante il metodo MCMC e la traccia delle catene di Markov.

az.plot_trace(idata, combined=True)
plt.show()
../_images/4dc2a2b2a544f6d55f0c0baf02d15bdc63f19da04c9932699302f62a1594e4f8.png

Estrazione degli attributi da InferenceData#

Ora che abbiamo ottenuto la distribuzione a posteriori, possiamo effettuare varie operazioni con essa:

  • Stima puntuale

  • Intervalli di credibilità

  • Test di ipotesi

  • Predizione

Stima puntuale#

Procediamo all’esame dell’oggetto idata.

idata
arviz.InferenceData
    • <xarray.Dataset>
      Dimensions:  (chain: 4, draw: 1000)
      Coordinates:
        * chain    (chain) int64 0 1 2 3
        * draw     (draw) int64 0 1 2 3 4 5 6 7 8 ... 992 993 994 995 996 997 998 999
      Data variables:
          theta    (chain, draw) float64 0.1733 0.1732 0.2063 ... 0.1187 0.1444 0.1424
      Attributes:
          created_at:     2024-01-23T07:45:24.000626
          arviz_version:  0.17.0

    • <xarray.Dataset>
      Dimensions:          (chain: 4, draw: 1000)
      Coordinates:
        * chain            (chain) int64 0 1 2 3
        * draw             (draw) int64 0 1 2 3 4 5 6 ... 993 994 995 996 997 998 999
      Data variables:
          acceptance_rate  (chain, draw) float64 0.9147 0.7716 ... 0.6239 0.9669
          step_size        (chain, draw) float64 1.183 1.183 1.183 ... 0.9726 0.9726
          diverging        (chain, draw) bool False False False ... False False False
          energy           (chain, draw) float64 5.58 5.612 5.119 ... 5.44 9.76 4.973
          n_steps          (chain, draw) int64 3 3 3 1 3 1 3 1 3 ... 3 3 3 3 3 3 1 3 3
          tree_depth       (chain, draw) int64 2 2 2 1 2 1 2 1 2 ... 2 2 2 2 2 2 1 2 2
          lp               (chain, draw) float64 4.508 4.507 5.118 ... 4.63 4.667
      Attributes:
          created_at:     2024-01-23T07:45:24.002825
          arviz_version:  0.17.0

    • <xarray.Dataset>
      Dimensions:    (obs_dim_0: 1)
      Coordinates:
        * obs_dim_0  (obs_dim_0) int64 0
      Data variables:
          obs        (obs_dim_0) int64 14
      Attributes:
          created_at:                 2024-01-23T07:45:24.004056
          arviz_version:              0.17.0
          inference_library:          numpyro
          inference_library_version:  0.13.2
          sampling_time:              1.29361

I vari attributi dell’oggetto InferenceData possono essere estratti come si farebbe con un dict, in cui ciascuna coppia è composta da una chiave e un valore, separati dal simbolo dei due punti. In questa situazione, le chiavi sono i nomi delle variabili, mentre i valori sono rappresentati da array di Numpy. Per esempio, possiamo recuperare la traccia di campionamento dalla variabile latente theta nel modo seguente.

idata.posterior["theta"]
<xarray.DataArray 'theta' (chain: 4, draw: 1000)>
array([[0.17326516, 0.17320269, 0.20629908, ..., 0.12454636, 0.16953616,
        0.18556175],
       [0.20671803, 0.14461032, 0.17306915, ..., 0.17527697, 0.1427207 ,
        0.15351922],
       [0.14086973, 0.19343519, 0.19594644, ..., 0.1142225 , 0.17836103,
        0.14123355],
       [0.11920471, 0.14753389, 0.26728206, ..., 0.11865411, 0.14442225,
        0.14238198]])
Coordinates:
  * chain    (chain) int64 0 1 2 3
  * draw     (draw) int64 0 1 2 3 4 5 6 7 8 ... 992 993 994 995 996 997 998 999

Si noti che l’oggetto ritornato è un array di dimensioni 4 \(\times\) 1000 (sul mio computer):

idata.posterior["theta"].shape
(4, 1000)

Per visualizzare il primi 10 valori della prima catena, ad esempio, usiamo:

idata.posterior["theta"][0, 1:10]
<xarray.DataArray 'theta' (draw: 9)>
array([0.17320269, 0.20629908, 0.18193757, 0.13434879, 0.14592237,
       0.1342829 , 0.12942379, 0.2265291 , 0.23713072])
Coordinates:
    chain    int64 0
  * draw     (draw) int64 1 2 3 4 5 6 7 8 9

Per combinare catene e iterazioni, utilizziamo la funzione arviz.extract().

post = az.extract(idata)
post
<xarray.Dataset>
Dimensions:  (sample: 4000)
Coordinates:
  * sample   (sample) object MultiIndex
  * chain    (sample) int64 0 0 0 0 0 0 0 0 0 0 0 0 ... 3 3 3 3 3 3 3 3 3 3 3 3
  * draw     (sample) int64 0 1 2 3 4 5 6 7 ... 992 993 994 995 996 997 998 999
Data variables:
    theta    (sample) float64 0.1733 0.1732 0.2063 ... 0.1187 0.1444 0.1424
Attributes:
    created_at:     2024-01-23T07:45:24.000626
    arviz_version:  0.17.0

La funzione extract ritorna un oggetto di classe xarray.DataArray il quale può essere trattato come un array NumPy.

post["theta"]
<xarray.DataArray 'theta' (sample: 4000)>
array([0.17326516, 0.17320269, 0.20629908, ..., 0.11865411, 0.14442225,
       0.14238198])
Coordinates:
  * sample   (sample) object MultiIndex
  * chain    (sample) int64 0 0 0 0 0 0 0 0 0 0 0 0 ... 3 3 3 3 3 3 3 3 3 3 3 3
  * draw     (sample) int64 0 1 2 3 4 5 6 7 ... 992 993 994 995 996 997 998 999

Si noti che post["theta"] contiene il numero totale (4000) dei campioni a posteriori della distribuzione di \(\theta\) escluso il burn-in:

post["theta"].shape
(4000,)

Di conseguenza, possiamo calcolare su di esso tutte le misure statistiche descrittive che si possono ottenere da un vettore di dati. Per esempio, possiamo calcolare la media a posteriori.

post.mean()
<xarray.Dataset>
Dimensions:  ()
Data variables:
    theta    float64 0.1647
post["theta"].mean()
<xarray.DataArray 'theta' ()>
array(0.1647053)

Possiamo calcolare la mediana a posteriori di \(\theta\).

post.median()
<xarray.Dataset>
Dimensions:  ()
Data variables:
    theta    float64 0.1626

Oppure la deviazione standard della stima a posteriori di \(\theta\).

np.std(post["theta"])
<xarray.DataArray 'theta' ()>
array(0.03581982)

Intervallo di credibilità#

L’inferenza bayesiana tramite l’intervallo di credibilità riguarda invece la stima dell’intervallo che contiene il parametro \(\theta\) ad un dato livello di probabilità soggettiva.

Usando idata, possiamo ottenere un sommario della distribuzione a posteriori con il metodo az.summary().

az.summary(idata, hdi_prob=0.94, round_to=3)
mean sd hdi_3% hdi_97% mcse_mean mcse_sd ess_bulk ess_tail r_hat
theta 0.165 0.036 0.099 0.233 0.001 0.001 1586.797 1643.117 1.001

Si ottiene così l’intervallo di credibilità a più alta densità a posteriori (HPD) al 94%. Questo intervallo ci informa sul fatto che, a posteriori, possiamo essere certi al 94%, che il vero valore del parametro \(\theta\) sia contenuto nell’intervallo [0.103, 0.23].

Dato che, nel caso presente, conosciamo la soluziona analitica, possiamo verificare il risultato precedente calcolando i quantili della distribuzione a posteriori Beta(18, 92) di ordine 0.03 e 0.97.

ll = stats.beta.ppf(0.03, 18, 92)
ul = stats.beta.ppf(0.97, 18, 92)
list([ll, ul])
[0.10303527075398666, 0.23457657606771784]

Verifica di ipotesi bayesiana#

Un secondo tipo di inferenza bayesiana riguarda problemi in cui siamo interessati a valutare la plausibilità che il parametro \(\theta\) assuma valori contenuti in un dato intervallo di valori. Per esempio, ci potrebbe interessare l’ipotesi \(\theta > 0.5\). In questo caso, possiamo calcolare la probabilità a posteriori che \(\theta\) cada nell’intervallo di interesse, integrando la distribuzione a posteriori Beta su tale intervallo.

Nel caso dell’esempio degli artisti della Generazione X, supponiamo di essere interessati alle due seguenti ipotesi:

\[\begin{split} \begin{split} H_0: & \; \; \pi \ge 0.2 \\ H_a: & \; \; \pi < 0.2 \end{split} \end{split}\]

La nostra domanda è la seguente: Date le nostre credenze iniziali e i dati disponibili, quale importanza relativa possiamo attribuire a queste due ipotesi?

Per affrontare questa questione, iniziamo a calcolare la probabilità \(P(\theta < 0.2)\).

print(np.mean(post["theta"] < 0.2))
<xarray.DataArray 'theta' ()>
array(0.83525)

Passiamo ora a calcolare gli odds a posteriori:

post_odds = (np.mean(post["theta"] < 0.2)) / (1 - np.mean(post["theta"] < 0.2))
print(post_odds)
<xarray.DataArray 'theta' ()>
array(5.06980273)

Ciò implica che la probabilità che \(\pi\) sia inferiore al 20% è circa 6 volte superiore rispetto alla probabilità che sia al di sopra del 20%.

Questo risultato si basa solo sulle informazioni relative alla distribuzione a posteriori. Prima di avere osservato i dati del campione, avevamo una distribuzione a priori \(\operatorname{Beta}(6, 4)\), e in quel contesto avevamo una probabilità del 9% che \(H_a\) fosse vera e una probabilità del 91% che fosse falsa.

threshold = 0.2
prior_prob = stats.beta.cdf(threshold, a=alpha_prior, b=beta_prior)
prior_odds = prior_prob / (1 - prior_prob)
print(prior_odds)
0.09366320688790145

Ora possiamo combinare le informazioni degli odds a posteriori e degli odds a priori in una quantità chiamata Bayes Factor, che è semplicemente il rapporto tra le due:

BF = post_odds / prior_odds
print(BF)
<xarray.DataArray 'theta' ()>
array(54.12800714)

In conclusione, dopo aver appreso informazioni riguardo a 14 artisti appartenenti alla generazione X, le probabilità posteriori della nostra ipotesi \(H_a: \; \pi < 0.2\) sono circa 60 volte superiori rispetto alle probabilità a priori.

Questo è un esempio di test di ipotesi bayesiano.

Commenti e considerazioni finali#

In questo capitolo sono state presentate le strategie per la trasformazione della distribuzione a posteriori. Sono state esplorate le diverse modalità per ottenere un intervallo di credibilità. In seguito, è stata affrontata l’analisi delle ipotesi a posteriori. Questo approccio permette di comparare in maniera relativa due ipotesi contrapposte riguardanti il parametro \(\theta\). In alcune circostanze, tale confronto viene tradotto in una misura denominata Fattore di Bayes.

%load_ext watermark
%watermark -n -u -v -iv -w
Last updated: Tue Jan 23 2024

Python implementation: CPython
Python version       : 3.11.7
IPython version      : 8.19.0

scipy     : 1.11.4
seaborn   : 0.13.0
matplotlib: 3.8.2
arviz     : 0.17.0
numpy     : 1.26.2
pandas    : 2.1.4
pymc      : 5.10.3

Watermark: 2.4.3