Introduction à l'analyse bayésienne en écologie

Les statistiques bayésiennes forment une trousse d'outils à garder dans votre pack sack.

Qu'est-ce que c'est?

En deux mots: modélisation probabiliste. Un approche de modélisation probabiliste se servant au mieux de l'information disponible. Pour calculer les probabilités d'une variable inconnu en mode bayésien, nous avons besoin:

  • De données
  • D'un modèle
  • D'une idée plus ou moins précise du résultat avant d'avoir analysé les données

De manière plus formelle, le théorème de Bayes (qui forme la base de l'analyse bayéseienne), dit que la distribution de probabilité des paramètres d'un modèle (par exemple, la moyenne ou une pente) est proportionnelle à la mutliplication de la distribution de probabilité estimée des paramètres et la distribution de probabilité émergeant des données.

Plus formellement,

$$P\left(\theta | y \right) = \frac{P\left(y | \theta \right) \times P\left(\theta\right)}{P\left(y \right)}$$

,

où $P\left(\theta | y \right)$ $-$ la probabilité d'obtenir des paramètres $\theta$ à partir des données $y$ $-$ est la distribution de probabilité a posteriori, calculée à partir de votre a prioti $P\left(\theta\right)$ $-$ la probabilité d'obtenir des paramètres $\theta$ sans égard aux données, selon votre connaissance du phénomène $-$ et vos données observées $P\left(y | \theta \right)$ $-$ la probabilité d'obtenir les données $y$ étant donnés les paramètres $\theta$ qui régissent le phénomène. $P\left(y\right)$, la probabilité d'observer les données, est appellée la vraissemblance marginale, et assure que la somme des probabilités est nulle.

Pourquoi utiliser?

Avec la notion fréquentielle de probabilité, on teste la probabilité d'observer les données recueillies étant donnée l'absence d'effet réel (qui est l'hypothèse nulle généralement adoptée). La notion bayésienne de probabilité combine la connaissance que l'on a d'un phénomène et les données observées pour estimer la probabilité qu'il existe un effet réel. En d'autre mots, les stats fréquentielles testent si les données concordent avec un modèle du réel, tandis que les stats bayésiennes évaluent la probabilité que le modèle soit réel.

Le hic, c'est que lorsqu'on utilise les statistiques fréquentielles pour répondre à une question bayésienne, on s'expose à de mauvaises interprétations. Si l'on désire, par exemple, évaluer la probabilité de l'existance de vie sur Mars, on devra passer par le bayésien, car avec les stats fréquentielles, l'on devra plutpot conclure si les données sont conforme ou non avec l'hypothèse de la vie sur Mars (exemple tirée du blogue Dynamic Ecology).

Comment l'utiliser?

Bien que la formule du théorème de Bayes soit plutôt simple, calculer une fonction a posteriori demandera de passer par des algorithmes de simulation, ce qui pourrait demander une bonne puissance de calcul, et des outils appropriés.

En particulier, le module générique pymc3 permet de générer une panoplie de modèles bayésiens. Le module bambi offre un interface convivial pour générer des modèles bayésiens mixtes.

Faucons pélerins

Empruntons un exemple du livre Introduction to WinBUGS for Ecologists: A Bayesian Approach to Regression, ANOVA and Related Analyses, de Marc Kéry et examinons la masse de faucons pélerins. Mais alors que Marc Kéry utilise WinBUGS, un logiciel de résolution de problème en mode bayésien, nous utiliserons PyMC3.

Source: Wikimedia Commons

Pour une première approche, nous allons estimer la masse moyenne d'une population de faucons pélerins.

À titre de données, générons des nombres aléatoires. Cette stratégie permet de valider les statistiques en les comparant aux paramètre que l'on impose. Ici, nous imposons une moyenne de 600 grammes et un écart-type de 30 grammes. Générons une séries de données avec 20 échantillons.


In [1]:
import numpy as np
import pandas as pd
import matplotlib.pylab as plt
import seaborn as sns
import scipy.stats as stats
%matplotlib inline

In [2]:
np.random.seed(63123) # fait en sorte que les mêmes nombres aléatoires sont tirés à chaque fois que la cellule est exécutée
y20 = np.random.normal(loc=600, scale=30, size=20)
y200 = np.random.normal(loc=600, scale=30, size=200)

plt.figure()
plt.subplot(121)
sns.distplot(y20)
plt.title('20 écchantillons')
plt.subplot(122)
sns.distplot(y200)
plt.title('200 écchantillons')


Out[2]:
Text(0.5,1,'200 écchantillons')

Calculons les statistiques de manière classique.


In [3]:
def confidence_interval(x, on='devidation', distribution = 't', confidence=0.95):
    m = np.mean(x)
    se = np.std(x)
    n = len(x)
    if distribution == 't':
        error = se * stats.t.ppf((1+confidence)/2., n-1)
    elif distribution == 'normal':
        error = se * stats.norm.ppf((1+confidence)/2.)
    if on == 'error':
        error = error/np.sqrt(n)
    return((m-error, m, m+error))

In [4]:
print('Déviation, 95%:', np.round(confidence_interval(y20, on='deviation', confidence=0.95), 2))
print('Erreur, 95%:', np.round(confidence_interval(y20, on='error', confidence=0.95), 2))
print('Écart-type:', np.round(np.std(y20), 2))


Déviation, 95%: [ 547.06  608.33  669.6 ]
Erreur, 95%: [ 594.63  608.33  622.03]
Écart-type: 29.28

In [5]:
print('Déviation, 95%:', np.round(confidence_interval(y20, on='deviation', distribution = 'normal', confidence=0.95), 2))
print('Erreur, 95%:', np.round(confidence_interval(y20, on='error', distribution = 'normal', confidence=0.95), 2))
print('Écart-type:', np.round(np.std(y20), 2))


Déviation, 95%: [ 550.95  608.33  665.71]
Erreur, 95%: [ 595.5   608.33  621.16]
Écart-type: 29.28

En faisant cela, vous prenez pour acquis que les données sont distribuées normalement. En fait, nous savons qu'elles devraient l'être pour de grands échantillons, puisque nous avons nous-même généré les données. Par contre, comme observateur par exemple de la série de 20 données générées, la distribution est définitivement asymétrique. Sous cet angle, la moyenne, ainsi que l'écart-type, pourraient être des paramètres biaisés.

Par ailleurs, il est instructif de visualiser jusqu'à quel point un petit échantillon peut aboutir à des distributions différentes. La cellule suivante peut être exécutées plusieurs fois.


In [6]:
sns.distplot(np.random.normal(loc=600, scale=30, size=20))


Out[6]:
<matplotlib.axes._subplots.AxesSubplot at 0x7fd7297de470>

Nous pouvons justifier le choix d'une loi normale par des connaissances a priori des distributions de masse parmi des espèces d'oiseau. Ou bien transformer les données pour rendre leur distribution normale.

Statistiques d'une population

PyMC3

PyMC3 isole la classe Model, qui initie la définition du problème, dans un segment de code indenté sous un contexte with. À l'intérieur de ce segment indenté, on définit la connaissance a priori sous forme de variables aléatoires non-observées (ou libres) selon une distribution. Les fonctions de vraissemblance sont quant à elles définient sous forme de variables aléatoires observées.

Prenons l'exemple des faucons pélerins.


In [7]:
#!export MKL_THREADING_LAYER=GNU
import pymc3 as pm

Initions l'instance model de pm.Model() avec un a priori sur la masse moyenne. Disons que nous ne savons pas à quoi ressemble la moyenne du groupe a priori. Nous pouvons utiliser un a priori vague, où la masse moyenne peut prendre n'importe quelle valeur entre 0 et 2000 grammes, sans préférence: nous lui imposons donc un a priori selon une distribution uniforme.


In [8]:
np.random.seed(17109)
with pm.Model() as model:
    mass_m = pm.Uniform('mass_m', lower=0, upper=2000)
    mass_sd = pm.Uniform('mass_sd', lower=0, upper=100)
    mass_pop = pm.Normal('mass_pop', mu=mass_m, sd=mass_sd) # vérifier si ça concorde avec l'analyse fréquentielle
    obs = pm.Normal('obs', mu=mass_m, sd=mass_sd, observed=y20)
    trace = pm.sample(1000, njobs=4)


Auto-assigning NUTS sampler...
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (4 chains in 4 jobs)
NUTS: [mass_pop, mass_sd_interval__, mass_m_interval__]
100%|██████████| 1500/1500 [00:02<00:00, 510.35it/s]
INFO (theano.gof.compilelock): Waiting for existing lock by process '25768' (I am process '25769')
INFO (theano.gof.compilelock): To manually release the lock, delete /home/essicolo/.theano/compiledir_Linux-4.13--generic-x86_64-with-debian-stretch-sid-x86_64-3.6.4-64/lock_dir
INFO (theano.gof.compilelock): Waiting for existing lock by process '25768' (I am process '25770')
INFO (theano.gof.compilelock): To manually release the lock, delete /home/essicolo/.theano/compiledir_Linux-4.13--generic-x86_64-with-debian-stretch-sid-x86_64-3.6.4-64/lock_dir
INFO (theano.gof.compilelock): Waiting for existing lock by process '25769' (I am process '25770')
INFO (theano.gof.compilelock): To manually release the lock, delete /home/essicolo/.theano/compiledir_Linux-4.13--generic-x86_64-with-debian-stretch-sid-x86_64-3.6.4-64/lock_dir
The acceptance probability does not match the target. It is 0.894386103289, but should be close to 0.8. Try to increase the number of tuning steps.

In [9]:
pm.traceplot(trace)


Out[9]:
array([[<matplotlib.axes._subplots.AxesSubplot object at 0x7fd71037b710>,
        <matplotlib.axes._subplots.AxesSubplot object at 0x7fd71033e128>],
       [<matplotlib.axes._subplots.AxesSubplot object at 0x7fd710367780>,
        <matplotlib.axes._subplots.AxesSubplot object at 0x7fd7104efc88>],
       [<matplotlib.axes._subplots.AxesSubplot object at 0x7fd710690400>,
        <matplotlib.axes._subplots.AxesSubplot object at 0x7fd71087ec50>]], dtype=object)

In [10]:
pm.plot_posterior(trace[:500], varnames=['mass_m', 'mass_sd', 'mass_pop']) # burn in [:500]


Out[10]:
array([<matplotlib.axes._subplots.AxesSubplot object at 0x7fd714360a58>,
       <matplotlib.axes._subplots.AxesSubplot object at 0x7fd7142ae710>,
       <matplotlib.axes._subplots.AxesSubplot object at 0x7fd71435d8d0>], dtype=object)

Population: entre 536 et 675. Braquette plus large que celle de l'analyse fréquenntielle. (uninformative prior).


In [11]:
print('Déviation, 95%:', np.round(confidence_interval(y20, on='deviation', confidence=0.95), 2))
print('Erreur, 95%:', np.round(confidence_interval(y20, on='error', confidence=0.95), 2))
print('Écart-type:', np.round(np.std(y20), 2))


Déviation, 95%: [ 547.06  608.33  669.6 ]
Erreur, 95%: [ 594.63  608.33  622.03]
Écart-type: 29.28

Rappelons les résultats obtenus en mode fréquentiels.

Même avec de vagues a prioris et peu de données, nous obtenons des résultats conformes à l'analyse fréquentielle. Néanmoins, les résultats peuvent être interprétés de manière différente. En ce qui a trait à la moyenne:

  • Fréquentiel. Il y a une probabilité de 95% que mes données aient été générées à partir d'une moyenne se situant entre 595 et 622 grammes.

  • Bayésien. Étant donnée mes connaissances (vagues) de la moyenne et de l'écart-type avant de procéder à l'analyse (a priori), il y a une probabilité de 95% que la moyenne de la masse de la population se situe entre 594 et 623 grammes.


In [12]:
pm.traceplot(trace)


Out[12]:
array([[<matplotlib.axes._subplots.AxesSubplot object at 0x7fd7101eb4a8>,
        <matplotlib.axes._subplots.AxesSubplot object at 0x7fd710198cc0>],
       [<matplotlib.axes._subplots.AxesSubplot object at 0x7fd710150828>,
        <matplotlib.axes._subplots.AxesSubplot object at 0x7fd7101868d0>],
       [<matplotlib.axes._subplots.AxesSubplot object at 0x7fd71012a828>,
        <matplotlib.axes._subplots.AxesSubplot object at 0x7fd7100e03c8>]], dtype=object)

Nous avons maintenant une idée de la distribution de moyenne de la population. Mais, rarement, une analyse s'arrêtera à ce stade. Il arrive souvent que l'on doive comparer les pparamètres de deux, voire plusieurs groupes. Par exemple, comparer des populations vivants dans des écosystèmes différents, ou comparer un traitement à un placébo. Ou bien, comparer, dans une même population de faucons pélerins, l'envergure des ailes des mâles et celle des femelles.

Test de t: Différence entre des groupes

Pour comparer des groupes, on exprime généralement une hypothèse nulle, qui typiquement pose qu'il n'y a pas de différence entre les groupes. Puis, on choisit un test statistique pour déterminer si les distributions des données observées sont plausibles dans si l'hypothèse nulle est vraie.

En d'autres mots, le test statistique exprime la probabilité que l'on obtienne les données obtenues s'il n'y avait pas de différence entre les groupes.

Par exemple, si

  1. vous obtenez une p-value de moins de 0.05 après un test de comparaison et
  2. l'hypothèse nulle pose qu'il n'y a pas de différence entre les groupes,

cela signifie qu'il y a une probabilité de 5% que vous ayiez obtenu ces données s'il n'y avait en fait pas de différence entre les groupe. Il serait donc peu probable que vos données euent été générées comme telles s'il n'y avait en fait pas de différence.

Exemple des envergures des ailes depuis Kéry (2010).


In [13]:
n_f = 30
moy_f = 105
n_m = 20
moy_m = 77.5
sd_fm = 2.75

np.random.seed(213156)
envergure_f = np.random.normal(loc=moy_f, scale=sd_fm, size=n_f)
envergure_m = np.random.normal(loc=moy_m, scale=sd_fm, size=n_m)
sns.distplot(envergure_f, label='Femelle')
sns.distplot(envergure_m, label='Mâle')
plt.legend()


Out[13]:
<matplotlib.legend.Legend at 0x7fd710258438>

In [14]:
print(pd.DataFrame(envergure_f).describe())
print(pd.DataFrame(envergure_m).describe())


                0
count   30.000000
mean   105.116181
std      2.220252
min     99.187298
25%    103.632750
50%    105.545896
75%    106.586969
max    108.694495
               0
count  20.000000
mean   77.213089
std     3.021790
min    71.612650
25%    74.806544
50%    77.309943
75%    79.936177
max    81.444380

Évaluer s'il y a une différence significative peut se faire avec un test de t (ou de Student).


In [15]:
stats.ttest_ind(envergure_f, envergure_m)


Out[15]:
Ttest_indResult(statistic=37.645375691066626, pvalue=2.7192150379292623e-37)

La probabilité que les données ait été générées de la sorte si les deux groupes n'était semblables est très faible. On obtiendrait sensiblement les mêmes résultats avec une régression linéaire.


In [16]:
import statsmodels.api as sm
import statsmodels.formula.api as smf


/home/essicolo/bin/anaconda3/lib/python3.6/site-packages/statsmodels/compat/pandas.py:56: FutureWarning: The pandas.core.datetools module is deprecated and will be removed in a future version. Please use the pandas.tseries module instead.
  from pandas.core import datetools

In [17]:
envergure = pd.DataFrame({'is_female': np.concatenate((np.repeat(1, n_f), np.repeat(0, n_m))),
                        'envergure': np.concatenate((envergure_f, envergure_m))})
linmod_envergure = smf.ols('envergure ~ is_female', data=envergure).fit()
linmod_envergure.summary()


Out[17]:
OLS Regression Results
Dep. Variable: envergure R-squared: 0.967
Model: OLS Adj. R-squared: 0.967
Method: Least Squares F-statistic: 1417.
Date: Tue, 20 Feb 2018 Prob (F-statistic): 2.72e-37
Time: 21:06:54 Log-Likelihood: -117.08
No. Observations: 50 AIC: 238.2
Df Residuals: 48 BIC: 242.0
Df Model: 1
Covariance Type: nonrobust
coef std err t P>|t| [0.025 0.975]
Intercept 77.2131 0.574 134.485 0.000 76.059 78.367
is_female 27.9031 0.741 37.645 0.000 26.413 29.393
Omnibus: 1.874 Durbin-Watson: 2.106
Prob(Omnibus): 0.392 Jarque-Bera (JB): 1.811
Skew: -0.396 Prob(JB): 0.404
Kurtosis: 2.509 Cond. No. 2.92

Les valeurs de t sont identiques, mais le modèle linéaire est plus informatif. Il nous apprend que l'envergure des ailes des femelles est plus en moyenne plus élevée de 28.8 cm que celle des mâles, avec un intervalle de confiance d'un niveau de 95% entre 27.4 et 30.1.

En mode bayésien,

Utilisons l'information dérivée de statistiques classiques dans nos a priori. Oui-oui, on peut faire ça. Mais attention, un a priori trop précis ou trop collé sur nos données orientera le modèle vers une solution préalablement établie: ce qui e constituerait aucune avancée par rapport à l'a priori. Nous allons utiliser a priori pour les deux groupes la moyenne des deux groupes, et comme dispersion la moyenne le double de l'écart-type. Rappelons que cet écart-type est l'a priori de écart-type sur la moyenne, non pas de la population.

Procédons à l'instanciation d'un modèle pm.Model() dans l'objet model. Nous lui ajouterons des objets par bloc. D'abord, utilisons les tendances centrales des envergures des mâles et des femelles.


In [18]:
prior_moy = envergure['envergure'].mean()
prior_moy_sd = envergure['envergure'].std()
with pm.Model() as model:
    env_f_moy = pm.Normal('env_f_moy', prior_moy, sd=prior_moy_sd)
    env_m_moy = pm.Normal('env_m_moy', prior_moy, sd=prior_moy_sd)

Quant à l'écart-type de la population, un a priori vague et uniforme peut être utilisé, s'étalant uniformément entre 5 et 30 cm.


In [19]:
prior_sd_ll = 1
prior_sd_ul = 10
with model:
    env_f_sd = pm.Uniform('env_f_sd', lower=prior_sd_ll, upper=prior_sd_ul)
    env_m_sd = pm.Uniform('env_m_sd', lower=prior_sd_ll, upper=prior_sd_ul)

Il serait possible d'utiliser une distribution normale pour comparer les moyennes, ce qui serait l'équivalent de procéder à une ANOVA. Toutefois, comparons notre modèle bayésien au test de t avec une distribution de t (ou de Student). La distribution de t comprend aussi un paramètre $\nu$ qui fait converger vers la courbe normale un échantillon dont le nombre augmente. Kruschke (2012), cité dans un tutoriel de test de t de PyMC3, suggère une loi exponentielle comme a priori pour ce paramètre. Posons 20 comme espérance de la distribution exponentielle.


In [20]:
with model:
    nu_f = pm.Exponential('nu_f_minus_one', 1.0/(20-1)) + 1
    nu_m = pm.Exponential('nu_m_minus_one', 1.0/(20-1)) + 1

Dans PyMC3, la distribution de t est définie non pas selon un écart-type, mais selon une précision (lam), égale à l'inverse de la variance.

$$ precision = \frac{1}{ecart~type^2} = ecart~type^{-2} $$

In [21]:
with model:
    precision_f = env_f_sd**-2
    precision_m = env_m_sd**-2

    female = pm.StudentT('female', nu=nu_f, mu=env_f_moy, 
                         lam=precision_f, observed=envergure_f)
    male = pm.StudentT('male', nu=nu_m, mu=env_m_moy, 
                       lam=precision_m, observed=envergure_m)

In [22]:
with model:
    diff_of_means = pm.Deterministic('difference of means', env_f_moy - env_m_moy)
    diff_of_stds = pm.Deterministic('difference of stds', env_f_sd - env_m_sd)
    effect_size = pm.Deterministic('effect size',
                                   diff_of_means / np.sqrt((env_f_sd**2 + env_m_sd**2) / 2))

In [23]:
with model:
    trace = pm.sample(2000, init=None, njobs=4)


Multiprocess sampling (4 chains in 4 jobs)
NUTS: [nu_m_minus_one_log__, nu_f_minus_one_log__, env_m_sd_interval__, env_f_sd_interval__, env_m_moy, env_f_moy]
100%|██████████| 2500/2500 [00:06<00:00, 368.29it/s]

In [24]:
pm.traceplot(trace)


Out[24]:
array([[<matplotlib.axes._subplots.AxesSubplot object at 0x7fd6f75a4d30>,
        <matplotlib.axes._subplots.AxesSubplot object at 0x7fd6f754f278>],
       [<matplotlib.axes._subplots.AxesSubplot object at 0x7fd6f7502da0>,
        <matplotlib.axes._subplots.AxesSubplot object at 0x7fd6f753b908>],
       [<matplotlib.axes._subplots.AxesSubplot object at 0x7fd6f74f4668>,
        <matplotlib.axes._subplots.AxesSubplot object at 0x7fd6f749f9e8>],
       [<matplotlib.axes._subplots.AxesSubplot object at 0x7fd6f745a550>,
        <matplotlib.axes._subplots.AxesSubplot object at 0x7fd6f74170b8>],
       [<matplotlib.axes._subplots.AxesSubplot object at 0x7fd6f73c3be0>,
        <matplotlib.axes._subplots.AxesSubplot object at 0x7fd6f73deac8>],
       [<matplotlib.axes._subplots.AxesSubplot object at 0x7fd6f73a7d30>,
        <matplotlib.axes._subplots.AxesSubplot object at 0x7fd6f734c128>],
       [<matplotlib.axes._subplots.AxesSubplot object at 0x7fd6f7322320>,
        <matplotlib.axes._subplots.AxesSubplot object at 0x7fd6f72db358>],
       [<matplotlib.axes._subplots.AxesSubplot object at 0x7fd6f7281390>,
        <matplotlib.axes._subplots.AxesSubplot object at 0x7fd6f72b4eb8>],
       [<matplotlib.axes._subplots.AxesSubplot object at 0x7fd6f72719b0>,
        <matplotlib.axes._subplots.AxesSubplot object at 0x7fd6f722e518>]], dtype=object)

In [25]:
pm.plot_posterior(trace[500:],
                  varnames=['env_f_moy', 'env_m_moy', 'env_f_sd', 'env_m_sd', 'nu_f_minus_one', 'nu_m_minus_one']);



In [26]:
pm.plot_posterior(trace[1000:], varnames=['difference of means', 'difference of stds', 'effect size'], ref_val=0);



In [27]:
pm.forestplot(trace[500:], varnames=[v.name for v in model.vars])


Out[27]:
<matplotlib.gridspec.GridSpec at 0x7fd6ff044f28>

In [28]:
pm.summary(trace[500:], varnames=['difference of means', 'difference of stds', 'effect size'])


Out[28]:
mean sd mc_error hpd_2.5 hpd_97.5 n_eff Rhat
difference of means 27.895530 0.851842 0.011127 26.265156 29.558432 5007.0 0.999989
difference of stds -0.994741 0.714791 0.009068 -2.414934 0.385911 5345.0 0.999946
effect size 10.459874 1.455050 0.018316 7.617473 13.326267 5810.0 0.999687

In [ ]:

Régression linéaire

Une test de t, comme une ANOVA, est une régression linéaire effectuée sur des catégories. Voir chapitre 6 de Marc Kéry.

Prenons l'exemple l'évolution de la population de renette faux-grillon (trouver les données)


In [29]:
np.random.seed(56428)
intercept = 40
pente = -1.5
variance_residuelle = 25
annees = np.arange(1, 17, 1)
erreur = np.random.normal(loc=0, scale=np.sqrt(variance_residuelle), size=len(annees))
population = intercept + pente * annees + erreur
population_df = pd.DataFrame({'Année': annees,
                           'Population': population})
population_df.plot('Année', 'Population')


Out[29]:
<matplotlib.axes._subplots.AxesSubplot at 0x7fd7143d4828>

In [30]:
import statsmodels.api as sm
import statsmodels.formula.api as smf
linmod_pop = smf.ols('Population ~ Année', data=population_df).fit()
linmod_pop.summary()


/home/essicolo/bin/anaconda3/lib/python3.6/site-packages/scipy/stats/stats.py:1390: UserWarning: kurtosistest only valid for n>=20 ... continuing anyway, n=16
  "anyway, n=%i" % int(n))
Out[30]:
OLS Regression Results
Dep. Variable: Population R-squared: 0.762
Model: OLS Adj. R-squared: 0.744
Method: Least Squares F-statistic: 44.70
Date: Tue, 20 Feb 2018 Prob (F-statistic): 1.03e-05
Time: 21:07:42 Log-Likelihood: -46.945
No. Observations: 16 AIC: 97.89
Df Residuals: 14 BIC: 99.43
Df Model: 1
Covariance Type: nonrobust
coef std err t P>|t| [0.025 0.975]
Intercept 42.6061 2.551 16.703 0.000 37.135 48.077
Année -1.7637 0.264 -6.686 0.000 -2.329 -1.198
Omnibus: 1.158 Durbin-Watson: 2.108
Prob(Omnibus): 0.560 Jarque-Bera (JB): 0.855
Skew: 0.255 Prob(JB): 0.652
Kurtosis: 1.989 Cond. No. 20.5

In [31]:
population_df['Prédit'] = linmod_pop.predict()
population_df.plot('Année', ['Population', 'Prédit'])


/home/essicolo/bin/anaconda3/lib/python3.6/site-packages/pandas/plotting/_core.py:1716: UserWarning: Pandas doesn't allow columns to be created via a new attribute name - see https://pandas.pydata.org/pandas-docs/stable/indexing.html#attribute-access
  series.name = label
Out[31]:
<matplotlib.axes._subplots.AxesSubplot at 0x7fd6fefe3fd0>

In [32]:
np.random.seed(10573)
with pm.Model() as model:
    intercept = pm.Normal('intercept', 40, sd=5)
    pente = pm.Normal('pente', 0, sd=5)
    erreur_sd = pm.HalfCauchy('erreur_sd', beta=10, testval=1.0)
    pop = pm.Normal('population', mu=intercept + pente*annees,
                    sd=erreur_sd, observed=population)
    # pm.glm.GLM.from_formula('population ~ annees', population_df) # class GLM non trouvée
    trace = pm.sample(2000, init=None, njobs=4)


Multiprocess sampling (4 chains in 4 jobs)
NUTS: [erreur_sd_log__, pente, intercept]
100%|██████████| 2500/2500 [00:04<00:00, 585.23it/s]
The acceptance probability does not match the target. It is 0.892383339377, but should be close to 0.8. Try to increase the number of tuning steps.

In [33]:
pm.traceplot(trace);


Il n'y a pas de manière unique d'aborder ce genre de problème. On doit choisir la manière de représenter les données (brutes, transformées, catégorisation, etc.) les données à inclure (valeurs aberrantes, données manquantes, effets aléatoires, etc.), le modèle utilisé (linéaire, non-linéaire) ainsi que les critères de significativité (p-value <= 0.05).

Les études scientifiques basent souvent ces choix arbitrairement, par conformité avec une littérature établie (Johnson 1999) REF. Cette approche est à haut risque de mauvaise interprétation des résultats (Goodman 1999) REF.

https://pymc-devs.github.io/pymc3/notebooks/BEST.html


In [ ]:

Régression linéaire mixte

La module bambi permet de générer des modèles pymc3 avec une interface de formule plus familiaire.


In [ ]:


In [ ]:

Pour aller plus loin

Les livres de Mark Kéry, bien que rédigés pour les calculs en langage R et WinBUGS, offre une approche bien structurée et applicable directement.

Le site web de pymc3 contient la documentation pour traduire en Python les codes R/WinBUGS écrits par Mark Kéry. Le livre Probabilistic Programming & Bayesian Methods for Hackers de Cameron Davidson-Pilon (biostatisticien renommé), disponible gratuitement en ligne, offre une approche plus éducative: il s'agit d'une ressource incontournable pour les statistiques bayésiennes en Python.