Indice / pandas: calidad y limpieza / 5.3
Fechas
Las fechas son el tipo de dato que más silenciosamente sale mal: se convierten sin quejarse, dan un resultado plausible, y recién lo notás cuando un informe tiene un mes de menos.
Para qué sirve
Una fecha en un archivo es texto. Hasta que la convertís no podés ordenar por
ella, ni restar dos, ni agrupar por mes: "2026-01-15" y
"2026-1-15" son dos cadenas distintas, y "31/12/2025" es
«mayor» que "01/01/2026" si las comparás como texto.
La conversión tiene dos trampas propias, y las dos son silenciosas. Una: una fecha puede tener el formato correcto y no existir. Otra: una fecha puede ser válida y estar escrita en otro formato, y descartarla es perder un dato bueno.
ELI5Explicado facil
«15/03/2026» y «2026-03-15» son el mismo día escrito de dos maneras. Vos lo ves enseguida; la computadora no, porque para ella son dos textos distintos y nada más.
Convertir a fecha es decirle: «esto no es una palabra, es un día del calendario». Recién ahí puede ordenarlas, calcular cuántos días pasaron entre dos, o agruparlas por mes.
Y hay un detalle traicionero: «30 de febrero» está bien escrito y no existe. La computadora te lo acepta como texto y lo rechaza como fecha, sin decir nada.
El ejemplo mínimo
import pandas as pd
fechas = pd.Series(["2026-01-15", "2026-02-30", "15/03/2026", ""])
print("como vienen :", fechas.dtype)
convertidas = pd.to_datetime(fechas, errors="coerce", format="%Y-%m-%d")
print("convertidas :", convertidas.dtype)
print(convertidas)
print()
print("NaT es el NaN de las fechas: 'no hay fecha aca'.")
print("2026-02-30 no existe (febrero tiene 28) y 15/03/2026 esta en otro formato.")
como vienen : str
convertidas : datetime64[us]
0 2026-01-15
1 NaT
2 NaT
3 NaT
dtype: datetime64[us]
NaT es el NaN de las fechas: 'no hay fecha aca'.
2026-02-30 no existe (febrero tiene 28) y 15/03/2026 esta en otro formato.
Sobre datos de verdad
Trabajamos con ventas_mensuales.csv : 180 filas de ventas de enero a
marzo.
import pandas as pd
ventas = pd.read_csv("ventas_mensuales.csv")
print("dtype al leer:", ventas["fecha"].dtype)
fecha = pd.to_datetime(ventas["fecha"], errors="coerce", format="%Y-%m-%d")
print("dtype convertido:", fecha.dtype)
print("filas totales :", len(fecha))
print("sin fecha valida:", int(fecha.isna().sum()))
print()
print("Las que no se pudieron convertir, tal como venian:")
print(ventas.loc[fecha.isna(), "fecha"].value_counts(dropna=False).to_string())
dtype al leer: str
dtype convertido: datetime64[us]
filas totales : 180
sin fecha valida: 6
Las que no se pudieron convertir, tal como venian:
fecha
NaN 4
2026-02-30 1
15/03/2026 1
Seis de 180 no se convirtieron. Y ahí ya se ven las tres causas distintas: cuatro vacías, una que no existe, y una que está bien pero en otro formato. Tratar esas tres igual sería un error: solo la primera es un dato que falta de verdad.
El peligro de dejar que pandas adivine
import pandas as pd
muestra = pd.Series(["01/02/2026", "15/03/2026"])
print("dayfirst por defecto (False): el primer numero es el MES")
print(pd.to_datetime(muestra, errors="coerce", dayfirst=False).to_list())
print()
print("dayfirst=True: el primer numero es el DIA")
print(pd.to_datetime(muestra, errors="coerce", dayfirst=True).to_list())
print()
print("Mira la primera fecha: 01/02/2026 es valida de las dos formas,")
print("y da 1 de febrero o 2 de enero segun lo que le digas.")
print("La segunda no deja duda porque no existe el mes 15.")
print("Por eso conviene decir siempre el formato en vez de dejar que adivine.")
dayfirst por defecto (False): el primer numero es el MES
[Timestamp('2026-01-02 00:00:00'), NaT]
dayfirst=True: el primer numero es el DIA
[Timestamp('2026-02-01 00:00:00'), Timestamp('2026-03-15 00:00:00')]
Mira la primera fecha: 01/02/2026 es valida de las dos formas,
y da 1 de febrero o 2 de enero segun lo que le digas.
La segunda no deja duda porque no existe el mes 15.
Por eso conviene decir siempre el formato en vez de dejar que adivine.
Si no declarás el formato, pandas elige uno. Con 01/02/2026 puede
darte el 1 de febrero o el 2 de enero, y las dos son fechas perfectamente válidas:
no hay error, no hay aviso, y el informe queda mal por un mes entero. Declarar
format= convierte un error invisible en un NaT que se ve.
Rescatar lo rescatable
import pandas as pd
ventas = pd.read_csv("ventas_mensuales.csv")
# Pasada 1: el formato que usa casi todo el archivo.
fecha = pd.to_datetime(ventas["fecha"], errors="coerce", format="%Y-%m-%d")
print("tras la pasada 1, sin fecha:", int(fecha.isna().sum()))
# Pasada 2: solo sobre las que fallaron, con el otro formato.
faltan = fecha.isna()
rescatadas = pd.to_datetime(ventas.loc[faltan, "fecha"], errors="coerce",
format="%d/%m/%Y")
fecha = fecha.fillna(rescatadas)
print("tras la pasada 2, sin fecha:", int(fecha.isna().sum()))
print("rescatadas :", int(rescatadas.notna().sum()))
print()
print("Lo que sigue sin fecha, y por que:")
resto = ventas.loc[fecha.isna(), "fecha"]
print(resto.value_counts(dropna=False).to_string())
tras la pasada 1, sin fecha: 6
tras la pasada 2, sin fecha: 5
rescatadas : 1
Lo que sigue sin fecha, y por que:
fecha
NaN 4
2026-02-30 1
Una fila recuperada. Parece poco, pero el criterio es lo que importa: primero el formato mayoritario, después una segunda pasada solo sobre lo que falló. Cada pasada es estricta, y al final sabés exactamente cuántas rescataste y cuántas no. Las cinco que quedan son las que de verdad no tienen fecha utilizable.
Lo mismo, paso a paso
- La columna fecha llega como texto. Casi todas estan en el formato ISO, pero hay tres que no.
ventas = pd.read_csv("ventas_mensuales.csv") - Primera pasada con el formato ISO. Las dos fechas normales pasan sin problema.
fecha = pd.to_datetime(ventas["fecha"], errors="coerce", format="%Y-%m-%d") - La celda vacia se vuelve NaT, que es el NaN de las fechas. Hasta aca nada raro.
# vacio -> NaT - 2026-02-30 tiene el formato correcto pero ese dia no existe: febrero de 2026 termina el 28. Es un error semantico, no de formato, y con coerce se vuelve NaT sin avisar.
# 2026-02-30 encaja en el patron... y aun asi no existe - 15/03/2026 tampoco paso, pero por otro motivo: es una fecha perfectamente valida en otro formato. Tirarla seria perder un dato bueno.
faltan = fecha.isna() # 6 en el archivo entero - Segunda pasada, solo sobre las que fallaron y con el otro formato. Rescata esa fila y deja las otras como estaban.
rescatadas = pd.to_datetime(ventas.loc[faltan, "fecha"], errors="coerce", format="%d/%m/%Y") fecha = fecha.fillna(rescatadas) - Resultado: en el archivo completo quedan 5 filas sin fecha de las 6 que fallaron al principio. Las que no se pueden rescatar son las que de verdad no tienen dato.
fecha.isna().sum() # 5 sobre 180 filas
Sacarle partes a una fecha
import pandas as pd
ventas = pd.read_csv("ventas_mensuales.csv")
fecha = pd.to_datetime(ventas["fecha"], errors="coerce", format="%Y-%m-%d")
validas = fecha.dropna()
print("primera:", validas.min().date(), " ultima:", validas.max().date())
print()
print("ventas por mes:")
print(validas.dt.to_period("M").value_counts().sort_index().to_string())
print()
print("por dia de la semana (0 = lunes):")
print(validas.dt.dayofweek.value_counts().sort_index().to_string())
print()
print("El accesor .dt solo funciona si la columna es datetime.")
print("Sobre texto tira AttributeError, y es la senal de que falto convertir.")
primera: 2026-01-01 ultima: 2026-03-31
ventas por mes:
fecha
2026-01 60
2026-02 55
2026-03 59
Freq: M
por dia de la semana (0 = lunes):
fecha
0 29
1 23
2 26
3 20
4 19
5 30
6 27
El accesor .dt solo funciona si la columna es datetime.
Sobre texto tira AttributeError, y es la senal de que falto convertir.
Dónde se rompe
Causa: la columna todavía es texto. Es el error más común de esta lección y en realidad es una buena noticia: te avisa antes de que hagas cuentas con datos que no son fechas.
Arreglo: pd.to_datetime(...) antes de usar
.dt.
Causa: declaraste el formato pero no pusiste
errors="coerce", así que la primera fecha que no encaja corta.
Arreglo: igual que con to_numeric: corré una vez sin
coerce para enterarte de qué hay, y después ponelo a propósito.
Causa: las fechas que quedaron NaT no forman grupo, y
groupby descarta las claves ausentes sin avisar.
Arreglo: contá los NaT antes de agrupar y decidí qué hacés
con ellos. Si querés verlos en el resultado, groupby(..., dropna=False).
Causa: no declaraste el formato y pandas eligió la interpretación contraria a la que esperabas.
Arreglo: format= siempre. Para detectarlo, mirá
fecha.dt.day.max(): si nunca pasa de 12, es que están al revés.
Causa: la columna sigue siendo texto y se está ordenando alfabéticamente.
"31/12/2025" va después de "01/01/2026".
Arreglo: convertir antes de ordenar. Si el formato es ISO, el orden alfabético coincide con el cronológico - por eso ese formato es el que conviene.
Acá hay que elegir
Las tres causas piden tratamientos distintos, y el enunciado de cualquier trabajo serio tiene que decir cuál se aplica:
- Vacías - es un ausente común. Se descarta o se conserva como
NaT, igual que cualquier hueco. - Imposibles (
2026-02-30,0000-00-00) - es un error de carga. Casi siempre se rechaza e informa: alguien tipeó mal. - En otro formato - es un dato bueno mal escrito. Descartarla es perder información; rescatarla cuesta dos líneas.
Meter las tres en la misma bolsa de «fechas inválidas» es el atajo que hace que un análisis pierda datos sin que nadie se entere.
Recetario
| Expresión | Qué hace | Cuándo |
|---|---|---|
pd.to_datetime(s, errors="coerce", format="%Y-%m-%d") | Series de fechas | Siempre con formato declarado |
errors="coerce" | Lo que no encaja pasa a NaT | Cuando ya sabés qué hay adentro |
dayfirst=True | Lee el día antes que el mes | Solo si no podés declarar el formato |
s.isna().sum() | Cuántos NaT | Después de cada pasada |
s.dt.year · .month · .day | Partes de la fecha | Columnas derivadas |
s.dt.dayofweek | 0 = lunes … 6 = domingo | Estacionalidad semanal |
s.dt.to_period("M") | Período mensual | Agrupar por mes |
s.dt.strftime("%d/%m/%Y") | Texto con formato | Al escribir el resultado, no antes |
(a - b).dt.days | Diferencia en días | Antigüedad, plazos |
read_csv(f, parse_dates=["fecha"]) | Convierte al leer | Cómodo, pero sin control del formato |