paint-brush
Lidando com dados ausentes em séries temporais financeiras – receitas e armadilhaspor@vkirilin
12,436 leituras
12,436 leituras

Lidando com dados ausentes em séries temporais financeiras – receitas e armadilhas

por Vladimir Kirilin10m2024/04/03
Read on Terminal Reader

Muito longo; Para ler

Eu me concentro em métodos para lidar com dados ausentes em séries temporais financeiras. Usando alguns dados de exemplo, mostro que o LOCF geralmente é um método decente em comparação com a eliminação e a imputação, mas tem suas falhas - ou seja, pode criar saltos artificiais indesejáveis nos dados. No entanto, alternativas como a interpolação têm seus próprios problemas, especialmente no contexto de previsão/previsão ao vivo.
featured image - Lidando com dados ausentes em séries temporais financeiras – receitas e armadilhas
Vladimir Kirilin HackerNoon profile picture
0-item

Se você for como eu, você já lidou com dados ausentes em seus conjuntos de dados pelo menos uma vez. Ou duas vezes. Ou muitas vezes…


Às vezes, tudo o que é necessário para lidar com esses NAs incômodos é eliminá-los, ou seja, remover as linhas que contêm dados ausentes. No entanto, isto pode nem sempre ser o ideal, especialmente para dados de séries temporais, e ainda mais para dados financeiros. É claro que esse problema é bem estudado e existem muitas alternativas para abandoná-lo.


Analisarei alguns deles (listados abaixo) e discutirei os prós e os contras:


  1. Caindo

  2. LOCF (última observação transportada)

  3. Imputação média (ou semelhante)

  4. Interpolação


Alerta de spoiler: não existe uma abordagem única para todos! Argumentarei que o LOCF é normalmente uma boa escolha para financiamento, mas também tem as suas desvantagens. Com isso em mente, deixe-me descrever os métodos e dados que usarei para apresentá-los.


Nota: se quisermos ser pedantes, todos os métodos 2 a 4 são exemplos de alguma imputação.


Dados do modelo

Vamos começar com alguns exemplos de por que alguém se importaria em desistir. Para ilustrar, gerei alguns dados diários excessivamente simplificados sobre os preços das ações, assumindo que seguem um passeio aleatório sem desvios (ou seja, o preço médio a longo prazo deve permanecer constante) - não sendo a hipótese mais precisa, mas mesmo assim benigna.


 np.random.seed(10) # needed for reproducibility price_moves = 3*pd.Series(np.random.randn(100)) # generating random "steps" with 0 mean price_vec = 100 + price_moves.cumsum() # generating brownian motion price_vec.plot() 


série de preços inalterada


Bem, o enredo parece bastante bem-intencionado.


Suponha que agora queiramos descobrir a média empírica das diferenças diárias de preços -

 price_vec.diff().mean() #sample mean >0.20030544816842052

Obviamente diferente de zero, ao contrário da série geradora - mas isso é apenas ruído de amostra. Até agora tudo bem.


Descartando dados

Agora vamos deformar um pouco esses dados, excluindo alguns pontos de dados:

 price_vec_na_simple = price_vec.copy() price_vec_na_simple.iloc[90:95] = np.array([np.NaN, np.NaN, np.NaN, np.NaN, np.NaN]) # price_vec_na_simple.diff().mean() >0.1433356258183252


Notamos algumas coisas imediatamente -

  1. A média é de alguma forma não-NA, embora o vetor diff contenha claramente NAs

  2. A média é diferente daquela que obtivemos antes


Agora, o número 1 é bastante fácil - pd.mean remove automaticamente NA por padrão.

Mas e quanto ao número 2? Vamos repensar o que estamos computando.


É fácil mostrar que, pelo menos sem NAs, a diferença média de preços deveria ser simplesmente (price_vec[99]-price_vec[0])/99 - na verdade, quando somamos as diferenças de preços, todas as peças “intermediárias” se cancelam, assim (price_vec[1] - price_vec[0]) + (price_vec[2] - price_vec[1]) + .. !


Agora, com os dados ausentes inseridos, se primeiro considerarmos as diferenças e depois eliminarmos NA s, esse cancelamento será quebrado - algumas contas fáceis mostram que agora você está computando (price_vec[99] - price_vec[0] - price_vec[95] + price_vec[89])/93 .


Para mostrar isso, observe que os dois termos a seguir são agora omitidos - price_vec[95] - price_vec[94] e price_vec[90] - price_vec[89] , uma vez que (NA - any number) é avaliado como NA e é então descartado.


Vamos verificar isso:

 (price_vec[99] - price_vec[0])/99 >0.20030544816842052 (price_vec[99] - price_vec[0] - price_vec[95] + price_vec[89])/93 >0.1433356258183252


Agora fica mais claro como podemos consertar as coisas - precisamos primeiro abandonar os NAs e depois diff -

 price_vec_na_simple.dropna().diff().mean() >0.21095999328376203


A média está quase de volta onde deveria estar - ocorre uma pequena discrepância porque agora temos menos termos na média - 94 em vez de 99.


Ok, parece que se nos importamos apenas com a média, estamos bem apenas usando dropna (desde que façamos certo)? Afinal, a diferença entre 0.2 e 0.21 está claramente dentro da nossa tolerância ao ruído. Bem, não exatamente - vamos ver por quê.


LOCF

O que é LOCF?

LOCF significa Última observação realizada. A ideia por trás disso é super simples - se eu registrar dados em alguns intervalos de tempo, que podem ou não ser regulares, se alguma observação de intervalo específico estiver faltando, simplesmente assumimos que nada mudou em nossa variável e a substituímos pela última não- valor ausente (por exemplo - [3, 5, NA, 8] → [3, 5, 5, 8]). Alguém pode perguntar - em primeiro lugar, por que se preocupar com um intervalo com uma observação faltante, ou seja, não apenas excluí-lo como no método de “descarte”? Bom, a resposta está no defeito inerente da “queda” que não mencionei acima.


Suponha que você registre várias quantidades de uma vez, especialmente aquelas que geralmente não mudam muito rapidamente - como registros horários de temperatura e umidade. Suponha que você tenha ambos os valores para 10h, 11h e 12h, mas apenas umidade às 13h. Você simplesmente exclui essa “linha” - ou seja, finge que não tem leitura para 13h? Bem, tudo bem se você tiver apenas duas variáveis - mesmo que tenha removido algumas informações potencialmente valiosas (a umidade das 13h). Mas se você tiver muitas dessas ocorrências ou muitas variáveis ao mesmo tempo, a eliminação poderá deixá-lo praticamente sem dados!


Uma alternativa muito atraente é simplesmente assumir que nada mudou na temperatura entre 12h e 13h. Afinal, se alguém viesse até nós às 12h30 e nos perguntasse - “qual é a temperatura atual”, teríamos respondido corretamente com a leitura das 12h (se não conseguirmos obter uma nova leitura imediatamente, é claro ). Por que não usar a mesma lógica para o valor 13h?


Por que usar LOCF (em finanças)?

Primeiro, vamos testar nossa abordagem recém-descoberta com os dados anteriores:

 price_vec_na_simple.ffill().diff().mean() # ffill performs LOCF by default >0.20030544816842052


Parece que recuperamos nosso antigo valor com precisão! Além disso, se você quiser fazer mais pesquisas sobre os dados de diferença de preço - parece mais “ordenado” agora, pois tem uma entrada para cada dia, embora cinco dessas entradas sejam agora 0 (por quê? tente executar price_vec_na_simple.ffill().diff().iloc[90:95] para ver por si mesmo).


Além disso, em finanças, os dados em falta e os dados atípicos muitas vezes se juntam. Deixe-me ilustrar isso:

 #inflate two observations, delete three next ones price_moves_na[90] += 20 price_moves_na[91] += 30 price_moves_na[92] -= 50 # to "deflate" the price shock back price_vec_na = (100 + price_moves_na.cumsum()) price_vec_na[92:95] = [np.NaN, np.NaN, np.NaN] price_vec_na.tail(20).plot() price_vec_na.diff().dropna().mean() >0.7093365245831178 

pico + dados ausentes


Podemos ver que, após um forte aumento de preços, os dados simplesmente não ficam disponíveis por 3 dias consecutivos. Este não é um exemplo tão “artificial” como pode parecer! Imagine que a negociação foi interrompida após o pico, pelo menos nesta bolsa em particular. acontecendo nos bastidores que realmente “conectaram” os pontos entre o pico e o pós-pico, acalme-se. Mas você não sabe disso e não tem nenhum dado para isso!


Qual é a suposição mais natural se não tivermos novos dados? Bem, lembre-se que o nosso modelo de geração de dados foi fundamentalmente baseado em variações de preços. Então, se não houver novos dados, talvez o preço não esteja mudando? Isto é exatamente o que o LOCF (Last Observation Carried Forward) assume.

Um pouco de matemática para contexto

Uma observação lateral para um leitor curioso - talvez uma visão mais fundamental sobre por que o LOCF é particularmente adequado para dados de preços de ações é que ele geralmente é modelado como um martingale . Grosso modo, um martingale é algo em que nossa melhor estimativa para amanhã é o que vemos hoje, ou E[x_{t+1} | x_t] = x_t


Ok, de volta aos dados reais! Vamos examinar as consequências do LOCF tanto visual quanto numericamente:

 price_vec_na.ffill().tail(20).plot() price_vec_na.ffill().diff().mean() >0.20030544816842052 

Imputação de LOCF


Imediatamente, vemos os prós e os contras do LOCF (literalmente)! Por um lado, a média está de volta onde “esperamos” que esteja – ou seja, o valor empírico inalterado. No entanto, introduzimos um período bastante feio onde o preço está fora do “típico” e uma queda artificial no preço entre os dias 94 e 95.

E a imputação?

Vamos contrastar os resultados que obtemos do LOCF com a imputação (média). É uma escolha muito comum para manipulação de NA, especialmente para dados que não são de séries temporais. No entanto, se feito de forma ingénua, tem muitas desvantagens quando utilizado para dados financeiros.


  • Se você usar apenas a média de todas as amostras, introduzirá um óbvio viés de antecipação - ou seja, usará dados futuros para imputar valores passados.

  • Usar algum tipo de retrospectiva ou média móvel é certamente melhor - no entanto, às vezes pode entrar em conflito com a visão de “linha de base” do martingale que descrevemos antes.


Vamos examinar isso com mais detalhes. Usarei a imputação retrospectiva em nossos dados antigos -

 price_vec_na_impute = price_vec_na.copy() price_vec_na_impute[price_vec_na_impute.isna()] = price_vec_na.iloc[:90].mean() price_vec_na_impute.diff().mean() >0.20030544816842052 

imputação média

Recuperamos a média de variação de preço “correta”, igual ao LOCF. MAS introduzimos uma queda artificial de preços entre os dias 91 e 92 que, em alguns aspectos, é ainda pior do que a que tivemos antes. Afinal, aquele aconteceu quando ou depois que as coisas provavelmente se acalmaram, enquanto este apenas pressupõe que tudo voltará ao normal imediatamente. Além disso, na prática pode ser um pouco desafiador equilibrar a janela retrospectiva de modo que a) captemos tendências recentes, mas também b) captemos tendências de longo prazo (o habitual equilíbrio entre viés e variância).


Adicionando uma segunda variável

Suponhamos agora que queremos realizar uma tarefa mais complexa – extrair a correlação entre os movimentos de preços de dois ativos a partir dos dados empíricos, quando uma ou ambas as séries de preços têm dados em falta. Claro, ainda podemos usar drop, mas:


  • mesmo que possamos usá-lo, é ideal?

  • e se tivermos muitas variáveis - então eliminar todas as linhas com pelo menos um NA poderia nos deixar sem nenhum dado!


Há muitas razões pelas quais alguém pode querer calcular a correlação - é o primeiro passo da EDA em quase todos os modelos multivariáveis, é amplamente utilizado em qualquer tipo de construção de portfólio e assim por diante. Portanto, medir esse número com a maior precisão possível é absolutamente necessário!


Para ilustrar, vamos gerar uma segunda variável com uma correlação “embutida” de 0,4 com a primeira. Para fazer isso, usaremos uma espécie de Modelo de Mistura Gaussiana . A imagem que se pode ter em mente é a de duas ações correlacionadas que partilham um fator de risco importante, mas a segunda ação também tem exposição a um fator de risco importante que a primeira não tem. Pense no Google e no Facebook, por exemplo – o primeiro factor poderia ser o sentimento genérico sobre o sector tecnológico e o segundo poderia ser a concorrência com redes sociais rivais.


 np.random.seed(2) # needed to ensure a fixed second series price_moves_2 = pd.Series(np.random.randn(100)) price_vec_2 = 50+(0.4*price_moves/3 + np.sqrt(1-0.4**2)*price_moves_2).cumsum() # all this math to ensure we get a 0.4 "theoretical" correlation with the first one


Vamos verificar a correlação empírica “linha de base” - isto é, sem os NAs e saltos.

 pd.concat([price_vec, price_vec_2], axis = 1).diff().corr().iloc[0,1] >0.4866403018044526


Agora, isso está razoavelmente próximo da correlação “teórica” – é bem sabido que a medição empírica da correlação é propensa a ruídos bastante grandes.


NAs sem outliers

Na próxima etapa, examinaremos o caso com NAs, mas sem valores discrepantes. Também compararemos o que acontece se dropna antes e depois diff

 pd.concat([price_vec_na_simple, price_vec_2], axis = 1).diff().corr().iloc[0,1] # implicit dropna after diff >0.5022675176281746 pd.concat([price_vec_na_simple, price_vec_2], axis = 1).dropna().diff().corr().iloc[0,1] >0.5287405341268966


Ambos os resultados estão bastante próximos e não muito distantes do valor “empírico” que obtivemos antes. Vamos verificar se o LOCF e a imputação também funcionam bem:

 pd.concat([price_vec_na_simple, price_vec_2], axis = 1).ffill().diff().corr().iloc[0,1] >0.5049380499525835 price_vec_na_simple_impute = price_vec_na_simple.copy() price_vec_na_simple_impute[price_vec_na_simple_impute.isna()] = price_vec_na_simple_impute.iloc[:90].mean() pd.concat([price_vec_na_simple_impute, price_vec_2], axis = 1).ffill().diff().corr().iloc[0,1] >0.4866728183859715


Ao comparar os 4 resultados acima, vemos que todos os métodos funcionam muito bem. Talvez devêssemos esperar o mesmo para o caso atípico, então?


NAs com outliers

Lembre-se, para permanecermos consistentes, precisamos expor a segunda série de preços aos mesmos choques de preços que a primeira sofreu, mas sem os NAs seguintes. Voltando ao exemplo acima – imagine algum evento importante causando um aumento no primeiro fator de risco que eventualmente interrompe a negociação do primeiro ativo. O segundo ativo também experimentará isso, claro, mas talvez em menor grau e, portanto, não ocorreria nenhuma parada e, portanto, nenhum NA.


 price_vec_na_2 = 50+(0.4*price_moves_na/3 + np.sqrt(1-0.4**2)*price_moves_2).cumsum()


Vamos comparar novamente o desempenho de todos os nossos métodos -

 pd.concat([price_vec_na, price_vec_na_2], axis = 1).diff().corr().iloc[0,1] >0.6527112906179914 pd.concat([price_vec_na, price_vec_na_2], axis = 1).dropna().diff().corr().iloc[0,1] >0.7122391279139506


Isso é uma grande diferença tanto no valor teórico quanto no empírico! Que tal LOCF e imputação?

 pd.concat([price_vec_na, price_vec_na_2], axis = 1).ffill().diff().corr().iloc[0,1] >0.33178239830519984 pd.concat([price_vec_na_impute, price_vec_na_2], axis = 1).dropna().diff().corr().iloc[0,1] >0.7280990594963112


Agora finalmente vemos quanto vale o LOCF! Supera claramente todos os outros métodos!


Potenciais armadilhas + interpolação

Claro, isso não é 100% robusto. Por um lado, ao fazer LOCF, introduzimos uma grande queda de preço quando os dados faltantes terminam. Se coincidir com alguns valores discrepantes no segundo vetor de preços, os resultados poderão mudar bastante. (*Um exercício para o leitor - vire o sinal do movimento de preço de price_vec_na_2[95] e verifique como isso afeta os resultados). Não está muito claro se é “limpo” apenas introduzir esta queda de preço em vez de, por exemplo, interpolar entre o pico de preço price_vec_na[91] e o valor “normal” posterior price_vec_na[95] . Contudo, especialmente para uma utilização “ao vivo”, a interpolação não é realmente possível! Afinal, se hoje é o dia 93, como podemos interpolar usando um valor futuro registrado no final do dia 95? Para um estudo histórico - claro, isso continua sendo uma opção, mas ainda não está claro como interpretá-lo e usá-lo para previsões reais! Concluindo, a interpolação através da dimensão temporal é possível, mas um pouco mais questionável.


Conclusões

Tentei apresentar um pequeno estudo de caso para apresentar e defender por que o LOCF costuma ser a opção mais atraente e simples para lidar com dados ausentes em séries temporais financeiras.


Para recapitular, os prós são:

  • Atraente do ponto de vista martingale/”fluxo de informação”
  • Super fácil de implementar
  • Não há necessidade de otimizar (em oposição à imputação)
  • Lida com outliers pelo menos decentemente


Alguns contras:

  • Pode potencialmente causar grandes saltos no final do período ausente
  • Pode perder algumas dinâmicas articulares diferenciadas quando usado para diversas variáveis


Como quant em uma loja de comércio de prop, eu o uso em quase todos os meus estudos como uma base eficaz. Algumas situações exigem medidas mais sutis, é claro, mas essas são poucas e raras e geralmente também não são 100% “resolvidas” por nenhum dos outros três métodos mencionados.