paint-brush
Manejo de datos faltantes en series de tiempo financieras: recetas y trampaspor@vkirilin
12,407 lecturas
12,407 lecturas

Manejo de datos faltantes en series de tiempo financieras: recetas y trampas

por Vladimir Kirilin10m2024/04/03
Read on Terminal Reader

Demasiado Largo; Para Leer

Me centro en métodos para manejar datos faltantes en series de tiempo financieras. Utilizando algunos datos de ejemplo, muestro que LOCF suele ser un método decente en comparación con la eliminación y la imputación, pero tiene sus fallas, es decir, puede crear saltos artificiales indeseables en los datos. Sin embargo, alternativas como la interpolación tienen sus propios problemas, especialmente en el contexto de la predicción/previsión en vivo.
featured image - Manejo de datos faltantes en series de tiempo financieras: recetas y trampas
Vladimir Kirilin HackerNoon profile picture
0-item

Si eres como yo, has lidiado con datos faltantes en tus conjuntos de datos al menos una vez. O dos veces. O demasiadas veces...


A veces, todo lo que se necesita para manejar esas molestas NA es eliminarlas, es decir, eliminar las filas que contienen datos faltantes. Sin embargo, es posible que esto no siempre sea óptimo, especialmente para los datos de series temporales, y más aún para los financieros. Por supuesto, este problema está bien estudiado, por lo que existen muchas alternativas al abandono.


Analizaré algunos de ellos (enumerados a continuación) y discutiré los pros y los contras:


  1. Goteante

  2. LOCF (última observación trasladada)

  3. Imputación media (o similar)

  4. Interpolación


Alerta de spoiler: ¡no existe un enfoque único que sirva para todos! Sostendré que LOCF suele ser una buena opción para las finanzas, pero tampoco está exenta de inconvenientes. Con eso en mente, permítanme describir los métodos y datos que usaré para mostrarlos.


Nota: si uno quiere ser pedante, todos los métodos 2 a 4 son ejemplos de alguna imputación.


Datos del modelo

Comencemos con algunos ejemplos de por qué a uno le importaría abandonar en primer lugar. Para ilustrar, generé algunos datos diarios demasiado simplificados sobre el precio de las acciones, asumiendo que siguen un recorrido aleatorio sin deriva (es decir, el precio promedio a largo plazo debe permanecer constante); no es la suposición más precisa, pero sí 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() 


serie de precios inalterada


Bueno, la trama parece bastante bien intencionada.


Supongamos que ahora queremos averiguar la media empírica de las diferencias de precios diarias:

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

Obviamente distinto de cero, a diferencia de la serie generadora, pero esto es solo ruido de muestra. Hasta ahora, todo bien.


Dejar caer datos

Ahora deformemos un poco estos datos eliminando algunos puntos de datos:

 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 un par de cosas de inmediato:

  1. La media de alguna manera no es NA, aunque el vector diff claramente contendrá NA

  2. La media es diferente a la que obtuvimos antes.


Ahora, el punto 1 es bastante fácil: pd.mean elimina automáticamente NA de forma predeterminada.

¿Pero qué pasa con el número 2? Repensemos lo que estamos computando.


Es fácil demostrar que, al menos sin NA, la diferencia de precio media debería ser simplemente (price_vec[99]-price_vec[0])/99 ; de hecho, cuando sumamos las diferencias de precios, todas las piezas "intermedias" se cancelan, así (price_vec[1] - price_vec[0]) + (price_vec[2] - price_vec[1]) + .. !


Ahora, con los datos faltantes insertados, si primero tomamos las diferencias y luego eliminamos NA , esta cancelación se rompe; algunas matemáticas sencillas muestran que ahora estás calculando (price_vec[99] - price_vec[0] - price_vec[95] + price_vec[89])/93 .


Para mostrar esto, observe que ahora se omiten los dos términos siguientes: price_vec[95] - price_vec[94] y price_vec[90] - price_vec[89] , ya que (NA - any number) se evalúa como NA y luego se elimina.


Verifiquemos esto:

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


Ahora, queda más claro cómo podemos arreglar las cosas: primero debemos eliminar las NA y luego diff .

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


La media casi ha vuelto a donde debería estar; se produce una pequeña discrepancia porque ahora tenemos menos términos en la media: 94 en lugar de 99.


Ok, parece que si solo nos importa la media, ¿estamos bien usando dropna (siempre que lo hagamos bien)? Después de todo, la diferencia entre 0.2 y 0.21 está claramente dentro de nuestra tolerancia al ruido. Bueno, no del todo; veamos por qué.


LOCF

¿Qué es LOCF?

LOCF significa Última observación realizada. La idea detrás de esto es súper simple: si registro datos en algunos intervalos de tiempo, que pueden ser regulares o no, si falta la observación de algún intervalo en particular, simplemente asumimos que nada ha cambiado con nuestra variable y la reemplazamos con la última no. valor faltante (por ejemplo - [3, 5, NA, 8] → [3, 5, 5, 8]). Uno podría preguntarse: ¿por qué preocuparse por un intervalo al que le falta una observación, es decir, no simplemente eliminarlo como en el método de “eliminación”? Bueno, la respuesta está en el defecto inherente de la “caída” que no mencioné anteriormente.


Suponga que registra varias cantidades a la vez, especialmente aquellas que normalmente no cambian demasiado rápidamente, como registros horarios de temperatura y humedad. Supongamos que tiene ambos valores para las 10:00, 11:00 y 12:00, pero solo la humedad a las 13:00. ¿Simplemente elimina esa “fila”, es decir, pretende que no tiene una lectura para las 13:00? Bueno, está bien si solo tienes dos variables, aunque acabas de eliminar información potencialmente valiosa (la humedad de las 13:00). Pero si tiene muchas ocurrencias de este tipo o muchas variables a la vez, eliminarlas podría dejarlo prácticamente sin datos.


Una alternativa muy atractiva es suponer que nada ha cambiado para la temperatura entre las 12:00 y las 13:00. Después de todo, si alguien viniera a nosotros a las 12:30 y nos preguntara: "¿cuál es la temperatura actual?", habríamos respondido con razón con la lectura de las 12:00 (si no podemos obtener una nueva lectura de inmediato, por supuesto). ). ¿Por qué no utilizar la misma lógica para el valor de las 13:00?


¿Por qué utilizar LOCF (en finanzas)?

Primero, probemos nuestro nuevo enfoque con los datos anteriores:

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


¡Parece que recuperamos nuestro antiguo valor precisamente! Además, si desea investigar más a fondo los datos de diferencia de precios, ahora parece más "ordenado" ya que tiene una entrada para cada día, aunque cinco de esas entradas ahora son 0 (¿por qué? Intente ejecutar price_vec_na_simple.ffill().diff().iloc[90:95] para comprobarlo usted mismo).


Además de eso, en finanzas, los datos faltantes y los datos atípicos a menudo se juntan. Permítanme ilustrar eso:

 #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 + datos faltantes


Podemos ver que después de un fuerte aumento de precios, los datos simplemente no están disponibles durante 3 días seguidos. ¡Este no es un ejemplo tan "artificial" como podría parecer! Imagínese que el comercio se detuvo después del pico, al menos en este intercambio en particular. Luego las cosas se calmaron un poco, por lo que el precio volvió al régimen normal. Tal vez algo gradual fue Lo que está sucediendo detrás de escena y que en realidad “conectó” los puntos entre el pico y el post-pico, cálmate, ¡pero no lo sabes y no tienes ningún dato al respecto!


¿Cuál es la suposición más natural si no tenemos ningún dato nuevo? Bueno, recordemos que nuestro modelo de generación de datos se basó fundamentalmente en cambios de precios. Entonces, si no hay datos nuevos, ¿quizás el precio no cambie en absoluto? Esto es exactamente lo que supone LOCF (Última Observación Realizada).

Algunas matemáticas para el contexto

Una nota al margen para un lector curioso: quizás una visión más fundamental sobre por qué LOCF es particularmente adecuado para los datos de precios de acciones es que generalmente se modela como una martingala . En términos generales, una martingala es algo en lo que nuestra mejor suposición para el mañana es lo que vemos hoy, o E[x_{t+1} | x_t] = x_t


Ok, volvamos a los datos reales. Examinemos las consecuencias de LOCF tanto visual como numéricamente:

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

Imputación LOCF


¡Inmediatamente, vemos los pros y los contras de LOCF (literalmente)! Por un lado, la media ha vuelto a donde “esperamos” que esté, es decir, el valor empírico inalterado. Sin embargo, introducimos un período bastante desagradable en el que el precio no está en línea con lo “típico” y una caída artificial del precio entre los días 94 y 95.

¿Qué pasa con la imputación?

Comparemos los resultados que obtenemos de LOCF con la imputación (media). Es una opción muy común para el manejo de NA, especialmente para datos que no son series de tiempo. Sin embargo, si se hace ingenuamente, tiene muchos inconvenientes cuando se utiliza para datos financieros.


  • Si sólo se utiliza la media de todas las muestras, se introduce un sesgo de anticipación obvio, es decir, se utilizan datos futuros para imputar valores pasados.

  • Sin duda es mejor utilizar algún tipo de media retrospectiva o móvil; sin embargo, a veces puede generar tensión con la vista de “línea base” de martingala que describimos antes.


Veamos esto con un poco más de detalle. Usaré la imputación retrospectiva de nuestros datos antiguos:

 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 

imputación media

Recuperamos la media de cambio de precio “correcta”, al igual que LOCF. PERO introducimos una caída de precio artificial entre los días 91 y 92 que en algunos aspectos es incluso peor que la que tuvimos antes. Después de todo, eso sucedió cuando o después de que las cosas probablemente se calmaron, mientras que este simplemente supone que todo vuelve a la normalidad de inmediato. Aparte de eso, en la práctica puede resultar algo complicado equilibrar la ventana retrospectiva de modo que a) capturemos las tendencias recientes pero también b) capturemos las tendencias a largo plazo (el equilibrio habitual entre sesgo y varianza).


Agregar una segunda variable

Supongamos que ahora queremos realizar una tarea más compleja: extraer la correlación entre los movimientos de precios de dos activos a partir de datos empíricos, cuando a una o ambas series de precios les faltan datos. Claro, todavía podemos usar drop, pero:


  • Incluso si podemos usarlo, ¿es óptimo?

  • ¿Qué pasa si tenemos muchas variables? Entonces eliminar todas las filas con al menos un NA podría dejarnos sin ningún dato.


Hay muchas razones por las que uno puede querer calcular la correlación: es el primer paso de EDA en casi todos los modelos multivariables, se usa ampliamente en cualquier tipo de construcción de cartera , etc. ¡Por lo tanto, medir este número con la mayor precisión posible es muy necesario!


Para ilustrar, generemos una segunda variable con una correlación "incorporada" de 0,4 con la primera. Para hacerlo, usaremos una especie de modelo de mezcla gaussiana . La imagen que uno puede tener en mente es la de dos acciones correlacionadas que comparten un factor de riesgo importante, pero la segunda acción también está expuesta a un factor de riesgo importante que la primera no tiene. Piense en Google y Facebook, por ejemplo: el primer factor podría ser un sentimiento genérico sobre el sector tecnológico y el segundo podría ser la competencia con las redes sociales rivales.


 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


Comprobemos la correlación empírica "de referencia", es decir, sin NA ni saltos.

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


Ahora bien, esto se acerca razonablemente a la correlación “teórica”; es bien sabido que la medición empírica de la correlación tiende a generar un ruido bastante grande.


NA sin valores atípicos

Como siguiente paso, examinaremos el caso con las AN, pero sin valores atípicos. También compararemos lo que sucede si dropna antes y después de 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 resultados están bastante cerca y no muy lejos del valor “empírico” que obtuvimos antes. Verifiquemos que LOCF y la imputación también funcionan correctamente:

 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


Al comparar los 4 resultados anteriores, vemos que todos los métodos funcionan bastante bien. ¿Quizás entonces deberíamos esperar lo mismo para el caso atípico?


NA con valores atípicos

Recuerde, para mantener la coherencia debemos exponer la segunda serie de precios a los mismos shocks de precios que experimentó la primera, pero sin los siguientes NA. Volviendo al ejemplo anterior, imagine que algún evento importante provoca un aumento en el primer factor de riesgo que eventualmente detiene la negociación del primer activo. El segundo activo también los experimentará, claro, pero quizás en menor medida, por lo que no se produciría ninguna detención y, por lo tanto, no se producirían AN.


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


Comparemos nuevamente el rendimiento de todos nuestros 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


¡Esa es una gran diferencia tanto con el valor teórico como con el empírico! ¿Qué tal LOCF e imputar?

 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


¡Ahora finalmente vemos cuánto vale LOCF! ¡Claramente supera a todos los demás métodos!


Errores potenciales + interpolación

Por supuesto, esto no es 100% sólido. Por un lado, al hacer LOCF introducimos una gran caída de precios cuando terminan los datos faltantes. Si coincide con algunos valores atípicos en el segundo vector de precios, los resultados podrían cambiar bastante. (*Un ejercicio para el lector: gire el signo sobre el movimiento del precio de price_vec_na_2[95] y compruebe cómo afecta a los resultados). No está del todo claro si es "limpio" simplemente introducir esta caída de precio en lugar de, por ejemplo, interpolar entre el precio máximo price_vec_na[91] y el valor "normal" posterior price_vec_na[95] . Sin embargo, especialmente para un uso “en vivo”, ¡la interpolación no es realmente posible! Después de todo, si hoy es el día 93, ¿cómo podemos interpolar utilizando un valor futuro registrado al final del día 95? Para un estudio histórico, claro, esa sigue siendo una opción, pero aún no está claro cómo interpretarlo y usarlo para pronósticos reales. En conclusión, la interpolación a lo largo de la dimensión temporal es posible, pero algo más cuestionable.


Conclusiones

Intenté ofrecer un pequeño estudio de caso para presentar y defender por qué LOCF es a menudo la opción más atractiva y sencilla de utilizar para manejar datos faltantes en series de tiempo financieras.


En resumen, las ventajas son:

  • Atractivo desde una perspectiva martingala/”flujo de información”
  • Súper fácil de implementar
  • No es necesario optimizar (a diferencia de la imputación)
  • Maneja los valores atípicos al menos decentemente


Algunas desventajas:

  • Potencialmente puede causar grandes saltos al final del período faltante.
  • Podría perderse algunos matices de la dinámica articular cuando se utiliza para varias variables


Como cuantitativo en una tienda de comercio de utilería, lo uso en casi todos mis estudios como base eficaz. Por supuesto, algunas situaciones requieren medidas más matizadas, pero son pocas y espaciadas y, por lo general, tampoco se “resuelven” al 100% con ninguno de los otros 3 métodos mencionados.