Mostrando entradas con la etiqueta Limpieza de datos. Mostrar todas las entradas
Mostrando entradas con la etiqueta Limpieza de datos. Mostrar todas las entradas

lunes, 25 de mayo de 2026

DATOS. Tratamiento de datos

Limpieza de datos (III)

Tratamiento de datos no estructurados (1)

En las entradas que preceden hemos concretado el tratamiento de datos a una tipología específica de éstos: los datos estructurados y los semi-estructurados, siendo que en ambos aspiramos a presentarlos difinitivamente en unos formatos que podemos identificar como tablas. Pero al tratar con datos no estructurados el panorama cambia radicalmente, ya que ahora estamos hablando de un flujo de texto que aunque a simple vista pueda parecer ordenado y limpio, para la máquina es caótico, está lleno de "ruido" y el acceso a la información relevante (los datos, en suma) está lejos de ser proceso simple. Por resumirlo: no es lo mismo enfrentarse a una tabla de datos tipo excel que contiene un listado de alumnos que al cuerpo textual de un informe psicopedagógico. En el primer caso deberemos resolver

Por continuar con la comparación, en términos de tratamiento y limpieza de datos, en el primer caso (la tabla excel), nos enfrentamos a tareas como tratar con celdas vacías, corregir datos erróneos, eliminar filas duplicadas... Pero el tratamiento (y limpieza) de datos no estructurados es mucho más complejo y crítico: además de la complejidad técnica, que debemos resolver sí o sí, si queremos operar con ellos, nos enfrentamos a las delimitaciones que conlleva la confidencialidad. La primera dimensión (la técnica) tiene como principal objetivo transformar el caos textual en un flujo homogéneo de datos cargados de significado; la segunda implica respetar estrictamente el principio de confidencialidad.

Es por esto que el tratamiento de datos no estructurados (textos complejos, fundamentalmente) se articula en las cuatro temáticas que aquí me limitaré a presentar brevemente para ir desarrollándolas en posteriores entradas, aunque sólo en función de y en la medida en que nos sea de utilidad para la intervención de los SEO, como corresponde a la filosofía de este blog.

  1. Limpieza de estructura y de "basura" textual
  2. Anonimización y respeto a la privacidad
  3. Normalización lingüística
  4. Segmentación semántica

La limpieza de estructura y ruido o basura textual tiene como objetivo eliminar todo aquello que los documentos digitales textual acumulan, normalmente en función de las prácticas de los usuarios y que, para éstos, ni siquiera son visibles, pero que están ahí y dificultan el tratamiento digital de los algoritmos en el proceso de automatización de procedimientos de acceso a los datos relevantes del mismo documento. Todo esto se concreta como...

  • la eliminación de números de página en mitad de un párrafo, encabezados repetidos o notas al pie descontextualizadas... que se "cuelan" inadvertidamente al extraer texto de diferentes tipos de archivo, como .pdf, .docx...
  • la normalización de espacios y códigos específicos, como la depuración de tabulaciones irregulares , saltos de línea múltiples y, cuando se producen, las inconsistencias en la codificación de determinados caracteres como tildes o como la "ñ"
  • el filtrado de elementos no semáticos, como código de script, html, urls o caracteres especiales que no aportan nada al análisis del contenido del texto

La anonimación nos permite proteger la confidencialidad mediante dos procedimientos: la detección de información personal y el uso de procedimientos de sustitución de esos datos por pseudonimos.

La detección de información de identificación personal (IIP o PII en inglés) se orienta a la identificación automatizada de nombres propios, números y códigos de identidad, direcciones postales, teléfonos o datos de contacto.

Pa pseudonimización (uso de "sinónimos") consiste en sustituir los datos identificativos por etiquetas normalizadas estructuradas, como, por ejemplo, sustituir "José Longinos Pacheco" por [PACIENTE_01] o [ESTUDIANTE_A]). Este procedimiento garantiza que el texto conserva la coherencia gramatical y la estructura contextual, pero eliminan el riesgo de fugas de información sensible.

La normalización lingüística, también llamada preprocesamiento NLP permite que los modelos de procesamiento lingüístico (semántico) trabajen con la máxima eficiencia y optimicen el uso de su ventana de contexto, reduciendo el consumo de tokens. Para ello el texto debe simplificarse a sus componentes esenciales, lo cual conseguimos mediante tres procedimientos: la estandarización, la lematización y la gestión de las palabras vacías.

La estandarización consiste en convertir el texto a minúsculas homogéneas, facilitando la identificación de términos que se prestan a variaciones de escritura, como podría ser el caso de, por ejemplo, "WISC", "Wisc" o "wisc".

La lematización y la reducción morfológica consiste en reducir las palabras flexivas a su raíz o lema de base. De este modo se unifican y reducen las variantes gramaticales de un mismo término, como "evaluando", "evaluó" o "evaluación", que se vinculan formalmente al verbo "evaluar". Esto facilita la indexación semántica, reduciendo el consumo de memoria de almacenamiento y de procesamiento del sistema.

Finalmente, la gestión de palabras vacías (o Stop Words) opera mediante el filtrado selectivo de artículos y preposiciones cuando se trabaja con motores de búsqueda clásicos, pero es menos necesaria (por lo que puede llegar a omitirse) cuando se emplean modelos LLM avanzados, para los que la sintaxis natural es relevante, ya que aporta matices contextuales valiosos.

Y finalmente, la segmentación semántica se orienta a afrontar el reto que supone la identificación de los datos contenidos en el texto, concretamente sus fronteras de inicio y final. Este reto no se presenta en los datos estructurados, ya que estos tienen límites claros (lo contenido en cada uno de los campos de cada uno de los registros), pero es vital en el procesamiento de los datos no estructurados, en los que se secesita deimitar la infornación con un criterio lógico.

No se trata, pues, de fragmentar el documento de manera ciega cada cierto número de palabras o caracteres; se trata de segmentar el texto respetando la arquitectura su semántica: aislar párrafos, delimitar secciones y diseñar ventanas de superposición (overlap) calculadas para asegurar que el hilo conductor y el contexto de la información no se destruyan durante la fragmentación.

Nota
1 Esta entrada ha sido elaborada a partir del resultado de una consulta realizada a IA-Gemini.

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.

sábado, 9 de mayo de 2026

DATOS. Tratamiento de datos

Limpieza de datos (I)

Datos faltantes

Es muy frecuente que en una colección de datos de cierta entidad se observen campos en blanco o con una marca sustituta (ver tabla abajo), a consecuencia de diversos motivos, incluídos los errores de recogida de datos y la simple ausencia de éstos por no producirse determinada respuesta. Lo que en realidad importa para su tratamiento posterior es que las ausencias sean razonablemente escasas; en caso contrario, la calidad del conjunto de datos se ve muy seriamente comprometida.

Entorno Elemento
Python puro None
Data Science (Pandas/NumPy) NaN
Base de datos (SQL) NULL
Lenguaje R NA

Las opciones disponibles para hacer frente a esta realidad son varias, desde la eliminación del registro hasta el relleno del campo con determinado valor, resultante de algún tipo de cálculo. Para conocer cuales son esas alternativas, además de consultar a la IA, que lo que hace es resumir el conjunto de posibilidades disponibles, sustituyendo así a una simple búsqueda web, también podemos consultar alguna página específicamente pensada para tratar este tema. Yo aquí te ofrezco un ejemplo de la segunda opción; la primera corre de tu cuenta.

No es por comodidad, pero en este caso voy a optar por la forma más simple de resolver el problema: eliminando el registro. No es precisamente la mejor solución, especialmente cuando no disponemos de muchos registros, pero sí la más adecuada para un caso como el que nos ocupa en el que no hay la pretensión de representatividad estadística ni interés por crear un procedimiento de (aprendizaje automático (AA). Además, en este caso, en la mayoría de los registros faltan todos los datos que pueden faltar, dado que son resultado de una opción de tratamiento del documento que ahora no me intersa comentar; sirva con decir que las tablas simplemente no están cumplimentadas, por lo que es hasta coherente eliminar estos registros. Puede que no lo sea tanto en los casos en que sólo falta la fecha, pero son muy escasos y el riesgo real de pérdida de información es mínimo.



import pandas as pd
import os
import numpy as np

# --- Función ---

def limpiar_tabla_especifica(ruta_absoluta):
    if not os.path.exists(ruta_absoluta):
        print(f"Error: No se localiza el archivo en {ruta_absoluta}")
        return

    try:
        df = pd.read_csv(ruta_absoluta, sep=';', engine='python')   # 1. Carga los datos con el separador identificado (sep=';')
        total_inicial = len(df)                                     # Conteo inicial para calcular los registros afectados

        df = df.replace(r'^\s*$', np.nan, regex=True)               # 2. Convertir espacios vacíos (" ") en nulos reales (NaN)

        columnas_a_validar = ['fecha', 'SEO', 'OE']                 # 3. Eliminar registros si falta dato en fecha-SEO-OE
        df_limpio = df.dropna(subset=columnas_a_validar)

        total_final = len(df_limpio)                                # 4. Cálcular los registros eliminados
        eliminados = total_inicial - total_final

        ruta_directorio = os.path.dirname(ruta_absoluta)            # 5. Exportar el resultado a csv
        nombre_salida = "lista_t1_sin_nulos.csv"
        ruta_salida = os.path.join(ruta_directorio, nombre_salida)

        df_limpio.to_csv(ruta_salida, sep=';', index=False)

        print("-" * 30)                                              # 6. Informe a mostrar por consola
        print("INFORME DE PROCESAMIENTO")
        print("-" * 30)
        print(f"Registros analizados: {total_inicial}")
        print(f"Registros eliminados (faltaba fecha, SEO o OE): {eliminados}")
        print(f"Registros válidos restantes: {total_final}")
        print(f"Archivo generado: {ruta_salida}")

    except Exception as e:
        print(f"Se produjo un error durante la ejecución: {e}")

# --- Llamada a la función ---

ruta_csv = r"" 														# Aqui la ruta de tu archivo csv
limpiar_tabla_especifica(ruta_csv)                                  # Llamada a la función


Mediante este script eliminamos los registros que carecen de datos en las columnas (variables) de interés columnas_a_validar = ['fecha', 'SEO', 'OE'], cosa que sucede gracias al uso de la función df.dropna() de Pandas (df_limpio = df.dropna(subset=columnas_a_validar)). Al especificar el identificador de las columnas hacemos que el script sea dependiente de la tabla, a la vez que nos permite identificar lo que deberemos cambiar si cambiamos de tabla.

Y hablando de especificidades, debo decir que para que el script funcione ha sido necesario especificar el separador de campos de la tabla (df = pd.read_csv(ruta_absoluta, sep=';', engine='python')), ya que el csv-base utiliza ; en vez del esperado (,) por defecto por Pandas.

El resultado tiene una doble expresión: archivo como csv (comentario 5 del script) se muestra una síntesis por consola (comentario 6). No proporciono ninguna prueba por innecesaria; es suficiente con que pienses en la última tabla de esta entrada sin el registro INF_003.dot.

miércoles, 22 de abril de 2026

DATOS. Limpieza de datos

Tablas Word (.docx) (II)

Conversión a Excel (xlsx)

Una vez que obtenemos las tablas de un documento .docx, una de las opciones de dar continuidad al procedimiento es transformar dichas tablas en un archivo .xlsx. Otra es hacerlo como.csv, pero la primera opción presenta ventajas sustantivas, como crear una estructura tabular clara para visualización directa, además de manejable desde un servicio de hoja de cálculo. Además incluye una ventaja más: los datos se almacenan como tipos de datos con formatos específicos (string, numéricos, fecha...), dado que así han sido procesados en el script (ver entrada anterior). Por todo esto, esta entrada es mera continuación de la precedente y aporta a aquella el archivo de las tablas y sus contenidos como hoja de cálculo .xlsx. Este es el script que lo hace posible:



# 0. Importamos la bibliotecas necesarias

import pandas as pd
from docx import Document
import os
import unicodedata
import warnings         #Importamos avisos para controlar...

warnings.simplefilter(action='ignore', category=FutureWarning)  # ... que los ocultamos explícitamente

# 1. Función para normalizar las cabeceras de las tablas y facilitar su tratamiento posterior

def normalizar_cabeceras(lista_cabeceras):
    nuevas_cabeceras = []
    for i, nombre in enumerate(lista_cabeceras):
        nombre = str(nombre).strip().lower()
        nombre = "".join(c for c in unicodedata.normalize('NFD', nombre) if unicodedata.category(c) != 'Mn')     # Limpieza básica
        nombre = nombre.replace(" ", "_").replace("/", "_") or f"columna_{i}"
        
        count = 0                            # Código para evitar duplicados (y el error 'duplicate keys') en el nombre de los campos
        temp_nombre = nombre
        while temp_nombre in nuevas_cabeceras:
            temp_nombre = f"{nombre}_{count}"
            count += 1
        nuevas_cabeceras.append(temp_nombre)
    return nuevas_cabeceras

# 2. Función para extraer los datos de las tablas evitando alteraciones previsibles (celdas combinadas)

def extraer_matriz_segura(tabla):
    matriz = []
    for fila in tabla.rows:                 # Extraer contenidos de las tablas evitando errores de celdas combinadas repetidas
        fila_datos = []
        for celda in fila.cells:
            fila_datos.append(celda.text.strip())
        matriz.append(fila_datos)
    
    if not matriz: return []                # Equilibrado de filas, dado que Pandas exige que todas midan lo mismo
    max_cols = max(len(f) for f in matriz)
    for f in matriz:
        while len(f) < max_cols:
            f.append("")                    # Rellenar huecos por celdas combinadas
    return matriz

# 3. Función para obtener las tablas del documento y su contenido

def extractor_universal(ruta_docx):
    try:
        doc = Document(ruta_docx)
        nombre_base = os.path.splitext(ruta_docx)[0]
        ruta_excel = f"{nombre_base}_DB.xlsx"
        lista_dfs = []

        print(f"=== ANALIZANDO: {ruta_docx} ===")

        for i, tabla in enumerate(doc.tables):
            matriz = extraer_matriz_segura(tabla)               # Llamada a función 2
            if len(matriz) < 1: continue

            cabeceras = normalizar_cabeceras(matriz[0])         # Normalizar cabeceras con función 1
            cuerpo = matriz[1:] if len(matriz) > 1 else []

            df = pd.DataFrame(cuerpo, columns=cabeceras)        # Crear el DataFrame de forma más segura
            
            for col in df.columns:                              # Convertir los datos numéricos
                df[col] = pd.to_numeric(df[col], errors='ignore')

            lista_dfs.append(df)
            print(f"Tabla {i+1}: Extraída con éxito ({df.shape[0]} registros).")
            print(df.head(3).to_string(index=False))

        if lista_dfs:                                            # Exportar a Excel, formato xlsx
            with pd.ExcelWriter(ruta_excel, engine='openpyxl') as writer:
                for idx, df in enumerate(lista_dfs):
                    df.to_excel(writer, sheet_name=f"Tabla_{idx+1}", index=False)
            print(f"\n[OK] Guardado en: {ruta_excel}")

    except Exception as e:
        print(f"\n[ERROR]: {e}")

# 4. Llamada a función principal

extractor_universal("tablas.docx")


Además de mostrar los datos por CMD, este script crea una hoja de cálculo Excel en formato .xlsx y almacena en ella las tablas en formato filas-coumnas y los datos que contienen, respetando el tipo de dato supuesto (string, numérico...). Esto tiene varias consecuencias para el tratamiento posterior:

  • se puede acceder a las tablas directamente desde la hoja de cálculo, pero también desde script mediante Pandas trabajando con los datos en función de su tipología.
  • el acceso se hace directamente a datos estructurados, dada la configuración de las tablas resultantes como tales, lo que facilita la manipulación de los datos mediante funciones asociadas a la biblioteca Pandas (y otras)

Además, mediante la función 1 (def normalizar_cabeceras():) evitamos posibles errores en el posterior tratamiento de las tablas. La segunda función (def extraer_matriz_segura()) permite extraer el contenido evitando los errores que derivan de la combinación de tablas.

Pero el núcleo del script sigue siendo la función principal (la 3, def extractor_universal(), que...

  • es a la que llamamdos desde el segmento de ejecución (4, extractor_universal("tablas.docx")), instrucción donde se pasa por parámetro el archivo .docx con el que trabajamos.
  • Esta función principal ejecuta el recorrido del documento, obtiene las tablas que contiene y sus datos...
  • ... asegurando su viabilidad mediante llamada a funciones auxiliares...
    • matriz = extraer_matriz_segura(tabla) y
    • cabeceras = normalizar_cabeceras(matriz[0]))
  • ... generando el dataframe (df = pd.DataFrame(cuerpo, columns=cabeceras))...
  • ... y crea el archivo .xlsx (with pd.ExcelWriter(ruta_excel, engine='openpyxl') as writer:)

El resultado es un .xlsx plenamente operativo, estructurado en hojas (una por tabla) que contienen tablas organizadas para que sus datos puedan ser tratados directamente como datos estructurados y que sea posible trabajar con ellos (consultar y manejo) manualmente, mediante lenguajes de macros o mediante python y sus bibliotecas.

Dada la simplicidad de contenido, pero sobre todo de organización de las tablas, el procedimiento de acceso a tablas y extracción de su contenido ha resultado satisfactorio, haciendo innecesario un tratamiento posterior para que sea posible trabajar directamente con el archivo resultante (.xlsx) y con las tablas que contiene.

jueves, 16 de abril de 2026

DATOS. Tratamiento de datos

Datos semi-estructurados

Estrategia básica Macro-Docap

Dejamos el procedimiento ofimático de converisón de las tablas pdf, pero sobre todo doc/docx/odt expresado como tabla-registro en nuestra hoja de cálculo, pendiente de su última transformación: de vertical a horizontal, a fin de presentarse como una tabla de doble entrada en la que cada columna es un campo o variable y cada fila una colección de datos o registro. Este último paso es más que una formalidad, ya que la recuperación y el tratamiento posterior de los datos y la propia creación de la base de datos depende de ello.

Para realizar este cambio Calc dispone de la función Trasponer, el cual IA-Gemini explica del siguiente modo:

  • Primero se selecciona el rango de datos que se quiere transponer
  • Clic derecho + opción Copiar
  • Posicionarse en la celda de destino que ocupará el primer campo de la base de datos
  • Hacer clic derecho y elegir Pegado especial
  • Seleccionar la opción emergente Transponer y hacer clic izquierdo en esa opción

Una vez resuelto el problema de la direccionalidad de este primer registro (y creada de paso la estructura de campos de la base de datos, la adición de nuevos registros nos obliga a procesar las celdas de la configuración vertical de un modo ligeramente diferente:

  • Generamos el nuevo registro (vertical) completo (etiqueta+dato) en la hoja 1 (igual que hicimos antes
  • Pero sólo seleccionamos de esta estrucura la columna de datos, siguiendo el procedimiento anterior
  • En la hoja 2 (base de datos) nos posicionamos en la celda que ocupará el dato del primer campo de este nuevo registro y ejecutamos la misma secuencia de acciones que cuando creamos la base de datos
  • Deberemos respetar escrupulosamente este procedimiento para evitar sustituir un registro antiguo por otro nuevo

Esta es la solución ofimática o manual, pero una vez que nuestros datos básicos ya están en una hoja de cálculo no resulta especialmente complicado automatizar la creación y la actualización de la base de datos (disposición tabular horizontal) mediante una macro y/o un script OOo Basic o la combinación de ambos. Obsérvese que lo que estoy planteando ahora supone un cambio importante respecto al procedimiento manual-ofimático anterior: ahora hablamos de automatizar el procedimiento, no de aplicar sistemática y repetidamente una secuencia de acciones.

Esta alternativa no se diferencia mucho de la que hemos aplicado a la creación de DocAp complejos de evaluación, basados en el desarrollo de un gestor sobre Calc que, entre otras funciones tiene la de recoger sistemáticamente los datos de la aplicación a modo de tabla de datos vertical y, postriormente, trasladarlos a una base de datos acumulativa diseñada en horizontal. Todo ello se basa en un conjunto de script que aquí deberán ser diferentes para cada manipulación de tablas-Word que deseemos realizar, pero como todos ellos se basan en el mismo procedimiento, no será complicado generar un código-base, el cual se ajustará lo necesario para adaptarlo a la nueva base de datos.

La automatización puede dividirse en dos fases y empezar con la selección y copia del contenido del documento .odt, pero lo fundamental se desarrolla asociado a la hoja de cálculo y se basa en un mecanismo básico de captura de datos y escritura de contenido, ambos en base a matriz y bucle. Dado que aquí no estamos desarrollando un procedimiento concretoy completo, me limitaré a construir el código básico de la automatización, el cual responde en esencia a lo que acabo de describir.

Este código está dividido en dos script que aquí creé mediante Grabar macro, en parte para ilustrar la forma más básica de automatizar el procedimiento, por decirlo de alguna manera, el procedimiento más "ofimático". El primer script genera la cabecera de la base de datos (listado de etiquetas de los campos)...



sub CrearEncabezados
rem ----------------------------------------------------------------------
rem definir variables
dim document   as object
dim dispatcher as object
rem ----------------------------------------------------------------------
rem Acceder al dispatcher
document   = ThisComponent.CurrentController.Frame
dispatcher = createUnoService("com.sun.star.frame.DispatchHelper")
rem Acceder a la hoja 1
dim args1(0) as new com.sun.star.beans.PropertyValue
	args1(0).Name = "Nr"
	args1(0).Value = 1
dispatcher.executeDispatch(document, ".uno:JumpToTable", "", 0, args1())

rem Seleccionar las celdas a copiar------------------------------------------
dim args2(0) as new com.sun.star.beans.PropertyValue
	args2(0).Name = "ToPoint"
	args2(0).Value = "$A$1:$A$20"
dispatcher.executeDispatch(document, ".uno:GoToCell", "", 0, args2())

rem Copiar el contendio de esas celdas----------------------------------------
dispatcher.executeDispatch(document, ".uno:Copy", "", 0, Array())

rem Posicionarse en la hoja 2----------------------------------------------------
dim args3(0) as new com.sun.star.beans.PropertyValue
	args3(0).Name = "Nr"
	args3(0).Value = 2
dispatcher.executeDispatch(document, ".uno:JumpToTable", "", 0, args3())

rem Posicionarse en la celda A1-----------------------------------------------------
dim args4(0) as new com.sun.star.beans.PropertyValue
	args4(0).Name = "ToPoint"
	args4(0).Value = "$A$1"
dispatcher.executeDispatch(document, ".uno:GoToCell", "", 0, args4())

rem Copiar (transponiendo) el contenido copiado ---------------------------------
dispatcher.executeDispatch(document, ".uno:PasteTransposed", "", 0, Array())

end sub


... y el segundo copia el contenido del registro.



sub CrearRegistro
rem ----------------------------------------------------------------------
rem definir variables
dim document   as object
dim dispatcher as object
rem ----------------------------------------------------------------------
rem Acceder al dispatcher
document   = ThisComponent.CurrentController.Frame
dispatcher = createUnoService("com.sun.star.frame.DispatchHelper")

rem Acceder a la hoja 1
dim args1(0) as new com.sun.star.beans.PropertyValue
	args1(0).Name = "Nr"
	args1(0).Value = 1
dispatcher.executeDispatch(document, ".uno:JumpToTable", "", 0, args1())

rem Posicionados en la hoja 1, seleccionar las celdas B1:B20-----------------
dim args2(0) as new com.sun.star.beans.PropertyValue
	args2(0).Name = "ToPoint"
	args2(0).Value = "$B$1:$B$20"
dispatcher.executeDispatch(document, ".uno:GoToCell", "", 0, args2())

rem Copiar el contendio de esas celdas----------------------------------------
dispatcher.executeDispatch(document, ".uno:Copy", "", 0, Array())

rem Posicionarse en la hoja 2----------------------------------------------------
dim args3(0) as new com.sun.star.beans.PropertyValue
	args3(0).Name = "Nr"
	args3(0).Value = 2
dispatcher.executeDispatch(document, ".uno:JumpToTable", "", 0, args3())

rem Posicionarse en la celda A2-----------------------------------------------------
dim args4(0) as new com.sun.star.beans.PropertyValue
	args4(0).Name = "ToPoint"	
	args4(0).Value = "$A$2"
dispatcher.executeDispatch(document, ".uno:GoToCell", "", 0, args4())

rem Copiar (transponiendo) el contenido copiado---------------------------------
dispatcher.executeDispatch(document, ".uno:PasteTransposed", "", 0, Array())

end sub


Como puedes ver, ambos script son prácticamente el mismo, y de hecho podríamos trabajar sólo con uno, realizando las mínimas modificaciones de posicionamiento, pero me ha parecido preferible diferenciar la creación de la base de datos (en realidad, la escritura de las etiquetas de los campos) de la copia del contenido del registro. Cierto que así el primer script sólo se modifica cuando se crea el proyecto concreto (el intervalo de celdas en la Hoja1: args2(0).Value = "$A$1:$A$20"), mientras que el segundo, además del asjuste del intervalo de celdas (args2(0).Value = "$B$1:$B$20") al inicio del proyecto, es necesario modificar también la identificación de la posición de inicio de copia transpuesta en Hoja2 (args4(0).Value = "$A$2") cada vez que creamos un nuevo registro en la base de datos.

La solución anterior no está completa, ya que se puede automatizar más y mejor, pero es suficiente para ilustrar lo que intento decir, así que para este objetivo es suficiente.

Como ves, los datos semi-estructurados, una vez que es posible manejarlos desde una hoja de cálculo, también es posible automarizar su conversión en datos estructurados, al menos en términos de forma. Otra cosa es qué se puede necesitar para realizar esta automatización (en esta entrada hemos visto la forma más simple de hacerlo) y qué limpiezas posteriores requieran ya formalmente como datos estructurados. Pero estas son otras cuestiones.

viernes, 27 de febrero de 2026

Datos

Fases del proceso

De forma resumida y para empezar, podemos decir que en el procesamiento de datos se pueden diferenciar tres fases: acceso, limpieza y análisis. Esta diferenciación, en lo que al orden de sucesión se refiere, es, con frecuencia, más formal que real, ya que no siempre sus fases se presentan en la secuencia expuesta. No obstante, la diferenciación es conceptualmente válida y necesaria.

La fase primera, la de acceso, es aquella en la que se recopilan los datos de la o las fuentes. Se trata de una fase muy sensible por la complejidad técnica que conlleva y por la necesaria atención a cuestiones de tipo ético-legal.

Puede ser que para los SEO, por lo que nos interesa y por el campo en que nos movemos, muchas de esas complejidades se simplifiquen bastante, pero también se expresan de forma radical. Por ejemplo, debemos garantizar absolutamente la confidencialidad de los datos, de modo que en ningún caso sean expuestos en la red, al menos (y sólo en determinadas circunstancias) sin antes haber sido sometidos a un riguroso de proceso de anonimación.

Salvadas estas cuestiones, como en cualquier otro campo de la investigación y de la intervención, deberemos resolver satisfactoriamente los problemas técnicos que derivan de las fuentes, empezando por los relativos al acceso.

Compartimos también con quienes trabajan con datos, el interés por que éstos sean fiables y valiosos; también porque sean suficientes, pero en esto nuestro interés no conlleva necesidades que sí afectan a otros campos. Para nosotros es más importante la calidad que la cantidad... dentro de ciertos límites de suficiencia, por supuesto.

La limpieza y depuración (data cleaning) busca mejorar la calidad de los datos, eliminando todo aquello que dificulta el posterior análisis (el "ruido") y corrigiendo errores. Esta fase consiste en...

  • Eliminar los datos duplicados y/o irrelevantes,
  • Dar a los valores faltantes (missing Data) el tratamiento que resulte más adecuado, bien sea por eliminación, bien por imputación (mediante qué procedimiento)
  • Corregir los errores estructurales: homogeneizar formatos, corregir errores tipográficos, estandarizar categorías y unificar unidades de medida.
  • Decidir el tratamiento de los valores atípicos (outliers) que pueden llegar a sesgar el análisis.
En resumen, la limpieza de datos garantiza la fiabilidad de los datos y, en consecuencia, de los resultados del análisis (fase siguiente del proceso), reduce el sesgo y asegura la compatibilidad entre datos.

La tercera fase, el análisis de datos (data analysis), es el proceso de inspeccionar, modelar y transformar los datos para generar información útil que sirva para la comprensión de los fenómenos y/o la toma de decisiones.

Los tipos de análisis básicos son los siguientes:

  • El análisis descriptivo, que, como su nombre indica, describe los datos y, en su caso, los resume mediante diversos estadísticos (descriptivos).
  • El análisis exploratorio (EDA), usado para entender la estructura de los datos, encontrar patrones y detectar anomalías.
  • El análisis de diagnóstico, que consiste en investiga por qué ha pasado algo, buscando relaciones causa-efecto.
  • El análisis predictivo, que utiliza datos históricos para predecir que podría suceder en el futuro.
  • Y el análisis prescriptivo, que partiendo de las predicciones, sugiere acciones para aprovecharlas o para reducir su probabilidad.

martes, 17 de febrero de 2026

DATOS

Objetivos del tratamiento de datos

Los datos son la base de todo programa informático, de los clásicos y de los basados en soluciones IA; para estos últimos aun más que para los primeros, puesto que los datos constituyen la base misma sobre la que se sustenta toda su arquitectura.

Tres son las áreas de trabajo que planteamos en esta sección: el acceso a los datos, su limpieza o tratamiento preparatorio y el análisis de datos.

El acceso a datos conlleva mucha más complejidad de lo que en principio se podría pensar, por lo que deberemos dar respuesta a sus diferentes condicionantes, entre los que se incluye la variedad de fuentes y sus implicaciones, el tipos de datos y el modo en que se presentan (no sólo en cuanto al soporte documental, que también), además de la diversidad de objetivos y de planteamientos de trabajo que nos propongamos desarrollar. De todo este conjunto de factores deriva la extensión y el peso de esta temática dentro de la sección.

La limpieza de datos es necesaria para disponer de datos de calidad, lo cual va a permitir el posterior desarrollo de nuestro análisis. En el caso de la automatización de procesos y/o del desarrollo de soluciones basadas en la IA esta calidad de los datos es fundamental, de ahí la importancia del correcto tratamiento preparatorio de nuestros datos.

Finalmente podemos considerar el análisis de datos junto con la automatización de textos, como razón de ser de este blog. En esta tercera subsección trataremos sobre las herramientas y las estrategias que facilitan este análisis. En otra sección plantearé líneas de concreción de estas prácticas adaptadas al trabajo de los SEO.