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:
Goteante
LOCF (última observación trasladada)
Imputación media (o similar)
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.
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()
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.
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:
La media de alguna manera no es NA, aunque el vector diff
claramente contendrá NA
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 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?
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
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 tú 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).
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
¡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.
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
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).
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.
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?
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!
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.
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:
Algunas desventajas:
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.