Open In Colab

40. Confronto tra gruppi#

L’obiettivo di questo capitolo è di ampliare la discussione del capitolo Inferenza bayesiana su una media, affrontando il confronto tra le medie di due o più gruppi indipendenti. Per cominciare, carichiamo le librerie necessarie.

from pymc import HalfNormal, HalfCauchy, Model, Normal, sample
import arviz as az
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import pymc as pm
import seaborn as sns
import xarray as xr
import warnings

warnings.filterwarnings("ignore")
warnings.simplefilter("ignore")
%matplotlib inline
%config InlineBackend.figure_format = 'retina'
# Initialize random number generator
RANDOM_SEED = 8927
rng = np.random.default_rng(RANDOM_SEED)

plt.style.use("bmh")
plt.rcParams["figure.figsize"] = [10, 6]
plt.rcParams["figure.dpi"] = 100
plt.rcParams["figure.facecolor"] = "white"

sns.set_theme(palette="colorblind")

%load_ext autoreload
%autoreload 2
%config InlineBackend.figure_format = "svg"

40.1. Stima della media di un gruppo#

Iniziamo con l’utilizzo del caso precedentemente trattato nel capitolo precedente, in cui abbiamo calcolato l’incertezza relativa alla media di un singolo gruppo. Qui useremo i dati relativi ai pinguini Palmer, che verranno letti da un file csv, escludendo quelli che presentano valori mancanti:

penguins = pd.read_csv("data/penguins.csv")
# Subset to the columns needed
missing_data = penguins.isnull()[
    ["bill_length_mm", "flipper_length_mm", "sex", "body_mass_g"]
].any(axis=1)
# Drop rows with any missing data
penguins = penguins.loc[~missing_data]
penguins.shape
(333, 8)

Otteniamo in questo modo un DataFrame con 333 righe e 8 colonne. Calcoliamo la media del peso body_mass_g in funzione della specie:

summary_stats = (
    penguins.loc[:, ["species", "body_mass_g"]]
    .groupby("species")
    .aggregate(["mean", "std", "count"])
)
summary_stats.round(1)
body_mass_g
mean std count
species
Adelie 3706.2 458.6 146
Chinstrap 3733.1 384.3 68
Gentoo 5092.4 501.5 119

Creiamo un grafico a violino per questi dati.

_ = sns.catplot(kind="violin", data=penguins, x="species", y="body_mass_g")
_images/e64023a3f45f4d327e72203f0db28425a0e00685dd0d7bd373f5d9c2da5d254c.svg

Fino ad ora abbiamo analizzato un campione di osservazioni. Tuttavia, la stima della media e della varianza ottenuta dal campione non è rappresentativa dell’intera popolazione di pinguini. Ci chiediamo, quindi, quale sia l’incertezza associata alla stima del peso per le tre specie di pinguini, considerando che abbiamo a disposizione solo un piccolo campione di osservazioni.

Una possibile soluzione per quantificare l’incertezza della nostra stima è l’utilizzo dei metodi bayesiani.

Per fare ciò, dobbiamo definire un modello statistico che descriva la relazione tra i dati e i parametri. Nel nostro caso, descriviamo la distribuzione a posteriori dei parametri, tenendo conto dei dati, come segue:

\[ p(\mu, \sigma \mid y) \propto p(y \mid \mu, \sigma) p(\mu)p(\sigma), \]

dove la verosimiglianza sarà una densità Normale, dipendente da due parametri: \(\mu\) e \(\sigma\). Inoltre, per definire le distribuzioni a priori di questi due parametri, useremo una distribuzione Normale per \(\mu\) (\(\mu \sim \mathcal{N}(4000, 2000)\)) e una distribuzione Normale troncata per \(\sigma\) (valori negativi sono impossibili: \(\sigma \sim \mathcal{HN}(2000)\)).

Per esempio, selezioniamo solo i pinguini della specie Adelie. I valori della variabile body_mass_g relativi a questa specie sono:

adelie_mass_obs = penguins[penguins["species"] == "Adelie"]["body_mass_g"]
print(adelie_mass_obs)
0      3750.0
1      3800.0
2      3250.0
4      3450.0
5      3650.0
        ...  
147    3475.0
148    3450.0
149    3750.0
150    3700.0
151    4000.0
Name: body_mass_g, Length: 146, dtype: float64

Per implementare il modello descritto in precedenza, utilizzeremo PyMC.

Prima di eseguire il campionamento della distribuzione a posteriori, esamineremo la distribuzione predittiva a priori per verificare l’adeguatezza delle assunzioni relative alle distribuzioni a priori dei parametri. Per fare ciò, useremo la funzione pm.sample_prior_predictive(), che ci permetterà di generare 1000 valori casuali dalle distribuzioni a priori dei due parametri.

with pm.Model() as model_adelie_penguin_mass:
    sigma = pm.HalfNormal("sigma", sigma=1000)
    mu = pm.Normal("mu", 4000, 2000)
    mass = pm.Normal("mass", mu=mu, sigma=sigma, observed=adelie_mass_obs)

    idata1 = pm.sample_prior_predictive(samples=1000, random_seed=rng)
Sampling: [mass, mu, sigma]

Dall’oggetto inferenceData idata1 recuperiamo i campioni delle distribuzioni a priori e li passiamo a az.plot_trace():

prior = idata1.prior
_ = az.plot_trace(prior)
plt.tight_layout()
_images/801a854cc4d0dbd04296db000993104da2aad8cecc7df68a75114e12a4cb41c8.svg

Dall’osservazione delle distribuzioni a priori dei due parametri, si può notare che non stiamo limitando troppo il campionamento della variabile adelie_mass_obs. Potremmo persino aver scelto distribuzioni a priori troppo ampie, dato che la distribuzione a priori per \(\mu\) include anche valori negativi. Tuttavia, poiché questo è un modello semplice e abbiamo un numero sufficiente di osservazioni, non c’è bisogno di preoccuparsi. Possiamo procedere con la stima della distribuzione a posteriori.

with pm.Model() as model_adelie_penguin_mass:
    
    sigma = pm.HalfNormal("sigma", sigma=1000)
    mu = pm.Normal("mu", 4000, 2000)
    mass = pm.Normal("mass", mu=mu, sigma=sigma, observed=adelie_mass_obs)

    idata2 = pm.sample(3000, chains=4)
Auto-assigning NUTS sampler...
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (4 chains in 4 jobs)
NUTS: [sigma, mu]
---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
Cell In[9], line 7
      4 mu = pm.Normal("mu", 4000, 2000)
      5 mass = pm.Normal("mass", mu=mu, sigma=sigma, observed=adelie_mass_obs)
----> 7 idata2 = pm.sample(3000, chains=4)

File ~/mambaforge/envs/pymc_env/lib/python3.11/site-packages/pymc/sampling/mcmc.py:766, in sample(draws, tune, chains, cores, random_seed, progressbar, step, nuts_sampler, initvals, init, jitter_max_retries, n_init, trace, discard_tuned_samples, compute_convergence_checks, keep_warning_stat, return_inferencedata, idata_kwargs, nuts_sampler_kwargs, callback, mp_ctx, model, **kwargs)
    764 _print_step_hierarchy(step)
    765 try:
--> 766     _mp_sample(**sample_args, **parallel_args)
    767 except pickle.PickleError:
    768     _log.warning("Could not pickle model, sampling singlethreaded.")

File ~/mambaforge/envs/pymc_env/lib/python3.11/site-packages/pymc/sampling/mcmc.py:1141, in _mp_sample(draws, tune, step, chains, cores, random_seed, start, progressbar, traces, model, callback, mp_ctx, **kwargs)
   1138 # We did draws += tune in pm.sample
   1139 draws -= tune
-> 1141 sampler = ps.ParallelSampler(
   1142     draws=draws,
   1143     tune=tune,
   1144     chains=chains,
   1145     cores=cores,
   1146     seeds=random_seed,
   1147     start_points=start,
   1148     step_method=step,
   1149     progressbar=progressbar,
   1150     mp_ctx=mp_ctx,
   1151 )
   1152 try:
   1153     try:

File ~/mambaforge/envs/pymc_env/lib/python3.11/site-packages/pymc/sampling/parallel.py:402, in ParallelSampler.__init__(self, draws, tune, chains, cores, seeds, start_points, step_method, progressbar, mp_ctx)
    399 if mp_ctx.get_start_method() != "fork":
    400     step_method_pickled = cloudpickle.dumps(step_method, protocol=-1)
--> 402 self._samplers = [
    403     ProcessAdapter(
    404         draws,
    405         tune,
    406         step_method,
    407         step_method_pickled,
    408         chain,
    409         seed,
    410         start,
    411         mp_ctx,
    412     )
    413     for chain, seed, start in zip(range(chains), seeds, start_points)
    414 ]
    416 self._inactive = self._samplers.copy()
    417 self._finished: List[ProcessAdapter] = []

File ~/mambaforge/envs/pymc_env/lib/python3.11/site-packages/pymc/sampling/parallel.py:403, in <listcomp>(.0)
    399 if mp_ctx.get_start_method() != "fork":
    400     step_method_pickled = cloudpickle.dumps(step_method, protocol=-1)
    402 self._samplers = [
--> 403     ProcessAdapter(
    404         draws,
    405         tune,
    406         step_method,
    407         step_method_pickled,
    408         chain,
    409         seed,
    410         start,
    411         mp_ctx,
    412     )
    413     for chain, seed, start in zip(range(chains), seeds, start_points)
    414 ]
    416 self._inactive = self._samplers.copy()
    417 self._finished: List[ProcessAdapter] = []

File ~/mambaforge/envs/pymc_env/lib/python3.11/site-packages/pymc/sampling/parallel.py:259, in ProcessAdapter.__init__(self, draws, tune, step_method, step_method_pickled, chain, seed, start, mp_ctx)
    242     step_method_send = step_method
    244 self._process = mp_ctx.Process(
    245     daemon=True,
    246     name=process_name,
   (...)
    257     ),
    258 )
--> 259 self._process.start()
    260 # Close the remote pipe, so that we get notified if the other
    261 # end is closed.
    262 remote_conn.close()

File ~/mambaforge/envs/pymc_env/lib/python3.11/multiprocessing/process.py:121, in BaseProcess.start(self)
    118 assert not _current_process._config.get('daemon'), \
    119        'daemonic processes are not allowed to have children'
    120 _cleanup()
--> 121 self._popen = self._Popen(self)
    122 self._sentinel = self._popen.sentinel
    123 # Avoid a refcycle if the target function holds an indirect
    124 # reference to the process object (see bpo-30775)

File ~/mambaforge/envs/pymc_env/lib/python3.11/multiprocessing/context.py:300, in ForkServerProcess._Popen(process_obj)
    297 @staticmethod
    298 def _Popen(process_obj):
    299     from .popen_forkserver import Popen
--> 300     return Popen(process_obj)

File ~/mambaforge/envs/pymc_env/lib/python3.11/multiprocessing/popen_forkserver.py:35, in Popen.__init__(self, process_obj)
     33 def __init__(self, process_obj):
     34     self._fds = []
---> 35     super().__init__(process_obj)

File ~/mambaforge/envs/pymc_env/lib/python3.11/multiprocessing/popen_fork.py:19, in Popen.__init__(self, process_obj)
     17 self.returncode = None
     18 self.finalizer = None
---> 19 self._launch(process_obj)

File ~/mambaforge/envs/pymc_env/lib/python3.11/multiprocessing/popen_forkserver.py:58, in Popen._launch(self, process_obj)
     55 self.finalizer = util.Finalize(self, util.close_fds,
     56                                (_parent_w, self.sentinel))
     57 with open(w, 'wb', closefd=True) as f:
---> 58     f.write(buf.getbuffer())
     59 self.pid = forkserver.read_signed(self.sentinel)

KeyboardInterrupt: 
pm.model_to_graphviz(model_adelie_penguin_mass)
_images/7c2decc5d741d2f8c430a19058140eecd974ab440597da6ead018918945fc951.svg

Il KDE plot e il rank plot della distribuzione a posteriori dei parametri del modello bayesiano ci aiutano a diagnosticare eventuali problemi durante il campionamento in modo visivo. Tuttavia, in questo caso, non ci sono segnali di problemi durante il campionamento.

axes = az.plot_trace(idata2, divergences="bottom", kind="rank_bars")
plt.tight_layout()
_images/abbb7a0ea5f796f17b05d3e80cb853ead054d3184cad06657ac8773388eb230e.png

Possiamo unire le quattro catene ottenute dal campionamento e utilizzarle per fare una stima più precisa dei parametri \(\mu\) e \(\sigma\) della distribuzione a posteriori.

Nella figura seguente possiamo vedere un confronto tra la distribuzione a posteriori stimata e le stime puntuali dei due parametri che abbiamo ottenuto dal campione. Le linee verticali indicano i valori puntuali stimati per \(\mu\) e \(\sigma\), mentre la curva rappresenta la distribuzione a posteriori stimata attraverso il campionamento delle catene.

axes = az.plot_posterior(idata2, hdi_prob=0.94)

axes[0].axvline(3706.2, linestyle="--")
axes[1].axvline(458.6, linestyle="--")
<matplotlib.lines.Line2D at 0x18c57fd50>
_images/44a29c4936c69f2882dc24b62df451d25a7645fbee08bbcfb189765b81e9e402.png

La stima bayesiana ci fornisce una distribuzione dei valori plausibili dei parametri. Le distribuzioni a posteriori ottenute per \(\mu\) e \(\sigma\) ci indicano il valore più credibile, ma anche la gamma di valori in cui ci si può aspettare che si trovi il “vero” valore del parametro, considerando differenti campioni di osservazioni. L’intervallo di valori plausibili è abbastanza stretto sia per \(\mu\) (tra 3634 e 3778), che per \(\sigma\) (94% HDI: [412, 513]).

In definitiva, le due distribuzioni a posteriori descrivono l’incertezza che abbiamo relativamente ai veri valori di \(\mu\) e \(\sigma\), tenendo in considerazione le nostre credenze precedenti (in questo caso, distribuzioni a priori debolmente informative, utilizzate per regolarizzare i dati), i dati osservati e l’ipotesi che abbiamo fatto sul meccanismo generatore dei dati.

Un riassunto numerico può essere ottenuto utilizzando le seguenti istruzioni:

az.summary(idata2).round(2)
mean sd hdi_3% hdi_97% mcse_mean mcse_sd ess_bulk ess_tail r_hat
mu 3706.25 38.4 3634.82 3779.04 0.35 0.25 11858.0 8525.0 1.0
sigma 462.49 27.4 412.13 513.78 0.25 0.18 11766.0 8595.0 1.0

40.2. Confronto tra più gruppi#

Per confrontare più gruppi, è necessario eseguire la stessa procedura di campionamento che abbiamo descritto precedentemente separatamente per ciascun gruppo di osservazioni. Pertanto, dobbiamo trovare un modo per comunicare a PyMC di effettuare questa operazione.

Prima di comprendere la procedura da seguire, dobbiamo capire come i dati sono organizzati in un oggetto della classe inferenceData.

40.3. Coordinate in PyMC e negli oggetti InferenceData#

I risultati dell’analisi a posteriori ottenuti tramite PyMC vengono memorizzati in un oggetto di tipo inferenceData. A volte, per analizzare questi dati, è necessario estrarli dall’oggetto inferenceData. Per capire come ciò sia possibile, analizzeremo in dettaglio le proprietà dell’oggetto inferenceData.

L’oggetto inferenceData di ArviZ è in realtà un oggetto di tipo Dataset di xarray. xarray è una libreria che generalizza sia gli array di Numpy che i dataframe di Pandas. Di seguito viene mostrato un esempio di un array standard di Numpy.

npdata = rng.standard_normal(size=(2, 3))
npdata
array([[ 1.32565874, -0.57097002,  0.6682492 ],
       [ 1.14538102,  0.58734406, -0.05144329]])

Useremo questo array di Numpy per creare un oggetto DataArray di xarray.

data = xr.DataArray(
    npdata,
    dims=("user", "day"),
    coords={"user": ["Alice", "Bob"], "day": ["yesterday", "today", "tomorrow"]},
)
data
<xarray.DataArray (user: 2, day: 3)>
array([[ 1.32565874, -0.57097002,  0.6682492 ],
       [ 1.14538102,  0.58734406, -0.05144329]])
Coordinates:
  * user     (user) <U5 'Alice' 'Bob'
  * day      (day) <U9 'yesterday' 'today' 'tomorrow'

Nell’esempio mostrato, i dati non sono stati modificati ma sono stati arricchiti con informazioni aggiuntive. L’oggetto xarray è di classe DataArray e rappresenta un array con due dimensioni: una chiamata “user” e l’altra chiamata “day”.

data.dims
('user', 'day')

Ogni dimensione è composta da diverse coordinate. La dimensione “user” comprende le coordinate “Alice” e “Bob”, mentre la dimensione “day” include le coordinate “yesterday”, “today” e “tomorrow”.

data.coords
Coordinates:
  * user     (user) <U5 'Alice' 'Bob'
  * day      (day) <U9 'yesterday' 'today' 'tomorrow'

Le coordinate in xarray possono essere utilizzate al posto degli indici numerici standard. Ad esempio, per selezionare il valore corrispondente a “Alice” e “yesterday” dall’array di Numpy, possiamo usare le coordinate nel seguente modo.

npdata[0,0]
1.3256587436158882

Eseguiamo nuovamente la stessa operazione usando questa volta l’oggetto DataArray di xarray.

data.sel(user="Alice", day="yesterday")
<xarray.DataArray ()>
array(1.32565874)
Coordinates:
    user     <U5 'Alice'
    day      <U9 'yesterday'

Uno dei vantaggi degli oggetti xarray consiste nella presenza di coordinate semanticamente etichettate. Non è più necessario tenere a mente che npdata[:,0] corrisponde a “yesterday” e npdata[1,:] corrisponde a “Bob”. Inoltre, non è nemmeno necessario ricordare l’ordine degli assi. L’ordine in cui le dimensioni sono memorizzate è irrilevante poiché usiamo le etichette delle dimensioni. Questo rende l’indicizzazione più intuitiva e meno soggetta a errori.

data.sel(day="yesterday", user="Alice")
<xarray.DataArray ()>
array(1.32565874)
Coordinates:
    user     <U5 'Alice'
    day      <U9 'yesterday'
data.sel(user="Alice", day="yesterday")
<xarray.DataArray ()>
array(1.32565874)
Coordinates:
    user     <U5 'Alice'
    day      <U9 'yesterday'

Inoltre, xarray permette la creazione di oggetti contenenti più oggetti DataArray, che prendono il nome di Dataset. Quando si ottiene un oggetto inferenceData di ArviZ dopo il campionamento, si ha a che fare con un oggetto Dataset di xarray. Consdieriamo un esempio.

var2 = xr.DataArray(
    rng.standard_normal(size=(2, 2)),
    dims=("x", "y"),
    coords={"x": [0, 1], "y": [11, 42]},
)
var3 = xr.DataArray(
    rng.standard_normal(size=(2, 2)),
    dims=("a", "b"),
    coords={"a": [4.2, 11.8], "b": ["Geneva", "London"]},
)
ds = xr.Dataset(dict(orig=data, v2=var2, v3=var3))
ds
<xarray.Dataset>
Dimensions:  (user: 2, day: 3, x: 2, y: 2, a: 2, b: 2)
Coordinates:
  * user     (user) <U5 'Alice' 'Bob'
  * day      (day) <U9 'yesterday' 'today' 'tomorrow'
  * x        (x) int64 0 1
  * y        (y) int64 11 42
  * a        (a) float64 4.2 11.8
  * b        (b) <U6 'Geneva' 'London'
Data variables:
    orig     (user, day) float64 1.326 -0.571 0.6682 1.145 0.5873 -0.05144
    v2       (x, y) float64 -1.845 -0.3072 -0.01176 0.6354
    v3       (a, b) float64 0.1133 1.419 0.7994 -0.4276

Si noti che il Dataset ha 6 dimensioni (user, day, x, y, a, b), ma ogni variabile all’interno del Dataset utilizza solo 2 di queste dimensioni.

Per accedere a ogni variabile singolarmente, possiamo utilizzare l’indicizzazione dei dizionari:

ds["orig"]
<xarray.DataArray 'orig' (user: 2, day: 3)>
array([[ 1.32565874, -0.57097002,  0.6682492 ],
       [ 1.14538102,  0.58734406, -0.05144329]])
Coordinates:
  * user     (user) <U5 'Alice' 'Bob'
  * day      (day) <U9 'yesterday' 'today' 'tomorrow'
ds["orig"].sel(user="Alice")
<xarray.DataArray 'orig' (day: 3)>
array([ 1.32565874, -0.57097002,  0.6682492 ])
Coordinates:
    user     <U5 'Alice'
  * day      (day) <U9 'yesterday' 'today' 'tomorrow'
ds["orig"].sel(user="Alice", day="yesterday")
<xarray.DataArray 'orig' ()>
array(1.32565874)
Coordinates:
    user     <U5 'Alice'
    day      <U9 'yesterday'

40.4. Confonto tra gruppi#

Analizziamo ora un esempio che ci permetterà di applicare le nozioni discusse in precedenza per effettuare il campionamento in modo separato su diversi gruppi di osservazioni, così da poter eseguire l’inferenza riguardante le differenze tra le medie dei gruppi.

Per indicare l’appartenenza al gruppo attraverso degli indici numerici, utilizziamo la funzione pd.factorize per creare dei valori numerici distinti (0, 1, 2) che corrispondono ai tre gruppi. Questo indice numerico verrà chiamato species_idx.

species_idx, species_codes = pd.factorize(penguins["species"])
print(species_idx)
[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1
 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2]

Il valore 0 identifica la specie Amelie, il valore 1 identifica la specie Gentoo e il valore 2 la specie Chinstrap.

print(*penguins["species"])
Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Adelie Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Gentoo Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap Chinstrap

Di seguito è riportata la descrizione del modello per il campionamento MCMC che distingue tra più gruppi di osservazioni:

Per prima cosa, utilizziamo pd.factorize per creare dei valori numerici che distinguono tra i tre gruppi di osservazioni, chiamati species_idx. Successivamente, specifichiamo la distribuzione a priori per mu e sigma per ciascun gruppo di osservazioni. In questo caso, abbiamo aggiunto l’argomento dims = "species" nella specificazione delle distribuzioni a priori. Nella verosimiglianza, usiamo mu_prior[species_idx] e sigma_prior[species_idx] per indicare che le osservazioni appartengono a tre gruppi diversi, come specificato dalla variabile species. Inoltre, passiamo il dizionario coords a pm.Model(), dove la chiave species è associata ad una lista di interi [0, 1, 2]. Il modello per il campionamento MCMC che distingue tra più gruppi di osservazioni ha quindi una specifica quasi identica a quella usata per un singolo gruppo di osservazioni, con la sola aggiunta della specificazione dei dims e dei valori numerici associati ai gruppi di osservazioni.

coords = {"species": [0, 1, 2]}

with pm.Model(coords=coords) as labeled_model:
    mu_prior = pm.Normal("mu", mu=4000, sigma=500, dims="species")
    sigma_prior = pm.HalfCauchy("sigma", beta=1000, dims="species")
    likelihood = pm.Normal(
        "likelihood",
        mu=mu_prior[species_idx],
        sigma=sigma_prior[species_idx],
        observed=penguins["body_mass_g"],
    )

Eseguiamo il campionamento.

with labeled_model:
    labeled_idata = pm.sample()
Auto-assigning NUTS sampler...
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (4 chains in 4 jobs)
NUTS: [mu, sigma]
100.00% [8000/8000 00:02<00:00 Sampling 4 chains, 0 divergences]
Sampling 4 chains for 1_000 tune and 1_000 draw iterations (4_000 + 4_000 draws total) took 17 seconds.

Esaminiamo le distribuzioni a posteriori e le diagnostiche del campionamento.

axes = az.plot_trace(
    labeled_idata, combined=True, divergences="bottom", kind="rank_bars"
)
plt.tight_layout()
_images/c6f4a7c19722a3ed2f809134a8fc94cdc35a449e74fd7153e522438c791bbfda.png

Abbiamo ottenuto tre distribuzioni a posteriori per ciascuno dei due parametri \(\mu\) e \(\sigma\), una distribuzione per ciascuna specie di pinguini.

Esaminiamo se le quattro catene hanno prodotto risultati diversi per il parametro \(\mu\).

axes = az.plot_forest(labeled_idata, var_names=["mu"], figsize=(8, 3))
axes[0].set_title("μ Mass Estimate: 94.0% HDI")
Text(0.5, 1.0, 'μ Mass Estimate: 94.0% HDI')
_images/075cd521224c757348311c2576c6bf0a396d47ae56c6ecfd587610307e452aa8.png

Facciamo la stessa cosa per il parametro \(\sigma\).

axes = az.plot_forest(labeled_idata, var_names=["sigma"], figsize=(8, 3))
axes[0].set_title("σ Mass Estimate: 94.0% HDI")
Text(0.5, 1.0, 'σ Mass Estimate: 94.0% HDI')
_images/74ad705fff049bee907fb9f029b3e1887552b2d32e70ce231b32e1703be50891.png

Un sommario numerico delle distribuzioni a posteriori si ottiene nel modo seguente.

az.summary(labeled_idata, kind="stats").round(2)
mean sd hdi_3% hdi_97%
mu[0] 3707.94 37.59 3641.64 3779.60
mu[1] 5083.49 45.63 4994.39 5164.83
mu[2] 3736.14 46.50 3650.66 3826.96
sigma[0] 461.56 27.02 408.23 511.25
sigma[1] 505.64 33.34 448.57 572.55
sigma[2] 390.13 34.40 328.97 455.25

Dalle distribuzioni a posteriori e dagli intervalli di credibilità, possiamo dedurre che il peso dei pinguini Adelie e Chinstrap è simile, mentre i pinguini Gentoo sono in genere più pesanti. Inoltre, l’analisi indica che il peso dei pinguini Gentoo ha una maggiore variabilità rispetto ai pinguini Adelie, seguita da quella dei pinguini Chinstrap. Al contrario, i pinguini Chinstrap mostrano una maggiore omogeneità nel peso.

40.5. Verifica di ipotesi bayesiana#

Una volta ottenuto un campione della distribuzione a priori del parametro di interesse \(\mu\) per ciascuna delle tre specie di pinguini, possiamo chiederci quale sia la probabilità che il peso di un pinguino di una specie sia maggiore di quello di un pinguino di un’altra specie. Possiamo rispondere a questa domanda utilizzando un campione casuale della distribuzione a posteriori. Per fare ciò, dobbiamo confrontare il parametro di interesse per molti valori e calcolare la media dei valori trovati.

Per recuperare i valori numerici della distribuzione a posteriori del parametro \(\mu\) dall’oggetto labeled_idata, possiamo utilizzare le funzioni di ArviZ. Esaminiamo le proprietà dell’oggetto labeled_idata.

labeled_idata
arviz.InferenceData
    • <xarray.Dataset>
      Dimensions:  (chain: 4, draw: 1000, species: 3)
      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
        * species  (species) int64 0 1 2
      Data variables:
          mu       (chain, draw, species) float64 3.713e+03 5.1e+03 ... 3.738e+03
          sigma    (chain, draw, species) float64 442.9 475.1 381.8 ... 515.7 370.2
      Attributes:
          created_at:                 2023-05-06T06:30:02.078800
          arviz_version:              0.15.1
          inference_library:          pymc
          inference_library_version:  5.3.0
          sampling_time:              43.66927099227905
          tuning_steps:               1000

    • <xarray.Dataset>
      Dimensions:                (chain: 4, draw: 1000)
      Coordinates:
        * chain                  (chain) int64 0 1 2 3
        * draw                   (draw) int64 0 1 2 3 4 5 ... 994 995 996 997 998 999
      Data variables: (12/17)
          diverging              (chain, draw) bool False False False ... False False
          lp                     (chain, draw) float64 -2.54e+03 ... -2.54e+03
          n_steps                (chain, draw) float64 3.0 7.0 3.0 3.0 ... 3.0 3.0 3.0
          tree_depth             (chain, draw) int64 2 3 2 2 2 3 2 2 ... 2 2 2 3 2 2 2
          max_energy_error       (chain, draw) float64 0.232 0.3705 ... 0.7087 1.87
          index_in_trajectory    (chain, draw) int64 -1 2 3 2 2 -1 ... 2 3 2 2 -1 -3
          ...                     ...
          reached_max_treedepth  (chain, draw) bool False False False ... False False
          step_size              (chain, draw) float64 0.9499 0.9499 ... 1.125 1.125
          energy                 (chain, draw) float64 2.541e+03 ... 2.546e+03
          perf_counter_diff      (chain, draw) float64 0.001301 0.002175 ... 0.000774
          smallest_eigval        (chain, draw) float64 nan nan nan nan ... nan nan nan
          energy_error           (chain, draw) float64 0.232 -0.01623 ... -0.4988
      Attributes:
          created_at:                 2023-05-06T06:30:02.095549
          arviz_version:              0.15.1
          inference_library:          pymc
          inference_library_version:  5.3.0
          sampling_time:              43.66927099227905
          tuning_steps:               1000

    • <xarray.Dataset>
      Dimensions:           (likelihood_dim_0: 333)
      Coordinates:
        * likelihood_dim_0  (likelihood_dim_0) int64 0 1 2 3 4 ... 328 329 330 331 332
      Data variables:
          likelihood        (likelihood_dim_0) float64 3.75e+03 3.8e+03 ... 3.775e+03
      Attributes:
          created_at:                 2023-05-06T06:30:02.102522
          arviz_version:              0.15.1
          inference_library:          pymc
          inference_library_version:  5.3.0

Recuperiamo i valori a posteriori del parametro \(\mu\).

mu_post = labeled_idata.posterior['mu']

L’oggett ottenuto è un array di dimensioni \(4 \times 1000 \times 3\). L’indice 4 si riferisce alle catente, l’indice 1000 si riferisce al numero di campioni ottenuti, l’indice 3 si riferisce ai gruppi.

mu_post.shape
(4, 1000, 3)

Per trovare la media a posteriori del parametro \(\mu\) della specie Adelie prendiamo le osservazioni che si riferiscono a tutte le catene (:), a tutti i valori campionati (:) per il primo gruppo (0). La media a posteriori dei pinguini Adelie è dunque uguale a

print(mu_post[:, :, 0].mean())
<xarray.DataArray 'mu' ()>
array(3707.93884115)
Coordinates:
    species  int64 0

Per tutte e tre le specie (Adelie, Gentoo, Chinstrap) abbiamo

np.array(
    [mu_post[:, :, 0].mean(), mu_post[:, :, 1].mean(), mu_post[:, :, 2].mean()]
).round(1)
array([3707.9, 5083.5, 3736.1])
summary_stats = (
    penguins.loc[:, ["species", "body_mass_g"]]
    .groupby("species")
    .aggregate(["mean", "std", "count"])
)
summary_stats.round(1)
body_mass_g
mean std count
species
Adelie 3706.2 458.6 146
Chinstrap 3733.1 384.3 68
Gentoo 5092.4 501.5 119

Per verificare che l’ordinamento dei gruppi corrisponda a Adelie, Gentoo, Chinstrap, esaminiamo la stima a posteriori della deviazione standard delle tre specie.

sigma_post = labeled_idata.posterior["sigma"]
np.array(
    [sigma_post[:, :, 0].mean(), sigma_post[:, :, 1].mean(), sigma_post[:, :, 2].mean()]
).round(1)
array([461.6, 505.6, 390.1])

I valori trovati confermano quello che ci aspettavamo. Ora che abbiamo capito come estrarre le stime a posteriori di un parametro per ciascun gruppo, possiamo passare al test di ipotesi bayesiano. Chiediamoci quale sia la probabilità che un pinguino Adelie abbia un peso minore di un pinguino Chinstrap.

np.mean([mu_post[:, :, 0] < mu_post[:, :, 2]])
0.68125

Chiediamoci quale sia la probabilità che un pinguino Adelie abbia un peso minore di un pinguino Gentoo.

np.mean([mu_post[:, :, 0] < mu_post[:, :, 2]])
0.68125

Chiediamoci quale sia la probabilità che un pinguino Adelie abbia un peso minore di un pinguino Gentoo.

np.mean([mu_post[:, :, 0] < mu_post[:, :, 1]])
1.0

Consideriamo ora le deviazioni standard. Troviamo la probabilità che la varianza del peso dei pinguini Adelie sia minore di quella dei pinguini Chinstrap.

np.mean([sigma_post[:, :, 2] > sigma_post[:, :, 0]])
0.055

40.6. Watermark#

%load_ext watermark
%watermark -n -u -v -iv -w -p pytensor
Last updated: Sat May 06 2023

Python implementation: CPython
Python version       : 3.11.3
IPython version      : 8.13.2

pytensor: 2.11.1

pandas    : 2.0.1
pymc      : 5.3.0
numpy     : 1.24.3
matplotlib: 3.7.1
seaborn   : 0.12.2
xarray    : 2023.4.2
arviz     : 0.15.1

Watermark: 2.3.1