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.
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
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.
| Parámetro | Resuelve |
|---|---|
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=False | Conservar 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»
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.
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
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
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
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
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
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.
Causa: el archivo no está en UTF-8. Casi siempre lo guardó Excel en Windows.
Arreglo: encoding="latin-1" o "cp1252".
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.
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.
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
| Expresión | Qué hace | Cuándo |
|---|---|---|
read_csv(f) | Lee inferiendo todo | Para empezar |
sep=";" | Otro separador | shape dice (n, 1) |
decimal=",", thousands="." | Números en formato europeo | Van juntos |
encoding="latin-1" | Otra codificación | Acentos rotos o UnicodeDecodeError |
na_values=["-", "N/A"] | Más tokens de ausente | Casi siempre |
keep_default_na=False | Nada es ausente: todo es texto | Para informar el valor original |
dtype={"c": "str"} | Fuerza el tipo de una columna | Códigos con ceros a la izquierda |
usecols=[...] | Solo esas columnas | Archivos anchos |
parse_dates=["f"] | Convierte al leer | Si el formato es ISO |
nrows=5 | Solo las primeras filas | Espiar un archivo grande |
chunksize=100000 | Devuelve un iterador | Cuando no entra en memoria |
on_bad_lines="skip" | Saltea las filas rotas | Con un conteo al lado |