In [1]:
import numpy as np
import numpy.random as npr
import matplotlib.pyplot as plt

import pandas as pd
from pandas import read_csv

from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
from statsmodels.tsa.stattools import acf, pacf, acovf  # acovf = auto-covariance
from statsmodels.tsa.arima_model import ARMA
from statsmodels.tsa.arima_process import ArmaProcess
from statsmodels.stats.diagnostic import acorr_ljungbox 

Exercice 1

Questions (1) & (2)

C'est un modèle AR(2) de paramètres $\phi_1=1.5$ et $\phi_2=-0.75$

In [2]:
ar_parms = [1,-1.5,0.75]
ma_parms = [1]

AR2 = ArmaProcess(ar=ar_parms,ma=ma_parms)
In [3]:
Xt = AR2.generate_sample(500)
plt.plot(Xt)
plt.show()
In [4]:
AR2.arroots # on voit que les racines du polynôme de la partie AR sont de module > 1. 
Out[4]:
array([1.-0.57735027j, 1.+0.57735027j])
In [5]:
AR2.isstationary
Out[5]:
True
In [6]:
AR2.isinvertible
Out[6]:
True
In [7]:
# ACF
plot_acf(Xt,lags=40)
plt.show()
In [8]:
# PACF             NB : Python prend pour convention PACF(0)=1
plot_pacf(Xt,lags = 40)
plt.show()

Question (4)

On s'intéresse maintenant au modèle ARMA(2,1) de paramètres $\phi_1=0.89$, $\phi_2=-0.5$ et $\theta_1=-0.23$.

In [11]:
ar_parms2 = [1,-0.89,0.5]   
ma_parms2 = [1,-0.23]

ARMA21 = ArmaProcess(ar=ar_parms2,ma=ma_parms2)
Yt = ARMA21.generate_sample(1000)
In [12]:
plt.plot(Yt)
plt.show()
In [13]:
plot_acf(Yt,lags=40)
plot_pacf(Yt,lags=40)
plt.show()
In [14]:
# Retrouvons les paramètres de ce modèle 

model = ARMA(Yt,order=(2,1)).fit(trend='nc')
model.summary()
Out[14]:
ARMA Model Results
Dep. Variable: y No. Observations: 1000
Model: ARMA(2, 1) Log Likelihood -1488.252
Method: css-mle S.D. of innovations 1.071
Date: Sun, 08 Mar 2020 AIC 2984.503
Time: 20:28:50 BIC 3004.134
Sample: 0 HQIC 2991.964
coef std err z P>|z| [0.025 0.975]
ar.L1.y 0.8111 0.073 11.045 0.000 0.667 0.955
ar.L2.y -0.4339 0.043 -10.201 0.000 -0.517 -0.351
ma.L1.y -0.1342 0.081 -1.653 0.099 -0.293 0.025
Roots
Real Imaginary Modulus Frequency
AR.1 0.9347 -1.1962j 1.5181 -0.1444
AR.2 0.9347 +1.1962j 1.5181 0.1444
MA.1 7.4495 +0.0000j 7.4495 0.0000
In [27]:
residus21 = model.resid
acorr_ljungbox(residus21,lags=range(100),boxpierce=True)[3] < 0.05
/Users/adrien/anaconda3/lib/python3.7/site-packages/ipykernel_launcher.py:2: RuntimeWarning: invalid value encountered in less
  
Out[27]:
array([False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False])

Les résidus passent le test de Box-Pierce à $\alpha=5\%$ pour des lags $h$ allant de $1$ à $60$ : pas d'évidence contre l'hypothèse que les résidus forment un bruit blanc.

On cherche maintenant à retrouver l'ordre de $(p,q)$ de l'ARMA en regardant quels les ordres qui ont un critère BIC et/ou AIC minimal pour, mettons, $1\leq p,q\leq 4$.

  • NB : Si on considère une classe de modèles paramétrés (comme ARMA(p,q)) de paramètres $\theta$ (ici $\phi_1,\ldots,\phi_p,\theta_1,\ldots,\theta_q$) et qu'on note $k$ le nombre de paramètres (ici $k=p+q$), on définit le critère d'information d'Akaike par :
$$ \text{AIC} := 2k-2\max_{\theta}\log\text{Vraisemblance}(\theta|\text{données}) $$

et le critère d'information Bayésien par

$$ \text{BIC} := \log(N)k-2\max_{\theta}\log\text{Vraisemblance}(\theta|\text{données}) $$

où $N$ est le nombre d'observations (données).

  • L'AIC pénalise donc le nombre de paramètres moins fortement que le BIC dès que $N\geq 8$.
In [16]:
l = 4

AIC = np.zeros((l,l))
BIC = np.zeros((l,l))

for p in range(l):
    for q in range(l):
        try:
            model = ARMA(Yt,order=(p,q)).fit(trend='nc')
            AIC[p,q] = model.aic
            BIC[p,q] = model.bic
        except: continue
            
# l'utilisation de "try"/"continue" permet de gérer les situations où ARMA.fit 
# n'arrive pas à trouver des paramètres adaptés pour un ARMA de paramètres p,q donnés
# (par exemple si le modèle n'est pas stationnaire)
In [22]:
AIC - 2900  # on soustrait 2900 pour lire plus facilement les résultats. 
Out[22]:
array([[-2900.        ,   158.28631302, -2900.        ,   114.73760306],
       [  235.29781549,   135.7058482 ,   118.5677158 ,    98.17800944],
       [   85.11155907,    84.50316398,    86.49893999,    88.42932156],
       [   84.5310524 ,    86.49941084,    88.37675469,    90.36817249]])

On trouve que le modèle ARMA(2,1) a un AIC minimal, suivit par un AR(3) et un AR(2)

In [24]:
BIC - 2900
Out[24]:
array([[-2900.        ,   168.10182358, -2900.        ,   134.36862418],
       [  245.11332605,   150.42911404,   138.19873692,   122.71678583],
       [   99.8348249 ,   104.13418509,   111.03771639,   117.87585324],
       [  104.16207351,   111.03818723,   117.82328636,   124.72245944]])

Ici le critère BIC est minimal pour AR(2), puis ARMA(2,1) et AR(3)

On retrouve donc le vrai modèle ARMA(2,1) dans le top 3 de ces deux critères. Il est aussi tentant de tester un AR(2) et AR(3) :

In [26]:
model_AR2 = ARMA(Yt,order=(2,0)).fit(trend='nc')
residus2 = model_AR2.resid
acorr_ljungbox(residus2,lags=range(100),boxpierce=True)[3] < 0.05
/Users/adrien/anaconda3/lib/python3.7/site-packages/ipykernel_launcher.py:3: RuntimeWarning: invalid value encountered in less
  This is separate from the ipykernel package so we can avoid doing imports until
Out[26]:
array([False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False])
In [29]:
model_AR3 = ARMA(Yt,order=(3,0)).fit(trend='nc')
residus3 = model_AR3.resid
acorr_ljungbox(residus3,lags=range(100),boxpierce=True)[3] < 0.05
/Users/adrien/anaconda3/lib/python3.7/site-packages/ipykernel_launcher.py:3: RuntimeWarning: invalid value encountered in less
  This is separate from the ipykernel package so we can avoid doing imports until
Out[29]:
array([False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False])

On voit alors que modéliser notre réalisation d'un ARMA(2,1) par un AR(2) ou un AR(3) est tout aussi raisonnable sur cet exemple : en général il n'y a de "bonne réponse" unique en modélisation.

EXERCICE 2

In [30]:
Oil_price = read_csv('oil.csv') 
type(Oil_price)
Out[30]:
pandas.core.frame.DataFrame
In [31]:
Oil_price.head()
Out[31]:
x
0 26.20
1 26.07
2 26.34
3 24.95
4 26.27
In [32]:
Oil_price.plot()
plt.show()
In [33]:
log_rend_Oil = np.log(Oil_price).diff()
log_rend_Oil.head()
Out[33]:
x
0 NaN
1 -0.004974
2 0.010303
3 -0.054215
4 0.051554
In [34]:
log_rend_Oil = log_rend_Oil[1:545]  # on supprime le Nan
In [35]:
log_rend_Oil.plot()
plt.show()
In [36]:
plot_acf(log_rend_Oil,lags=10)
plot_pacf(log_rend_Oil,lags=10)
plt.show()

L'ACF et la PACF ne semblent pas indiquer quelconque préférence vers un modèle AR ou MA. On essaie quand même une modélisation avec un AR pour voir ce qu'il se passe.

Modélisation AR

In [37]:
l = 5

# MODELISATION AR

AIC_AR = np.zeros(l)
BIC_AR = np.zeros(l)

for p in range(l):
    model = ARMA(log_rend_Oil,order=(p+1,0)).fit(trend='nc')
    AIC_AR[p] = model.aic
    BIC_AR[p] = model.bic
In [38]:
AIC_AR + 1800, BIC_AR + 1800
Out[38]:
(array([11.24818551,  9.37961379, -3.00431369, -2.90753184, -2.58152729]),
 array([19.846084  , 22.27646153, 14.1914833 , 18.5872144 , 23.21216819]))

A la vue des deux criètes, le modèle AR(3) semble le plus adapté.

In [39]:
model_AR3 = ARMA(log_rend_Oil,order=(3,0)).fit(trend='nc')
model_AR3.summary()
Out[39]:
ARMA Model Results
Dep. Variable: x No. Observations: 544
Model: ARMA(3, 0) Log Likelihood 905.502
Method: css-mle S.D. of innovations 0.046
Date: Sun, 08 Mar 2020 AIC -1803.004
Time: 20:38:56 BIC -1785.809
Sample: 0 HQIC -1796.281
coef std err z P>|z| [0.025 0.975]
ar.L1.x 0.1582 0.042 3.734 0.000 0.075 0.241
ar.L2.x -0.1069 0.043 -2.505 0.013 -0.190 -0.023
ar.L3.x 0.1621 0.042 3.819 0.000 0.079 0.245
Roots
Real Imaginary Modulus Frequency
AR.1 -0.6114 -1.7040j 1.8104 -0.3048
AR.2 -0.6114 +1.7040j 1.8104 0.3048
AR.3 1.8820 -0.0000j 1.8820 -0.0000

On obtient le modèle : $$ X_t = 0.1582 X_{t-1}- 0.1069 X_{t-2} +0.1621 X_{t-3}+\epsilon_t. $$ Celà conduit à la prédiction (en négligeant le bruit) :

In [40]:
X_pred = 0.1582*log_rend_Oil.x[544] - 0.1069*log_rend_Oil.x[543] + 0.1621*log_rend_Oil.x[542]

X_pred
Out[40]:
-0.01342752588312648

Mais peut-on négliger le bruit ? L'erreur est de l'ordre de grandeur de sa moyenne plus ou moins sa déviation standard. Il faudrait aussi montrer que $\epsilon_t$ forme un bruit blanc (décorélé du temps) : on fera un test de blancheur par la suite.

In [41]:
# résidus de la modélisation
Err_AR3 = model_AR3.resid

# moyenne et déviation standard
np.mean(Err_AR3), np.std(Err_AR3)
Out[41]:
(0.001350024855940628, 0.04577745840292325)

On peut aussi essayer de prédire une valeur passée, pour vérifier. Par exemple la dernières valeur des log-rendements est log_rend_Oil.x[544] que l'on va prédire par

In [42]:
X544_pred = 0.1582*log_rend_Oil.x[543] - 0.1069*log_rend_Oil.x[542] + 0.1621*log_rend_Oil.x[541]

X544_pred, log_rend_Oil.x[544] # pas terrible vu les ordres de grandeur...
Out[42]:
(-0.0029902648714112577, -0.08108797681428737)
In [43]:
# Etude graphique des résidus 

Err_AR3.plot()
plot_acf(Err_AR3)
plot_pacf(Err_AR3)
plt.show()
In [44]:
# Test de blancheur ?

acorr_ljungbox(Err_AR3,lags=range(60),boxpierce=True)[3] < 0.05

# L'hypothèse que les résidus forment un bruit blanc est refusée (à 5%) ! 
/Users/adrien/anaconda3/lib/python3.7/site-packages/ipykernel_launcher.py:3: RuntimeWarning: invalid value encountered in less
  This is separate from the ipykernel package so we can avoid doing imports until
Out[44]:
array([False, False, False, False, False, False, False, False, False,
        True, False, False, False, False, False, False, False, False,
       False, False, False, False,  True,  True,  True,  True,  True,
        True,  True,  True,  True,  True,  True,  True,  True,  True,
        True,  True,  True,  True,  True,  True,  True,  True, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False])

La modélisation AR permet une prédiction interessante comme indiqué ci-dessus car on sait que le prédicteur ci-dessus est également le prédicteur linéaire optimal. Malheureusement l'étude des résidus de la modélisation ne permet pas de supposer que l'erreur de modélisation forme un bruit blanc : la modélisation AR et la prédiction associée ne sont pas adaptées.

Modélisation ARMA

In [45]:
# le cas MA

l = 5

AIC_MA = np.zeros(l)
BIC_MA = np.zeros(l)

for q in range(l):
    model = ARMA(log_rend_Oil,order=(0,q+1)).fit(trend='nc')
    AIC_MA[q] = model.aic
    BIC_MA[q] = model.bic
    
# le cas ARMA non AR ni MA

AIC_ARMA = np.zeros((l,l))
BIC_ARMA = np.zeros((l,l))

for p in range(l):
    for q in range(l):
        try:
            model = ARMA(log_rend_Oil,order=(p+1,q+1)).fit(trend='nc')
            AIC_ARMA[p,q] = model.aic
            BIC_ARMA[p,q] = model.bic
        except: continue
In [46]:
AIC_AR + 1900, AIC_MA + 1900 , AIC_ARMA + 1900
Out[46]:
(array([111.24818551, 109.37961379,  96.99568631,  97.09246816,
         97.41847271]),
 array([108.81220331, 102.75745403,  93.17275657,  95.07388743,
         96.66172279]),
 array([[  96.84986085,   98.2763769 ,   95.09808657,   96.95751351,
         1900.        ],
        [  98.04392454,   99.61765882,   96.81434852,   98.95740488,
         1900.        ],
        [  95.02509301,   96.94858438,   94.19349887,   96.17108352,
         1900.        ],
        [  97.00043199,   98.42119001,   96.76890094,   91.06506652,
         1900.        ],
        [  98.23315683,   95.86458482,   91.29399435,   93.01721937,
         1900.        ]]))

Les modèles minimisant AIC sont donc (classés par AIC croissant) : ARMA(4,4), ARMA(5,3), MA(3), ARMA(3,3), ...

NB : On pourrait automatiser cette recherche de valeur minimale (par exemple en utilisant la fonction argmin de numpy)

In [48]:
BIC_AR + 1800, BIC_MA + 1800, BIC_ARMA + 1800 
Out[48]:
(array([19.846084  , 22.27646153, 14.1914833 , 18.5872144 , 23.21216819]),
 array([17.41010181, 15.65430177, 10.36855356, 16.56863367, 22.45541827]),
 array([[   9.74670859,   15.47217389,   16.5928328 ,   22.75120899,
         1800.        ],
        [  15.23972153,   21.11240505,   22.60804401,   29.0500496 ,
         1800.        ],
        [  16.51983924,   22.74227986,   24.28614359,   30.56267749,
         1800.        ],
        [  22.79412747,   28.51383474,   31.16049491,   29.75560974,
         1800.        ],
        [  28.32580156,   30.25617879,   29.98453758,   36.00671184,
         1800.        ]]))

Les modèles minimisant BIC sont donc (classés par BIC croissant) : ARMA(1,1), MA(3), AR(3), ...

En testant tous ces quelques modèles, on trouve que le seul qui passe le test de blancheur pour les résidus est ARMA(4,4).

In [49]:
model_ARMA44 = ARMA(log_rend_Oil,order=(4,4)).fit(trend='nc')
model_ARMA44.summary()
Out[49]:
ARMA Model Results
Dep. Variable: x No. Observations: 544
Model: ARMA(4, 4) Log Likelihood 913.467
Method: css-mle S.D. of innovations 0.045
Date: Sun, 08 Mar 2020 AIC -1808.935
Time: 20:40:08 BIC -1770.244
Sample: 0 HQIC -1793.808
coef std err z P>|z| [0.025 0.975]
ar.L1.x -0.3425 0.116 -2.951 0.003 -0.570 -0.115
ar.L2.x -0.0013 0.083 -0.015 0.988 -0.164 0.161
ar.L3.x -0.6784 0.076 -8.871 0.000 -0.828 -0.528
ar.L4.x -0.5969 0.094 -6.334 0.000 -0.782 -0.412
ma.L1.x 0.5065 0.110 4.591 0.000 0.290 0.723
ma.L2.x -0.0486 0.056 -0.860 0.390 -0.159 0.062
ma.L3.x 0.7906 0.049 15.977 0.000 0.694 0.888
ma.L4.x 0.7439 0.091 8.142 0.000 0.565 0.923
Roots
Real Imaginary Modulus Frequency
AR.1 0.5768 -0.8484j 1.0259 -0.1550
AR.2 0.5768 +0.8484j 1.0259 0.1550
AR.3 -1.1451 -0.5297j 1.2617 -0.4310
AR.4 -1.1451 +0.5297j 1.2617 0.4310
MA.1 0.5547 -0.8321j 1.0000 -0.1564
MA.2 0.5547 +0.8321j 1.0000 0.1564
MA.3 -1.0861 -0.4059j 1.1594 -0.4431
MA.4 -1.0861 +0.4059j 1.1594 0.4431
In [50]:
# Test de blancheur :

Err_ARMA44 = model_ARMA44.resid
acorr_ljungbox(Err_ARMA44,lags=range(100),boxpierce=True)[3] < 0.05

# L'hypothèse que les résidus forment un bruit blanc est n'est pas réfusée  (à 5%)
/Users/adrien/anaconda3/lib/python3.7/site-packages/ipykernel_launcher.py:4: RuntimeWarning: invalid value encountered in less
  after removing the cwd from sys.path.
Out[50]:
array([False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False])

Le modèle ARMA(4,4) est donc une modélisation raisonnable des log-rendements. Par contre, pour la prédiction, il n'est pas clair comment procéder avec ce qui a été vu en cours pour l'instant...

Exercice 3

On implémente l'algorithme de Durbin-Levinson comme vu dans le cours :

$$ \varphi_{11}= \frac{\gamma(1)}{\gamma(0)},\qquad \sigma_1^2=\gamma(0)-\frac{\gamma(1)^2}{\gamma(0)}=\gamma(0)(1-\varphi_{11}^2) $$

puis, pour tout $m\geq 1$ et si on pose $$ \kappa_m:= \frac{\gamma(m+1)-\sum_{k=1}^m\varphi_{k,m}\gamma(m-k+1)}{\sigma_m^2}, $$ alors on a la relation de récurrence $$ \begin{pmatrix}\varphi_{1,m+1}\\\vdots\\\varphi_{m,m+1}\end{pmatrix}=\begin{pmatrix}\varphi_{1,m}\\\vdots\\\varphi_{m,m}\end{pmatrix} -\kappa_m \begin{pmatrix}\varphi_{m,m}\\\vdots\\\varphi_{1,m}\end{pmatrix},\qquad \varphi_{m+1,m+1}=\kappa_m $$ ainsi que $$ \sigma_{m+1}^2=\sigma_m^2(1-\kappa_m^2) $$

In [51]:
def Durbin_Levinson(serie,M):
    # retourne phi = [[phi_11],[phi_12,phi_22],...,[phi_1M,...,phi_MM]] 
    # et sigma2 = [sigma_1^2,...,sigma_M^2]
    
    # initialisation de l'algorithme
    gamma = acovf(serie,fft=False)
    phi = []  
    sigma2 = []
    
    for m in range(M): 
        
        if m==0: 
            phi11 = gamma[1]/gamma[0]
            phi.append( [phi11] )
            sigma2.append( gamma[0]*(1-phi11**2) )
        
        else:
            phi_m = phi[m-1] #  [phi_1m,...,phi_mm]
            gamma_m_inv = np.flip(gamma[1:m+1]) # [gamma(m),...,gamma(1)]
        
            kappa = (gamma[m+1] - phi_m @ gamma_m_inv)/sigma2[m-1]**2

            phi_mplus1 = list(phi_m - kappa * np.flip(phi_m))
            phi_mplus1.append(kappa)
            phi.append(phi_mplus1)
        
            sigma2.append(sigma2[m-1]*(1-kappa**2))
        
    return(phi,sigma2)
    

Vérifions l'algorithme sur un modèle AR(p),

$$ X_t = \varphi_1 X_{t-1}+\dots+\varphi_pX_{t-p}+\epsilon_t, $$

où l'on sait que pour tout $m\geq p$ on a : $$ \varphi_{k,m}=\begin{cases} \varphi_k & \text{ si } k\leq p\\ 0 & \text{ sinon } \end{cases} $$

In [52]:
# Modèle AR(2)
ar_parms_testDL = [1,-0.9,0.4]   
ma_parms_testDL = [1]

testDL = ArmaProcess(ar=ar_parms_testDL,ma=ma_parms_testDL).generate_sample(2000)
In [53]:
Durbin_Levinson(testDL,4)[0][1]
Out[53]:
[0.8538076870648852, -0.36022564271574153]
In [54]:
Durbin_Levinson(testDL,4)[0][2]
Out[54]:
[0.8176624090303221, -0.2745540175522869, -0.10034065804439633]
In [55]:
Durbin_Levinson(testDL,4)[0][3]  
Out[55]:
[0.8223056203596746,
 -0.26184917437442,
 -0.1381775572155722,
 0.04627447556999375]