ML em Ações

Author

Prof. Dinilson Pedroza Jr.

Quarto

Quarto enables you to weave together content and executable code into a finished document. To learn more about Quarto see https://quarto.org.

Machine Learning em ações

Exercício prático e introdutório.

Como sempre, começamos acionando os pacotes necessários.

library(tidyverse)
library(quantmod)
library(caret)
library(randomForest)
library(lubridate)
library(rpart)
library(rpart.plot)

A biblioteca caret é usada para se desenvolver máquinas de aprendizagem em ciência de dados (sigla para Classification And Regression Training). A biblioteca randomForest é usada na classificação dos dados (escolher os “galhos” que melhor separam e classificam os dados) e previsão.

Coletando ações

getSymbols("PETR4.SA", src = "yahoo", from = "2020-01-01")
[1] "PETR4.SA"
petr4 <- PETR4.SA

Vamos criar um tibble para melhor adequar nossa base de dados aos processos de previsão que faremos. Como já visto antes, o tibble é um data frame com mais recursos que facilitam o trabalho em ciência de dados.

Mas antes, vamos criar uma variável com os retornos diários do preço da ação ajustado. Esse preço ajustado significa que são levados em conta fatores como a distribuição de dividendos e a reformulação eventual da empresa. Para criar esse vetor, recorremos ao formato xts, o qual precisa das datas em formato de datas para realizar a conta. Atenção para o uso da função dailyReturn do pacote Quantmod. O objeto criado é, exatamente, um xts, pois aquela função precisa de datas como explícitas em objetos de séries temporais.

ret <- dailyReturn(petr4$PETR4.SA.Adjusted)
class(ret)
[1] "xts" "zoo"

Tanto o zoo (Zealous Ordered Observations) quanto o xts (Extensible Time Series) são formatos bem adequados para se tratar séries temporais de dados financeiros.

Criando o tibble.

petr4_tibble <- petr4 %>% 
  as_tibble() %>% # Criando o tibble.
  mutate( # Gerando novas colunas.
    data = index(petr4), # Coluna de datas do tibble corresponde ao index do data frame origianl.
    retorno = as.numeric(ret), # Trasnformando o objeto xts em numérico.
    retorno_lag = lag(retorno, 1), # Subtrai o valor de hoje pelo de ontem.
    variacao = PETR4.SA.Close - PETR4.SA.Open # Variação no preço da ação no dia.
  ) %>% 
  drop_na()

Separando a base de dados em treino e teste

Vamos separar os dados em duas categorias. Uma será usada para treinar ou deduzir o modelo e outra para testá-lo. Na operação, usaremos a função createDataPartition, do pacote caret. É uma função poderosa que não só divide os dados em dois blocos, mas mantém em cada um deles a proporção original dos dados, por exemplo, se nos dados originais temos 40% de valores positivos e 60% de valores negativos, os dois blocos criados vão manter essas proporções. Ou seja, a função createDataPartition filtra os dados na montagem dos blocos de treino e teste. Os dados que vão compor cada bloco serão selecionados aleatoriamente, ou seja, os dados não serão escolhidos simplesmente dividindo a série no tempo. Mas essa aleatoriedade será seletiva - ou estratificada - por “tipo” de número: muito pequenos, muito grandes etc. Note que no código abaixo vamos usar a função set.seed para permitir a reprodutabilidade de nosso estudo.

set.seed(123)

train_idx <- createDataPartition(petr4_tibble$retorno, p = 0.8, list = FALSE)

train <- petr4_tibble[train_idx, ]
test  <- petr4_tibble[-train_idx, ]

O código acima é o mais importante do modelo. Por isso, vamos explicá-lo em detalhes. Estamos escolhendo a variável a ser prevista, no caso, “retorno” e dividindo esses dados em dois blocos, como dito acima. Na segunda linha do chunk, estamos definindo, na verdade quais linhas dos dados farão parte de cada um dos blocos. A instrução list=FALSE pede que o train_idx não venha como uma lista, mas como números representando cada linha de nosso conjunto de dados. Queremos um vetor de valores, cada número representando uma linha dos dados originais.

Na quarta linha do chunk estamos criando o bloco de dados de treino (que, sabemos será composto por 80% dos dados originais). Note o argumento [train_idx, ]: duas coluns de dados: os índices de cada linha dos dados originais e seus respectivos valores (os retornos).

Na quinta linha do chunk estamos montando o bloco de testes com os itens que não foram selecionados para treino: por isso o sinal negativo à frente do train_idx.

Previsões com o modelo de regressão linear

Vamos imaginar que o valor do retorno hoje é dependente, basicamente, do valor do retorno de ontem. Para estimar o modelo de regressão linear vamos usar a função lm, do pacote stats que já vem no R-base.

modelo_lm <- lm(retorno ~ retorno_lag, data = train)
summary(modelo_lm)

Call:
lm(formula = retorno ~ retorno_lag, data = train)

Residuals:
      Min        1Q    Median        3Q       Max 
-0.218163 -0.011683 -0.000817  0.012066  0.192220 

Coefficients:
              Estimate Std. Error t value Pr(>|t|)    
(Intercept)  0.0020197  0.0007278   2.775  0.00561 ** 
retorno_lag -0.1364670  0.0271036  -5.035 5.53e-07 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 0.02492 on 1172 degrees of freedom
Multiple R-squared:  0.02117,   Adjusted R-squared:  0.02034 
F-statistic: 25.35 on 1 and 1172 DF,  p-value: 5.528e-07

Análise dos resultados do modelo

Vamos usar três critérios na avaliação do modelo. A significância estatística dos parâmetros estimados; o seu grau de ajuste (\(R^2\)) e a significância do modelo como um todos (usando a estatística F).

\(intercepto = 0,0020197\) : o intercepto da conta da variação no retorno que não é influenciada pela variação do retorno defasado.

O intercepto é estatisticamente significante. Isso porque foi aplicado a ele o seguinte teste de hipóteses:

  • \(H_0\) : o valor do intercepto é igual a zero.

  • \(H_1\) : o valor do intercepto é diferente de zero.

\(p-value = 0,00561 < 0,05\) : rejeita-se \(H_0\), portanto devemos considerar que o intercepto é diferente de zero. Os detalhes deste teste podem ser vistos em outra oportunidade.

\(retornolag = -0.1364669\) : mostra como o retorno responde a mudanças no retorno_lag. O sinal negativo faz jus à tese do Retorno à media, tão cara a Kahneman.

\(p-value = 0,0005528 < 0,05\) : coeficiente estatisticamente significante.

\(R^2 = 0.02034\) ou 2%: o modelo só explica 2% das variações no retorno. Ou seja, os valores passados explicam pouco o valor presente. O que não surpreende, considerando a aleatoriedade nos preços das ações.

\(F = 25,35\) e \(p-value = 0,000528 < 0,05\) : o modelo como um todo é estatisticamente significante.

Conclusão: o modelo não é muito útil para fazer previsões.

Realizando previsões com o modelo de regressão linear

pred_lm <- predict(modelo_lm, newdata = test)
df_prev <- data.frame(
  data = test$data,        # coluna de datas
  real = test$retorno,     # valores observados
  previsto = pred_lm       # valores previstos
)

Gráfico.

ggplot(df_prev, aes(x = data)) +
  geom_line(aes(y = real), color = "blue", size = 1) +
  geom_line(aes(y = previsto), color = "red", linetype = 1, size = 1) +
  labs(title = "Retorno Real vs Previsto (Teste)",
       y = "Retorno", x = "Data",
       color = "") +
  theme_minimal()
Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
ℹ Please use `linewidth` instead.

Previsões com a árvore de decisões

É um procedimento para realizar previsões considerando a estrutura dos dados existente. Em particular, avalia o padrão de variação dos dados para classificá-lo conforme características em comum.

modelo_arvore <- rpart(
  retorno ~ retorno_lag,
  data = train,
  method = "anova"
)
rpart.plot(modelo_arvore)

Interpretando o gráfico

Queremos prever o retorno de hoje com base no retorno de ontem. Em cada bola temos duas informações:

  • em cima: o retorno médio previsto;

  • embaixo: a parcela dos dados de treino que pertencem a esse grupo.

A primeira bola (nó raiz) informa o seguinte: o retorno médio é de 0,19%, considerando 100% dos dados de treino.

Os dados são então divididos em dois grupos:

1.Lado yes: retorno_lag >= -0.092

Nesse grupo, o retorno médio previsto é de 0,15%, usando 99% dos dados. Isso significa que, em 99% dos dias, o retorno do dia anterior não caiu mais do que –9,2% (por exemplo: –8%, –5%, 0%, +1%, +3% etc). Quando isso ocorre, o retorno esperado para o dia seguinte é 0,15%.

2.Lado no: retorno_lag < -0.092

Aqui o retorno esperado é de 6,4%, usando 1% dos dados de treino. Ou seja, em apenas 1% dos dias, a queda do dia anterior foi maior que –9,2%. Quando ocorre uma queda tão forte, o retorno médio no dia seguinte tende a ser uma alta de 6,4%.

Nova divisão no ramo yes:

a) Lado esquerdo: retorno_lag < -0.062

Nesse caso, o retorno médio é de –3,1%, usando apenas 1% dos dados. Isso significa que, em aproximadamente 1% dos dias, o retorno anterior ficou entre –9,2% e –6,2%. Quando isso ocorre, o retorno do dia seguinte costuma ser uma queda de 3,1%.

b) Lado direito: retorno_lag ≥ -0.062

Aqui o retorno esperado é de 0,17%, usando 99% dos dados. Ou seja, em 99% dos dias, o retorno do dia anterior é maior ou igual a –6,2%. Quando isso acontece, o retorno médio do dia seguinte tende a ser 0,17%.

O método da árvore tenta encontrar pontos onde é válido separar separar os dados. Ele faz isso testando vários possíveis cortes no conjunto de dados e verificando qual desses cortes ou separações, deixa os grupos com estatísticas mais parecidas entre si. Quanto mais homogêneos esses grupos ficam, melhor a divisão. O processo continua até que não haja ganho significativo ao dividir mais.