1. Cómo guardar una serie temporal sin arrepentirte
Nivel 5: Guardar, actualizar y escalar
Llegas aquí con el dato ya descargado y ya validado. Queda la decisión que parece administrativa y no lo es: dónde lo dejas y con qué forma. Un almacén mal montado casi nunca da errores, que sería lo cómodo. Lo que hace es cobrarte cuarenta segundos cada vez que quieres probar una idea, hasta que dejas de probar ideas.
El CSV es cómodo y por eso sale caro
El CSV tiene una virtud enorme: lo abre cualquier cosa, se inspecciona con los ojos y no obliga a instalar nada. Por eso todo el mundo empieza ahí y hace bien. Tiene tres defectos que crecen con el proyecto:
- No guarda tipos. Un CSV es texto, y pandas vuelve a adivinar en cada lectura. Adivina bien casi siempre, y el casi es el problema: el símbolo NA de Nabors Industries se convierte en valor ausente si no se lo prohíbes, un identificador con ceros a la izquierda los pierde, y una fecha ambigua puede leerse con el día y el mes cambiados sin avisar.
- No comprime. Cada dígito de cada precio ocupa un byte, y las comas y los saltos de línea también.
- Se lee entero. No existe forma de leer solo la columna de cierre sin descodificar las otras cinco, ni solo marzo de 2019 sin recorrer el fichero completo hasta encontrarlo.
Pon cifras. Quinientos valores, veinticinco años, unas 252 sesiones al año: alrededor de 3,1 millones de filas OHLCV. Como orden de magnitud declarado, y no como promesa, eso son cientos de megabytes en CSV y decenas en un formato columnar comprimido. La proporción concreta depende de tus columnas; mídela en tu máquina antes de citarla.
Qué gana un formato columnar comprimido
Un fichero columnar guarda cada columna por separado, con su tipo en el esquema y su propia compresión. De ahí salen cuatro ventajas que no son cosméticas:
- Los tipos vuelven tal cual los guardaste. La fecha vuelve como fecha y el entero como entero, sin adivinanzas ni argumentos de lectura que se te olvidan.
- Comprime mucho más porque los vecinos se parecen. La columna de símbolo repite AAPL seis mil veces seguidas y se guarda como un diccionario; la de precios contiene valores contiguos y muy parecidos entre sí.
- Lees solo las columnas que pides. Pedir cierre y volumen de una tabla de seis columnas mueve aproximadamente un tercio de los bytes.
- Guarda estadísticas por bloque de filas. Con el mínimo y el máximo de cada bloque, el lector descarta bloques enteros sin descodificarlos.
El precio a pagar es real: ya no lo abres con un editor de texto, no corriges una celda a mano y añadir una fila obliga a reescribir un bloque en vez de pegar una línea al final. Es un almacén, no un cuaderno.
import pandas as pd
# La tabla viene en formato largo: una fila por (simbolo, fecha)
precios = pd.read_csv("precios_crudos.csv", parse_dates=["fecha"],
keep_default_na=False, na_values=[""])
# Tipos explicitos: lo que no declares, parquet lo congelara como lo adivino pandas
precios = precios.astype({
"simbolo": "string",
"cierre": "float64", # float32 ocupa la mitad y pierde precision al acumular
"volumen": "int64",
})
# La columna de particion se deriva del dato, nunca se inventa
precios["anio"] = precios["fecha"].dt.year
precios.to_parquet(
"almacen/precios",
engine="pyarrow",
compression="zstd",
partition_cols=["anio"],
index=False,
)Largo o ancho: la misma tabla con dos costes distintos
En formato largo hay una fila por combinación de símbolo y fecha. El esquema no cambia nunca: añadir el valor quinientos uno son filas nuevas, no columnas nuevas. Filtra bien, particiona bien y no obliga a rellenar huecos con ausentes para los valores que todavía no cotizaban.
En formato ancho el índice es la fecha y hay una columna por símbolo. Es lo que quieren las operaciones matriciales: retornos, correlaciones, betas, cualquier cosa que trate a los quinientos valores como un vector. Su problema es que el esquema depende de los datos, y que con las cinco columnas OHLCV te plantas en 3.000 columnas o en cinco tablas paralelas que hay que mantener sincronizadas.
La regla práctica es simple: guarda en largo y pivota a ancho en memoria justo antes del cálculo que lo necesita. El pivote es barato comparado con lo que evita. Una matriz de 6.300 sesiones por 500 columnas son 3,15 millones de celdas, unos 25 MB en coma flotante de ocho bytes: cabe de sobra.
ancho = precios.pivot(index="fecha", columns="simbolo", values="cierre")
retornos = ancho.pct_change()
matriz = retornos.corr() # 500 x 500, calculada sobre datos alineadosParticionar es escribir un índice con carpetas
Particionar significa partir el almacén en subcarpetas según el valor de una columna, y que el lector abra solo las que necesita. La elección no es de estilo, responde a qué preguntas haces más veces:
- Por fecha favorece las consultas de corte transversal: dame todo el mercado entre 2020 y 2021. Es lo habitual en investigación, porque casi todo lo que se prueba se prueba sobre una ventana temporal.
- Por símbolo favorece las consultas de serie: dame el histórico completo de AAPL. Es lo habitual en producción, cuando cada proceso atiende a un valor.
Puedes anidar las dos, pero cuidado con la aritmética. Símbolo y luego año son 500 por 25 igual a 12.500 carpetas con unas 250 filas cada una: ficheros de pocos kilobytes en los que los metadatos pesan más que los datos, y miles de aperturas de fichero que se notan. Busca ficheros de decenas a cientos de megabytes; muchos ficheros diminutos rinden peor que uno grande.
cierres = pd.read_parquet(
"almacen/precios",
columns=["simbolo", "fecha", "cierre", "anio"],
filters=[("anio", ">=", 2023), ("anio", "<=", 2024)],
)
# Las columnas de particion vuelven como categoricas: comparar con > falla
cierres["anio"] = cierres["anio"].astype("int16")Caso: el mismo dato, dos almacenes
Toma los 3,1 millones de filas de antes. Guardadas en un único CSV, cualquier pregunta empieza por parsear el fichero entero: decenas de segundos en un portátil normal, orden de magnitud, y la memoria ocupada varias veces el tamaño del fichero mientras se construyen los tipos.
Guardadas en columnar particionado por año, la pregunta media del volumen de 2024 abre una carpeta de veinticinco y descodifica dos columnas de seis. El trabajo real cae por debajo del uno por ciento del anterior, y la lectura pasa de decenas de segundos a una fracción de segundo. No es magia del formato: es que has dejado de leer el 99% del almacén para responder algo que solo necesitaba el 1%.