38. La predizione delle frequenze#
La predizione delle frequenze si riferisce al problema di prevedere il numero di volte che un evento si verificherà in futuro, sulla base delle informazioni disponibili. Questo problema è spesso affrontato utilizzando il teorema di Bayes, che consente di calcolare la probabilità di un evento sulla base delle informazioni precedenti e delle nuove informazioni acquisite. Per fare ciò, si utilizza una distribuzione a priori per rappresentare le nostre credenze iniziali sulla frequenza dell’evento e si aggiorna questa distribuzione utilizzando le nuove informazioni acquisite. La distribuzione risultante, ovvero la distribuzione a posteriori, rappresenta le nostre credenze aggiornate sulla frequenza dell’evento e può essere utilizzata per fare previsioni sulle future occorrenze dell’evento.
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import scipy as sc
import statistics
import arviz as az
import seaborn as sns
import scipy.stats as st
import pymc as pm
import xarray as xr
import warnings
warnings.filterwarnings("ignore")
warnings.simplefilter("ignore")
print(f"Running on PyMC v{pm.__version__}")
Running on PyMC v5.5.0
%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"
38.1. La ricerca sul trauma#
Per fare un esempio di questo approccio, in questo capitolo faremo riferimento alla ricerca sul trauma. In questo campo di ricerca, come in altri, il risultato di interesse può essere rappresentato dalla frequenza del numero di episodi che si verificano in un dato periodo di tempo. Nel caso della ricerca sulla violenza domestica, ad esempio, potremmo esaminare il tasso di atti aggressivi durante l’intervallo tra un momento temporale di baseline e un’intervista di follow-up. Altri esempi di risultati esprimibili in termini di frequenze nalla ricerca post-traumatica includono la frequenza dell’abuso di sostanze durante un periodo di osservazione o il numero di interventi della polizia durante un dato periodo.
Nell’esempio seguente esamineremo l’uso della predizione bayesiana per predire il numero di aggressioni nei confronti del partner nelle relazioni di coppia. I dati presentati sono tratti da uno studio che esamina la frequenza di episodi di aggressione messi in atto da pazienti di sesso maschile che avevano recentemente iniziato un programma di trattamento dell’alcol nei confronti del loro partner di sesso femminile.
La frequenza degli episodi di violenza, così come altri fenomeni quantificabili in termini di frequenze assolute, può essere modellata da un processo di Poisson, che si basa sul presupposto che gli eventi siano casuali e abbiano la stessa probabilità di verificarsi in qualsiasi momento. Naturalmente, questa ipotesi non è sempre valida, ma spesso è sufficiente per la modellazione.
38.2. La distribuzione a priori#
In una ricerca sui pazienti che avevano recentemente iniziato un programma di trattamento per l’abuso o la dipendenza da alcol, Gagnon et al. [GDLB+08] hanno trovato che, in un periodo di 6 mesi, il numero di assalti fisici da parte dei pazienti di genere maschile nei confronti del loro partner femminile è uguale, in media, a 11.46 (\(SD\) = 25.79; \(n\) = 114).
Per questi dati, è dunque sensato pensare che la distribuzione del numero di episodi di aggressione fisica può essere rappresentata dalla distribuzione esponenziale.
Dal punto di vista statistico, ricordiamo che la distribuzione esponenziale modella il numero di eventi che si verificano in un intervallo di tempo quando questi eventi si verificano raramente e indipendentemente l’uno dall’altro.
La funzione di densità di probabilità della distribuzione esponenziale è data da:
dove λ è il parametro della distribuzione e rappresenta il tasso medio di occorrenza degli eventi. La media e la varianza della distribuzione esponenziale sono entrambe uguali a 1/λ.
Poniamoci dunque il problema di rappresentare le nostre credenze a priori relative al numero di episodi di aggressione fisica mediante una distribuzione esponenziale.
Dalla ricerca di Gagnon et al. [GDLB+08] sappiamo che, in un periodo di 6 mesi, il numero di assalti fisici nei confronti del partner femminile, in questa popolazione, è uguale a 11.46.
Considereremo una distribuzione esponenziale per rappresentare le nostre credenze a priori circa la frequenza media \(\mu\) degli episodi di aggressione nei confronti del partner per questa popolazione, in un periodo di 6 mesi. In Python, scipy.stats.expon
è un modulo che fornisce funzioni per lavorare con la distribuzione esponenziale. In particolare, la funzione pdf
(probability density function) calcola la funzione di densità di probabilità della distribuzione esponenziale per un dato valore di x.
La sintassi per utilizzare questa funzione è la seguente:
scipy.stats.expon.pdf(x, loc=0, scale=1)
dove x
è il valore per il quale si vuole calcolare la funzione di densità di probabilità. Il parametro loc
(opzionale) e scale
specificano rispettivamente la posizione e la scala della distribuzione. La posizione (loc
) di solito è impostata a 0, mentre la scala (scale
) è l’inverso del parametro λ della distribuzione esponenziale. In altre parole, la scala è uguale alla media della distribuzione esponenziale.
Per il caso presente, dunque, se vogliamo che la distribuzione esponenziale abbia una media di 11.46, procediamo come indicato sotto
x = np.linspace (0, 50, 100)
y = st.expon.pdf(x, 0, 11.46)
plt.plot(x, y)
[<matplotlib.lines.Line2D at 0x128e926d0>]
Per verificare che abbiamo implementato correttamente la funzione esponenziale con il parametro voluto, estraiamo un grande numero di realizzazioni della v.c. e calcoliamo la media.
r = st.expon.rvs(0, 11.46, size=100000)
r.mean()
11.45794424722882
38.3. Inferenza bayesiana#
Una volta capito come descrivere le nostre credenze a priori, poniamoci il problema di usare PyMC per l’inferenza Bayesiana.
Consideriamo un singlo individuo di genere maschile appartenente a questa popolazione. Se, in media, in 6 mesi ci aspettiamo un numero di episodi di violenza pari a 11.46, possiamo descrivere il numero di episodi di violenza per un singolo individuo con la seguente distribuzione esponenziale. Si noti che, in questo caso, la funzione pm.Exponential
è parametrizzata usando il parametro l
che è uguale a \(\lambda = 1/ \mu\).
l = 1/11.46
with pm.Model() as model:
mu = pm.Exponential("mu", l)
idata = pm.sample_prior_predictive(samples=10000, random_seed=rng)
Sampling: [mu]
Esaminiamo 10000 campioni casuali estratti dalla distribuzione a priori. Il risultato è simile alla distribuzione di densità teorica rappresentata nel grafico precedente.
_ = az.plot_posterior(idata.prior.mu)
Si noti che, in questo caso, stiamo usando una distribuzione a priori informativa.
38.4. La verosimiglianza#
Adesso inseriamo nel modello PyMC la verosimiglianza.
In questo caso, usiamo quale modello generativo dei dati una distribuzione di Poisson.
Le distribuzioni di Poisson sono usate per modellare il numero di eventi rari che si verificano in un intervallo di tempo fisso. Ad esempio, il numero di episodi di comportamento inappropriato per settimana in individui con disturbi alimentari, nascite in un giorno o incidenti in una settimana.
La verosimiglianza di Poisson è simile a quella binomiale, ma non ha un limite superiore al numero di successi.
Per esempio, consideriamo un paziente chiamato Mario. Usando gli item della sottoscala relativa agli episodi di violenza fisica della Conflict Tactics Scales-2, troviamo che Mario ha avuto 8 episodi violenti nei confronti del partner negli ultimi 6 mesi.
Inseriamo questa informazione nel modello bayesiano usando 8 come dato che specifica una verosimiglianza di Poisson con parametro sconosciuto mu
, a cui abbiamo attribuito una distribuzione a priori esponenziale ed eseguiamo il campionamento.
with pm.Model() as model:
mu = pm.Exponential("mu", l)
episodes = pm.Poisson("episodes", mu, observed=8)
idata2 = pm.sample(2000)
Auto-assigning NUTS sampler...
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (4 chains in 4 jobs)
NUTS: [mu]
Bad value in file PosixPath('/Users/corrado/mambaforge/envs/pymc_env/lib/python3.11/site-packages/arviz/plots/styles/arviz-whitegrid.mplstyle'), line 40 ("axes.prop_cycle: cycler('color', ['2a2eec', 'fa7c17', '328c06', 'c10c90', '933708', '65e5f3', 'e6e135', '1ccd6a', 'bd8ad5', 'b16b57'])"): Key axes.prop_cycle: "cycler('color', ['2a2eec', 'fa7c17', '328c06', 'c10c90', '933708', '65e5f3', 'e6e135', '1ccd6a', 'bd8ad5', 'b16b57'])" is not a valid cycler construction:
---------------------------------------------------------------------------
KeyboardInterrupt Traceback (most recent call last)
Cell In[7], line 4
2 mu = pm.Exponential("mu", l)
3 episodes = pm.Poisson("episodes", mu, observed=8)
----> 4 idata2 = pm.sample(2000)
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:
38.5. La distribuzione a posteriori#
Estraiamo dall’oggetto idata2
i campioni della distribuzione a posteriori del parametro mu
(media del numero di episodi di violenza negli ultimi 6 mesi).
sample_posterior = idata2.posterior['mu']
Generiamo un grafico della distribuzione a posteriori del parametro mu
.
az.plot_posterior(sample_posterior)
<Axes: title={'center': 'mu'}>
Possiamo dunque concludere, con un livello di certezza soggettiva del 94%, che per Mario, il numero di episodi di violenza nei confronti del partner varierà da un minimo di 3.5 ad un massimo di 13, in un periodo di 6 mesi, con una media di 8.2.
Supponiamo ora di volere confrontare due individui, Mario e Paolo. Di Mario abbiamo osservato 8 episodi di violenza in 6 mesi; di Paolo abbiamo osservato 12 episodi di violenza negli ultimi 6 mesi. Possiamo dire che Paolo è più violento di Mario? Oppure dobbiamo pensare che la differenza tra i due sia solo una fluttuazione casuale?
Scriviamo il modello bayesiano nel modo seguente, usando sempre la distribuzione a priori che abbiamo definito in precedenza.
with pm.Model() as model3:
mu_A = pm.Exponential("mu_A", l)
mu_B = pm.Exponential("mu_B", l)
episodes_A = pm.Poisson("episodes_A", mu_A, observed=[8])
episodes_B = pm.Poisson("episodes_B", mu_B, observed=[12])
idata3 = pm.sample(2000)
Auto-assigning NUTS sampler...
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (4 chains in 4 jobs)
NUTS: [mu_A, mu_B]
Sampling 4 chains for 1_000 tune and 2_000 draw iterations (4_000 + 8_000 draws total) took 21 seconds.
Esaminiamo le distribuzioni a posteriori dei parametri mu_A
e mu_B
che rappresentano la media del numero di episodi di violenza per i due individui.
with model3:
az.plot_trace(idata3, kind="rank_bars")
plt.tight_layout()
Estraiamo le distribuzioni a posteriori dei due parametri da idata3
.
mu_A = idata3.posterior['mu_A']
mu_B = idata3.posterior['mu_B']
mu_B.mean(), mu_A.mean()
(<xarray.DataArray 'mu_B' ()>
array(11.93233794),
<xarray.DataArray 'mu_A' ()>
array(8.21862358))
Rappresentiamo graficamente le due distribuzioni a posteriori.
az.plot_posterior(mu_A)
az.plot_posterior(mu_B)
<Axes: title={'center': 'mu_B'}>
Vogliamo eseguire un test di ipotesi bayesiano per determinare la probabilità che la media del numero di episodi di violenza di Mario sia maggiore di quella di Paolo. Per fare ciò, calcoliamo quante volte mu_B
è maggiore di mu_A
nelle due distribuzioni a posteriori.
(mu_B > mu_A).mean()
<xarray.DataArray ()> array(0.810875)
Possiamo dunque dire che, se confrontiamo i valori dei parametri delle due distribuzioni a posteriori, nell’81% di casi risulta che Paolo è più violento di Mario.
Il grafico seguente mostra le stime degli intervalli di credibilità del 94% per ciascuna delle 4 catene, pe i due parametri.
_ = az.plot_forest(idata3, var_names=["mu_A", "mu_B"])
Dato che gli intervalli di credibilità sono sovrapposti, concludiamo che non ci sono evidenze credibili di una differenza. Ovvero, sulla base delle nostre credenze a priori e sulla base dei dati osservati, ad un livello di certezza soggettiva del 94%, non possiamo concludere che Paolo sia più violento di Mario.
38.6. La predizione di episodi di violenza futuri#
Consideriamo ora il problema della predizione di dati futuri. Utilizziamo nuovamente il modello che abbiamo già usato in precedenza, ovvero model3
.
Creiamo la distribuzione predittiva a posteriori per il parametro mu_A
, ovvero la media del numero di eventi di violenza attesi in futuro per Mario.
with model3:
post_pred = pm.sample_posterior_predictive(idata3)
Sampling: [episodes_A, episodes_B]
post_pred
-
<xarray.Dataset> Dimensions: (chain: 4, draw: 2000, episodes_A_dim_2: 1, episodes_B_dim_2: 1) Coordinates: * chain (chain) int64 0 1 2 3 * draw (draw) int64 0 1 2 3 4 5 ... 1994 1995 1996 1997 1998 1999 * episodes_A_dim_2 (episodes_A_dim_2) int64 0 * episodes_B_dim_2 (episodes_B_dim_2) int64 0 Data variables: episodes_A (chain, draw, episodes_A_dim_2) int64 6 13 12 12 ... 5 3 9 episodes_B (chain, draw, episodes_B_dim_2) int64 11 11 13 ... 11 8 16 Attributes: created_at: 2023-05-29T09:57:52.433904 arviz_version: 0.15.1 inference_library: pymc inference_library_version: 5.4.0
-
<xarray.Dataset> Dimensions: (episodes_A_dim_0: 1, episodes_B_dim_0: 1) Coordinates: * episodes_A_dim_0 (episodes_A_dim_0) int64 0 * episodes_B_dim_0 (episodes_B_dim_0) int64 0 Data variables: episodes_A (episodes_A_dim_0) int64 8 episodes_B (episodes_B_dim_0) int64 12 Attributes: created_at: 2023-05-29T09:57:52.435634 arviz_version: 0.15.1 inference_library: pymc inference_library_version: 5.4.0
Rappresentiamo la distribuzione predittiva a posteriori di mu_A
con un istogramma.
_ = az.plot_posterior(post_pred.posterior_predictive.episodes_A, hdi_prob=0.94)
Facciamo la stessa cosa per Paolo.
_ = az.plot_posterior(post_pred.posterior_predictive.episodes_B, hdi_prob=0.94)
In base alle nostre credenze precedenti e ai dati osservati negli ultimi 6 mesi, possiamo aspettarci con una certezza soggettiva del 94% che nei prossimi 6 mesi Mario avrà tra 1 e 15 episodi di violenza, mentre Paolo ne avrà tra 3 e 20.
38.7. Watermark#
%load_ext watermark
%watermark -n -u -v -iv -w
Last updated: Sat May 06 2023
Python implementation: CPython
Python version : 3.11.3
IPython version : 8.13.2
numpy : 1.23.5
xarray : 2023.4.2
pandas : 1.5.3
pymc : 5.3.0
arviz : 0.15.1
seaborn : 0.12.2
scipy : 1.10.1
matplotlib: 3.7.1
Watermark: 2.3.1