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.