Saltar al contenido

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

EjemploUna fecha en un archivo es texto hasta que la convertis
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 (7.3 kB): 180 filas de ventas de enero a marzo.

EjemploConvertir la columna fecha de un archivo real
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

EjemploSin format, pandas adivina, y adivinar es peligroso
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.
Esta es la trampa más cara de la lección

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

EjemploDos pasadas para rescatar las fechas en otro formato
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

Paso a pasoDos pasadas de to_datetime, y lo que queda afuera
  1. 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")
  2. 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")
  3. La celda vacia se vuelve NaT, que es el NaN de las fechas. Hasta aca nada raro.
    # vacio -> NaT
  4. 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
  5. 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
  6. 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)
  7. 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

EjemploEl accesor .dt: 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

AttributeError: Can only use .dt accessor with datetimelike values

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.

ValueError: time data "15/03/2026" doesn't match format "%Y-%m-%d"

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.

(sin error) el informe pierde un mes entero

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).

(sin error) las fechas quedan intercambiadas día por mes

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.

(sin error) ordenar por fecha da un orden raro

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

Qué hacer con las fechas que no se pudieron convertir

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

Fechas
ExpresiónQué haceCuándo
pd.to_datetime(s, errors="coerce", format="%Y-%m-%d")Series de fechasSiempre con formato declarado
errors="coerce"Lo que no encaja pasa a NaTCuando ya sabés qué hay adentro
dayfirst=TrueLee el día antes que el mesSolo si no podés declarar el formato
s.isna().sum()Cuántos NaTDespués de cada pasada
s.dt.year · .month · .dayPartes de la fechaColumnas derivadas
s.dt.dayofweek0 = lunes … 6 = domingoEstacionalidad semanal
s.dt.to_period("M")Período mensualAgrupar por mes
s.dt.strftime("%d/%m/%Y")Texto con formatoAl escribir el resultado, no antes
(a - b).dt.daysDiferencia en díasAntigüedad, plazos
read_csv(f, parse_dates=["fecha"])Convierte al leerCómodo, pero sin control del formato

Ponete a prueba

1. ¿Qué es NaT?Nivel 1 · Recordar
2. Sobre una columna de fechas ya convertida, ¿qué devuelve s.dt.year?Nivel 1 · Recordar
3. pd.to_datetime(pd.Series(["2026-02-30"]), errors="coerce", format="%Y-%m-%d")
¿Qué devuelve?Nivel 2 · Predecir la salida
4. pd.to_datetime(pd.Series(["01/02/2026"]), dayfirst=True) ¿qué fecha da?Nivel 2 · Predecir la salida
5. El archivo tiene casi todas las fechas en YYYY-MM-DD y unas pocas en DD/MM/YYYY. Querés convertir todas sin perder ninguna. ¿Cuál conviene?Nivel 3 · Elegir entre dos casi correctas
6. ¿Por qué conviene pasar format= aunque las fechas ya se conviertan bien sin él?Nivel 3 · Elegir entre dos casi correctas
7. Este código agrupa ventas por mes y da un mes de menos. ¿Por qué?
ventas = pd.read_csv("ventas_mensuales.csv")
fecha = pd.to_datetime(ventas["fecha"], errors="coerce",
                       format="%Y-%m-%d")
ventas["mes"] = fecha.dt.to_period("M")
print(ventas.groupby("mes").size())
Nivel 4 · Encontrar el defecto

Prácticas de esta lección