%pip install pymc arviz numpy pandas matplotlib ipywidgetsInsea 2025 Statistiques Bayésiennes
TP3: Introduction à PyMC
Author: Hicham Janati
Objectifs:
- Découvrir la librairie PyMC
- Implémenter les premiers modèles bayésiens et faire le diagnostic de convergence
- Interpréter les résultats et comparer avec les statistiques fréquentistes
On importe les librariries et un crée un générateur aléatoire:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import pymc as pm
import arviz as az
seed = 42
rng = np.random.default_rng(seed)Modèle Poisson-Gamma simple
On considère l’ex 1 du TD1. On observe le nombre de sinistres par année \(N | \lambda \sim \mathcal{P}(\lambda)\), où \(\lambda\) suit une loi a priori \(\text{Gamma}(a, b)\). On suppose que les données historiques mènent au choix a = 4 et b = 2. On utilise la définition de Gamma où b correspond au “rate” et non pas au scale (comme en TD), la moyenne de cette Gamma est a/b. (voir wikipedia).
On suppose que les 5 observations sont données par:
data = np.array([4, 0, 2, 1, 0])
rng = np.random.default_rng(42)
lambda_true = 2
data = rng.poisson(lambda_true, size=5)On définit le modèle bayésien dans un contexte avec pymc ou on précise la distribution, le nom et les paramètres de chaque variable
a = 4
b = 2
with pm.Model() as model:
lambda_ = pm.Gamma("lambda", a, b) # non observée
N_obs = pm.Poisson("N_obs", mu=lambda_, observed=data) # observée
trace = pm.sample(1000) # on simule une chaine MCMC de la loi a posterioriOn voit que pymc a automatiquement choisi NUTS et a simulé 4 chaînes avec 2000 échantillons chacune dont 1000 jetés (tuning / burn-in). Voyons ce que contient l’objet trace:
traceC’est un objet InferenceData du package arviz. On peut obtenir les échantillons simulés dans l’attribut posterior:
trace.posterior["lambda"].datatrace.posterior["lambda"].data.shapeOn a effectivement généré 4 chaines avec 1000 échantillons chacune. On peut les visualiser:
plt.figure()
plt.plot(trace.posterior["lambda"].data.T)
plt.grid(True)
plt.title("Trace plot")
plt.show()Ou utiliser la librairie arviz directement qui donne également une estimation de la densité a posteriori:
az.plot_trace(trace)On fait le diagnostic de convergence:
az.plot_autocorr(trace)
plt.show()az.summary(trace)az.summary(trace)- Les tracés sont bien mélangés
- Rhat = 1.0 < 1.01 : pas de différence significative entre les 4 chaines
- ESS très larges
- Autocorrélations diminuent très rapidement
Cette chaîne réussit les diagnostics de convergence.
On peut visualiser la densité a posteriori avec l’intervalle de crédibilité HDI:
az.plot_posterior(trace, hdi_prob=0.94)Les bornes de cet intervalle sont également présente dans le tableau du summary ci-dessus. On peut calcule un HDI directement:
az.hdi(trace, hdi_prob=0.94).to_pandas()On peut calculer un ESS relatif (divisé par le nombre d’échantillons):
az.ess(trace, method="bulk", relative=True).to_pandas()Ainsi, on en déduit que 44% des échantillons “sont efficaces” pour estimer “le centre” (bulk) de la distribution
Question 1: Augmenter le nombre de sinistres observés de 5 à 10 puis 100, comment changent les statistiques du az.summary ?
Question 2: Les métriques de convergence sont-elle très différentes ? Est-ce surprenant ?
Question 3: Comment peut-on interpréter le HDI obtenu ?
Question 4: En utilisant le fait que la loi a priori soit conjuguée, générez des échantillons a posteriori directement (sans pymc) et comparez
La loi a posteriori est \(\text{Gamma}(a + \sum_{i=1}^n N_i, b + n)\)
lambda_post_samples = rng.gamma(a + data.sum(), 1/(b + len(data)), size=(4, 1000))
ax = az.plot_posterior(trace)
ax.hist(lambda_post_samples, bins=100, density=True, alpha=0.7)
plt.show()trace_iid = az.convert_to_inference_data(dict(iid=lambda_post_samples))
az.summary(trace_iid)lambda_post_samples.mean()az.summary(trace)Modèle Poisson-Gamma à plusieurs conducteurs
On considère désormais les données de plusieurs individus avec un \(\lambda_i\) différent mais un a priori commun. Chaque conducteur \(i\) a ses données \(N_i^1, \dots, N_i^m\). Le modèle pymc s’adapte facilement en changeant le shape des paramètres:
data = np.array([[4, 0, 0],
[0, 2, 1],
[2, 2, 0],
[1, 3, 0],
[0, 0, 1]])
# donnéees de 3 conducteurs
n_drivers = data.shape[1]
a = 4
b = 2
with pm.Model() as model:
lambda_ = pm.Gamma("lambda", a, b, shape=n_drivers) # non observée, on précise le nombre de lambda
# le shape des données par défaut est n_observation x n_features, pymc associe chaque lambda_i a une colonne de data
N_obs = pm.Poisson("N_obs", mu=lambda_, observed=data, ) # observée
trace = pm.sample(1000) # on simule une chaine MCMC de la loi a posteriori
pm.compute_log_likelihood(trace)L’objet trace contient désormais plusieurs variables lambda:
print(az.summary(trace))Question 4: Comparez l’estimation fréquentiste avec l’estimation bayésienne avec (a, b) = (4, 2) puis (a, b) = (10, 1).
data.mean(0)Question 5: Déterminez un intervalle de confiance fréquentiste de niveau 95% pour chaque \(\lambda_i\).
\([\bar{N} \pm \frac{Q_{97.5}\hat{\sigma}}{\sqrt{n}}]\) avec \(Q_{97.5}\) le quantile de la loi de student à n-1 degrés de liberté.
from scipy.stats import t
n = 5
q975 = t.ppf(0.975, df=n-1)
sigma = data.std(0)
data.mean(0) - q975 * sigma / n ** 0.5, data.mean(0) + q975 * sigma / n ** 0.5, az.summary(trace)Question 6: On reprend à présent une loi a priori Uniforme([0, 5]). Déterminez des HDI de niveau 95% pour chaque \(\lambda_i\). Comment se comparent-ils aux intervalles de confiance fréquentistes ?
data = np.array([[4, 0, 0],
[0, 2, 1],
[2, 2, 0],
[1, 3, 0],
[0, 0, 1]])
# donnéees de 3 conducteurs
n_drivers = data.shape[1]
a = 4
b = 2
with pm.Model() as model:
lambda_ = pm.Uniform("lambda", 0, 5, shape=n_drivers) # non observée, on précise le nombre de lambda
# le shape des données par défaut est n_observation x n_features, pymc associe chaque lambda_i a une colonne de data
N_obs = pm.Poisson("N_obs", mu=lambda_, observed=data, ) # observée
trace = pm.sample(1000) # on simule une chaine MCMC de la loi a posterioridata = np.array([[4, 0, 0],
[0, 2, 1],
[2, 2, 0],
[1, 3, 0],
[0, 0, 1]])
# donnéees de 3 conducteurs
n_drivers = data.shape[1]
a = 4
b = 2
with pm.Model() as model_unif:
# lambda_ = pm.Gamma("lambda", a, b, shape=n_drivers) # non observée, on précise le nombre de lambda
lambda_ = pm.Uniform("lambda", 0, 100, shape=n_drivers)
# le shape des données par défaut est n_observation x n_features, pymc associe chaque lambda_i a une colonne de data
N_obs = pm.Poisson("N_obs", mu=lambda_, observed=data, ) # observée
trace_unif = pm.sample(1000) # on simule une chaine MCMC de la loi a posterioriaz.summary(trace_unif)data.mean(0) - q975 * sigma / n ** 0.5, data.mean(0) + q975 * sigma / n ** 0.5,