Saltar al contenido

Indice / Entrada y salida / 7.1

read_csv a fondo

La función que más se usa del curso tiene cincuenta parámetros. Diez importan, y tres de ellos deciden si el archivo se lee bien o se lee mal sin dar error.

Antes: 4.2 Leer y mirar 5.2 Tipos y texto 5.3 Fechas Despues: cierra su rama

Para qué sirve

Un CSV no es un formato: es una convención con variantes. Cambia el separador, cambia el símbolo decimal, cambia la codificación, y hay filas con más campos de los que dice el encabezado. Todo eso se resuelve al leer, y resolverlo ahí es mucho más barato que arreglarlo después.

La regla que ordena la lección: si podés declararlo al leer, declaralo. Un na_values bien puesto ahorra media hora de diagnóstico.

ELI5Explicado facil

«CSV» es como decir «una carta». Puede estar en castellano o en alemán, escrita a mano o a máquina, con la fecha arriba o abajo.

read_csv intenta adivinar, y adivina bien casi siempre. El problema es el casi: cuando adivina mal no se disculpa, te entrega algo que parece una tabla y sigue.

Los parámetros que más rinden

EjemploLos cuatro parametros que cambian el resultado
import pandas as pd

ARCHIVO = "clientes_crudos.csv"

por_defecto = pd.read_csv(ARCHIVO)
print("por defecto:", por_defecto.shape)
print(por_defecto.dtypes.to_string())

declarado = pd.read_csv(
    ARCHIVO,
    usecols=["id", "edad", "fecha_alta"],       # solo estas columnas
    dtype={"id": "str"},                        # y con este tipo
    na_values=["-", "N/A", "null"],             # ademas de los de siempre
)
print()
print("declarado:", declarado.shape)
print(declarado.dtypes.to_string())

print()
print("ausentes en edad, por defecto:", int(por_defecto["edad"].isna().sum()))
print("ausentes en edad, declarando :", int(declarado["edad"].isna().sum()))
print()
print("La diferencia es el guion. edad sigue siendo texto porque tambien")
print("hay una fila que dice 'treinta', y eso no es un ausente: es basura.")
por defecto: (120, 6)
id            str
nombre        str
correo        str
edad          str
ciudad        str
fecha_alta    str

declarado: (120, 3)
id            str
edad          str
fecha_alta    str

ausentes en edad, por defecto: 3
ausentes en edad, declarando : 4

La diferencia es el guion. edad sigue siendo texto porque tambien
hay una fila que dice 'treinta', y eso no es un ausente: es basura.
Diez parámetros y qué problema resuelven
ParámetroResuelve
sep=";"El archivo no usa comas
decimal=","Los números vienen con coma decimal
thousands="."Y con punto de miles
encoding="latin-1"Los acentos salen rotos o falla al leer
na_values=["-"]Tokens de ausente que pandas no reconoce
keep_default_na=FalseConservar los tokens tal cual, para informarlos
dtype={"id": "str"}Un código con ceros a la izquierda que no debe volverse número
usecols=[...]Leer solo lo que hace falta
parse_dates=["f"]Convertir fechas al leer
on_bad_lines="skip"Filas con la cantidad de campos equivocada

El CSV «europeo»

EjemploEl CSV "europeo": punto y coma, y coma decimal
import io

import pandas as pd

# Asi exporta Excel en una configuracion regional de espanol.
EUROPEO = """producto;unidades;precio
Teclado;3;1.234,50
Monitor;1;98.765,00
Webcam;12;1.099,90
"""

mal = pd.read_csv(io.StringIO(EUROPEO))
print("read_csv sin decir nada:")
print("forma:", mal.shape, "-> una sola columna")
print("columnas:", list(mal.columns))

print()
a_medias = pd.read_csv(io.StringIO(EUROPEO), sep=";", decimal=",")
print("con sep=';' y decimal=',' pero SIN thousands:")
print("dtype de precio:", a_medias["precio"].dtype)
print("suma:", a_medias["precio"].sum())
print("Quedo como texto, y sum() sobre texto CONCATENA. No da error.")

print()
bien = pd.read_csv(io.StringIO(EUROPEO), sep=";", decimal=",", thousands=".")
print("con sep=';', decimal=',' y thousands='.':")
print(bien.to_string(index=False))
print(bien.dtypes.to_string())

print()
print("total:", bien["precio"].sum())
print()
print("Si shape te devuelve (n, 1), el separador esta mal. Es el diagnostico")
print("mas rapido que hay, y por eso conviene mirar shape apenas se lee.")
read_csv sin decir nada:
forma: (3, 1) -> una sola columna
columnas: ['producto;unidades;precio']

con sep=';' y decimal=',' pero SIN thousands:
dtype de precio: str
suma: 1.234,5098.765,001.099,90
Quedo como texto, y sum() sobre texto CONCATENA. No da error.

con sep=';', decimal=',' y thousands='.':
producto  unidades  precio
 Teclado         3  1234.5
 Monitor         1 98765.0
  Webcam        12  1099.9
producto        str
unidades      int64
precio      float64

total: 101099.4

Si shape te devuelve (n, 1), el separador esta mal. Es el diagnostico
mas rapido que hay, y por eso conviene mirar shape apenas se lee.
Si shape devuelve (n, 1), es el separador

Es el diagnóstico más rápido de todos y por eso df.shape va en la primera línea después de leer. Con el separador equivocado, pandas mete la fila entera en una sola columna y no se queja: el archivo «se leyó bien», solo que tiene una columna que se llama producto;unidades;precio.

Codificación

EjemploCodificacion: el mismo archivo leido de dos formas
import io

import pandas as pd

TEXTO = "ciudad,pais\nMalargue,Argentina\nMuenchen,Alemania\n"
CON_ACENTOS = TEXTO.replace("Malargue", "Malargüe").replace("Muenchen",
                                                            "München")

# El mismo contenido, guardado en dos codificaciones distintas.
como_utf8 = CON_ACENTOS.encode("utf-8")
como_latin1 = CON_ACENTOS.encode("latin-1")
print("bytes en utf-8  :", len(como_utf8))
print("bytes en latin-1:", len(como_latin1))
print("Los acentos ocupan 2 bytes en UTF-8 y 1 en latin-1: por eso difieren.")

print()
bien = pd.read_csv(io.BytesIO(como_utf8), encoding="utf-8")
print("utf-8 leido como utf-8:")
print(bien.to_string(index=False))

print()
try:
    pd.read_csv(io.BytesIO(como_utf8), encoding="latin-1")
    mal = pd.read_csv(io.BytesIO(como_utf8), encoding="latin-1")
    print("utf-8 leido como latin-1 (no falla, pero rompe los acentos):")
    print(mal.to_string(index=False))
except UnicodeDecodeError as error:
    print("UnicodeDecodeError:", error)

print()
try:
    pd.read_csv(io.BytesIO(como_latin1), encoding="utf-8")
except UnicodeDecodeError as error:
    print("latin-1 leido como utf-8 ->", type(error).__name__)
    print(str(error))

print()
print("El caso peligroso es el primero: NO da error y deja los acentos rotos.")
print("El segundo si falla, que es mas comodo. Todos los archivos de este")
print("curso estan en UTF-8.")
bytes en utf-8  : 50
bytes en latin-1: 48
Los acentos ocupan 2 bytes en UTF-8 y 1 en latin-1: por eso difieren.

utf-8 leido como utf-8:
  ciudad      pais
Malargüe Argentina
 München  Alemania

utf-8 leido como latin-1 (no falla, pero rompe los acentos):
   ciudad      pais
Malargüe Argentina
 München  Alemania

latin-1 leido como utf-8 -> UnicodeDecodeError
'utf-8' codec can't decode byte 0xfc in position 18: invalid start byte

El caso peligroso es el primero: NO da error y deja los acentos rotos.
El segundo si falla, que es mas comodo. Todos los archivos de este
curso estan en UTF-8.

De los dos errores posibles, el peligroso es el que no falla: leer UTF-8 como latin-1 no da ninguna excepción y deja Malargüe donde decía Malargüe. El caso contrario sí explota, que es más cómodo.

Filas rotas

EjemploFilas con la cantidad de columnas equivocada
import io

import pandas as pd

ROTO = """id,producto,precio
1,Teclado,10.5
2,Monitor,20.0,SOBRA
3,Webcam
4,Mouse,5.25
"""

try:
    pd.read_csv(io.StringIO(ROTO))
except pd.errors.ParserError as error:
    print("por defecto ->", type(error).__name__)
    print(str(error).splitlines()[0])

print()
saltando = pd.read_csv(io.StringIO(ROTO), on_bad_lines="skip")
print('con on_bad_lines="skip":')
print(saltando.to_string(index=False))
print("Se salteo la fila 2 entera. La 3, a la que le FALTA una columna,")
print("no es una fila mala: pandas la completa con NaN.")

print()
print("Ojo con skip: descarta en silencio y no dice cuantas. Si el archivo")
print("tiene 500.000 filas y 300 estan rotas, nadie se entera.")
print("filas leidas:", len(saltando), "de 4 en el archivo")
por defecto -> ParserError
Error tokenizing data. C error: Expected 3 fields in line 3, saw 4

con on_bad_lines="skip":
 id producto  precio
  1  Teclado   10.50
  3   Webcam     NaN
  4    Mouse    5.25
Se salteo la fila 2 entera. La 3, a la que le FALTA una columna,
no es una fila mala: pandas la completa con NaN.

Ojo con skip: descarta en silencio y no dice cuantas. Si el archivo
tiene 500.000 filas y 300 estan rotas, nadie se entera.
filas leidas: 3 de 4 en el archivo
Faltar una columna no es lo mismo que sobrar

Una fila con menos campos de los esperados no es «mala»: pandas la completa con NaN y sigue. Una fila con más campos sí rompe el parseo, porque no hay dónde poner el sobrante.

Y on_bad_lines="skip" descarta en silencio. Sobre 500.000 filas con 300 rotas, nadie se entera. Si vas a saltear, contá: len(df) contra las líneas del archivo.

Archivos grandes

Ejemploparse_dates, nrows y leer de a pedazos
import pandas as pd

# parse_dates convierte al leer, sin una linea aparte.
pedidos = pd.read_csv("tienda_pedidos.csv", parse_dates=["creado"])
print("dtype de creado:", pedidos["creado"].dtype)

print()
# nrows: mirar la punta de un archivo grande sin cargarlo entero.
punta = pd.read_csv("dw_hecho_ventas.csv", nrows=3)
print("nrows=3:")
print(punta.to_string(index=False))

print()
# chunksize: procesar por pedazos. Devuelve un iterador, no un DataFrame.
total, filas = 0.0, 0
for pedazo in pd.read_csv("dw_hecho_ventas.csv", chunksize=100):
    ingreso = pd.to_numeric(pedazo["ingreso"], errors="coerce")
    total += float(ingreso.sum())
    filas += len(pedazo)
print(f"leido de a 100 filas: {filas} filas, total {total:,.2f}")

entero = pd.read_csv("dw_hecho_ventas.csv")
print("de una sola vez     :", len(entero),
      f"filas, total {entero['ingreso'].sum():,.2f}")

print()
print("Con 500 filas no hace falta. Con 50 millones, chunksize es la")
print("diferencia entre que ande y que se quede sin memoria.")
dtype de creado: datetime64[us]

nrows=3:
 venta_id      fecha  producto_id  cliente_id  cantidad  ingreso   costo
        1 2026-01-13           25          22        18 10115.14 7375.11
        2 2026-05-26           28          28         4  2456.66 1778.94
        3 2026-06-05           27          29        17  2041.28 1072.75

leido de a 100 filas: 500 filas, total 12,010,121.52
de una sola vez     : 500 filas, total 12,010,121.52

Con 500 filas no hace falta. Con 50 millones, chunksize es la
diferencia entre que ande y que se quede sin memoria.

Dónde se rompe

(sin error) el DataFrame tiene una sola columna

Causa: el separador no es la coma.

Arreglo: sep=";", o sep=None, engine="python" para que lo detecte solo - que anda, y es mejor declararlo.

UnicodeDecodeError: 'utf-8' codec can't decode byte 0xfc

Causa: el archivo no está en UTF-8. Casi siempre lo guardó Excel en Windows.

Arreglo: encoding="latin-1" o "cp1252".

ParserError: Expected 3 fields in line 3, saw 4

Causa: una fila tiene más campos que el encabezado, casi siempre por una coma dentro de un texto sin comillas.

Arreglo: mirá esa línea del archivo antes de decidir. Si de verdad es basura, on_bad_lines="skip" y contá cuántas se fueron.

(sin error) un código con ceros a la izquierda perdió los ceros

Causa: pandas leyó "00123" como el número 123. Pasa con códigos postales, DNI y SKU numéricos.

Arreglo: dtype={"codigo": "str"}. Una vez que se convirtió a número, los ceros no se recuperan.

(sin error) la suma de los importes salió concatenada

Causa: 1.234,50 leído sin thousands=".". pandas no pudo convertirlo a número y dejó la columna como texto, y sum() sobre texto concatena.

Arreglo: declarar decimal y thousands juntos. Y mirar dtypes: una columna de importes que dice str es el diagnóstico completo.

Recetario

read_csv
ExpresiónQué haceCuándo
read_csv(f)Lee inferiendo todoPara empezar
sep=";"Otro separadorshape dice (n, 1)
decimal=",", thousands="."Números en formato europeoVan juntos
encoding="latin-1"Otra codificaciónAcentos rotos o UnicodeDecodeError
na_values=["-", "N/A"]Más tokens de ausenteCasi siempre
keep_default_na=FalseNada es ausente: todo es textoPara informar el valor original
dtype={"c": "str"}Fuerza el tipo de una columnaCódigos con ceros a la izquierda
usecols=[...]Solo esas columnasArchivos anchos
parse_dates=["f"]Convierte al leerSi el formato es ISO
nrows=5Solo las primeras filasEspiar un archivo grande
chunksize=100000Devuelve un iteradorCuando no entra en memoria
on_bad_lines="skip"Saltea las filas rotasCon un conteo al lado

Ponete a prueba

1. df.shape devuelve (180, 1) despues de leer un CSV. ¿Que pasa?Nivel 1 · Recordar
2. ¿Para que sirve dtype={"codigo": "str"}?Nivel 1 · Recordar
3. Un archivo trae importes como 1.234,50. ¿Como se lee bien?Nivel 2 · Predecir la salida
4. ¿Cual de los dos errores de codificacion es peor?Nivel 2 · Predecir la salida
5. ¿Cual es la diferencia entre una fila con menos campos y una con mas?Nivel 3 · Elegir entre dos casi correctas
6. Vas a usar on_bad_lines="skip" sobre un archivo de 500.000 filas. ¿Que hay que agregar?Nivel 3 · Elegir entre dos casi correctas
7. Este codigo lee un archivo exportado de Excel en espanol. No da ningun error, y sum() imprime 1.234,5098.765,001.099,90. ¿Que paso?
df = pd.read_csv("ventas.csv", sep=";", decimal=",")
print(df["precio"].sum())
Nivel 4 · Encontrar el defecto