viernes, 15 de mayo de 2026

DATOS. Tratamiento de datos

Limpieza de datos (II)

Normalización de datos

Cuando en la tabla-formulario de un documento de texto cabe la posibilidad de que los datos se expresen de diferentes maneras, existe una elevada probabilidad de que así sea. Aunque predomine una determinada forma, vamos a decir que canónica, es altamente probable que también se empleen otras formas que no lo son tanto. Un buen ejemplo de ello lo tienes en la tabla que sigue.

Como puedes ver en este ejemplo ficticio, que no lo es en este aspecto de la variabilidad de expresión de la fecha, junto a la forma canónica (círculo naranja), nos encontramos con otras forma diferentes (por ejemplo, pero no sólo, la marcada con el círculo verde). Aunque para un análisis visual de los datos esto no supone ningún problema (salvo en casos muy concretos), para el tratamiento automatizado de los datos se convierte en un serio problema: esas fecha no-canónicas no son reconocidas como tales fechas. Para evitar la pérdida de datos que esto implica, es necesario convertir estos datos al formato fecha. Podemos hacerlo manualmente, con ayuda de las funciones de formato de Calc (o Excel), aunque esto nos llevará tiempo. También contamos con la opción de automatizar el reajuste de formato mediante un script Python como el que sigue.



# 0. Bibliotecas y módulos ---

import pandas as pd
import re
from datetime import datetime

# 1. Funciones ---

# 1.a Función secundaria. Transforma string en fechas ---

def normalizar_fecha(texto):

    if pd.isna(texto) or str(texto).strip() == "":
        return None
    
    dato = str(texto).lower().strip()
    
    meses_map = {
        'enero': 1, 'febrero': 2, 'marzo': 3, 'abril': 4, 'mayo': 5, 'junio': 6,
        'julio': 7, 'agosto': 8, 'septiembre': 9, 'octubre': 10, 'noviembre': 11, 'diciembre': 12
    }

    try:
        return pd.to_datetime(dato, dayfirst=True).date()     # Intento 1: Formato numérico completo (ej. 2/02/2026)
    except:
        pass
    anio_detectado = re.search(r'(\d{4})', dato)               # Intento 2: Formato parcial (ej. enero 2014)
    if anio_detectado:
        anio = int(anio_detectado.group(1))
        for mes_nombre, mes_numero in meses_map.items():
            if mes_nombre in dato:
                return datetime(anio, mes_numero, 1).date()      # Retornamos siempre el día 1 del mes encontrado
    return None                                                  # Si llega aquí, no se pudo convertir y devolvemos None

# 1.b Función principal ---

def procesar_limpieza(ruta_entrada, columna_objetivo, ruta_salida):
    
    try:
        print(f"Cargando archivo: {ruta_entrada}...")
        df = pd.read_csv(ruta_entrada, sep=';', encoding='utf-8')
        df['fecha_corregida'] = df[columna_objetivo].apply(normalizar_fecha)                # Aplicamos la normalización (funcion 1a)
        df['fecha_corregida'] = pd.to_datetime(df['fecha_corregida'], errors='coerce')      # Convertimos a datetime Pandas (AAAA-MM-DD)
        df.to_csv(ruta_salida, sep=';', index=False, encoding='utf-8')                      # Exportamos el resultado (csv)
        print(f"Éxito: Archivo '{ruta_salida}' generado con fechas estandarizadas.")
        
    except Exception as e:
        print(f"Error durante el proceso de código: {e}")

# 2. Script principal (Llamada a la función principal) ---

if __name__ == "__main__":

    archivo_origen = r''            # Aquí la ruta del archivo csv de trabajo
    columna_fechas = 'fecha'        # Aquí el nombre de la columna a procesar
    archivo_destino = r''      		# Aquí el nombre del csv resultante
    
    procesar_limpieza(archivo_origen, columna_fechas, archivo_destino) # Llamada a la función


La estructura del script es relativamente simple: dos funciones y un script. El script llama a la función principal pasando parámetros (procesar_limpieza(archivo_origen, columna_fechas, archivo_destino)) que el profesional asigna a variables (vg. archivo_origen = r'') y la función principal hace uso de la secundaria para tratar el contenido textual del campo y devolver un formato fecha del módulo datetime en formato Pandas (AAA-MM-DD).

La función principal es sumamente importante: (1) Carga el csv de trabajo, (2) Llama a la función secundaria para normalizar el formato (3) convierte el dato devuelto por esa función al formato datetime de Pandas y (4) genera el scv de salida.

  1. df = pd.read_csv(ruta_entrada, sep=';', encoding='utf-8')
  2. df['fecha_corregida'] = df[columna_objetivo].apply(normalizar_fecha)
  3. df['fecha_corregida'] = pd.to_datetime(df['fecha_corregida'], errors='coerce')
  4. df.to_csv(ruta_salida, sep=';', index=False, encoding='utf-8')

Pero la función secundaria no es menos interesante, como veremos a continuación. Recordemos:



def normalizar_fecha(texto):

    if pd.isna(texto) or str(texto).strip() == "":
        return None
    
    dato = str(texto).lower().strip()
    
    meses_map = {
        'enero': 1, 'febrero': 2, 'marzo': 3, 'abril': 4, 'mayo': 5, 'junio': 6,
        'julio': 7, 'agosto': 8, 'septiembre': 9, 'octubre': 10, 'noviembre': 11, 'diciembre': 12
    }

    try:
        return pd.to_datetime(dato, dayfirst=True).date()     # Intento 1: Formato numérico completo (ej. 2/02/2026)
    except:
        pass
    anio_detectado = re.search(r'(\d{4})', dato)               # Intento 2: Formato parcial (ej. enero 2014)
    if anio_detectado:
        anio = int(anio_detectado.group(1))
        for mes_nombre, mes_numero in meses_map.items():
            if mes_nombre in dato:
                return datetime(anio, mes_numero, 1).date()      # Retornamos siempre el día 1 del mes encontrado
    return None                                                  # Si llega aquí, no se pudo convertir y devolvemos None



En esta función podemos diferenciar varias partes:
  • Primero un procedimiento de control para cuando la variable texto está vacía o no es válida
  • A continuación la variable texto se convierte a str y se normaliza a minúscula, pasando esta doble conversión a la varible dato
  • Después construímos una diccionario de meses:numerales meses_map para capturar el contenido del campo que procesamos y que a esta función hemos pasado como parámetro def normalizar_fecha(texto)

Lo que viene a continuación es el núcleo central de la función, que se desarrolla dentro de una estructura de control de excepciones try...except, en la que la parte try controla la presencia en dato de la formulación de la fecha según el formato dd/mm/aaaa y la transformar una entrada un objeto de fecha puro Año-Mes-Día de la librería Pandas. La parte except controla la posible comisión de errores

Si la primera solución no resolvió el procesamiento de la variable, la segunda utiliza la expresión regular re.search(r'(\d{4})', dato) para localizar y obtener el dato año (4 dígitos dentro del texto) y si lo encuentra recorre el listado de meses para ver si el término nombre del mes está escrito en dato. Si lo está, sobre el dato año y la conversión numérica del dato mes se construye una fecha, usando 1 como dato día.

Con este script hemos normalizado reformulandolo el campo fecha para que sea posible trabajar con esta variable en la fase de análisis de datos. El resultado es una nueva columna o campo (fecha_corregida). Cierto que que no todos los datos se han podido modificar según lo esperado debido a que el dato original no se ajusta a las formas esperadas. esto puede dar lugar a una nueva pérdida de datos, aunque al ser una circunstancia controlada y de relativa poca importancia, también podemos optar por realizar una última revisión "manual" a fin de evitar una pérdida de datos innecesaria y no deseada. Llegados a este punto, no es una mala solución.