Mostrando entradas con la etiqueta Python. Mostrar todas las entradas
Mostrando entradas con la etiqueta Python. Mostrar todas las entradas

miércoles, 23 de septiembre de 2026

NÚMEROS. Python

Medidas de tendencia central (II)


Estadística descriptiva

Con Pandas podemos calcular las medidas de tendencia central (media, mediana y moda) sobre un DataFrame de forma rápida y directa. Estas funciones se integran de forma natural con la capacidad de la librería para manipular y filtrar conjuntos de datos.

Aunque en proyectos reales cargaremos los datos desde fuentes externas (como archivos CSV), para facilitar el aprendizaje aquí crearemos un DataFrame directamente a partir de un diccionario:



import pandas as pd

# Creamos un DataFrame de prueba
data = {
    'Nombre': ['Ana', 'Luis', 'Carlos', 'Marta', 'Sofía', 'Pedro', 'Elena'],
    'Edad': [25, 30, 25, 40, 30, 25, 35],
    'Nota': [2.5, 3.1, 2.5, 4.2, 3.1, 2.1, 3.8]
}

df = pd.DataFrame(data)


A partir de este conjunto de datos, podemos aplicar los principales estadísticos descriptivos tanto a columnas individuales como a todo el conjunto de datos numéricos de forma simultánea:



#---------------------------------------------------------------
#Medidas de tendencia central
#----------------------------------------------------------------

# Promedio -----------------------------

# Media de una sola columna
nota_media = df['Nota'].mean()
print(f"Nota media: {nota_media:.2f}")

# Media de todas las columnas numéricas del DataFrame
media_total = df.mean(numeric_only=True)
print("\nMedia de las columnas numéricas:\n", media_total)

# Mediana-----------------------------

# Mediana de una sola columna
mediana_nota = df['Nota'].median()
print(f"Mediana de la nota: {mediana_nota}")

# Mediana de todas las columnas numéricas
mediana_total = df.median(numeric_only=True)
print("\nMediana de todas las columnas numéricas:\n", mediana_total)

# Moda---------------------------------------

# Moda de una columna específica (devuelve una Series)
moda_nota = df['Nota'].mode()[0]  # Tomamos el primer elemento con [0]
print(f"Moda de la nota: {moda_nota}")

# Moda de todo el DataFrame
moda_total = df.mode()
print("\nModa de las columnas:\n", moda_total)


Calcular estos valores sobre todo el DataFrame a la vez refleja el verdadero potencial de Pandas para procesar colecciones de datos sin necesidad de iterar manualmente sobre ellas.

Además de los métodos individuales, disponemos del método describe(), que genera un resumen con los principales estadísticos descriptivos (incluyendo recuento, desviación estándar y percentiles):



resumen = df.describe()
print(resumen)


Si necesitamos un control más preciso sobre qué métricas aplicar a cada columna, podemos utilizar el método agg():



# Agregación personalizada con agg() sobre columnas existentes

def primera_moda(x):
    m = x.mode()
    return m.iloc[0] if not m.empty else None

estadisticos = df[['Edad', 'Nota']].agg(['mean', 'median', primera_moda])
estadisticos.rename(index={'primera_moda': 'mode'}, inplace=True)

print("\nAgregación personalizada (Edad y Nota):")
print(estadisticos)


En conclusión, Pandas ofrece diversas alternativas para obtener estadísticos descriptivos de manera eficiente, adaptándose tanto a análisis puntuales sobre una columna como a resúmenes globales sobre grandes volúmenes de datos.

martes, 22 de septiembre de 2026

NÚMEROS. Python

Pandas


Bibliotecas Python

Al contrario que NumPy, Pandas sí tienen un interés directo para la intervención de los SEO, tanto por sus capacidades para el trabajo con tablas de datos, ese conjunto de datos que tanto uso tiene en nuestra práctica profesional y que bajo Pandas pasa a identificarse como DataFrame, como por servirnos de base para la elaboración y la presentación de la información, en este caso complementado por bibliotecas orientadas a la representación gráfica.

Además de estos usos directos o como soporte de base, Pandas también tiene utilidad para que desde la Orientación educativa en proyectos dirigidos al desarrollo de recursos de utilidad en este campo, nos planteemos generar herramientas basadas en el Machine Learning. En concreto Pandas sirve para facilitar la limpieza y preparación de los dados con los que entrenar nuestros modelos y para la manipulación, transformación y creación de variables. Como podemos apreciar, estamos hablando del enfoque o modelo estadístico de la IA, fundamental en los planteamientos actuales de un ML orientado a la resolución de problemas prácticos también en nuestro ámbito profesional.

Pandas es una biblioteca de uso muy frecuente en el análisis de datos y en su tratamiento estadístico. Te recomiendo consultar su documentación oficial

Por lo que se refiere al uso práctico de Pandas, además de lo ya visto al hablar de los DataFrame en la sección LENGUAJES, en la presente nos centraremos en el uso de Pandas como herramienta para el análisis estadístico, por lo que me limito aquí a su mera presentación.

LENGUAJES. Python

DataFrame (Pandas) (II)

Colecciones de datos

En la entrada anterior creamos un DataFrame partiendo de un diccionario integrado en el propio script. Esta fórmula es adecuada desde el punto de vista didáctico, ya que ayuda a comprender la conexión entre las estructuras nativas de Python y las de Pandas, así como a experimentar con sus primeras manipulaciones. Sin embargo no refleja la dinámica de trabajo habitual.

En esta segunda entrada vamos a proceder del modo en que es más común trabajar, ya que en los proyectos reales, los datos residen en fuentes externas, como archivos CSV u hojas de cálculo. Mediante funciones de lectura se cargan los datos en memoria en forma de DataFrame, lo que permite separar el almacenamiento de los datos y su procesamiento. Esto mejora el flujo de trabajo y permite aprovechar al máximo la potencia analítica de Pandas.

Para el caso vamos a utilizar el archivo Avandono_escolar.csv, obtenido de la página web del MEFPD, sobre el cual realizaremos diferentes manipulaciones mediante Pandas, una vez convertido en un DataFrame de trabajo.

Empezaremos por acceder al archivo .cvs (1) para cargarlo en nuestro script, siguiendo lo que te propongo a continuación.


import pandas as pd

# 1. Definir la ruta del archivo CSV
ruta_archivo = r'directorio_de_archov.csv' #Sustituye por tu ruta

# 2. Leer el archivo CSV y cargarlo en un DataFrame
df = pd.read_csv(ruta_archivo, sep=';', decimal=',')


Lo primero que deberás hacer es sustituir directorio desde el que se cargará el archivo .csv. Después debes prestar atención a la instrucción mediante la que se carga ese archivo con DataFrame df = pd.read_csv(ruta_archivo, sep=';', decimal=','), ya que contiene dos especificaciones que son necesarias teniendo en cuenta las características de nuestro archivo de origen, dado que los campos están separados por ; y se utiliza la , como separador de la parte decimal del número. Normalmente separamos los campos por , y en los números empleamos . como separador de decimales, por ello la instrucción normal de carga es más simple df = pd.read_csv(ruta_archivo), pero es este caso fallaría (2)

Ahora podremos, por ejemplo, visualizar los primeros y los últimos registros de nuestro DataFrame, lo que nos sirve para confirmar que la carga ha resultado exitosa y ajustada a la estructura del DataFrame. Para ello añadiremos el siguiente código al script.



# 3. Mostrar las primeras cinco filas del DataFrame
print("Primeras 5 filas y últimas 5 filas del DataFrame:")
print(df)

print("\nTipos de datos asignados por Pandas:")
print(df.dtypes)


Si sólo deseamos ver las primeras filas usamos la instrucción print(df.head()) y si añadimos una cantidad dentro de la función print(df.head(10)) esteremos explicitando cuantas filas queremos ver (3)

Visualizar el DataFrame puede ser interesante para conocer su contenido y estructura, pero muy posiblemente ambos nos sean ya conocidos (4). Lo que realmente hace útil a Pandas son las posibilidades que nos ofrece de manipular el contenido del DataFrame, utilizando filtros para seleccionar parte del mismo, realizando cálculos sobre sus datos o generando nuevas columnas a partir de las existentes. Veamos en el código que sigue algunas de estas opciones.



#Ejemplos de trabajos sobre el DataFrame

# 1. Filtros. Aplicar un filtro con dos condiciones combinadas con '&'. Cada condición DEBE ir encerrada entre paréntesis (...)
sub_df = df[(df['Territorio'] == 'TOTAL') & (df['Sexo'] == 'Ambos')] #Establecemos los filtros
sub_df = sub_df.sort_values(by='periodo', ascending=True) # Reordenamos el DataFrame en orden ascendente
n = len(sub_df) # Obtenemos el número de registros (filas) del DataFrame para su posterior visualización

# 2. Cálculos. Calcular la diferencia interanual sobre la columna del valor ('Total %'). diff() resta a la fila actual el valor de la fila anterior
sub_df['dif'] = sub_df['Total %'].diff().round(2)

# 3. Hacer una copia explícita (Buena práctica recomendada en Pandas). Así evitamos avisos tipo 'SettingWithCopyWarning' si se modifica el sub_df
sub_df = sub_df.copy()

# 4. Visualizar el resultado
print(f"Filas resultantes en el sub-dataframe: {len(sub_df)}")
print(sub_df.head(n))


Finalmente, nos puede interesar guardar un archivo CSV con el trabajo realizado, creando un nuevo archivo (sin modificar el original) con los resultados de nuestro trabajo. Para ello añadiremos al script el código que sigue (4).



# Guardamos el sub-DataFrame como csv

# 1. Definir la ruta completa del nuevo archivo CSV (en la misma carpeta)
ruta_salida = r'C:\Mi_Ruta\Nuevo_Archivo.csv'

# 2. Guardar el sub-DataFrame
sub_df.to_csv(ruta_salida, sep=';', decimal=',', index=False, encoding='utf-8-sig')

print(f"El archivo se ha guardado correctamente en: {ruta_salida}")


Mediante Pandas es posible realizar manipulaciones de un DataFrame mucho más complejas que las explicadas en estos script, lo que permite realizar estudios diversos sobre los datos que contiene. También podemos construir nuevos DataFrames a partir de otros combinando sus datos, siempre que sus estructuras y contenidos lo permitan. Todo ello implica concretar un plan de trabajo, lo que es más propio de un proyecto de análisis que de una entrada como la actual, pensada para explicar la naturaleza de la colección de datos llamada DataFrame.

Otro campo de interés en el trabajo con estas colecciones de datos usando Pandas es la ejecutar funciones estadísticas sobre sus datos, pero esto lo dejaremos para tratarlo en otra sección. De momento esta la damos por cerrarda aquí.

NOTAS
1 Para lo que previamente habremos descargado dicho archivo desde el enlace que ofrezco en el párrafo anterior o, si se prefiere, desde el enlace a la web del MEFPD. En ambos casos deberemos haber guardado el archivo con .csv en un determinado directorio. Esa ubicación será la que sustituya a que empleamos en el script a modo de ejemplo.
2 Fallaría no en el momento de la carga, sino al trabajar con el DataFrame. Por eso doy toda esta explicación para evitarte tener que investigar sobre esta cuestión. En todo caso, es conveniente que conozcas de antemano cómo está construído tu archivo .csv para que ajuste la instrucción de carga.
3 En este caso veremos 10 filas o registros. Para ver sólo las últimas filas empleamos print(df.tail())
4 Ya sabes que puedes visualizar un archivo csv desde Bloc de notas o desde Calc, lo que te recomiendo como alternativa a visualizarlo en el CMD mediante Python.
4 Como supondrás, deberás modificar la instrucción ruta_salida = r'C:\Mi_Ruta\Nuevo_Archivo.csv' sustituyendo la ruta por la que corresponda en tu caso. Te sugiero que sea la misma que empleaste para guardar el archivo csv original, dando al nuevo un nombre que ilustre su contenido. En este, por ejemplo, caso algo así como Datos_Totales.csv.

sábado, 19 de septiembre de 2026

LENGUAJES. Python

DataFrame (Pandas) (I)

Colecciones de datos

Un DataFrame es una estructura de datos bidimensional (organizada en filas y columnas) similar a una tabla de base de datos, una hoja de cálculo o una matriz. Es el objeto fundamental para la manipulación y análisis de datos en programación, especialmente en bibliotecas como Pandas (Python).

Los datos de cada columna pueden ser de distinto tipo (pero los de la misma columna han de ser del mismo), lo que le otorga gran flexibilidad, y dispone de índices por filas y por columnas, lo que le proporciona un funcionamiento eficiente en cuanto al acceso, el filtrado de datos y su alineación. Además es posible realizar operaciones vectorizadas, lo que nos permite aplicar transformaciones, filtros y cálculos matemáticos a columnas completas de forma eficiente sin necesidad de recurrir a bucles explícitos. En todas estas características se hace notar la influencia de los array de NumPy, base sobre la que se diseñó la librería Pandas y el concepto de DataFrame en Python (1).

La creación de un DataFrame (2) puede partir de la transformación de colecciones de datos nativas de Python como, por ejemplo, a partir de un diccionario cuyos valores son listas de datos:



import pandas as pd

# Creacción del diccionario
datos = {
    'Nombre': ['Ana', 'Carlos', 'Elena', 'David'],
    'Edad': [4, 6, 8, 10],
    'Centro': ['CP San Tirso', 'Col. Donato Barato', 'CP Sevilla', 'CP Luna LLena'],
    'Madre': ['María', 'Rosana', 'Carlota', 'Venancia']
}

# Conversión de diccionario a DataFrame

df = pd.DataFrame(datos)


Primero importamos la biblioteca Pandas con el alias que es costumbre import pandas as pd y después de crear el diccionario datos lo convertimos en una DataFrame df = pd.DataFrame(datos). A partir de aquí podemos realizar diferentes operaciones con el DataFrame creado haciendo uso de instrucciones Python y de otras específicas de Pandas.

Por ejemplo, podemos mostar el contenido del DataFrame en el CMD mediante print(df), obteniendo...

También podemos incrementar el contenido en ambas estructuras (diccionario y DataFrame), comprobando de paso las diferencias en el manejo de ambas estructuras.



# ==================================================
# PROCEDIMIENTO A: Añadir "registro" al DICCIONARIO
# ===================================================

# Al tratarse de un diccionario de listas, hay que añadir cada elemento clave a clave en su lista correspondiente.

datos['Nombre'].append('Lucas')
datos['Edad'].append(5)
datos['Centro'].append('CP San Tirso')
datos['Madre'].append('Laura')

print("Diccionario actualizado:")
print(datos)
print("--------------------------")


# =================================================
# PROCEDIMIENTO B: Añadir "registro" al DATAFRAME
# =================================================

# Los DataFrames de Pandas son estructuras inmutables en tamaño sobre la memoria de NumPy.
# A partir de Pandas 2.0+, la forma de añadir una fila es crear un mini-DataFrame y concatenarlo mediante pd.concat().

nuevo_df = pd.DataFrame([{
    'Nombre': 'Lucas',
    'Edad': 5,
    'Centro': 'CP San Tirso',
    'Madre': 'Laura'
}])

df = pd.concat([df, nuevo_df], ignore_index=True)

print("DataFrame actualizado:")
print(df)
print("--------------------------")

También podemos ejecutar diferentes modificaciones en el mismo, como, por ejemplo...



# 1. Selección de columnas

nombres = df['Nombre']
centros_y_madres = df[['Centro', 'Madre']]

# 2. Filtrado por condición (operación vectorizada)

# Alumnos mayores de 6 años (Elena y David)
mayores_de_6 = df[df['Edad'] > 6]

# Alumnos del colegio "CP San Tirso"
san_tirso = df[df['Centro'] == 'CP San Tirso']

# 3. Creación y modificación de columnas (operación vectorizada)

# Calcular el año de nacimiento estimado
df['Año_Nacimiento'] = 2026 - df['Edad']

# Indicador booleano para etapa infantil (menores o iguales a 5 años)
df['Es_Infantil'] = df['Edad'] <= 5

# 4. Agregación y resumen de datos

edad_promedio = df['Edad'].mean()


... que realizan las modificaciones y cálculos explicados en los comentarios, pero que no se muestran en pantalla al faltar las instrucciones print() que dejo de tu mano. Una vez realizadas estas modificaciones, lo lógico es guardar el DataFrame modificado para poder utilizarlo más adelante. Para ello es suficiente con implementar al final del script el código siguiente (3)



# Guardar el DataFrame en la ruta especificada

df.to_csv("D:/datos/mi_df.csv", index=False, encoding='utf-8')

print("DataFrame guardado correctamente en D:/datos/mi_df.csv")
print("--------------------------")


NOTAS
1 Pandas es una librería diseñada en 2008 por Wes McKinney, quien construyó la arquitectura del DataFrame para que usara las ventajas de bajo nivel de NumPy.
2 Lo frecuente es trabajar con archivos ya creados (tablas en hojas de datos o archivos CSV), pero en esta entrada nos limitaremos a la transformación de colecciones de datos insertas en el script.
3 La ruta donde se guardar realmente queda de tu mano, por lo que deberás modificar la instrucción "D:/datos/mi_df.csv" de df.to_csv("D:/datos/mi_df.csv", index=False, encoding='utf-8'). Observa que el DataFrame se guarda como archivo csv. Finalmente, mediante index=False evitamos que se guarde la columna del índice numérico (0, 1, 2, 3)

jueves, 17 de septiembre de 2026

LENGUAJES. Python

Array (NumPy)

Colecciones de datos

Tras estudiar las colecciones de datos nativas de Python (listas, tuplas, conjuntos y diccionarios), nos centraremos a continuación en las colecciones no nativas pertenecientes a bibliotecas externas. La primera de ellas será el array, estructura de datos fundamental de la biblioteca NumPy.

En términos generales, un array es una estructura de datos homogénea que almacena elementos del mismo tipo en un bloque de memoria contiguo, lo que permite un acceso rápido a sus elementos mediante índices.

Aunque Python incluye el módulo nativo array, en la práctica se recurre al array de NumPy. Esta estructura replica el modelo del lenguaje C: datos homogéneos, contiguos en memoria y de tamaño fijo. Esta disposición optimizada permite ejecutar operaciones vectorizadas directamente a nivel de hardware sin necesidad de iterar con bucles, ofreciendo un rendimiento muy superior en el cómputo masivo de datos. Esto último se entiende mejor considerando que, a diferencia de lo que sucede al usar únicamente Python —donde los datos se procesan de uno en uno mediante instrucciones repetitivas (bucles)—, con NumPy los datos se organizan en la memoria de forma tan eficiente que el procesador puede realizar el cálculo sobre millones de elementos de un solo golpe.

Si quieres comparar la diferencia de rendimiento de Python respecto a NumPy, puedes ejecutar este script en tu consola:



import time
import numpy as np

# Datos: 1 millón de enteros
N = 1_000_000

# ==========================================
# 1. MODO NATIVO (Python puro)
# ==========================================
lista_python = list(range(N))

inicio_python = time.time()
# Procesamiento de uno en uno usando un bucle/comprensión de listas
resultado_python = [x ** 2 for x in lista_python]
tiempo_python = time.time() - inicio_python


# ==========================================
# 2. MODO NUMPY (Operación vectorizada)
# ==========================================
array_numpy = np.arange(N)

inicio_numpy = time.time()
# Procesamiento "de un solo golpe" (vectorizado)
resultado_numpy = array_numpy ** 2
tiempo_numpy = time.time() - inicio_numpy


# ==========================================
# RESULTADOS
# ==========================================
print(f"Tiempo Python nativo: {tiempo_python:.4f} segundos")
print(f"Tiempo NumPy:         {tiempo_numpy:.4f} segundos")

velocidad = tiempo_python / tiempo_numpy
print(f"\nNumPy fue aproximadamente {velocidad:.1f} veces más rápido.")


Lo que este script demuestra es que NumPy procesa los arrays de forma mucho más eficiente que Python nativo, pero la verdad es que esa diferencia es de escasa relevancia para el uso que, como orientadores y usuarios de a pie, podemos hacer de esta tecnología. De hecho, el uso directo de NumPy nos aporta muy poco, ya que ni vamos a desarrollar modelos de Inteligencia Artificial ni necesitamos procesar millones de datos en el día a día.

Sin embargo, comprender la existencia del array de NumPy es crucial por una razón de peso: es el motor invisible de todo lo que sí vamos a usar. Aunque raramente escribamos import numpy en nuestros propios scripts, esta biblioteca es la piedra angular sobre la que se construyen las herramientas que sí resultan verdaderamente útiles en nuestro trabajo:

  • Es la base de Pandas, la biblioteca por excelencia para manejar tablas, hojas de cálculo y datos de alumnos, que está construida internamente sobre arrays de NumPy. Gracias a esta arquitectura, Pandas nos permite cruzar listas de asistencia con notas, detectar patrones de riesgo de abandono escolar o baremar baterías de tests psicopedagógicos en un solo clic.
  • Permite la estadística automatizada: cuando calculamos puntuaciones típicas (z, T), percentiles o desviaciones en pruebas diagnósticas, las herramientas de cálculo estadístico que usamos por debajo emplean la precisión y rapidez de NumPy.
  • Facilita la visualización y generación de gráficos para nuestros informes, los cuales se construyen leyendo los datos directamente a través de esta estructura.

En definitiva, el array de NumPy es una pieza invisible para el usuario de a pie: no necesitamos programarlo directamente para sacar provecho de Python, pero saber que existe nos permite entender cómo y por qué nuestro ordenador es capaz de procesar toda la información del centro educativo con tanta solidez, orden y precisión. Por esta razón, en posteriores entradas haremos uso de NumPy como herramienta subyacente en otras tecnologías; concretamente, en Pandas.

martes, 15 de septiembre de 2026

LENGUAJES. Python

Diccionarios complejos (III)

Colecciones de datos

En entradas anteriores analizamos cómo estructurar los datos básicos de un expediente educativo en formato JSON (nombre, curso, nee) y cómo gestionar colecciones sencillas como la lista de medidas_apoyo.

Sin embargo también necesitamos almacenar información más compleja de tipo evolutivo, como la que supone el campo intervenciones, un elemento complejo formado por una lista de diccionarios. En esta entrada nos centraremos en cómo trabajar con este tipo de datos mediante Python.


import json
import os
from collections import Counter

# 1. Definimos la ruta del archivo JSON (puedes ajustarla según tu estructura de carpetas)
RUTA_ARCHIVO = "D:/Tu_Ubicacion/archivo_JSON/exped_orienta.JSON"

def cargar_datos_expediente(ruta):
    """Carga y devuelve el contenido completo del archivo JSON."""
    if not os.path.exists(ruta):
        print(f" Error: El archivo no existe en la ruta: '{ruta}'")
        return None

    try:
        with open(ruta, "r", encoding="utf-8") as f:
            return json.load(f)
    except json.JSONDecodeError:
        print(f" Error: El archivo '{ruta}' no tiene un formato JSON válido.")
        return None
    except Exception as e:
        print(f" Error inesperado al leer el archivo: {e}")
        return None


def mostrar_datos (datos_totales):
    """
    Itera sobre todos los registros, muestra las intervenciones,
    el total de actuaciones por alumno y un resumen estadístico global.
    """
    if not datos_totales:
        print("No hay datos para mostrar.")
        return

    total_registros = len(datos_totales)
    sumatorio_actuaciones = 0
    conteo_actuaciones_por_alumno = []

    print("=" * 65)
    print(f" REPORTES DE INTERVENCIONES - TOTAL REGISTROS: {total_registros}")
    print("=" * 65)

    # Iteramos sobre cada alumno en el diccionario principal
    for id_alumno, expediente in datos_totales.items():
        intervenciones = expediente.get("intervenciones", [])
        num_actuaciones = len(intervenciones)

        # Actualizamos métricas globales
        sumatorio_actuaciones += num_actuaciones
        conteo_actuaciones_por_alumno.append(num_actuaciones)

        print(f"\n>>> EXPEDIENTE: {id_alumno}")
        print("-" * 45)

        if not intervenciones:
            print("  (Sin intervenciones registradas)")
        else:
            for idx, sesion in enumerate(intervenciones, 1):
                fecha = sesion.get("fecha", "Sin fecha")
                motivo = sesion.get("motivo", "Sin especificar")
                acuerdos = sesion.get("acuerdos", "Sin acuerdos registrados")

                print(f"  [{idx}] Fecha:   {fecha}")
                print(f"      Motivo:  {motivo}")
                print(f"      Acuerdos: {acuerdos}\n")

        # Muestra del total al final de cada expediente
        print(f"  --> Total de actuaciones en {id_alumno}: {num_actuaciones}")

    # ---------------------------------------------------------
    # RESUMEN ESTADÍSTICO FINAL
    # ---------------------------------------------------------
    print("\n" + "=" * 65)
    print(" RESUMEN ESTADÍSTICO DE ACTUACIONES")
    print("=" * 65)
    print(f" • Total de alumnos/registros analizados: {total_registros}")
    print(f" • Sumatorio total de actuaciones:        {sumatorio_actuaciones}")
    print("\n Distribución de registros según número de actuaciones:")

    # Contamos la frecuencia de cada cantidad de actuaciones
    distribucion = Counter(conteo_actuaciones_por_alumno)

    # Ordenamos por número de actuaciones de menor a mayor
    for num_act, cant_alumnos in sorted(distribucion.items()):
        porcentaje = (cant_alumnos / total_registros) * 100
        print(
            f"   - Registros con {num_act} actuaciones: "
            f"{cant_alumnos} alumno(s) ({porcentaje:.1f}%)"
        )

    print("=" * 65)


# --- EJECUCIÓN DEL SCRIPT ---
if __name__ == "__main__":
    datos_json = cargar_datos_expediente(RUTA_ARCHIVO)
    mostrar_datos (datos_json)


Con este primer script cargamos un archivo JSON que contiene los datos desde una ubicación que será distintan en cada caso y que en este script de identifica como RUTA_ARCHIVO = "D:/Tu_Ubicacion/archivo_JSON/exped_orienta.JSON" siendo exped_orienta.JSON el nombre del archivo (datos inventados) que puedes descargar desde este enlace. Así sólo tienes que ajustar la ruta en el script.

La primera función del script def cargar_datos_expediente(ruta): se encarga de cargar el archivo, controlando los posibles errores que se pudieran cometer, por lo que se puede simplificar una vez que, como programadores de nuestro script y usuarios, ya tengamos asegurada la carga del documento JSON. En ese caso la función podría quedar simplificada como sigue:



def cargar_datos_expediente(ruta):
    """Carga y devuelve el contenido completo del archivo JSON."""
    with open(ruta, "r", encoding="utf-8") as f:
        return json.load(f)


El siguiente paso consiste en acceder a los datos y mostrarlos por pantalla. Para ello implementamos en el script la segunda función def mostrar_datos (datos_totales):, que además de mostrar el listado de registros con las actuaciones de cada uno de ellos, también realiza un recuento de cuantas actuaciones tiene cada registro y finaliza con una pequeña estadística que se muestra al final del listado. Por este motivo esta función se vuelve especialmente extensa y compleja, requiendo una explicación de cada una de sus partes.

Además de controlar la existencia de datos en el archivo JSON, en el inicio de la función se establecen unas variables que nos servirán para realizar los recuentos solicitados...



if not datos_totales:
        print("No hay datos para mostrar.")
        return

    total_registros = len(datos_totales)
    sumatorio_actuaciones = 0
    conteo_actuaciones_por_alumno = []


... y después se define el formato de la cabecera del informe que se imprimirá en su momento en el CMD



print("=" * 65)
    print(f" REPORTES DE INTERVENCIONES - TOTAL REGISTROS: {total_registros}")
    print("=" * 65)


Pero es a partir de este punto que empezamos a construir la estructura del contenido de ese informe, recorriendo el contenido del archivo...



# Iteramos sobre cada alumno en el diccionario principal
    for id_alumno, expediente in datos_totales.items():
        intervenciones = expediente.get("intervenciones", [])
        num_actuaciones = len(intervenciones)

        # Actualizamos métricas globales
        sumatorio_actuaciones += num_actuaciones
        conteo_actuaciones_por_alumno.append(num_actuaciones)


... y accediendo a los datos del diccionario anidado en el campo...



print(f"\n>>> EXPEDIENTE: {id_alumno}")
        print("-" * 45)

        if not intervenciones:
            print("  (Sin intervenciones registradas)")
        else:
            for idx, sesion in enumerate(intervenciones, 1):
                fecha = sesion.get("fecha", "Sin fecha")
                motivo = sesion.get("motivo", "Sin especificar")
                acuerdos = sesion.get("acuerdos", "Sin acuerdos registrados")

                print(f"  [{idx}] Fecha:   {fecha}")
                print(f"      Motivo:  {motivo}")
                print(f"      Acuerdos: {acuerdos}\n")

        # Muestra del total al final de cada expediente
        print(f"  --> Total de actuaciones en {id_alumno}: {num_actuaciones}")


El cuarto bloque de esta función está dedicada a la composición del conteo de datos con el que finaliza el informe que se muestra por pantalla, por lo que se puede omitir si no se desea esta información.



  # ---------------------------------------------------------
    # RESUMEN ESTADÍSTICO FINAL
    # ---------------------------------------------------------
    print("\n" + "=" * 65)
    print(" RESUMEN ESTADÍSTICO DE ACTUACIONES")
    print("=" * 65)
    print(f" • Total de alumnos/registros analizados: {total_registros}")
    print(f" • Sumatorio total de actuaciones:        {sumatorio_actuaciones}")
    print("\n Distribución de registros según número de actuaciones:")

    # Contamos la frecuencia de cada cantidad de actuaciones
    distribucion = Counter(conteo_actuaciones_por_alumno)

    # Ordenamos por número de actuaciones de menor a mayor
    for num_act, cant_alumnos in sorted(distribucion.items()):
        porcentaje = (cant_alumnos / total_registros) * 100
        print(
            f"   - Registros con {num_act} actuaciones: "
            f"{cant_alumnos} alumno(s) ({porcentaje:.1f}%)"
        )

    print("=" * 65)


Además de mostrar los registros (las actuaciones) por pantalla, también podemos realizar modificaciones sobre esos contenidos. Una de ellas (posiblemente la de mayor interés en la práctica) es la de añadir una nueva actuación dentro del registro de un determinado alumno. Para ello añadiremos este script al anterior.



# =============================================================================
# FASE 2: EDICIÓN E INCREMENTO DE INTERVENCIONES
# =============================================================================

def seleccionar_alumno(datos_totales):
    """
    Presenta los alumnos con un índice numérico para seleccionar uno.
    """
    print("\n" + "=" * 60)
    print(" SELECCIÓN DE ALUMNO / REGISTRO PARA AÑADIR INTERVENCIÓN")
    print("=" * 60)

    lista_ids = list(datos_totales.keys())

    for idx, id_alumno in enumerate(lista_ids, 1):
        nombre = datos_totales[id_alumno].get("nombre", "Sin nombre")
        num_intervenciones = len(datos_totales[id_alumno].get("intervenciones", []))
        print(f"  [{idx}] ID: {id_alumno} | Nombre: {nombre} ({num_intervenciones} intervenciones)")

    print("-" * 60)

    while True:
        opcion = input(f"Seleccione un alumno (1-{len(lista_ids)}): ").strip()
        if opcion.isdigit() and 1 <= int(opcion) <= len(lista_ids):
            return lista_ids[int(opcion) - 1]
        print(" Opción no válida. Introduzca un número de la lista.")


def capturar_nueva_intervencion():
    """
    Captura por consola los campos del nuevo diccionario anidado.
    """
    print("\n--- NUEVA ACTUACIÓN ---")
    fecha = input("• Fecha (AAAA-MM-DD): ").strip()
    motivo = input("• Motivo de la intervención: ").strip()
    acuerdos = input("• Acuerdos alcanzados: ").strip()

    return {
        "fecha": fecha if fecha else "Sin fecha",
        "motivo": motivo if motivo else "Sin especificar",
        "acuerdos": acuerdos if acuerdos else "Sin acuerdos registrados"
    }

def agregar_intervencion_interactiva(datos_totales):
    """
    Permite agregar de forma iterativa intervenciones en el mismo registro
    o cambiar a otros expedientes.
    """
    while True:
        id_alumno = seleccionar_alumno(datos_totales)
        expediente = datos_totales[id_alumno]

        if "intervenciones" not in expediente:
            expediente["intervenciones"] = []

        # Bucle para añadir varias intervenciones al mismo alumno
        while True:
            print(f"\nAñadiendo actuación para: {id_alumno} ({expediente.get('nombre', '')})")
            
            nueva_actuacion = capturar_nueva_intervencion()
            expediente["intervenciones"].append(nueva_actuacion)
            
            print(f"\n [OK] Actuación añadida. Total actual en {id_alumno}: {len(expediente['intervenciones'])}")

            mas_mismo = input("\n¿Desea añadir OTRA intervención a ESTE MISMO alumno? (s/n): ").strip().lower()
            if mas_mismo != "s":
                break

        mas_otros = input("\n¿Desea seleccionar OTRO alumno para añadir intervenciones? (s/n): ").strip().lower()
        if mas_otros != "s":
            print("\n Finalizada la sesión de altas.")
            break


Con él creamos un listado de alumnos para identificar aquel en el que queremos añadir una nueva actuación def seleccionar_alumno(datos_totales): y después añadimos los datos de esa actuación def agregar_intervencion_interactiva(datos_totales):, previa identificación de sus campos def capturar_nueva_intervencion():

Además podemos añadir más de una actuación respecto al mismo alumno, o añadir actuaciones realizadas con otro alumno def agregar_intervencion_interactiva(datos_totales):

Hasta el momento sólo hemos consultado los registros de actuaciones, identificado a un alumno en concreto e incrementado las actuaciones que se han realizado respecto a ese alumno, pero si dejamos así el script perderemos los cambios introducidos. Para que esto no suceda deberemos implementar otro segmento de código a nuestro script que se encargue de guardar en la dirección que corresponda un nuevo archivo JSON.



# =============================================================================
# FASE 3: PERSISTENCIA Y ACTUALIZACIÓN EN DISCO
# =============================================================================

def guardar_datos_actualizados(datos_totales, ruta=RUTA_ARCHIVO):
    """
    Sobrescribe el archivo JSON original con la estructura de datos
    actualizada en memoria, aplicando formato indentado.
    """
    print("\n" + "=" * 60)
    print(" GUARDANDO CAMBIOS EN EL ARCHIVO JSON")
    print("=" * 60)

    # Sobrescribimos el archivo con la información acumulada en memoria
    with open(ruta, "w", encoding="utf-8") as f:
        json.dump(datos_totales, f, ensure_ascii=False, indent=4)

    print(f" [OK] El archivo '{ruta}' se ha actualizado correctamente.")
    

En este segmento sólo se identifica una función def guardar_datos_actualizados(datos_totales, ruta=RUTA_ARCHIVO): cuyo nombre es suficientemente expresivo y que no requiere más explicación.

Lo que sí es necesario (ahora) es modificar el segmento inicial de la función if __name__ == "__main__": para incluir las nuevas funcinalidades. El resultado es el que sigue. Corre de tu cuenta actualizarla en el script final que construyas con los segmentos que te he ido proporcionando.



# =============================================================================
# BLOQUE PRINCIPAL DE EJECUCIÓN
# =============================================================================

if __name__ == "__main__":

    # 1. Cargar archivo JSON
    datos_json = cargar_datos_expediente(RUTA_ARCHIVO)

    # 2. Mostrar datos iniciales y resumen estadístico
    mostrar_datos(datos_json)

    # 3. Iniciar proceso interactivo de alta de intervenciones
    agregar_intervencion_interactiva(datos_json)

    # 4. Guardar los cambios acumulados en el archivo JSON
    guardar_datos_actualizados(datos_json, RUTA_ARCHIVO)


Podríamos implementar más utilidades dentro de este script, pero para el objetivo de la entrada del blog creo que tenemos más que suficiente. Puede que lo qua haga falta sea crear un procedimiento de trabajo que incluya la creación, actualización, manejo y archivo de un documentoJSON a modo de base de datos. En ese caso habría que identificar el objetivo y la estructura de contenidos. Un sistema de seguimiento de actuaciones como el que hemos simulado podría ser un buen tema de trabajo.

viernes, 7 de agosto de 2026

LENGUAJES. Python

Diccionarios complejos (II)

Colecciones de datos

En un diccionario complejo el value (par clave:valor) de un posible "campo" puede estar formado por una lista de datos ([]), de modo que el tratamiento de esos datos haga encesario implementar estrategias específicas. Sin pretender ser exhaustivos, ese tratamiento incluye operaciones como conteo, visualización y consulta (de toda la lista o de un elemento), añadido, supresión o modificación... En esta entrada estudiaremos cómo realizar estas operaciones.

Ya que por algún punto hay que empezar, nos situaremos en el siguiente contexto: hemos definido la estructura de diccionario...



 "ALU-cod-n": {
        "nombre": "al_nombre",
        "curso": "al_curso",
        "nee": true,
        "medidas_apoyo": [
            "mad_1"
        ],
        "intervenciones": [
            {
                "fecha": "int_aa_mm_dd",
                "motivo": "int_mot",
                "acuerdos": "int_acuerdos"
            }
        ]
    },


... ya sabemos cómo añadir elementos a los "campos" simples nombre - curso - nee...



import json
import os

def solicitar_booleano(mensaje):
    """
    Solicita una respuesta booleana al usuario garantizando que la entrada sea s/n.
    """
    while True:
        respuesta = input(f"{mensaje} (s/n): ").strip().lower()
        if respuesta in ['s', 'si', 'sí']:
            return True
        elif respuesta in ['n', 'no']:
            return False
        print(" [!] Entrada no válida. Por favor, introduzca 's' para Sí o 'n' para No.")

def capturar_ficha(codigo_num):
    cod_alumno = f"ALU-cod-{codigo_num}"
    print(f"\n==========================================")
    print(f"  NUEVA FICHA DE EXPEDIENTE: {cod_alumno}")
    print(f"==========================================")
    
    print("\n--- AVISO DE FORMATO DE ENTRADA ---")
    print("  * Nombre del alumno: Apellidos, Nombre (ejemplo: García López, María)")
    print("  * Curso: Etapa y grupo (ejemplo: 2º ESO B  o  1º Bachillerato A)")
    print("-------------------------------------\n")
    
    nombre = input("Nombre completo del alumno (ej: García López, María): ").strip()
    curso = input("Curso y grupo (ej: 2º ESO B): ").strip()
    nee = solicitar_booleano("¿Presenta Necesidades Educativas Especiales (NEE)?")
    

... pero todavía no sabemos cómo introducir datos en el "campo" "medidas_apoyo": [] cuyo value es una lista, como podemos identificar por el uso corchetes [] como delimitadores. El script que sigue cubre esta necesidad, aunque tiene una funcionalidad limitada.



import json
import os


def solicitar_booleano(mensaje):
    """Solicita una respuesta booleana al usuario garantizando que la entrada sea s/n."""
    while True:
        respuesta = input(f"{mensaje} (s/n): ").strip().lower()
        if respuesta in ["s", "si", "sí"]:
            return True
        elif respuesta in ["n", "no"]:
            return False
        print(
            " [!] Entrada no válida. Por favor, introduzca 's' para Sí o 'n' para No."
        )


# --- FUNCIÓN Cumplimentar datos de medidas_apoyo ---------------------------------------

def capturar_medidas_apoyo():
    """Captura de forma interactiva la lista de medidas de apoyo."""
    medidas = []
    quiere_anadir = solicitar_booleano(
        "¿Desea registrar alguna medida de apoyo?"
    )

    contador = 1
    while quiere_anadir:
        medida = input(f" -> Ingrese la medida de apoyo #{contador}: ").strip()
        if medida:
            medidas.append(medida)
            contador += 1
        quiere_anadir = solicitar_booleano("¿Desea añadir otra medida de apoyo?")

    return medidas


# --- FUNCIÓN PRINCIPAL de ejecución. -----------------------------------------------

def capturar_ficha(codigo_num):
    cod_alumno = f"ALU-cod-{codigo_num}"
    print(f"\n==========================================")
    print(f"  NUEVA FICHA DE EXPEDIENTE: {cod_alumno}")
    print(f"==========================================")

    print("\n--- AVISO DE FORMATO DE ENTRADA ---")
    print(
        "  * Nombre del alumno: Apellidos, Nombre (ejemplo: García López, María)"
    )
    print(
        "  * Curso: Etapa y grupo (ejemplo: 2º ESO B  o  1º Bachillerato A)"
    )
    print("-------------------------------------\n")

    nombre = input("Nombre completo del alumno (ej: García López, María): ").strip()
    curso = input("Curso y grupo (ej: 2º ESO B): ").strip()
    nee = solicitar_booleano(
        "¿Presenta Necesidades Educativas Especiales (NEE)?"
    )

    # 1. Llamamos a la nueva utilidad para generar la lista
    medidas_apoyo = capturar_medidas_apoyo()

    # 2. Retornamos el diccionario completo con el nuevo campo
    registro = {
        "codigo_alumno": cod_alumno,
        "nombre": nombre,
        "curso": curso,
        "nee": nee,
        "medidas_apoyo": medidas_apoyo,
    }

    return registro

# ==============================================================================
# BLOQUE DE EJECUCIÓN PRINCIPAL
# ==============================================================================
if __name__ == "__main__":
    # Solicitamos o asignamos el número identificador para el código de alumno
    num_expediente = 1

    # Llamada a la función principal para capturar los datos interactivos
    expediente_alumno = capturar_ficha(num_expediente)

    # Mostramos el resultado por pantalla con formato JSON legible
    print("\n==========================================")
    print("      REGISTRO GENERADO CON ÉXITO")
    print("==========================================")
    print(json.dumps(expediente_alumno, indent=4, ensure_ascii=False))


Este script complementa el de creación de registro añadiendo, a partir de def capturar_medidas_apoyo(): le procedimiento para obtener los primeros posibles datos de medidas_apoyo. Esta función es llamada desde la función principal def capturar_ficha(codigo_num): y se basa, en lo fundamental, en el bucle while quiere_anadir: que permite al usuario introducir cuantas medidas de apoyo desee.

Resuelto el procedimiento de creación de datos, nos situamos ahora en el segundo contexto: ya disponemos de un archivo JSON que contiene datos suficientes como para que sea pertinente realizar diferentes operaciones con la información de medidas_apoyo de los distintos registros-diccionarios.

En este contexto empezamos por acceder al archivo JSON para trabajar con él desde el script (en memoria) como diccionario complejo. Este procedimiento ya nos es conocido.



import json

# Ruta absoluta al archivo JSON
ruta_archivo = r"C:\MisDatosPropios\exped_orienta.JSON"

# 1. Cargar el contenido del archivo JSON en memoria
with open(ruta_archivo, "r", encoding="utf-8") as archivo:
    expedientes = json.load(archivo)
    

De las entradas anteriores sabemos crear el listado las claves principales, acceder al contenido de un archivo concreto o a todos ellos. También sabemos manipular el contenido de deteminados campos. Ahora nos detendremos en el acceso y las posibles modificaciones del contenido del campo cuyo value es una lista. Empezaremos por obtener el listado completo de registros mostrando cuántas y cuales son las medidas educativas en cada uno de ellos.



import json

# Ruta absoluta al archivo JSON
ruta_archivo = r"C:\MisDatosPropios\exped_orienta.JSON"

try:
    # 1. Cargar el contenido del archivo JSON en memoria
    with open(ruta_archivo, "r", encoding="utf-8") as archivo:
        expedientes = json.load(archivo)

    print("\n==================================================")
    print("    LISTADO COMPLETO DE EXPEDIENTES Y MEDIDAS     ")
    print("==================================================\n")

    # Adaptación dinámica según si el JSON es una lista [...] o un diccionario {...}
    registros = (
        expedientes.items()
        if isinstance(expedientes, dict)
        else enumerate(expedientes, 1)
    )

    for id_registro, datos in registros:
        # Obtenemos la clave principal (clave del dict o campo 'codigo_alumno')
        if isinstance(datos, dict):
            clave_principal = datos.get("codigo_alumno", f"Registro #{id_registro}")
            medidas = datos.get("medidas_apoyo", [])
        else:
            clave_principal = str(id_registro)
            medidas = datos if isinstance(datos, list) else []

        # Contamos la cantidad total de elementos en la lista
        total_medidas = len(medidas)

        # Imprimimos la salida formateada en CMD
        print(f"📌 Clave Principal: {clave_principal}")
        print(f"   Total de medidas educativas aplicadas: {total_medidas}")

        if total_medidas > 0:
            print("   Listado de medidas:")
            for i, medida in enumerate(medidas, 1):
                print(f"      {i}. {medida}")
        else:
            print("   Listado de medidas: (Sin medidas registradas)")

        print("-" * 50)

except FileNotFoundError:
    print(f" [!] Error: No se encontró el archivo en la ruta '{ruta_archivo}'.")
except json.JSONDecodeError:
    print(
        " [!] Error: El archivo JSON no tiene un formato válido o está corrupto."
    )


Podríamos dirigir nuestra atención al análisis del contenido de este "campo", pero dejaremos estos procedimientos para una tercera fase del estudio de los diccionarios y nos atendremos a explicar los procedimientos de manejo y modificación del value lista, si bien no es muy diferente del del que ya estudiamos en el tratamiento de estas colecciones de datos.

Al tratarse de un diccionario ya desarrollado en cuanto a su contenido, lo primero que deberemos hacer es identificar y acceder al registro de nuestro interés. Para ello debemos usar la clave principal /(vg. "ALU-2024-01") y, a partir de ahí a la clave "medidas_apoyo".

Disponemos de dos procedimientos para acceder a los datos de la lista. Bien directamente identificando al alumno por su clave:



import json

# Ruta absoluta al archivo JSON
ruta_archivo = r"C:\MisDatosPropios\exped_orienta.JSON"

# 1. Cargar el contenido del archivo JSON en memoria
with open(ruta_archivo, "r", encoding="utf-8") as archivo:
    expedientes = json.load(archivo)

# 2. Identificamos directamente al alumno por su clave

id_alumno = "ALU-2024-01"

# 3. Verificar existencia y mostrar las medidas elemento por elemento
if id_alumno in expedientes:
    print(f"Medidas de apoyo para {id_alumno}:")
    for medida in expedientes[id_alumno]["medidas_apoyo"]:
        print(f"- {medida}")



... o bien hacerlo de forma interactiva introducciendo por input() el identificador de su clave, suponiendo que tenemos acceso a un listado de claves y nombres que nos facilitan la identificación del sujeto deseado.



import json

# Ruta absoluta al archivo JSON
ruta_archivo = r"C:\MisDatosPropios\exped_orienta.JSON"

# 1. Cargar el contenido del archivo JSON en memoria
with open(ruta_archivo, "r", encoding="utf-8") as archivo:
    expedientes = json.load(archivo)
    
# 2. Listado de claves-sujetos

listado = [f"{clave} - {datos['nombre']}" for clave, datos in expedientes.items()]
for al in listado:
    print(al)

# 3. Solicitar la clave directamente al usuario
clave = input("Introduce la clave del expediente: ")

# 4. Acceder al valor del campo 'medidas_apoyo' del registro indicado
medidas = expedientes[clave]["medidas_apoyo"]

# 5. Mostrar el contenido actual
print("Medidas de apoyo actuales:", medidas)


Una vez que accedemos al listado de medidas, podemos limitarnos a mostrarlo, pero también podemos actuar sobre él. Veamos algunas acciones posibles. Se escriben a continuación del script anterior).



print("\n--- ACCIONES SOBRE LAS MEDIDAS ---")

# (1) Conocer el número de medidas
print(f"\n4. Número total de medidas registradas: {len(medidas)}")

# (2) Añadir una nueva medida
nueva = input("1. Introduce la nueva medida a añadir: ").strip()
medidas.append(nueva)
print("   Lista actualizada:", medidas)

# (3) Borrar una medida
a_borrar = input("\n2. Introduce la medida a eliminar: ").strip()
medidas.remove(a_borrar)
print("   Lista tras borrar:", medidas)

# (4) Ordenar alfabéticamente las medidas
medidas.sort()
print("\n3. Medidas ordenadas alfabéticamente:", medidas)

# (5) Saber si existe una determinada medida
buscar = input("\n5. Consulta si existe una medida concreta: ").strip()
print(f"   -> ¿Existe '{buscar}' en la lista?: {buscar in medidas}")


Lo suyo sería actualizar el archivo JSON, pero por ahora no es necesario ya que sólo estamos aprendiendo a trabajar con los componentes del diccionario.

martes, 4 de agosto de 2026

LENGUAJES. Python

Diccionarios complejos (I)

Colecciones de datos

En las dos entradas previas en las que hemos trabajado con diccionarios hemos empleado de forma intencional formulaciones simples de esta colección de datos. Se trataba de comprender en qué consistía y cómo se podía trabajar con ella, así que la simplicidad se imponía como necesidad. Pero la realidad es que un diccionario "normal" no suele ser tan sencillo ni su virtud o interés requiere de que los sea.

En esta entrada idearemos una estructura compleja (y más realista, incluso) de un diccionario y trabajaremos con ella. Se trata de comprobar la realidad y la utilidad real de un diccionario. Para ello vamos a plantearlo como colección de datos potencialmente aplicable a un proyecto real de trabajo (como SEO) aunque ajustado a las necesidades didácticas del momento.

Imaginemos que sobre una estructura de diccionario queremos construir un recurso de seguimiento del alumnado. Se trata de una forma de expediente, muy parecido a lo que se podría dar soporte con una base de datos, pero también registrar y mantener como un documento JSON. Algo como esto:



 "ALU-cod-n": {
        "nombre": "al_nombre",
        "curso": "al_curso",
        "nee": true,
        "medidas_apoyo": [
            "mad_1"
        ],
        "intervenciones": [
            {
                "fecha": "int_aa_mm_dd",
                "motivo": "int_mot",
                "acuerdos": "int_acuerdos"
            }
        ]
    },


Muestro aquí la estructura de un elemento del documento CSV sin contenidos ni reales ni supuestos (eso viene después) para que podemos observar en qué consiste la complejidad de la que estamos hablando cuando lo trasladamos a diccionario (ya sabemos que estructura y contenido se mantienen): ahora cada elemento del diccionario - aquí "ALU-cod-n": - es la clave de un value que es a su vez un diccionario, de ahí que el conjunto se pueda considerar como un diccionario de diccionarios.

Ese value además de ser un diccionario (de ahí que todo él se encuentre delimitado por {}, es un diccionario complejo ya que sus diferentes elementos-campos lo son de diferente tipología:

  • Tenemos contenidos-campos clave:valor simples (y de distinto tipo de datos), como "nombre": "al_nombre"
  • Pero también un contenido-campo cuyo value es una lista "medidas_apoyo": ["mad_1"],
  • Y más aun, un contenido-campo cuyo value es una lista de diccionarios (de la que ahora sólo muestro su primer componente clave:valor por simplificar: "intervenciones": [{"fecha": "int_aa_mm_dd",}]

Sobra decir que debermos conocer cómo tratar a cada subestructura o tipo de componente, tanto como con el conjunto. Empezaremos con ello a continuación.



import json
import os

def solicitar_booleano(mensaje):
    """
    Solicita una respuesta booleana al usuario garantizando que la entrada sea s/n.
    """
    while True:
        respuesta = input(f"{mensaje} (s/n): ").strip().lower()
        if respuesta in ['s', 'si', 'sí']:
            return True
        elif respuesta in ['n', 'no']:
            return False
        print(" [!] Entrada no válida. Por favor, introduzca 's' para Sí o 'n' para No.")

def capturar_ficha(codigo_num):
    cod_alumno = f"ALU-cod-{codigo_num}"
    print(f"\n==========================================")
    print(f"  NUEVA FICHA DE EXPEDIENTE: {cod_alumno}")
    print(f"==========================================")
    
    print("\n--- AVISO DE FORMATO DE ENTRADA ---")
    print("  * Nombre del alumno: Apellidos, Nombre (ejemplo: García López, María)")
    print("  * Curso: Etapa y grupo (ejemplo: 2º ESO B  o  1º Bachillerato A)")
    print("-------------------------------------\n")
    
    nombre = input("Nombre completo del alumno (ej: García López, María): ").strip()
    curso = input("Curso y grupo (ej: 2º ESO B): ").strip()
    nee = solicitar_booleano("¿Presenta Necesidades Educativas Especiales (NEE)?")
    
    # Se mantienen las estructuras de listas para su posterior implementación
    return cod_alumno, {
        "nombre": nombre,
        "curso": curso,
        "nee": nee,
        "medidas_apoyo": [],
        "intervenciones": []
    }

def guardar_en_json(nuevos_expedientes, ruta_salida):
    # Limpieza de comillas iniciales o finales introducidas por el usuario
    ruta_salida = ruta_salida.strip('\"\'')
    ruta_salida = os.path.normpath(ruta_salida)
    
    # 1. Comprobar si el directorio existe y crearlo si no
    directorio = os.path.dirname(ruta_salida)
    if directorio and not os.path.exists(directorio):
        os.makedirs(directorio, exist_ok=True)
        print(f" [+] Directorio creado: {os.path.abspath(directorio)}")
        
    datos_acumulados = {}
    
    # 2. Comprobar si el archivo JSON existe para cargar su contenido previo
    if os.path.exists(ruta_salida):
        try:
            with open(ruta_salida, mode="r", encoding="utf-8") as archivo:
                contenido = archivo.read().strip()
                if contenido:
                    datos_acumulados = json.loads(contenido)
            print(f" [+] Archivo existente detectado. Se añadirán los nuevos registros a los {len(datos_acumulados)} existentes.")
        except Exception as e:
            print(f" [!] Advertencia: No se pudo leer el archivo existente ({e}). Se iniciará un JSON nuevo.")
            datos_acumulados = {}
    else:
        print(" [+] El archivo no existe. Se creará un nuevo archivo JSON.")

    # 3. Anexar/actualizar los registros nuevos sobre el objeto cargado
    datos_acumulados.update(nuevos_expedientes)
    
    # 4. Sobrescribir el archivo con el diccionario actualizado en formato JSON legible
    with open(ruta_salida, mode="w", encoding="utf-8") as archivo:
        json.dump(datos_acumulados, archivo, ensure_ascii=False, indent=4)
        
    return os.path.abspath(ruta_salida)

def main():
    expedientes_sesion = {}
    contador = 1
    
    print("----------------------------------------------------------------")
    print(" SISTEMA DE REGISTRO DE EXPEDIENTES ACADÉMICOS")
    print("----------------------------------------------------------------")
    
    # Bucle de captura continua de fichas
    while True:
        codigo, ficha = capturar_ficha(contador)
        expedientes_sesion[codigo] = ficha
        contador += 1
        
        if not solicitar_booleano("\n¿Desea incorporar la ficha de otro alumno?"):
            break
            
    # Guardado de datos en JSON
    if expedientes_sesion and solicitar_booleano("\n¿Desea guardar los expedientes recopilados en un archivo JSON local?"):
        print("\n=========================================================================")
        print(" INSTRUCCIONES PARA LA RUTA DEL ARCHIVO JSON:")
        print(" 1. NO use comillas al principio ni al final (ejemplo: NO use \"C:\\datos\\alumnos.json\")")
        print(" 2. Puede usar barras inclinadas normales ( / ) o barras invertidas ( \\ ).")
        print(" 3. Asegúrese de incluir la extensión .json al final del nombre.")
        print(" 4. Ejemplos de rutas válidas:")
        print("    - Windows (ruta absoluta): C:/MisDatos/expedientes.json  ó  C:\\MisDatos\\expedientes.json")
        print("    - Carpeta actual (ruta relativa): ./expedientes.json  ó  expedientes.json")
        print("=========================================================================\n")
        
        ruta_archivo = input("Introduzca la ruta y nombre del archivo JSON: ").strip()
        try:
            ruta_final = guardar_en_json(expedientes_sesion, ruta_archivo)
            print(f"\n[OK] Registros guardados con éxito en:\n     {ruta_final}")
        except Exception as e:
            print(f"\n[ERROR] No se pudo guardar el archivo JSON: {e}")
            
    print("\nCierre de sesión del sistema.")

if __name__ == "__main__":
    main()



Aunque el script resultante sea mejorable, ya resulta suficientemente complejo (además de funcional) como para que nos hagamos una idea de qué estamos planteando cuando implementamos un diccionario de este nivel de complejidad. Lo que hemos pretendido es ejemplificar lo que sería la primera y básica implementación de una utilidad de este tipo: obtener del usuario los datos básicos (y simples, se podría añadir) que componen la estructura del diccionario y archivar esa información partiendo desde cero y en formato JSON. Cualquier proyecto de este tipo debe contener estas funcionalidades y ajustarse a la estructura del JSON deseado.

Pero si el contexto primero es correcto (y realista), el que planteo a continuación también lo es. Dando por supuesto el exito del anterior, creamos ahora una utilidad de acceso a los datos. Partimos de que ya tenemos algunos creados y deseamos acceder a ellos, entendiendo que ya hemos recopilado cierto volumen de información, disponible como archivo JSON. Evidentemente los datos que incluímos son inventados (sintéticos, que se dice) y no nos vamos a detener en valorar su ajuste a la realidad ni su interés pedagógico real. Para que lo que aquí te presento te sea de ayuda, proporcionaré acceso al documento "inventado" al final de esta entrada.

Dicho lo anterior, lo que ahora necesitamos es un script que nos permita acceder al documento JSON para trabajar con él en memoria como diccionario.



import json

# Ruta absoluta al archivo JSON
ruta_archivo = r"C:\MisDatosPropios\exped_orienta.JSON"

# 1. Cargar el contenido del archivo JSON en memoria
with open(ruta_archivo, "r", encoding="utf-8") as archivo:
    expedientes = json.load(archivo)

# 2. Obtener la clave y los datos del primer registro
primer_id = list(expedientes.keys())[0]
primer_registro = expedientes[primer_id]

# 3. Mostrar el resultado por pantalla (CMD)
print(f"--- PRIMER REGISTRO: {primer_id} ---")
print(json.dumps(primer_registro, ensure_ascii=False, indent=4))


Dado que estamos trabajando con archivos JSON, lo primero que haremos será importar la biblioteca correspondiente import json para cargar en memoria el archivo expedientes = json.load(archivo) desde su ubicación local ruta_archivo y accedemos a él with open() as archivo. Después obtenemos las claves de todos los archivos en formato lista list(expedientes.keys()) y el contenido del primer registro primer_registro = expedientes[primer_id] y lo mostramos por CMD print(json.dumps(primer_registro, ensure_ascii=False, indent=4)).

Mediante este par de script abordamos las dos maniobras básicas que deberemos realizar con diccionarios, simples o complejos. Pero como hay tarea para rato, parece conveniente finalizar ahora esta entrada.

Archivo de datos (formato JSON). Acceso para descarga desde este enlace. Deberás ajustar la dirección ruta_archivo a la que tú elijas como directorio del archivo que has descargado.

jueves, 30 de julio de 2026

LENGUAJES. Python

Diccionarios (II)

Colecciones de datos

Damos continuidad en esta entrada a la temática iniciada en la anterior sobre los diccionarios Python y sus usos. Ahora trataremos cuestiones básicas de manejo de los diccionarios y su contenido.



#A. Procedimientos directos de creación de diccionarios simples ========================

# 1. Creación estándar con llaves

usuario_ana = {
    "nombre": "Ana",
    "edad": 28,
    "lenguajes": ["Python", "JavaScript"],
    "activo": True
}

# 2. Creación con la función dict()

usuario_carlos= dict(nombre="Carlos", edad=35,lenguajes=["OOo Basic","Python"],activo=True)

#B. Acceso a datos =================================================

# 1. Acceso directo con llaves

print(usuario_ana["nombre"])  # Salida: Ana
print(usuario_carlos["edad"])  #Salida: 35


# 2. Acceso seguro con .get() (evita que el programa falle si la clave no existe)

print(usuario_ana.get("edad"))  # Salida: True
print(usuario_carlos.get("salario", "No especificado"))  # Salida: No especificada

#3. Acceso cuando el valor es una lista 

# Cuando el valor es una lista, sólo con la clave obtenemos como valor la propia lista

print(usuario_ana.get("lenguajes"))  # Salida: ["Python", "JavaScript"]

# 3.1. Usando corchetes. Obtener el primer elemento de la lista (índice 0)

primer_lenguaje = usuario_ana["lenguajes"][0]
print(primer_lenguaje)  # Salida: Python

# 3.2. Usando get(). Obtener el primer elemento con .get()

primer_lenguaje = usuario_carlos.get("lenguajes")[0]
print(primer_lenguaje)  # Salida: OOo Basic

# C. Modificar datos: cambiar, borrar y añadir datos ===============================

# 1. Modificar un valor existente
usuario_ana["edad"] = 29

# 2. Agregar una nueva pareja clave-valor
usuario_ana["ciudad"] = "Madrid"

# 3. Agregar elemento a un valor-lista (ej. lenguajes:[lista_lenguajes]

# 3.1. Agregar un nuevo elemento a la lista 'lenguajes' con append()
usuario_ana["lenguajes"].append("C++")

# (3.1.b) Si no estás seguro de que exista la clave 'lenguajes' usa setdefault() para crearlo y luego añade el elemento con append()
usuario_ana.setdefault("lenguajes", []).append("LUA")

# 3.2. Agregar un nuevo elemento a la lista 'lenguajes' usando .extend() (varios elementos)
usuario_carlos["lenguajes"].extend(["JavaScript", "C++", "PHP"])

# (3.2.b) ...o usando +=
usuario_ana["lenguajes"] += ["OOo Basic"]

print(usuario_carlos["lenguajes"])
print(usuario_ana["lenguajes"])

# 4. Borrar

# 4.1 Eliminar una clave específica con 'del'
del usuario_ana["activo"] #Elimina el par activo:True

# Extraer y eliminar un valor con .pop()
edad = usuario_ana.pop("edad") # Elimina el valor (29) del par "edad":29

# Limpiar todo el diccionario
usuario_carlos.clear() # Elimina todo el diccionario (usuario_carlos)

print(usuario_ana.get("activo", "No especificado")) #con get() evitamos mensaje de error. Devuelve mensaje ("No especificado")
print(edad) #Devuelve el valor del par 'edad':29
print(usuario_ana.get('edad')) #Devuelve None por defecto (ya que no se espifica mensaje)
print (usuario_carlos) # Devuelve un diccionario vacío ({})


Reproduzco en este script la creación de dos diccionarios simples para desarrollar la lógica de posibles manipulaciones sobre un diccionario y sus contenidos. La división del script en cuatro partes y la explicitación como comentarios de cada una de las posibles acciones facilita la comprensión del contenido y agiliza esta explicación complementaria.

Podemos diferenciar tres tipos de actuaciones sobre un diccionario:

  • Los procedimientos de acceso a los datos (sección B)
    • mediante el uso de llaves print(usuario_ana["nombre"]) - Salida: Ana
    • mediante la función get() para controlar el posible error de acceso
    • y el acceso en el caso específico de valores-lista, bien para visualizar la lista completa de valores print(usuario_ana.get("lenguajes")), bien para acceder a uno de ellos en concreto, por ejemplo, print(primer_lenguaje = usuario_ana["lenguajes"][0]), mediante el uso de corchetes
  • La modificación de datos (cambiar o añadir) (sección C del script)
    • mediante procedimientos simples usuario_ana["edad"] = 29 de cambio
    • o usuario_ana["ciudad"] = "Madrid" para añadir clave:valor, ambos basados en el uso de corchetes
    • o más complejos como es el caso de la modificación de valores-listas, que requieren el uso de funciones como append(), (sóla en combinación con setdefault()) para añadir un elemento-valor o la función extend() para añadir varios.
  • Finalmente, en la sección D tratamos el borrado de datos: clave: valor del usuario_ana["activo"], sólo valor edad = usuario_ana.pop("edad") y del diccionario completo usuario_carlos.clear()

Finalizo aquí esta segunda entrada sobre los diccionarios, pero aun hay materia para más...

miércoles, 29 de julio de 2026

LENGUAJES. Python

Diccionarios (I)

Colecciones de datos

Aunque ya sabemos algo sobre diccionarios en Python, lo cierto es que en esta subsección de LENGUAJES hemos hablado mucho de listas (y de tuplas) y muy poco de diccionarios, al menos mucho menos de lo que se merece.

Cuando uno comienza a programar en OOo Basic deberá trabajar con listas o arrays. Si después programa en Python lo "lógico" es que traslade su interés al uso las listas. Además las listas en Python son sencillas e intuitivas de usar y muy útiles.

Pero Python es mucho más rico y complejo que OOo Basic también es esto de las colecciones de datos y los diccionarios son también expresión de esa superioridad: un diccionario mejora la organización de los datos y también la velocidad y precisión del acceso, que está en función del significado y no de la posición como en las listas.

Un diccionario es una colección de elementos organizados mediante pares clave → valor (key:value) en la que cada dato-valor (que sí se puede modificar y duplicar) se almacena asociado a una clave única (por lo que es inmutable y única).



persona = {
    "nombre": "Ana",
    "edad": 34,
    "profesion": "Ingeniera"
}


En este caso el diccionario persona consta de tres claves y cada clave contiene un dato o valor. v.g. la clave "nombre" contiene el valor "Ana"

Si estuviéramos trabajando con listas persona = ["Ana", 34, "Ingeniera"] (1), para obtener la profesión deberíamos expresarlo como persona[2], mientras que como diccionario diremos persona["profesion"]. En el caso de las listas, cualquier modificación del contenido implica un cambio en el procedimiento de acceso que deberemos conocer de antemano; en el caso de los diccionarios es suficiente con conocer el nombre de la clave.

Si lo anterior no fuera suficiente motivo para entender las ventajas de los diccionarios sobre las listas, piensa en la cantidad de "objetos" del mundo real que se pueden expresar como diccionarios:

  • ejemplo: alumno - identificador - nombre - curso - etapa - centro - resultados académicos - MAD - ...
  • Pero también archivo - ruta - tamaño - fecha...

Estos "objetos" y otros muchos se pueden expresar en estructuras clave - valor. Además, Python implementa internamente los diccionarios mediante tablas hash (2), lo que hace que la búsqueda de un elemento por su clave sea, en condiciones normales, extremadamente rápida. En la práctica, acceder a un valor suele realizarse en tiempo constante, independientemente del tamaño del diccionario.

El método más habitual para crear un diccionario consiste en utilizar llaves, como en provincias = {}, instrucción con la que creamos un diccionario vacío que después podemos dotar de contenido, aunque también es posible (y muy frecuente) crear directamente el diccionario definiendo su estructura y contenido, como en este caso...



provincias = {
    "Asturias": "Oviedo",
    "León": "Leon",
    "Cantabria": "Santander"
}


... aunque también se puede crear un diccionario mediante la función dict() - provincias=dict().



# Bien...

provincias = dict(
    Asturias="Oviedo",
    Leon="León",
    Cantabria="Santander"
)

# O también...

provincias = dict([
    ("Asturias", "Oviedo"),
    ("León", "León"),
    ("Cantabria", "Santander")
])


Aunque esta es la forma simple de expresar un diccionario, lo normal es que su contenido sea más complejo. Pensemos, por ejemplo, que creamos el diccionario provincias pero queremos que contenga una colección de datos (capital - población_2025 - peso_sector_primario , peso_sector_secundario, peso_sector_terciario. Ahora ya no hay un ínico dato asociado a la clave, por lo que debemos expresar esta complejidad de otro modo. Y caben dos posibilidades:

  • Como lista provincia:[capital - población_2025 - peso_sector_primario , peso_sector_secundario]
  • o Como diccionarios anidados

Veamos el desarrollo de ambas posibilidades en un script.



# Valor como lista de valores

provincias = {
    "Asturias": ["Oviedo", 1004326, 4.1, 22.7, 73.2],
    "León": ["León", 442000, 8.5, 24.3, 67.2],
    "Cantabria": ["Santander", 593000, 3.8, 20.6, 75.6]
}


# Valor como diccionario anidado lista:valor

provincias = {
    "Asturias": {
        "capital": "Oviedo",
        "poblacion_2025": 1004326,
        "sector_primario": 4.1,
        "sector_secundario": 22.7,
        "sector_terciario": 73.2
    },

    "León": {
        "capital": "León",
        "poblacion_2025": 442000,
        "sector_primario": 8.5,
        "sector_secundario": 24.3,
        "sector_terciario": 67.2
    },

    "Cantabria": {
        "capital": "Santander",
        "poblacion_2025": 593000,
        "sector_primario": 3.8,
        "sector_secundario": 20.6,
        "sector_terciario": 75.6
    }
}


Aunque la primera formulación parece más sencilla de crear, en realidad su manejo resulta más complicado, por lo que es preferible recurrir a los diccionarios anidados por varias razones que no vamos a analizar aquí. Además, los diccionarios anidados constituyen la representación natural en Python de la estructura de un documento JSON. Esta correspondencia casi directa explica que los diccionarios anidados sean la estructura de datos preferida para trabajar con información procedente de diferentes fuentes, espcialmente con las que se expresan como archivos JSON.

Entre otras ventajas, esto hace posible que...



import json

with open("provincias.json", "w", encoding="utf-8") as archivo:
    json.dump(provincias, archivo, ensure_ascii=False, indent=4)


... nos permita almacenar los datos en un documento en formato JSON, el cual podremos visualizar desde bloc de notas.

{ "Asturias": { "capital": "Oviedo", "poblacion_2025": 1004326, "sector_primario": 4.1, "sector_secundario": 22.7, "sector_terciario": 73.2 }, "León": { "capital": "León", "poblacion_2025": 442000, "sector_primario": 8.5, "sector_secundario": 24.3, "sector_terciario": 67.2 }, "Cantabria": { "capital": "Santander", "poblacion_2025": 593000, "sector_primario": 3.8, "sector_secundario": 20.6, "sector_terciario": 75.6 } }

Como puedes comprobar, el archivo JSON reproduce prácticamente la misma estructura jerárquica que nuestro diccionario. Esta correspondencia es precisamente una de las razones por las que los diccionarios tienen un papel tan importante en Python cuando se trabaja con datos estructurados.

Para realizar el proceso inverso (cargar en Python un archivo JSON) deberemos ejecutar este script:



import json

with open("provincias.json", "r", encoding="utf-8") as archivo:
    provincias = json.load(archivo)


Llegados a este punto parece evidente que los diccionarios dan para algo más que una única entrada. Este es un buen momento para reconocerlo y dar por finalizada la actual. Continuará...

NOTAS
1 Esta sería una de las formas en que se podría expresar lo mismo como lista. Pero no es la única. Personalmente, para casos como ese, yo previero usar un procedimiento multilista (una por "campo") y trabajar después con procedimientos de acceso basados precisamente en las posiciones de los elementos de cada una de ellas.
2 Sin entrar en explicaciones que poco nos aportan aquí, una tabla hash es una estructura de datos que utiliza una función hash para transformar una clave arbitraria en un índice numérico que determina dónde se almacenará o buscará el dato asociado dentro de una tabla. Esto hace que se puedan localizar los datos igual de rápido con independencia del tamaño del diccionario (tiempo constante o O(1)), ya que evita la búsqueda secuencial.

lunes, 15 de junio de 2026

DATOS. Tratamiento de datos

Datos no estructurados (VIII)

Reconocimiento de entidades (NER) (VII)

Continuamos ahora la entrada anterior. En ella planteamos que los SLM son adecuados para extraer las NER en local, permitiendo que el tratamiento dependa exclusivamente del blindaje de nuestro propio equipo y no de servicios en la nube. No obstante observamos que esos modelos pequeños de lenguaje presentan determinadas limitaciones en la gestión de tareas de cierta complejidad. En esta entrada abordaremos cómo superar esos techos mediante la segmentación de procesos y el desarrollo de un modelo mixto.

Los resultados del empleo directo del SLM demostraron que las limitaciones de los obtenidos mediante el script no se debían a una incapacidad para extraer datos (capacidad del modelo para leer el texto), sino a la falta de recursos para abordar simultáneamente dos tareas: esa lectura y la estructuración del formato JSON.

Es cierto que existe un margen de mejora mediante el ajuste del prompt haciendo uso de estrategias de prompt engineering, y es conveniente investigar este enfoque; pero cuando la causa radica en las restricciones estructurales, la mejora del prompt no es la solución: los datos de uso indican claramente que el cuello de botella no está en la claridad de las instrucciones, sino en las limitaciones de la memoria de trabajo del modelo.

Además pudimos comprobar en la práctica que modificaciones de la redacción de prompt, lejos de mejorar los resultados, solo trasladaban la causa del fallo de un punto a otro sin resolver el problema de fondo. Esto provocaba un comportamiento pendular de ineficiencia del que era imposible salir. Sólo el cambio de perspectiva consiguió una mejora significativa: la segmentación de procesos.

Este es el script unificado de ambas fases, resultante de la fusión en uno de lo que originalmente fueron dos script diferenciados aunque relacionados.



import json
import re
from typing import Literal
from ollama import chat
from pydantic import BaseModel, create_model

# ==========================================
# 0. CONFIGURACIÓN Y DATOS DE ENTRADA
# ==========================================

MODELO = 'llama3.2:3b'

texto_analizar = (
    "El alumno Alejandro Martínez Soler, escolarizado en 4º de Primaria del CEIP San Juan Bautista "
    "de Oviedo, presenta dificultades de adaptación significativas. La tutora, Doña Carmen Villalobos, "
    "informa de que tras la reunión mantenida el pasado martes 12 de mayo con la madre del menor, "
    "la Sra. Elena Soler, no se ha observado mejoría en el aula. El Equipo de Orientación Educativa "
    "(EOEP) ha sido notificado por el Director del centro, Don Roberto Pando, clasificando el expediente "
    "como intervención prioritaria. Se ha solicitado formalmente la colaboración del Servicio de Salud "
    "del Principado de Asturias (SESPA) para coordinar la valoración psiquiátrica del menor antes "
    "del viernes 19 de junio. Nota: Toda la documentación clínica de Alejandro ha sido archivada en "
    "el aplicativo institucional SAECE según el protocolo de la Consejería de Educación."
)

# ==========================================
# 1. FASE 1: EXTRACCIÓN DE BLOQUES SEMÁNTICOS
# ==========================================

PROMPT_SISTEMA_FASE1 = """
[OBJETIVO]
Localiza y extrae las frases, nombres, fechas e instituciones que representan entidades clave en el texto.
Devuelve el resultado como una lista de texto plano, un elemento por línea, precedido por '- '.

[REGLAS]
1. Extrae los bloques semánticos completos (ej: si hay un nombre con tratamiento de cortesía o cargo, extráelo completo).
2. NO incluyas introducciones, notas ni explicaciones.
3. Conserva las mayúsculas y siglas idénticas al original.

[EJEMPLO]
INPUT: El orientador del CEIP Marcelo Gago de Avilés remitió el informe técnico a la Consejería el pasado lunes 5 de octubre.
OUTPUT:
- El orientador
- CEIP Marcelo Gago
- Avilés
- la Consejería
- lunes 5 de octubre
"""

print("Ejecutando Fase 1 (Extracción de Bloques Semánticos)...")

response_fase1 = chat(
    model=MODELO,
    messages=[
        {'role': 'system', 'content': PROMPT_SISTEMA_FASE1},
        {'role': 'user', 'content': f"INPUT: {texto_analizar}"}
    ],
    options={'temperature': 0.0, 'top_p': 0.1}
)

salida_bruta = response_fase1.message.content
lines = [line.strip().lstrip('- ').strip() for line in salida_bruta.strip().split('\n') if line]

# --- CAPA DE LIMPIEZA DETERMINISTA (PYTHON) ---
STOP_WORDS_BLOQUES = {
    'alumno', 'menor', 'tutora', 'reunión', 'madre', 'director', 
    'aplicativo institucional', 'intervención prioritaria', 'informe'
}

lista_entidades_fase1 = []
for cadena in lines:
    # 1. Eliminar artículos y fórmulas de cortesía al inicio mediante regex
    procesada = re.sub(r'^(el|la|los|las|del|de|don|doña|sr|sra|sra\.)\s+', '', cadena, flags=re.IGNORECASE)
    
    # 2. Filtrar si coincide con una stop word o si quedó vacía
    if procesada.lower() not in STOP_WORDS_BLOQUES and len(procesada) > 2:
        lista_entidades_fase1.append(procesada)

print(f"-> Fase 1 Finalizada. Se detectaron {len(lista_entidades_fase1)} entidades potenciales.")


# ==========================================
# 2. FASE 2: CLASIFICACIÓN SEMÁNTICA (NER)
# ==========================================

# Definimos la taxonomía estricta mediante Literal
CategoriasNER = Literal[
    'ALUMNO', 'ROL_PROFESIONAL', 'CENTRO_EDUCATIVO', 
    'UBICACION', 'FAMILIAR', 'FECHA', 'ORGANISMO', 'APLICATIVO'
]

# Creamos el esquema Pydantic dinámico usando la salida real de la Fase 1
EsquemaClasificacion = create_model(
    'EsquemaClasificacion',
    __base__=BaseModel,
    **{entidad: (CategoriasNER, ...) for entidad in lista_entidades_fase1}
)

PROMPT_SISTEMA_FASE2 = """
[TASK]
Clasifica cada uno de los términos de la lista proporcionada asignándole exclusivamente una de las categorías permitidas.

[CATEGORÍAS PERMITIDAS]
- ALUMNO: Nombres de estudiantes.
- ROL_PROFESIONAL: Cargos, puestos, figuras docentes o equipos de orientación (individuales o colectivos).
- CENTRO_EDUCATIVO: Nombres de escuelas o colegios.
- UBICACION: Localidades, ciudades o provincias.
- FAMILIAR: Relaciones familiares o nombres de familiares del alumno.
- FECHA: Expresiones temporales.
- ORGANISMO: Servicios públicos, organismos de salud o consejerías.
- APLICATIVO: Software o plataformas informáticas.

[CRITICAL RULES]
1. Responde única y exclusivamente con el JSON estructurado según el esquema.
2. No agregues introducciones, notas ni texto fuera del JSON.

[EJEMPLO DE EJECUCIÓN]
INPUT LIST:
- CEIP Marcelo Gago
- Avilés
- lunes 5 de octubre
OUTPUT JSON:
{
    "CEIP Marcelo Gago": "CENTRO_EDUCATIVO",
    "Avilés": "UBICACION",
    "lunes 5 de octubre": "FECHA"
}

[FIN DEL EJEMPLO]
"""

# Convertimos la lista depurada en texto plano para el prompt
input_model_fase2 = "\n".join([f"- {item}" for item in lista_entidades_fase1])

print("\nEjecutando Fase 2 (Clasificación Semántica Estructurada)...")

response_fase2 = chat(
    model=MODELO,
    messages=[
        {'role': 'system', 'content': PROMPT_SISTEMA_FASE2},
        {'role': 'user', 'content': f"INPUT LIST:\n{input_model_fase2}"}
    ],
    format=EsquemaClasificacion.model_json_schema(),
    options={'temperature': 0.0}
)

# ==========================================
# 3. SALIDA FINAL CONSOLIDADA
# ==========================================

try:
    clasificacion_final = json.loads(response_fase2.message.content)
    print("\n--- Resultado Final Consolidado (NER Estructurado) ---")
    print(json.dumps(clasificacion_final, indent=4, ensure_ascii=False))
except Exception as e:
    print(f"\nError al estructurar el JSON final: {e}")
    print("Respuesta cruda del modelo:", response_fase2.message.content)


Este script presenta una arquitectura ajustada a los modelos de lenguaje pequeños y a sus limitaciones. En lugar de pedirle al modelo que extraiga y clasifique todo en un solo paso, el script implementa un patrón de diseño conocido como pipeline de dos fases con validación estricta y que obedece al siguiente esquema:

En la fase 1 se procede a la extracción de los datos mediante SLM y un prompt localizador de texto" que exige al modelo que extraiga bloques semánticos completos. Se definen hiperparámetros (temperature: 0.0 y top_p: 0.1) que fuerza al modelo a un comportamiento determinista y a ceñirse estrictamente al texto.

Además de la extracción de texto, en la fase 1 implementamos una capa híbrida de limpieza determinista mediante Python puro para que asuma funciones de limpieza de ruido en las que este modelo es mucho más eficiente que un modelo de lenguaje. Para esa limpieza empleamos estructuras RegEx (re.sub) y Stop Words (STOP_WORDS_BLOQUES

La fase 2 se inicia con la creación de un modelo dinámico basado en Pydantic. En este punto, el script genera en tiempo de ejecución una estructura de datos adaptada a las entidades encontradas en la fase anterior, definiendo un conjunto estricto de categorías permitidas.

Tras este proceso, de nuevo interviene el SLM para ejecutar el prompt de clasificación. Gracias a que el esquema de Pydantic se inyecta directamente en el motor de inferencia, el modelo queda restringido a devolver, única y exclusivamente, el JSON estructurado con el NER resultante, que es el siguiente:

{
"Alejandro Martínez Soler": "ALUMNO",
"CEIP San Juan Bautista": "CENTRO_EDUCATIVO",
"Oviedo": "UBICACION",
"Carmen Villalobos": "FAMILIAR",
"madre del menor": "FAMILIAR",
"Sra. Elena Soler": "FAMILIAR",
"pasado martes 12 de mayo": "FECHA",
"Roberto Pando": "ALUMNO",
"Equipo de Orientación Educativa (EOEP)": "ROL_PROFESIONAL",
"Director del centro": "ROL_PROFESIONAL",
"Servicio de Salud del Principado de Asturias (SESPA)": "ORGANISMO",
"viernes 19 de junio": "FECHA"
}

Como podemos observar en la salida del script, el formateo es impecable gracias al blindaje de Pydantic. Sin embargo, un modelo pequeño de lenguaje puede perder la relación de contexto y cometer errores importantes. Con todo, la mejora de resultados respecto al enfoque anterior es evidente, lo que demuestra que la segmentación del proceso y la combinación de Python + SLM presenta ventajas respecto a un modelo IA puro, ahorrando un 90% del trabajo de extracción.

Además, aun caben posibilidades de mejora de esta arquitectura modular... pero también plantear una tercera vía —la cual ya hemos esbozado en nuestro último script—: dividir el proceso de extracción de entidades combinando un modelo heurístico basado en SpaCy con otro basado en IA local, para que cada tecnología asuma las tareas en las que presenta mejor rendimiento. Esta podría ser la línea de desarrollo de propuestas para el futuro.

domingo, 14 de junio de 2026

DATOS. Tratamiento de datos

Datos no estructurados (VII)

Reconocimiento de entidades (NER) (VI)

Finalizaba la entrada anterior planteando la existencia de alternativas al modelo heurístico por pipeline híbrido, las cuales se concretan en varias opciones teóricas según vimos anteriormente). En realidad, esas opciones son eso, más teóricas que reales, ya que ni la basada en ML clásico ni la basada en DL discriminativo son realmente viables en función de las restricción reales de datos (confidencialidad, dificultad para conseguir los datos suficientes y coste del etiquetado) ni de hardware (limitaciones de procesamiento y de memoria) que puede afrontar un organismo con escasos recursos como es un SEO. La consecuencia es que sólo es viable la opción basada en IA generativa, pero únicamente la que se puede correr en local, lo que conlleva, además, una limitación en cuanto a los modelos de lenguaje a emplear, que no son otros que los modelos pequeños o SLM. Pero lo que esto no garantizada a priori es la viabilidad real y funcional del modelo IA local.

En consecuencia, la única alternativa viable es la IA generativa ejecutada en local. Esto conlleva, además, una restricción en los modelos de lenguaje a emplear, reduciéndolos a modelos pequeños o SLM. No obstante, esta viabilidad técnica no garantiza, a priori, la eficacia funcional del modelo de IA local para resolver un determinado problema. En el contexto de esta entrada y de las que la preceden, el problema a resolver es (sigue siendo) la extracción de las NER de un texto sintético que ya nos es conocido.

Como tendremos ocasión de comprobar en esta entrada, en la práctica pretender replicar la competencia de un LLM comercial online mediante el simple prompting de un SLM local es una vía infructuosa; las limitaciones en la capacidad de atención y la baja densidad de parámetros de estos modelos pequeños afectan radicalmente a su precisión en el desempeño de tareas como la extracción de datos a partir de un texto no estructurado. Por tanto, alcanzar una equivalencia funcional exige un cambio estructural en el procedimiento. Dado que el modelo en la nube queda descartado por motivos estrictos de confidencialidad, la solución real no radica en el aislamiento del modelo de lenguaje, sino en el diseño de una arquitectura híbrida: un pipeline robusto donde un modelo heurístico asuma la segmentación previa y la estructuración del texto, delegando en el SLM únicamente la extracción fina allí donde las reglas rígidas no alcanzan. El éxito, en consecuencia, pasa de depender de la potencia del modelo a depender de la inteligencia del proceso.

Dado que el tema requiere mucho más espacio del que podemos darle en esta entrada atendiendo a su propia finalidad, no me voy a detener en explicar qué es Ollama y cómo hemos llegado hasta el punto en que somos capaces de utilizar un modelo pequeño de lenguaje SLM como herramienta de trabajo; simplemente lo damos por ahora como un hecho , aunque nos tengamos que detener en algún detalle a la hora de explicar el funcionamiento del script.



# 0. Implementamos las bibliotecas necesarias: json, ollama, pydantic y typing

import json
from ollama import chat
from pydantic import BaseModel, Field
from typing import List

# 1. Definimos el esquema de salida esperado usando pydantic

class EntidadExtraida(BaseModel):
    texto: str = Field(description="El fragmento de texto exacto tal como aparece en el documento original.")
    etiqueta: str = Field(
        description=(
            "La categoría de la entidad. Debe ser una de las siguientes: "
            "'ALUMNO', 'ROL_PROFESIONAL', 'CENTRO_EDUCATIVO', 'UBICACION', "
            "'FAMILIAR', 'FECHA', 'ORGANISMO', 'APLICATIVO'."
        )
    )

class ResultadoNER(BaseModel):
    entidades: List[EntidadExtraida]

# 2. Entregamos el documento a procesar (obtener NER)

texto_analizar = (
    "El alumno Alejandro Martínez Soler, escolarizado en 4º de Primaria del CEIP San Juan Bautista "
    "de Oviedo, presenta dificultades de adaptación significativas. La tutora, Doña Carmen Villalobos, "
    "informa de que tras la reunión mantenida el pasado martes 12 de mayo con la madre del menor, "
    "La Sra. Elena Soler, no se ha observado mejoría en el aula. El Equipo de Orientación Educativa "
    "(EOEP) ha sido notificado por el Director del centro, Don Roberto Pando, clasificando el expediente "
    "como intervención prioritaria. Se ha solicitado formalmente la colaboración del Servicio de Salud "
    "del Principado de Asturias (SESPA) para coordinar la valoración psiquiátrica del menor antes "
    "del viernes 19 de junio. Nota: Toda la documentación clínica de Alejandro ha sido archivada en "
    "el aplicativo institucional SAECE según el protocolo de la Consejería de Educación."
)

print("Enviando petición a llama3.2:3b...")

# 3. Llamada a Ollama utilizando salida estructurada (en el paso 1). Incluye la llamada al SLM y el prompt

response = chat(
    model='llama3.2:3b',
    messages=[
        {
            'role': 'system',
            'content': (
                "Eres un asistente experto en procesamiento del lenguaje natural especializado en el ámbito educativo. "
                "Tu tarea es realizar Reconocimiento de Entidades Nombradas (NER) sobre el texto proporcionado. "
                "Extrae únicamente las entidades literales que correspondan a estas categorías estrictas:\n"
                "- ALUMNO: Nombre del estudiante.\n"
                "- ROL_PROFESIONAL: Cargos, puestos o figuras docentes/técnicas (ej. tutora, Director, Equipo de Orientación Educativa, EOEP).\n"
                "- CENTRO_EDUCATIVO: Nombres de colegios o institutos.\n"
                "- UBICACION: Ciudades, municipios o provincias.\n"
                "- FAMILIAR: Nombres de padres, madres o tutores legales.\n"
                "- FECHA: Referencias temporales explícitas.\n"
                "- ORGANISMO: Entidades públicas, servicios de salud o consejerías.\n"
                "- APLICATIVO: Software, plataformas web o bases de datos institucionales.\n"
                "Sé riguroso y extrae solo texto literal, sin omitir ni inventar nada."
            )
        },
        {
            'role': 'user',
            'content': f"Analiza el siguiente texto:\n\n{texto_analizar}"
        }
    ],
    format=ResultadoNER.model_json_schema(), # Forzamos la estructura JSON mediante el esquema de Pydantic
    options={'temperature': 0.0}  			 # Forzamos a literalidad tratsando de evitar alucinaciones
)

# 4. Procesar y mostrar el resultado

try:
    datos_extraidos = json.loads(response.message.content)
    print("\n--- Entidades Detectadas por llama3.2:3b ---")
    print(json.dumps(datos_extraidos, indent=4, ensure_ascii=False))
except Exception as e:
    print(f"Error al parsear la respuesta: {e}")
    print("Respuesta cruda del modelo:", response.message.content)


Dentro de este script podemos identificar varias partes, empezando por las bibliotecas necesarias para su funcionamiento: json, ollama, pydantic y typing. Cada una de ellas aporta componentes fundamentales para el logro del objetivo propuesto: ollama proporciona el canal de comunicación con el modelo SLM, que es el encargado de leer y "comprender" el documento, pero las demás contribuyen determinando el correcto funcionamiento de éste para que el análisis y la salidad se ajuste estrictamente al objetivo.

El esquema de funcionamiento del script queda descrito por el orden de acciones que se recogen en los comentarios:

  • Primero definimos qué queremos obtener y cómo mediante pydantic y typing
  • Despues porporcionamos el material (texto o conjunto de datos no estructurados) sobre el que trabajar
  • En tercer lugar implementamos el SLM y el instrumento que permite la comunicación entre con él (Ollama), precisamente en orden inverso. En este punto se acoplan las directrices de rol del system prompt con el esquema JSON derivado de Pydantic, fijando la temperatura en 0.0 para garantizar el determinismo absoluto de la prueba.
  • finalmente procesamos los resultados y los mostramos en la consola, controlando cualquier posible error mediante una estructura try...except.

Una breve nota aclaratoria sobre el uso de una herramienta de IA como es Ollama dentro de este script, sin más pretensiones. El uso de un servicio Chat de IA generativa, como Gemini o Chat-GPT, puede dar a entender que esa es, si no la única, sí la principal forma de utilizar la IA, pero lo cierto es que existen otras formas que pasan por integrarla dentro de un script de Python (por ejemplo). Y esto es precisamente lo que hemos hecho en el script anterior; algo que, como podemos observar, resulta muy fácil de realizar en Python al contar con bibliotecas específicas, en nuestro caso from ollama import chat .

En sentido estricto y sumamente restringido, import ollama y...



response = chat(
    model='llama3.2:3b'
    

... son las instrucciones que hacen posible que usemos Ollama response = chat( y el SLM seleccionado model='llama3.2:3b', al que sigue el prompt, que es fundamental para su implementación. Este prompt debe ajustarse a una redacción concreta, ya que de ella depende el funcionamiento efectivo del SLM. Esto es válido también para los LLM, pero aun más para un SLM, dadas sus características y limitaciones respecto a sus hermanos mayores. El prompt que implementamos en este script se puede considerar ajustado a esos condicionantes.



response = chat(
    model='llama3.2:3b',
    messages=[
        {
            'role': 'system',
            'content': (
                "Eres un asistente experto en procesamiento del lenguaje natural especializado en el ámbito educativo. "
                "Tu tarea es realizar Reconocimiento de Entidades Nombradas (NER) sobre el texto proporcionado. "
                "Extrae únicamente las entidades literales que correspondan a estas categorías estrictas:\n"
                "- ALUMNO: Nombre del estudiante.\n"
                "- ROL_PROFESIONAL: Cargos, puestos o figuras docentes/técnicas (ej. tutora, Director, Equipo de Orientación Educativa, EOEP).\n"
                "- CENTRO_EDUCATIVO: Nombres de colegios o institutos.\n"
                "- UBICACION: Ciudades, municipios o provincias.\n"
                "- FAMILIAR: Nombres de padres, madres o tutores legales.\n"
                "- FECHA: Referencias temporales explícitas.\n"
                "- ORGANISMO: Entidades públicas, servicios de salud o consejerías.\n"
                "- APLICATIVO: Software, plataformas web o bases de datos institucionales.\n"
                "Sé riguroso y extrae solo texto literal, sin omitir ni inventar nada."
            )
        },
        {
            'role': 'user',
            'content': f"Analiza el siguiente texto:\n\n{texto_analizar}"
        }
    ],
    

A priori, este diseño satisface los niveles de exigencia del prompting para trabajar con modelos de lenguaje:

  • Asignación de rol que ayuda al modelo a, probabilísticamente, "acotar" el vocabulario y entender el contexto institucional.
  • Instrucciones de restricción rigurosas mediante el uso de palabras ("únicamente", "categorías estrictas" y "solo texto literal, sin omitir ni inventar nada") que actuan como barreras efectivas contra las alucinaciones.
  • Taxonomía con ejemplos In-line, como en ROL_PROFESIONAL, donde se incluyen ejemplos del dominio (tutora, Director, EOEP), lo que permite acotar entidades (v.g. identificar si "Equipo de Orientación" es un rol o un organismo).

Hagamos ahora un sencillo ejercicio de comparación entre lo que nos revuelve el uso de este prompt con Gemini y su LLM...

ALUMNO: Alejandro Martínez Soler
ROL_PROFESIONAL: tutora, Carmen Villalobos, Equipo de Orientación Educativa, EOEP, Director, Roberto Pando
CENTRO_EDUCATIVO: CEIP San Juan Bautista
UBICACION: Oviedo
FAMILIAR: Elena Soler
FECHA: martes 12 de mayo, viernes 19 de junio
ORGANISMO: Servicio de Salud del Principado de Asturias, SESPA, Consejería de Educación
APLICATIVO: SAECE

... y el resultado de su empleo con Ollama + llama3.2:3b

- ALUMNO: Alejandro Martínez Soler
- ROL_PROFESIONAL:
* Tutora: Doña Carmen Villalobos
* Director: Don Roberto Pando
* Equipo de Orientación Educativa (EOEP)
- CENTRO_EDUCATIVO: CEIP San Juan Bautista de Oviedo
- UBICACION: Oviedo
- FAMILIAR:
* Elena Soler (madre del alumno)
* FECHA:
* pasado martes 12 de mayo
* viernes 19 de junio
- ORGANISMO: Servicio de Salud del Principado de Asturias (SESPA)
- APLICATIVO: SAECE

Resulta muy interesante comprobar que el modelo LLM no consigue mejores resultados que el SLM en la tarea propuesta (NER) y sobre un texto plano; esto refuerza la idea de que hoy en día es posible obtener buenos resultados en según que tareas trabajando con modelos pequeños en local, lo que hace posible desarrollar proyectos con estas tecnologías con confianza en los resultados que podemos obtener, facilitando así dar respuesta a condicionantes como son los derivados de la privacidad. no obstante estos modelos no son superiores, ni siquiera iguales, a los LLM en cuanto la tarea se complica o entramos en contextos más extensos o interconectados. Pero cada cosa a su tiempo. De momento nos podemos quedar con lo que hasta ahora nos resulta útil: para un NER, un SLM corriendo en local puede que no tenga mucho que envidiar a un LLM... incluso es posible que se inviertan los papeles...

Pero veamos qué obtenemos al aplicar nuestro script:

{
"entidades": [
{
"texto": "Alejandro Martínez Soler",
"etiqueta": "ALUMNO"
},
{
"texto": "Doña Carmen Villalobos",
"etiqueta": "ROL_PROFESIONAL"
},
{
"texto": "La Sra. Elena Soler",
"etiqueta": "FAMILIAR"
},
{
"texto": "Don Roberto Pando",
"etiqueta": "ROL_PROFESIONAL"
},
{
"texto": "CEIP San Juan Bautista de Oviedo",
"etiqueta": "CENTRO_EDUCATIVO"
},
{
"texto": "Oviedo",
"etiqueta": "UBICACION"
},
{
"texto": "SEPA",
"etiqueta": "ORGANISMO"
},
{
"texto": "SAECE",
"etiqueta": "APLICATIVO"
}
]
}

Es de destacar el cambio formal que se ha producido, ya que pasamos de un listado estructurado de elementos a la expresión formal de una estructura JSON. Pero además también podemos comprobar cómo el mismo SLM que antes extraía los datos solicitados, ahora comete muchos más errores y omisiones. La explicación hay que buscarla en la naturaleza del modelo (y sus limitaciones) enfrentada a las exigencias de la tarea: al ser texto y prompt los mismos, la diferencia estriba en la exigencia de devolver una estructura que obliga a prestar atención a muchos detalles de forma como es un JSON. Ante todo ello, un modelo pequeño de lenguaje es incapaz de ofrecer una respuesta de calidad, la misma que sí puede ofrecer si no se le exige más allá de sus posibilidades. Esto abre una segunda vía de trabajo o, si se prefiere, un diseño en el que el código Python se complemente eficientemente con la IA.

Hasta aquí esta entrada, que evidentemente queda inconclusa. Lo que resta, que no es poco, para la próxima.

NOTAS
1 Puede que sea necesario dedicar una entrada al tema, pero diré que, más por cacharrear que por otra cosa, lo cierto es que tengo instalado Ollama en mi ordenador y con este servicio una serie de modelos. La instalación de Ollama no supone ninguna dificultad y la instalación de los modelos tampoco, así que me voy a ahorrar en estos momentos.
2 Según Gemini, en sentido estricto, Ollama es un entorno de ejecución (runtime) y un gestor de modelos ligero y de código abierto, diseñado para empaquetar, desplegar y ejecutar Modelos de Lenguaje (LLMs y SLMs) de forma local. No es un modelo de inteligencia artificial en sí mismo, sino la infraestructura que permite interactuar con ellos sin dependencia de servicios en la nube.