Monte Carlo e cadeias de Markov (III)

A referência básica para esse post é Zellner (1970)

Nesse post nós vamos estimar uma regressão linear com duas variáveis, um regressando e um regressor.

y_i = \beta_0 + \beta_1 x_i + \varepsilon_i

O regressando y_i e o regressor x_i são números reais, \varepsilon_i são termos aleatórios. Os parâmetros de interesse são \beta_0 e \beta_1, que eu vou colocar em um vetor \mathbf{\beta} \equiv (\beta_0, \beta_1)^\prime. O modelo (a verossimilhança) é \varepsilon_i \sim \mathcal{N}(0, \sigma^2). No fim, temos um vetor de parâmetros, \theta \equiv (\mathbf{\beta}, \sigma^2)^\prime.

Mecanicamente, o processo funciona assim: nós começamos com uma distribuição a priori sobre os parâmetros, observamos os dados, e atualizamos essa distribuição (a posteriori). O objetivo é encontrar a “distribuição dos parâmetros”. Aqui eu acho que vale um adendo. Eu já vi muita gente falando que os parâmetros são uma variável aleatória. No meu ponto de vista, eles não são variáveis aleatórias mas sim são tratados como aleatórios porque a aleatoriedade apenas reflete nossa falta de conhecimento sobre eles.

Ou seja, nós iniciamos com uma descrição do nosso conhecimento acerca os parâmetros do modelo (ou a falta dele). Daí observamos os dados e atualizamos essa descrição. Seguindo o primeiro post dessa saga,

p(\theta | dados) \propto p(dados | \theta) p(\theta)

No fim do dia, vamos obter amostras aleatórias de p(\theta | dados), e por isso passamos os últimos dois posts fazendo esse tipo de coisa. Vamos gerar uma amostra de (y_i, x_i) usando

y_i = 2 + 0.5 x_i +\varepsilon_i com \varepsilon_i \sim^{i.i.d.} \mathcal{N}(0, 1) e x_i \sim^{i.i.d.} \mathcal{N}(0; 1,5)

Vou iniciar o código incluindo alguns pacotes e definindo a pdf da normal.

import numpy as np
import pandas as pd
import seaborn as sns
from scipy.stats import norm as gauss, uniform

def ll_normal(theta, x, y):
    n = x.shape[0]
    e = y - theta[0] - theta[1] * x
    variancia = theta[2]

    if variancia <= 0: # variância não pode ser negativa
        return -np.inf
        
    else:
        ll = -0.5 * n * np.log(2 * np.pi * variancia) - np.sum(e**2) / (2 * variancia)
        return ll

Uma amostra típica…

Para gerar essa amostra, eu uso o seguinte código:

# Parâmetros
N = 500

b0 = 2
b1 = 0.5
sigma2e = 1
sigma2x = 1.5

y = np.zeros((N,))
x = np.zeros((N,))

# Gerar amostra
x = gauss.rvs(size=N) * np.sqrt(sigma2x)
e = gauss.rvs(size=N) * np.sqrt(sigma2e)
y = b0 + b1 * x + e

df = pd.DataFrame({'y': y, 'x': x})
sns.scatterplot(df, x='x', y='y')

Para amostrar da distribuição a posteriori, vamos aplicar o algoritmo Metropolis-Hastings.

1. Iniciar a cadeia. Aqui, a cadeia começa no ponto (0,0,1).

B = 10000
theta = np.zeros((B,3))
theta[0,2] = 1

2. Gerar um candidato. Vou gerar usando uma distribuição normal (então o Metropolis-Hastings vai ser igual ao Metropolis).

d_theta = gauss.rvs(size=3) * 1/50
theta_cand = theta[b-1] + d_theta

3. Calcular a “distância”.

ll_old = ll_normal(theta[b-1], x, y)
ll_new = ll_normal(theta_cand, x, y)
alpha = np.exp(ll_new - ll_old)

4. Aceita/rejeita

u = uniform.rvs(size=1)
if u >= alpha: # rejeita
    theta[b,:] = theta[b-1]
else:
    theta[b,:] = theta_cand

5. Iterar. Na verdade, fica tudo dentro de um loop.

No fim, o código fica assim:

# MCMC - Metropolis-Hastings
B = 10000
theta = np.zeros((B,3))
theta[0,2] = 1

for b in range(1,B):
    # Candidato
    d_theta = gauss.rvs(size=3) * 1/50
    theta_cand = theta[b-1] + d_theta
    
    ll_old = ll_normal(theta[b-1], x, y)
    ll_new = ll_normal(theta_cand, x, y)

    alpha = np.exp(ll_new - ll_old)
    u = uniform.rvs(size=1)
    
    if u >= alpha: # rejeita
        theta[b,:] = theta[b-1]
    else:
        theta[b,:] = theta_cand

Finalmente, podemos plotar a cadeia. Aqui, vou plotar as últimas 1000 iterações.

Código
df_theta = pd.DataFrame(theta, columns=['b0', 'b1', 's2'])

# Create a figure with 3 subplots, arranged vertically
fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(12, 15), sharex=True)

# Plot each time series in its own subplot
df_theta['b0'][-1000:].plot(ax=ax1, ylabel='$\\beta_0$')
df_theta['b1'][-1000:].plot(ax=ax2, ylabel='$\\beta_1$')
df_theta['s2'][-1000:].plot(ax=ax3, ylabel='$\\sigma^2$')

ax1.axhline(b0, color='black', linestyle='-', label='$\\beta_0$')
ax1.axhline(np.mean(df_theta['b0'][-1000:]), color='red', linestyle='--', label='Média')

ax2.axhline(b1, color='black', linestyle='-', label='$\\beta_1$')
ax2.axhline(np.mean(df_theta['b1'][-1000:]), color='red', linestyle='--', label='Média')

ax3.axhline(sigma2e, color='black', linestyle='-', label='$\\sigma^2$')
ax3.axhline(np.mean(df_theta['s2'][-1000:]), color='red', linestyle='--', label='Média')


# Adjust the layout and add a main title
plt.tight_layout()

Vemos que todos os parâmetros estão perto dos verdadeiros valores — como era de se esperar.

No próximo post, vou comentar sobre a convergência do MCMC. Fiquem ligados!

Comentários

Deixe um comentário