Leer datos: CSV, Excel, Parquet

python
pandas
eda
pd.read_csv() y los 5 argumentos que importan, manejo de tipos por columna con dtype, locale para datos europeos, read_excel con sheets, Parquet como formato moderno y lectura por chunks para datos grandes.

pd.read_csv(): el verbo más usado

pd.read_csv() es la puerta de entrada de casi todo análisis. Sus defaults funcionan en el 80 % de los casos, pero el otro 20 % requiere conocer cinco argumentos clave:

import pandas as pd

df = pd.read_csv("data/ventas.csv")
df.head()

Listo. pandas detecta automáticamente delimitador, tipos y encoding. Para datos sencillos, esto basta.

Cuando los defaults fallan, las cinco opciones que de verdad importan:

Argumento Para qué
sep Delimitador (default ,. Usa ; para CSVs europeos)
dtype Forzar tipo por columna
parse_dates Convertir columnas a fechas automáticamente
encoding Codificación del archivo (UTF-8 por defecto)
na_values Qué strings se consideran NA

Ejemplo combinando todas:

df = pd.read_csv(
    "data/ventas.csv",
    sep=";",                              # punto y coma (formato europeo)
    dtype={"codigo_postal": "string"},    # forzar a string (no perder ceros iniciales)
    parse_dates=["fecha_venta", "fecha_envio"],
    encoding="latin-1",
    na_values=["", "N/A", "-", "?"]
)

Tipos por columna con dtype

pandas adivina los tipos. A veces se equivoca:

  • Códigos postales como "01234" los convierte a número 1234 (perdiendo el cero).
  • Identificadores muy largos los puede pasar a float perdiendo precisión.
  • Columnas con texto y números mezclados quedan como object (genérico).

dtype te permite forzar:

df = pd.read_csv(
    "data/clientes.csv",
    dtype={
        "codigo_postal": "string",
        "id_cliente":    "string",
        "edad":          "Int64",       # Int64 con I mayúscula: soporta NA
        "ingresos":      "float64",
        "activo":        "boolean"
    }
)

Tipos modernos que conviene conocer:

  • "string": strings nativos. Reemplaza al object tradicional.
  • "Int64" (con I mayúscula), entero que soporta NA. El int64 clásico no.
  • "Float64": análogo. Soporta pd.NA como NA explícito.
  • "boolean": booleano que soporta NA.

Para versiones de pandas 2.0+, considera usar dtype_backend="pyarrow":

df = pd.read_csv("data/ventas.csv", dtype_backend="pyarrow")

pyarrow backend es mucho más rápido, usa menos memoria, y trata NAs consistentemente. Es la dirección hacia donde va pandas.

Parsing de fechas

Por defecto, las columnas de fecha quedan como object. Para convertirlas:

df = pd.read_csv("data/ventas.csv", parse_dates=["fecha"])

Si la fecha está en formato europeo (dd/mm/yyyy), parse_dates puede fallar o interpretar mal (01/03/2024 lo parsea como 3 de enero en lugar de 1 de marzo). Hay que ser explícito:

df = pd.read_csv(
    "data/ventas.csv",
    parse_dates=["fecha"],
    date_format="%d/%m/%Y"
)

date_format usa los códigos strftime (%Y año, %m mes, %d día, %H hora, etc.).

Para combinar columnas de fecha (year, month, day separadas):

df["fecha"] = pd.to_datetime(df[["year", "month", "day"]])

pd.to_datetime() es el conversor universal, admite strings, columnas combinadas, números epoch, lo que sea.

Encoding: datos europeos

UTF-8 es el default sensato. Pero archivos antiguos de Windows suelen estar en latin-1 (también llamado cp1252):

df = pd.read_csv("data/clientes.csv", encoding="latin-1")

Síntoma típico: acentos que aparecen como ñ en lugar de ñ. Solución: cambiar encoding. Si dudas, prueba primero "utf-8", luego "latin-1", luego "cp1252". Uno de los tres suele funcionar.

Para CSVs con separador europeo (punto y coma) y decimal con coma:

df = pd.read_csv(
    "data/ventas.csv",
    sep=";",
    decimal=",",
    thousands="."
)

decimal="," interpreta los decimales con coma. thousands="." ignora el punto como separador de miles. Tres argumentos para no pelearse con datos en formato español.

read_excel: Excel multi-hoja

Para archivos .xlsx:

df = pd.read_excel("data/ventas.xlsx", sheet_name="2024")

Argumentos útiles:

  • sheet_name="...": qué hoja. Acepta nombre o índice (0-based).
  • sheet_name=None: devuelve un diccionario {hoja: DataFrame} con todas.
  • header=0: qué fila contiene los nombres (default: primera).
  • skiprows=2: saltar las primeras N filas (útil cuando hay título o metadata arriba).
  • usecols="B:G": solo columnas B a G.

Para leer todas las hojas de un Excel:

hojas = pd.read_excel("data/ventas.xlsx", sheet_name=None)
hojas.keys()       # nombres de las hojas
hojas["2024"]      # DataFrame de esa hoja

read_excel necesita el paquete openpyxl para archivos .xlsx (o xlrd para .xls antiguos). Si uv no lo tiene, añádelo:

uv add openpyxl

Parquet: el formato moderno

CSV es ubicuo pero ineficiente: texto plano, sin tipos, lento de parsear con archivos grandes. Parquet es un formato columnar binario que resuelve esto:

# Escribir
df.to_parquet("data/ventas.parquet")

# Leer
df = pd.read_parquet("data/ventas.parquet")

Ventajas:

  • 10-100× más pequeño que CSV (compresión columnar).
  • 5-10× más rápido de leer/escribir.
  • Preserva tipos: los datetime quedan datetime, no strings.
  • Soporta NA correctamente.

Cuándo usarlo:

  • Datos intermedios en pipelines (entre un script y el siguiente).
  • Datasets grandes que abres muchas veces.
  • Compartir entre Python y R (el paquete arrow en R lee Parquet nativo).

Cuándo NO:

  • Compatibilidad con sistemas legacy que solo aceptan CSV.
  • Datos que un humano va a abrir en Excel.

Para Parquet, necesitas pyarrow:

uv add pyarrow

pyarrow es además el backend recomendado para muchos paquetes modernos. Vale la pena tenerlo.

Lectura por chunks: archivos grandes

CSVs de varios GB no caben en memoria. pd.read_csv los maneja con chunksize:

chunks = pd.read_csv("data/big.csv", chunksize=100_000)

# chunks es un iterador. Procesas un trozo a la vez.
totales = []
for chunk in chunks:
    chunk_filt = chunk[chunk["ventas"] > 1000]
    totales.append(chunk_filt["ventas"].sum())

total_general = sum(totales)

Patrón: leer en bloques, procesar cada bloque, agregar resultados. Es el equivalente al streaming en memoria limitada.

Alternativas más modernas para datos grandes:

  • polars: DataFrame backend nativo en Rust, 10-100× más rápido que pandas en operaciones grandes. Si tus datos no entran en pandas, polars suele entrar.
  • duckdb: base de datos analítica embebida. Puedes hacer SQL sobre Parquet/CSV directamente sin cargar todo a memoria.
  • pyarrow + pyarrow.dataset, el sistema moderno de Apache Arrow para datasets enormes particionados.

Para análisis ad-hoc < 1 GB, pandas basta. Para más, considera estas alternativas.

Trampas habituales

  • CSV que abrió Excel. Excel modifica datos silenciosamente: cambia separadores, transforma fechas según el locale, pierde ceros iniciales. Si recibes un CSV que pasó por Excel, ábrelo en un editor de texto antes de cargarlo a pandas, confirma que es lo que esperas.
  • Encoding incorrecto que parece funcionar. A veces el archivo se lee sin errores pero los acentos aparecen mal. Comprueba siempre df["columna_con_acentos"].head() después de leer.
  • parse_dates que falla silenciosamente. Si la fecha no se puede parsear, queda como object sin warning fuerte. Verifica con df.dtypes que la columna es datetime64[ns].
  • Olvidar low_memory=False con archivos heterogéneos. Por defecto, read_csv lee en chunks para ahorrar memoria. Si una columna empieza con números pero después aparecen strings, pandas la tipará como object con un warning. low_memory=False lee todo de una vez y suele resolverlo (aunque usa más RAM).

En la siguiente entrega

Has aprendido a traer datos. La siguiente pieza es seleccionar partes de un DataFrame, .loc, .iloc, boolean indexing. Es la operación más frecuente en análisis y donde más errores se cometen al venir de R. Lo siguiente (en la próxima sesión).