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")
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:
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()
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)
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()
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>
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]
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()
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')
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')
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
-
<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