jueves, 26 de marzo de 2026

Expedientes

No-Expedientes (II)

Hemos aprendido mucho del análisis de lo que hemos venido a llamar no-expedientes de archivo único, pero el siguiente grupo de directorios (no-expedientes de dos archivos) requiere nuevos planteamientos de análisis.

Conocemos por la fase previa de este análisis que son 64 los directorios que contienen dos archivos y que el número de archivos se eleva a 128, pero desconocemos de qué tipo son estos archivos y en cuantos de los no-expedientes está presentes. Estos fueron cálculo que obtuvimos para los no-expedientes de un archivo y no veo motivo para no aplicarlos también a este nuevo subconjunto: además de obtener resultados de interés (respecto a la tipología documental), también podremos aplicar el mismo criterio que en el grupo anterios e identificar cual es la incidencia de esas tipologías no-procesador que en su momento consideramos indicadores fallidos de generación de expedientes. Para ello necesitamos realizar una asociación entre tipos de archivos y directorios.

El modo en que esto se ha concretado implica procedimientos de mayor complejidad que en el caso de los directorios con un único archivo, ya que es ncesario proceder a diferenciar los directorios en dos subgrupos, dado que sólo deberemos trabajar con uno de ellos para el análisis subsiguiente. Pero no avancemos acontecimiento antes de tiempo.

Primero debemos obtener el listado de los archivos del subconjunto (los 128 archivos) e información sobre su tipología, datos que te aporto en la tabla que va a continuación:

Tipología Número Porcentaje
.doc 79 61.7
.docx 9 7
.odt 7 5.4
Procesador 95 74.2
PDF 28 21.8
H. cálculo 5 3.9
No procesador 33 25.8

Aunque no es tiempo aun para este tipo de análisis, no me resisto a decir que respecto a los directorios de un único archivo se observa ahora un incremento porcentual del peso de los archivos que no derivan del uso del procesador de texto (pasamos de suponer el 9% del total de archivos a alcanzar cerca del 26%) lo que lógicamente tendrá una incidencia importante en el número de directorios que podremos considerar expedientes fallidos y no meros no-expdientes. Pero para obtener datos al respecto necesitamos que nuestro script nos proporcione información directa sobre la indidencia y reparto de esos archivos (.pdf y H. de cálculo) en el conjunto de directorios de dos archivos.

La segunda fase consiste precisamente en realizar esa asociación entre archivos y expedientes, pero necesitamos algo más que los datos, ya que posteriormente deberemos trabajar de forma diferenciada y específica con el subgrupo de aquellos no-expedientes que no contienen no-Procesadores. Es por ello que generamos un script que, además de identificar de qué archivo se trata (según extensión), identifique el directorio en que se encuentra y lo traslade a un directorio específico.

El resultado de este proceso son dos directorios que contienen respectivamente 26 expedientes (que podemos identificar como expedientes fallidos por contener documentos no-Procesador) y 38 no-expedientes (que contienen archivos Procesador exclusivamente. Esto supone que el 43.75% de los directorios (inicialmente no-expedientes) de dos archivos no entran dentro del grupo predominante (90,9%) de los no-expdientes de un archivo. Evidentemente un cambio muy significativo que posiblemente requiera de un análisis complementario (¿qué son en realidad los archivos .pdf?, ¿se pueden considerar directamente como archivos-con-intención-de-conformar-expediente?); aunque no nos tiene que hacer olvidar que en este segundo subgrupo siguen predominando (cerca del 60%) los no-expedientes. ¿O no?

Esa es la tercera cuestión de interés por ser la que más nos hace avanzar en el procedimiento de análisis y su automatización. Para que nos situemos: inicialmente habíamos considerado que cierta agrupación de cierto tipo de archivos podría estar indicando indicios de pre-expedientes que no podríamos considerar expedientes fallidos (por carecer de determinada composición, la antes vista, en la que no constan los informes psicopedagógicos) pero tampoco como no-expediente por observarse interés por recopilar precisamente esos documentos (los informes psicopedagógicos), que no son suficientes pero sí necesarios para que un grupo de documentos pase a constituirse en expediente.

Para facilitar el desarrollo de este análisis antes diferenciamos físicamente dos subconjuntos de directorios y ahora trabajaremos sólo sobre uno de ellos: el que contiene únicamente directorios ("expedientes) con archivos .doc, .docx y .odt. Esta disparidad tipológica, aunque reducida, sigue siendo un inconveniente para automarizar el análisis, así que vamos a realizar una copia de ese directorio y, sobre ellas, aplicaremos un procedimiento para convertir todos los archivos en .docx.



#Bibliotecas necesarias ---------------------------------------------------

import os
import subprocess
import pathlib

#Función -------------------------------------------------------------------

def convertir_y_limpiar(ruta_absoluta):
    # Configuración de ruta
    RUTA_LIBREOFFICE = r'C:\Program Files\LibreOffice\program\soffice.exe'
    
    if not os.path.exists(RUTA_LIBREOFFICE):
        print(f"Error: No se encontró LibreOffice en {RUTA_LIBREOFFICE}")
        return

    extensiones_a_convertir = {'.doc', '.odt'}
    
    if not os.path.exists(ruta_absoluta):
        print(f"Error: La ruta {ruta_absoluta} no existe.")
        return

    print(f"Procesando archivos en: {ruta_absoluta}...\n")
    
    for raiz, dirs, archivos in os.walk(ruta_absoluta):
        for nombre_archivo in archivos:
            ruta_original = pathlib.Path(os.path.join(raiz, nombre_archivo))
            ext = ruta_original.suffix.lower()

            # 1. Ignorar si ya es .docx
            if ext == '.docx':
                continue

            # 2. Procesar solo .doc y .odt
            if ext in extensiones_a_convertir:
                # Definimos cómo se llamaría el nuevo archivo
                ruta_nuevo_docx = ruta_original.with_suffix('.docx')
                
                print(f"Transformando: {nombre_archivo} -> {ruta_nuevo_docx.name}")
                
                try:
                    # Ejecutar conversión
                    resultado = subprocess.run([
                        RUTA_LIBREOFFICE, 
                        '--headless', 
                        '--convert-to', 'docx', 
                        '--outdir', str(ruta_original.parent),
                        str(ruta_original)
                    ], check=True, capture_output=True)

                    # 3. Verificación de seguridad: ¿Existe el nuevo archivo?
                    if ruta_nuevo_docx.exists():
                        # Eliminamos el original solo si el nuevo ya existe
                        os.remove(ruta_original)
                        print(f"   [OK] Convertido y eliminado original.")
                    else:
                        print(f"   [!] Error: No se encontró el archivo convertido {ruta_nuevo_docx.name}")

                except Exception as e:
                    print(f"   [X] Error procesando {nombre_archivo}: {e}")

    print("\n¡Limpieza y conversión completadas!")

#Script de aplicación de función ---------------------------------------------------------------------

if __name__ == "__main__":
    mi_ruta = "Mi_Ruta"
    convertir_y_limpiar(mi_ruta)


Mediante este script, y usando la utilidad de conversión de LibreOffice RUTA_LIBREOFFICE = r'C:\Program Files\LibreOffice\program\soffice.exe', accedemos a cada uno de los archivos de la ruta mi_ruta = "Mi_Ruta" que pasamos como parámetro a la función convertir_y_limpiar(mi_ruta), identificamos su extensión y convertimos a docx ruta_nuevo_docx = ruta_original.with_suffix('.docx'). Gracias a ello, a partir de este momento podemos aplicar la lógica de análisis, que se basa parcialmente en el uso de la biblioteca python-docx. Explico primero la lógica y después muestro y comento brevemente el script.

Después de analizar posibilidades opto por una combinación de procedimientos que puede resultar restrictiva, pero que evitará falsos positivos, lo que constituye el objetrivo prioritario en este momento: se trata de seleccionar aquellos no-expedientes que pudieran ser expedientes-incipientes, habiendo definido (en su momento) como tales aquellos en los que su creador recogió los documentos indispensables pero insuficientes para condicionar que un directorio o carpeta pueda ser considerado como expediente SEO cuando cuenta con el número mínimo de archivos. Todo esto, para directorios de dos archivos se concreta del siguiente modo: los dos archivos son informes psicopedagógicos.

La consecuencia que deriva del planteamiento anterior es que tenemos que identificar aquellos directorios que contienen dos archivos-procesador (ahora ambos .docx) cuyo contenido sea un informe psicopedagógico. Sólo en ese caso, sólo con esa combinación de contendio, un directorio pasa de la categoría de no-expedientes a la de expediente-incipiente; tercera categoría en que se pueden dividir los directorios-expedientes de dos archivos.

Para identificar estos directorios he pensado que sería útil analizar primero el nombre del archivo, ya que es común que los informes psicopedagógicos lleven la secuencia "inf" , "info" o "informe" en su nombre; pero esto no es definitorio, ya que otros documentos también llevan esa secuencia sin ser informes psicopedagógicos en sentido estricto. Por ello es necesario ir más allá del análisis del título y acceder al contenido del documento (para lo es que necesaria la biblioteca python-docx) y confirmar la presencia de cierto contenido que sea propio de un informe psicopedagógico.

Este segundo paso, que sólo se ejecuta si se cumple el primer criterio (lo que puede resultar demasiado restictivo) supone dar un salto muy importante en el acceso al contenido de un documento y debemos ser cautos y comedidos al darlo, ya que se abren muchas posibilidades y ahora sólo nos interesan unos mínimos, aunque claros y suficientes. Esa cautela implica que opte por identificar en el encabezado y/o título del documento una expresión que resulte prototípica de un informe psicopedagógico, como es precisamente ese misma secuencia de palabras. Como veremos, la prueba de esta fórmula resultó adecuada, pero fue necesario corregirla para evitar falsos negativos, ya que un número relativamente importante de informes tienen un título ligeramente diferente: Informe de Evaluación Psicopedagógica, por lo que fue necesario ampliar los referentes a buscar. Este es el script:



import os
import docx
import re
import unicodedata
from pathlib import Path

def normalizar(texto):
    """Elimina tildes y pasa a minúsculas para evitar fallos de coincidencia."""
    texto = unicodedata.normalize('NFD', texto)
    return "".join(c for c in texto if unicodedata.category(c) != 'Mn').lower()

def procesar_archivos(ruta_raiz):
    patrones_nombre = ['inf', 'info', 'informe']
    # Expresión regular: 'informe' seguido de cualquier cosa (.*?) y luego 'psicopedagogic'
    # El 'ic' al final cubre psicopedagógico, psicopedagógica, psicopedagógicos...
    patron_contenido = r"informe.*?psicopedagogic"
    
    conteo_por_carpeta = {}
    
    print(f"Buscando informes en: {ruta_raiz}\n")

    for raiz, dirs, archivos in os.walk(ruta_raiz):
        for nombre in archivos:
            if not nombre.lower().endswith('.docx'):
                continue
            
            # 1. Filtro por nombre de archivo
            if any(p in nombre.lower() for p in patrones_nombre):
                try:
                    doc = docx.Document(os.path.join(raiz, nombre))
                    # Unimos los primeros 10 párrafos (título/encabezado) en un solo bloque
                    texto_inicio = " ".join([p.text for p in doc.paragraphs[:10]])
                    texto_ready = normalizar(texto_inicio)
                    
                    # 2. Filtro por contenido (incluye 'de evaluación' automáticamente)
                    if re.search(patron_contenido, texto_ready):
                        print(f"[OK] ENCONTRADO: {nombre}")
                        if raiz not in conteo_por_carpeta:
                            conteo_por_carpeta[raiz] = []
                        conteo_por_carpeta[raiz].append(nombre)
                except:
                    print(f"[!] Error leyendo: {nombre}")

    # Escribir el informe TXT final
    ruta_txt = Path(__file__).parent / "carpetas_con_dos_informes.txt"
    with open(ruta_txt, "w", encoding="utf-8") as f:
        f.write("LISTADO DE CARPETAS CON EXACTAMENTE 2 INFORMES\n")
        f.write("="*50 + "\n")
        for carpeta, lista in conteo_por_carpeta.items():
            if len(lista) == 2:
                f.write(f"Directorio: {carpeta}\n")
                f.write(f"Archivos: {', '.join(lista)}\n\n")

    print(f"\nProceso finalizado. Informe guardado en: {ruta_txt}")

if __name__ == "__main__":
    ruta = input("Introduce la ruta absoluta de los archivos: ").strip().replace('"', '')
    procesar_archivos(ruta)


Primero aseguramos que se cumple el criterio 1: que el nombre del archivo indique que puede tratarse de un informe. Para ello analizamos si esos términos patrones_nombre = ['inf', 'info', 'informe'] forman parte del nombre del archivo...

... y condicionamos if any(p in nombre.lower() for p in patrones_nombre) el análisis de la segunda condición al cumplimiento de la primera. Esta segunda condición (que el texto contenga determinadas palabras) requiere de instrucciones más complejas...


# Unimos los primeros 10 párrafos (título/encabezado) en un solo bloque
texto_inicio = " ".join([p.text for p in doc.paragraphs[:10]])
texto_ready = normalizar(texto_inicio)

# 2. Filtro por contenido (incluye 'de evaluación' automáticamente)
if re.search(patron_contenido, texto_ready):

... lo que da muestra de la complejidad que conlleva el estudio del contenidos de un documento no estructurado; pero esta es otra cuestión: para lo que ahora nos intereresa, la solución encontrada es suficiente.

Suficiente y funcional, ya que nos permite localizar todos los archivos que contienen una expresión competible con que sean un informe psicopedagógico y confirmar que efectivamente lo son, o que no lo son. El resultado en términos de expedientes es el esperado: sólo cuatro de los 38 pueden ser considerados expedientes-incipientes; tres son identificados por el script de automatización y uno resulta del análisis visual de una coincidencia que indica el listado de los archivos potenciales candidatos a informe.

Aunque es posible que los datos no sean totalmenten correctos, principalmente por quedar pendiente el análisis de los archivos .pdf, el procedimiento seguido, apoyado en Python sí lo es y los resultados obtenidos se aproximan mucho a los esperados:

Concepto Valores Porcentajes
Directorios con dos archivos 64 100%
Expedientes fallidos 26 40.6%
No-expedientes 34 53.1%
Expedientes-incipientes 4 6.3

En cierto modo sorprende la importancia cuantitativa del grupo de expedientes fallidos (cerca del 40%), debido en gran medida al peso de los archivos .pdf, lo que sugiere que puede ser necesario analizarlos de forma específica (pero no en estos momentos), pero sí es ajustado a pronóstico la escasa incidencia de los expedientes incipientes (6% del total), lo que confirma como correcta la consideración inicial de este bloque de directorios como no-expedientes y su aislamiento (cuanto menos para un análisis diferencial) del conjunto de potenciales expedientes SEO. Toda esta coherencia se basa en el criterio cuantitativo: es necesario un mínimo para que un expedientes sea algo más que una colección de documentos; a partir de ese mínimo, además, deben cumplirse criterios de tipología y relevancia para que se confirmen como tales expedientes; pero ese mínimo es fundamental.

Precisamente en grupo de no-expdientes que nos falta por analizar es el que se encuentra en el límite del criterio cuantitativo. Esperemos a ver qué resulta de su análisis en una próxima entrada.

domingo, 22 de marzo de 2026

Expedientes

No-Expedientes (I)

Voy a llamar colecciones de archivos a este conjunto de carpetas que contienen archivos (documentos) de alumnos pero que no podemos considerar expedientes SEO. Las razones para ello fueron explicadas (al menos parcialmente) en esta entrada, pero posiblemente sólo en parte. Empezaré por completar esta cuestión.

Cuando un directorio contiene un número muy pequeño de archivos no es posible considerarlo de utilidad para facilitar la información necesaria para el seguimiento del alumno. Eso es lo que sucede obviamente cuando el directorio sólo contiene un archivo y este es el caso de un total de 99 directorios o carpetas inicialmente consideradas parte del conjunto de directorios de nuestra muestra.

Respecto a este amplio grupo de "carpetas" cabe decir que muy posiblemente su existencia no tiene nada que ver con ningún intento deliberado de generar algo parecido a un expediente SEO; posiblemente tan sólo está ahí casualmente o como mero recuerdo de que el ordenador es para el SEO, primero, una compleja máquina de escribir. Al menos así fue en un principio, momento en el que el procesador de texto se convirtió en el principal servicio y la creación de documentos con esa herramienta simplemente el sustituto de la máquina de escribir. Los documentos se imprimian en papel y algunos en vez de perderse quedaban almacenados, facilitando de paso un futuro uso a modo de "modelo-para-el siguiente".

Aunque lo anterior no sea ni del todo cierto ni la única o principal causa del fenómeno que acumulativamente se nos presenta ahora en forma de 99 carpetas con nombres de alumno, lo cierto es que su presencia abultada y peso mucho, distorsionando de forma importante los datos que resultan necesarios para nuestro análisis. Empezando por los estadisticos de centralidad, como la moda, que se ubica precisamente en este grupo, y la media, queda rebajada a un límite extremadamente próximo al mínimo de utilidad para el objetivo pretendido (recuerda, 3,68 archivos por expediente)

El segundo grupo que entraría dentro de este bloque es el formado por las carpetas que contienen dos archivos. Tampoco con dos archivos podemos garantizar un mínimo suficiente para el seguimiento del alumnado, aunque es cierto que en condiciones extraordinarias esa afirmación podría ser puesta en duda.

Podría darse el caso (siempre extraordinario) de que dos archivos condensen la información necesaria para que de su estudio pudiera derivarse información suficiente para el seguimiento del alumno, pero no lo sería con el tipo de documentos que se incluyen en estos expedientes, salvo que lo que deramos decir es que hacemos de la necesidad virtud y nos conformamos con lo que tenemos; pero no es esa la cuestión.

Podemos imaginar un teórico escenario en el que los dos archivos son dos informes psicopedagógicos entre los que han transcurrido algunos cursos (para que puedan aportar información diferenciada y de evolución), o un informe psicopedagógico del inicio de la intervención y un informe de seguimiento que se prolonga satisfactoriamente en el tiempo. Es cierto que en esos casos, idealmente, podríamos hablar de documentación suficiente para facilitar el seguimiento del alumno, pero sólo idealmente.

Puede que en estos extraordinarios casos nos encontráramos en el umbral de lo que es "suficiente", pero más en el plano teórico que en el real: empezando por que un informe psicopedagógico no tiene por objetivo recopilar información de seguimiento del alumno, sino identificar sus necesidades educativas y orientar la intervención, así que no es de esperar que disponer de dos informes (sin más) sea ni suficiente ni satisfactorio. Y de serlo no es posible considerarlo como causa para aceptar las carpetas de dos documentos como expedientes.

El segundo caso (informe + informe+de+seguimiento) es absolutamente extraordinario y es preciso profundizar en el contenido del segundo de esos dos documentos para aceptarlo como expediente. Desde luego no podemos hacerlo sólo por el título de los documentos, así que tampoco en este caso es aceptable como pasaporte para la consideración de expediente SEO.

Y lo que no se puede admitir sin análisis (en general incluso con análisis) como expediente, especialmente si su peso cuantitativo es relevante, debe ser eliminado de la muestra por las mismas razones que lo fueron las carpetas de un documento: para que no distrosiones la muestra. Este es, de nuevo, el caso de las carpetas de dos documentos: son muchas y afectan a la moda y a la media. Como ejemplo: si eliminamos de la muestra las carpetas con un documento, son las carpetas de dos documentos las que determinan la moda de la distribución.

Por último, tenemos las carpetas con tres archivos, el conjunto que mayores dudas suscita precisamente por encontrarse en el límite del mínimo suficiente. En este caso no me atrevo a decir que su descuento pueda ser una medida exenta de dudas, ya que vimos antes que existen casos en los que teóricamente dos archivos podrían constituir ese mínimo. Si añadimos un tercer archivo tenemos, idealmente, ese mínimo suficiente. Lo malo es que sólo en teoría, así que toca analizar carpeta (de tres) por carpeta para comprobar que efectivamente se cumple la condición mínima de dos informes psicopedagógicos + un informe-de-seguimiento. Pueden aceptarse otras combinaciones, pero es necesario un estudio que va más allá de la identificación del documento, cosa que en el momento actual del análisis no nos podemos permitir.

Así que, si bien en este caso partimos de la duda como planteamiento de base y debemos someter a cada una de las carpetas a un análisis de contenido para reubicarlas como expdientes SEO o mantenerlas al margen, inicialmente debemos considerarlas como parte de este bloque de no-suficientes-para-expedientes, ya que tampoco es aceptable su incorporación a la espera de expulsión: es preferible operar al contrario, que es como aquí se ha planteado el procedimiento.

La consecuencia de todo lo dicho hasta este momento es la diferenciación del conjunto muestral inicial en dos bloques: el de las carpetas-no-expdientes y el de los expedientes SEO. El primero está formado por los conjuntos directorio-de-un-archivo + directorio-de-dos-archivos y directorio-de-tres-archivos (pendiente de estudio del contenido). En el segundo estarán todos los directorios que cuentan al menos con cuatro archivos, no por estar garantizado que su contenido es ciertamente el apropiado para el seguimiento, sino porque de la cantidad de documentos de que constan es posible detraer información suficiente para generar datos de utilidad para el seguimiento. Y con eso, al menos inicialmente, nos debe ser suficiente para poder avanzar. Tiempo tendremos de matizar las cuestiones los suficiente para alcanzar consensos mínimos de lo que es una composición y una organización que satisface criterios para hacer de los expedientes SEO herramientas de trabajo útiles.

Y hablando de utilidad, antes de entrar en el análisis que justifica esta entrada, decir que la no pertenencia de determinados conjuntos de carpetas al colectivo expedientes SEO no implica que sus archivos sea irrelevantes para otros análisis. En tiempos en los que la documentación y los datos son un tesoro, ningún archivo es desdeñable. Pero su utilidad no lo es en tanto que expediente, sino en tanto que archivo, por su tipología, por los datos que contiene, por el motivo por el que se creo o por el momento en que se hizo... por la razón que sea, pero como archivo. Ese es otro nivel de análisis, no el que toca aquí y ahora

Empecemos por recordar que en la entrada anterior habíamos creado herramientas basadas en Python que nos permitieron construir un documento csv donde constaban los directorios y el número de archivos que contenía cada uno de ellos. Gracias a esta base de datos nos fue posible observar el peso de cada conjunto de directorios, incluyendo la identificación de aquellos que contenían de 1 a 3 archivos (los que aquí consideramos). Pudimos crear una gráfica de frecuencias (aun no explicamos cómo, ni todavía toca), la cual nos permitió visulizar el peso de las carpetas con menos archivos en nuestra muestra, con las consecuencias que antes expliqué: no podemos permitir que ese gran peso de las carpetas-no-expedientes distorsionen el análisis que nos planeamos realizar respecto al potencial de lo que denominamos expedientes SEO si es que queremos convertirlos realmente en una herramienta de trabajo.

Mediante el simple acceso al archivo csv desde LO-Calc ha sido posible dividir la base de datos original en dos bases de datos, la primera con las carpetas de las que ahora nos ocupamos, la segunda con los que consideramos expedientes SEO. Convertimos la primera en documento Calc y trabajamos esos datos para obtener la información cuantitativa básica que te muestro a continuación.

Gráfico y tabla describen la misma realidad en lo que a "carpetas" se refiere: el grupo de un archivo representa cerca de la mitad del total de carpetas y el de dos archivos cerca de un tercio del total; pero si lo analizamos en términos de archivos, se invierten los pesos. De momento nos vamos a centrar en el peso de las carpetas, ya que estamos hablando de "expedientes", no de su contenido.

Pero estos datos poco más nos pueden aportar, y nada de ello es relevante para el segundo objetivo de este primer análisis: identificar el contenido para conocer sus características y, en el caso de las carpetas con 3 archivos, ver cuales de ellas podrían ser consideradas expedientes SEO

Para ello necesitamos algo más que acceso a una base de datos csv, necesitamos acceso a las carpetas físicas para generar agrupaciones en función del número de archivos de que constan para poder listarlos de forma diferenciada. No nos salimos (aun) de los límites de los archivos como fuente de datos, pero tenemos que avanzar en esa línea si queremos conocer las características específicas de esos no-expedientes o aun-no-expdedientes. Y eso no es algo que podamos hacer mediante OOo Basic. Sí mediante Python



#Librerías necesarias ----------------------------------------------------------

from pathlib import Path
import shutil

#Función -------------------------------------------------------------------------

def procesar_directorios(ruta,archivos,directorio):

    base_path = Path(ruta)
    destino_path = base_path / directorio

    if not base_path.is_dir():	    # Validar que la ruta existe
        print(f"Error: La ruta {ruta} no es un directorio válido.")
        return

    if not destino_path.exists():	  # Crear directorio de destino si no existe
        destino_path.mkdir(parents=True)

# Analizar contenido del directorio

    for item in base_path.iterdir():
        if item.is_dir() and item.name != directorio:   # Solo procesamos carpetas y evitamos la propia carpeta de destino
            # Conteo en profundidad (recursivo). f.is_file() asegura que solo contamos archivos
            conteo = sum(1 for f in item.rglob('*') if f.is_file())
            if conteo == archivos:	  # Criterio de traslado
                print(f"Moviendo '{item.name}' ({conteo} archivos)...")
                try:
                    shutil.move(str(item), str(destino_path / item.name)) # Se mueve la carpeta completa con todo su contenido
                except Exception as e:
                    print(f"No se pudo mover {item.name}: {e}")

#Ejecución de la función------------------------------------------------------------

if __name__ == "__main__":

#Primero introduce los datos necesarios
    # 1. Introduce aquí la ruta completa de la carpeta a analizar
    ruta_origen = r'Aquí_Tu_ruta' 
    # 2. Número de archivos para que una carpeta sea movida
    num_archivos =  #Aquí_número_archivos
    # 3. Nombre de la carpeta donde se trasladarán los resultados
    destino = 'Aquí_Nombre_directorio_para_carpetas'

#Despues llama a la función
    procesar_directorios(ruta_origen,num_archivos,destino)

    print("Proceso completado.")


Mediante este script obtenemos los "expedientes" que contienen un determinado número de archivos. Por ejemplo, las carpetas con uno, dos o tres archivos (con independencia de que existan subdirectorios internos) y los trasladamos íntegramente a un directorio de destino, específico para ese grupo de "expedientes". La isntrucción if conteo == archivos: es fundamental para ello.

Debes tener en cuenta que los directorios de destino se crean en el mismo directorio donde se buscan las carpetas-expedientes, así que debes poner nombres de directoriosq que resulten fáciles de identificar y que expliciten el contenido, por ejemplo Dir_2_archivos.

Gracias a este script podemos disponer de diferentes directorios donde se guardan los no-expedientes (y los expedientes), diferenciándolos por le criterio número-de-archivos. Concretamente he creado cuatro (sub)directorios para los no-expedientes (uno, dos u tres archivos) y un (sub)directorio para los expedientes (4 y más archivos). De este modo me puedo centrar en el análisis de cada uno de los conjuntos de datos.

Empezando por el subconjunto no-expedientes de un archivo, me interesa obtener un listado de todos los archivos que contiene (no de los directorios, que ya está disponible) para conocer detalles relevantes de los mismos que puedan servir para realizar análisis pertinentes de los subconjuntos restantes, además de para conocer las características de los de este subconjunto.

Para obtener este listado ejecutaré el siguiente script:

 

#Librería -------------------------------------------------------------

from pathlib import Path

#Función ---------------------------------------------------------------

def listar_y_guardar_archivos(ruta_base, nombre_txt):
    """
    Lista archivos de forma recursiva, los muestra por pantalla y los guarda
    en el subdirectorio 'documentos' ya existente en la raíz del script.
    """
    # Definición de rutas según estructura solicitada
    directorio_script = Path(__file__).parent
    ruta_txt_salida = directorio_script / "documentos" / nombre_txt
    directorio_explorar = Path(ruta_base)

    # Validación de la ruta de origen
    if not directorio_explorar.exists() or not directorio_explorar.is_dir():
        print(f"Error: La ruta '{ruta_base}' no es válida.")
        return

    # Obtención de archivos en profundidad
    archivos = [archivo for archivo in directorio_explorar.rglob("*") if archivo.is_file()]

    if archivos:
        # Escritura en el archivo .txt dentro de /documentos/
        with open(ruta_txt_salida, "w", encoding="utf-8") as f:
            for a in archivos:
                nombre_solo = a.name
                print(nombre_solo)          # Mostrar solo nombre por pantalla
                f.write(nombre_solo + "\n") # Guardar solo nombre en el archivo
    else:
        print("No se encontraron archivos.")

#Script de llamada a la función -------------------------------------------------------------------------

if __name__ == "__main__":
    # Variables locales pasadas como parámetros
    ruta_a_explorar = "Aqui_Tu_Ruta"
    archivo_reporte = "Mi_Lista.txt"
    
    listar_y_guardar_archivos(ruta_a_explorar, archivo_reporte)


Gracias a este script podemos crear los listados de archivos (únicamente de archivos) que deseemos en función del directorio raiz (ruta) en que se encuentren los no-expedientes o los expedientes (o cualquier otra configuración de archivos). Esta lista se exporta como documento .txt y queda disponoble para un posterior manejo mediante procedimiento de acceso a documentos de datos no estructurados, aunque el formato de organización del contendio (como lista simple) facilita el acceso y el recorrido del citado .txt, lo que no evita que necesitemos recurrir a procedimientos específicos de acceso a este tipo de archivos, como es el caso.

Con fines didácticos más que por necesidad voy a proceder ahora como si de dos fases diferenciadas dentro del análisis se tratara y generaré un script para acceder al contenido del archivo-lista recien creado para ejecutar sobre ella un procedimiento simple de análisis que consiste en estudiar los tipos de archivos a fin de responder a una cuestión que ha estado en la base del análisis planteado hasta el momento respecto al colectivo documental de no-expdientes de un elemento: la ausencia de interés por generar un expediente SEO como motivación para la creación de estas carpetas.

En sentido estricto es esta una suposición difícil de comprobar ya que implica un conocimiento de intenciones del que no podemos disponer, pero sí podemos estudiarlo de forma indirecta en el siguiente supuesto (que ya se expuso en otro momento): son documentos que reflejan fundamentalmente el uso del medio informático como mero procesador-de-texto o máquina-de-escribir. De ser esto cierto, los documentos deben tener extensiones propias de estos servicios y la presencia de extensiones propias de servicios que implican cierto interés por el almacenamiento en formato pdf y más aun por documentos basados en hojas de cálculo deben estar ausentes o mínimamente representados. Veamos hasta qué punto se cumple esta hipótesis en el bloque de documentos en que con más facilidad y razón de ser se pruee cumplir.

Para ello crearemos un script que accede al listado, lo carga en memoria, analiza en cada línea (entrada o registro) la extensión del documento y realiza un recuento de tipología, lo expresa mediante una tabla simple y genera una gráfica de barras con esos datos.




#Bibliotecas ----------------------------------------------------------------------------------

import pandas as pd
import matplotlib.pyplot as plt
import os

#Función ---------------------------------------------------------------------------------------

def analizar_extensiones_txt(ruta_archivo):

# 1. Validación de existencia
    if not os.path.exists(ruta_archivo):
        print(f"⚠️ Error: No se localizó el archivo en: {ruta_archivo}")
        return pd.DataFrame(), None

    try:
        with open(ruta_archivo, 'r', encoding='utf-8') as f:
            lineas = f.read().splitlines()
        
# 2. Extraer extensiones
        extensiones = [os.path.splitext(doc.strip())[1].lower() or 'Sin extensión' 
                       for doc in lineas if doc.strip()]

        if not extensiones:
            print("pst... El archivo está vacío.")
            return pd.DataFrame(), None

# 3. Crear Tabla de Datos
        df = pd.DataFrame(extensiones, columns=['Extensión'])
        tabla_frecuencia = df['Extensión'].value_counts().reset_index()
        tabla_frecuencia.columns = ['Extensión', 'Frecuencia']

# 4. Configurar el gráfico (sin mostrarlo)
        fig, ax = plt.subplots(figsize=(10, 6))
        ax.bar(tabla_frecuencia['Extensión'], tabla_frecuencia['Frecuencia'], color='teal')
        ax.set_title('Análisis de Extensiones')
        ax.set_ylabel('Cantidad')
        
        return tabla_frecuencia, fig

    except Exception as e:
        print(f"Ocurrió un error inesperado: {e}")
        return pd.DataFrame(), None

# --- Script ----------------------------------------------------------------------------------------------------------------

mi_ruta_archivo = r"C:/Users/alons/proyectos_python/proyecto_expedientes/documentos/listado_uno.txt"

# Ejecución con validación
resultado_tabla, grafico = analizar_extensiones_txt(mi_ruta_archivo)

if not resultado_tabla.empty:
    print("### TABLA DE RESULTADOS ###")
    print(resultado_tabla)
    if grafico:
        plt.show()
else:
    print("No se pudieron generar resultados. Revisa la ruta o el contenido del archivo.")


Este gráfico muestra la distribución de la tipología de archivos. Predominan claramente los archivos .doc, creados con el procesador de textos Word. Además tienen cierta antigüedad, ya que las versiones actuales de Word tienen .docx por extensión. Este dato confirma nuestra hipótesis, que se ve reforzada por el sumatorio que resulta de añadir a ese grupo los documentos de las extensiones .docx (3 archivos) y .odt (5 archivos); esto hace un total de 90 archivos, el 90,9% del total.

Tipo de archivos Número Porcentaje
Documento de texto 90 90,9%
PDF 5 5%
Hoja de cálculo 4 4%
Esta tabla recoge ese sumatorio y facilita la observación de que el resto (9 archivos) se dividen casi a partes iguales entre .pdf (5 archivos) y otros 4 sobre hojas de cálculo. Estos 9 archivos (el 9,1% del total) podemos considerarlos como contrarios a la hipótesis inicial, lo que no impide que ésta se confirme, pero la matiza: el 9% de los no-expedientes de un archivo se pueden considerar expedientes fallidos, aunque el 90% encaja perfectamente con la interpretación desarrollada respecto a considerar los 99 directorios que constan de un único archivo como mera supervivencia de las formas iniciales de uso de los medios informáticos por parte de los SEO, que generaban y mantenian expdientes SEO analógicos, aunque se servían de los medios informáticos a modo de "máquina de escribir", sin más pretensión.

Finalizo aquí esta primera parte del análisis de los no-expedientes, en la que me he centrado en el estudio del primer subconjunto de este bloque. Para su desarrollo he empleado funciones de LO-Calc, aunque predominaron los script basados en Python

En una próxima entrada trabajaré con los directorios de dos archivos, ya que interesa observar en ese grupo el peso de determinadas configuraciones de archivos en relación a la distinción observada en este primer bloque (entre no-expedientes y expedientes fallidos), pero también para apreciar la incidencia de un tipo concreto de archivos (documentos): los informes psicopedagógicos.

sábado, 21 de marzo de 2026

Orient-IA

Documentos para el debate (II)

Segundo documento para la reflexión.

Orient-IA

Documentos para el debate (I)

Me parece sensato
Aporto en esta entrada un material para la reflexión. Y no abunda en YouTube, por desgracia.

¿Tú que opinas?

viernes, 20 de marzo de 2026

Expedientes

Números básicos

Vamos a analizar en esta entrada los aspectos cuantitativos de nuestra muestra de expedientes, empezando por su cuantificación básica: número de expedientes y cantidad de archivos que contienen. El segundo análisis de esta entrada se centrará en el estudio de la tipología de los documentos. En ambos casos se exppondrán y analizarán los datos resultates del estudio y los recursos informáticos empleados.

Con el conocimiento de los datos cuantitativos básicos nos podremos hacer una idea de la masa documental que suponen, lo cual es un dato importante para conocer la capacidad que tienen de servir al objetivo (o a los objetivos) que nos planteamos con ellos, además de el trabajo que representa su mentanimiento.

El conocimiento de la tipología documental y su peso en esa masa de documentos nos acercará a un primer conocimiento de las prácticas de trabajo que implica la creación de los expedientes y su manteniento, y nos permitirá realizar previsiones sobre las tecnologías que necesitaremos para acceder a su contenido.

Empezaremos por indicar la necedidad de archivar en una unidad que vamos a considerar unidad central del SEO, un directorio al que llamaremos Expedientes_SEO, en que se archivarán todos los directorios o expedientes individuales identificados únicamente por el nombre o identificador del alumno. Una alternativa puede ser sudvidir este directorio principal en subdirectorios por centro, pero a la larga resulta más sencillo de gestionar un directorio general no diferenciado, aunque sea necesario generar algún sistema de gestión que incluya la posible elección de expediente en función del centro. De todas formas esta es una cuestión de gestión de la que nos ocuparemos en su momento. De momento sirva únicamente como base para comprende los procedimientos de trabajo que explicaré a continuación.

Para lo que ahora nos ocupa partiré de la existencia de dicho directorio Expedientes_SEO en el que se encuentran organidazos en subdirectorios los archivos de que consta cada uno de los expedientes. Lo que ahora queremos saber es de cuanto expedientes consta nuestra muestra, cosa que podemos conocer de diferentes formas. Yo voy a emplear un script Python creado para contar las carpetas o directorio que existe en una ruta dada.



import os

#Función para contar directorios ---------------------------------------

def contar_subdirectorios(ruta_principal):
    """
    Cuenta cuántas carpetas o dirctorios hay dentro de una ruta específica.
    """
    try:
        # Verificamos si la ruta existe
        if not os.path.exists(ruta_principal):
            return "La ruta especificada no existe."

        # Listamos el contenido y filtramos los directorios. os.path.isdir une la ruta principal con el nombre del elemento
        subdirs = [nombre for nombre in os.listdir(ruta_principal) if os.path.isdir(os.path.join(ruta_principal, nombre))]
        return len(subdirs)

    except PermissionError:
        return "Error: No tienes permisos para acceder a esta carpeta."

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

ruta = "ruta" # Cambia "ruta" por tu ruta absoluta (ej.: "C:/Usuarios/Documentos").
resultado = contar_subdirectorios(ruta)

print(f"Número de subdirectorios en '{ruta}': {resultado}")


En este script debemos fijarnos especialmente en...

subdirs = [nombre for nombre in os.listdir(ruta_principal) if os.path.isdir(os.path.join(ruta_principal, nombre))]

... instrucción de síntesis que nos permite recorrer el directorio que contiene los expedientes y contar los directorios (expdientes) gracias a la función os.path.isdir().

En cuanro al resultado que obtenemos de aplicar esta función, el total es 482 expedientes, número suficiente para que nuestro análisis pueda considerarse representativo del número de expedientes y de la documentación que se puede manejar en un SEO.

El siguiente paso es conocer el número total de documentos de que constan esos expedientes, lo que nos proporciona una primera idea de su contenido, de cómo han sido tratados estos expedientes y de la utilidad que pueden tener para la finalidad que buscamos: facilitar el seguiento del alumnado.

Para realizar este cálculo vamos autilizar tambien un script de Python a modo de "herramienta".



from pathlib import Path

def contar_archivos_recursivos_pathlib(ruta_base):
    # Convertimos la cadena en un objeto Path
    path_principal = Path(ruta_base)
    
    if not path_principal.exists():
        return "La ruta no existe."

    # rglob("*") busca TODO de forma recursiva (carpetas y archivos). Usamos una "comprensión de lista" para filtrar y contar
    conteo = sum(
        1 for elemento in path_principal.rglob("*") 
        if elemento.is_file() and elemento.parent != path_principal
    )
    
    return conteo

# --- Configuración ---
	ruta = "Tu ruta"	# Ecribe aquí la ruta de tu directorio
	total = contar_archivos_recursivos_pathlib(ruta)

print(f"Total de archivos dentro de los subdirectorios: {total}")


También es este caso podemos identificar una expresión que resulta clave para el funcionamiento del script. En este caso es la expresión...

conteo = sum(1 for elemento in path_principal.rglob("*") if elemento.is_file() and elemento.parent != path_principal)

...en la que resulta clave la función elemento.is_file().

El resultado es un total de 3.777 archivos, lo que también resulta ser un volumen significativo de documentos; suficiente en principio para que su análisis arroje alguna luz sobre los expedientes SEO.

Conocidos los datos previos, podemos saber cual es la media de archivos por expediente, dato necesario para una primera aproximación al objetivo básico de este análisis: ¿sirven los expedientes SEO como recurso para proporcionar información relevante para el siguimiento del alumnado?. Ahora podríamos concretarlo como sigue: ¿cuentan los expedientes SEO del número suficiente de archivos como para ofrecer información relevante para el seguimiento del alumnado?. La respuesta cuantitativa es la siguiente: 3,68 archivos por expediente.

Realmente este número no nos permite responde a nuestra pregunta, ya que esos casi 4 archivos por expediente sitúan a nuestros expedientes en un nivel muy próximo al mínimo necesario para que, según la experiencia, se pueda considerar que un expediente contiene información suficiente para servir a nuestro objetivo. Realmente ese podría ser el mínimo de archivos resultante del inicio de una intervención, pero no para aquellas que llevan tiempo desarrollándose. Dico de otro modo: son pocos archivos por expediente, lo que supone, presumiblemente, que existen importantes disparidades en cuanto al contenido entre este conjunto de expedientes.

Y esa es precisamente la cuestión que debemos resolver ahora: conocer la composición real de cada uno de los expedientes en cuanto al número de archivos que contienen. Esto nos permitirá conocer el grado de homogeneidad vs. disparidad en la composición, dato éste de interés para pareciar también la nivel de sistematicidad con que se tratan los expedientes, cuestión esta que es clave para entender el planteamiento de trabajo del SEO al respecto y todo lo que de ello se puede derivar

Pero antes de analizar datos debemos disponer de ellos, cosa que aun no hemos resuelto.

Podemos ahordar esta tarea, igual que las dos precedentes, de forma manual, pero también mediante un script-herramienta Python. Definitivamente la opción manual queda descartada (disponiendo de un recurso eficiente de automatización), ya que su coste es muy elevado en tiempo, como tambien lo hubiera sido el recuento de archivos, aunque bien podríamos haberlo resuelto directamente mediante un recuento sistemático basado en la opción Propiedades del sistema operativo, o empleando la instrucción dir sobre cada carpeta-expediente.

Esta imagen muestra la primera opción, pero ambas comporten el mismo problema: aunque es un procedimiento que funciona, es realmente muy costoso en tiempo ejecutar este procedimiento cuanto el número de directorios es elevado, como es el caso. Como alternativa podemos hacer uso de los comandos del sistema operativo, creando incluso un archivo .bat, pero llegados a este extremo, la opción Python resulta más funcional.


import csv
from pathlib import Path

#ruta = Path('Tu_Ruta')				# Definir aquí tu ruta

# Definimos la carpeta 'documentos' en el mismo nivel de directorio que este script
ruta_destino = Path(__file__).parent / 'documentos'
archivo_csv = ruta_destino / 'listado_archivos.csv'

ruta_destino.mkdir(exist_ok=True)	# Nos aseguramos que la carpeta 'documentos' existe

datos_para_csv = []

if ruta.exists() and ruta.is_dir():
    for carpeta in ruta.iterdir(): 	# Nos aseguramos que la carpeta 'documentos' existe
        if carpeta.is_dir():
            total_archivos = sum(1 for f in carpeta.rglob('*') if f.is_file())	# Contamos todos los archivos dentro de ESTA carpeta y sus subcarpetas usando rglob('*')
            print(f"{carpeta.name} || {total_archivos}")
            datos_para_csv.append([carpeta.name, total_archivos])	# Guardamos el nombre y el total como una fila
# Creación del archivo CSV
    with open(archivo_csv, mode='a', newline='', encoding='utf-8') as f:
        escritor = csv.writer(f)
        if archivo_csv.stat().st_size == 0:					# Verificamos que el csv está vacío y (en su caso) escribimos la cabecera
            escritor.writerow(['Carpeta', 'Total_Archivos'])
        escritor.writerows(datos_para_csv)					# Escribimos los datos recolectados
    print(f"\n✅ Listado guardado en: {archivo_csv}")
else:
    print("La ruta 'Mi_Directorio' no es válida.")


Lo que ahora necesitamos es algo tan simple como un recuento expediente a expediente, que es lo que hace este script, que además archiva el resultado del recuento en un documento csv que contiene el listado de expedientes (identificador) y del número de archivos que contiene cada expediente. Se trata de crear la base de datos necesaria para el posterior análisis de datos, que requerirá acceder al citado archivo csv mediante un segundo script Python.

Para no repetir script, me voy a adelantar ahora a los resultados de ese futuro análisis mostrando el resultado de un primer análisis que resulta revelador de la realidad de nuestra muestra de datos. Muestro el gráfico resultante para ilustrar este primer análisis.

Considero que este gráfico, creado a partir del recuento de datos mediante la librería matplotlib muestra con claridad que nuestra muestra de expedientes se aleja bastante de la uniformidad que engañosamente parece indicar el promedio de archivos por expediente. Pero en realidad muestra mucho más. Veamos qué.

Cuando calculé el citado promedio dije que era un promedio bajo, demasiado próximo al mínimo suficiente (o no tanto) para considerar funcional a ese "expediente": si la documentación de que consta un expediente debe servir para proporcionar datos para el seguimiento del "caso", es necesario que conste de un número mínimo de archivos (además de otros requisitos que veremos en su momento), y es evidente que ni uno ni dos archivos alcanza ese mínimo, y bastante dudoso que tres archivos lo haga (diríamos que si y sólo sí se cumplen otras condiciones, pero de momento no vamos a entrar en estas cuestiones). Con cuatro archivos es posible que estemos ya dentro de posibles, en este caso, y al contrario que con tres, siempre que no se cumplan los criterios opuestos.

Y resulta que las dos mayores frecuencias observadas, y en orden, son precisamente esos valores: en nuestra distribución predominan ampliamente los expedientes que constan de 1 (99) y de 2 (64), en total 163 expedientes (prácticamente el 34% del total de los expedientes) no contienen el mínimo necesario de archivos para permitir (potencialmente) el cumlimiento del objetivo planteado. Pero tenemos, además, otros 43 expedientes cuya capacidad para lo mismo resulta dudosa. Esto eleva el total "inseguro" a cerca del 43% del total de los expedientes disponibles.

Aun con todo seguimos disponiendo de un total de 276 expedientes que, a priori, nos permiten avanzar en nuestro análisis, lo que nos deja un margen suficiente para dar respuesta a ese objetivo, pero los datos anteriores dicen algo más sobre el trabajo con los expedientes SEO: al menos en este caso concreto se puede observar que no existe en el SEO una praxis sistemática de trabajo con la documentación generada en la intervención, no siendo infrecuente (todo lo contrario) generar "expedientes" que me resisto a considerar tales, sin dar continuidad ni desarrollar un planteamiento de trabajo coherente con el mantenimiento (y mucho menos el uso) de dichos "agrupamientos de documentos".

De esto se puede derivar perfectamente que sólo esos 276 expedientes pueden ser considerados de utilidad para nuestro estudio; y en ese sentido vamos a reformular el conteo y posterior análisis de datos. Pero no está justificado que despreciemos sin más esa nada desdeñable cantidad de "carpetas" (ya que no expedientes) y los archivos que contienen (356) por más que, en este caso, representen sólo el 9.5% del total de archivos.

En consecuencia me propongo realizar dos análisis, aunque sólo uno de ellos (el segundo) seguirá la línea del objetivo inicialmente planteado:

  • Primero. Analizar la composición documental de las carpetas de alumnos (antiguos expedientes con 1, 2 y 3 archivos)
  • Segundo. Analizar los expedientes con potencial para serlo en términos de consecución del objetivo planteado: facilitar datos para el seguimeinto del alumnado

En una próxima entrada retomaré estos análisis.

miércoles, 18 de marzo de 2026

Expedientes

Por qué expedientes SEO

Lo primero que podemos decir respecto a los expedientes SEO es que, de una u otra forma, por un motivo o por otro, son una realidad persistente. Lo que entiendo está indicando que los SEO los considerar (ayer y hoy) como un recurso necesario.

Primero fueron carpetas y folios, más notas en cuadernos y agendas. Después de fueron conformando en formato digital hasta ser ahora predominante pero no exclusivamente digitales. La necesidad inicial derivaba del modelo de intervención en centros como servicio de carácter marcadamente externo y como servicio autónomo y diferenciado. Después las Unidades de Orientación, incluso en centros sin SEO propio, articularon un modelo de intervención más ligado al centro, en el que el SEO del EOE se iba identificando con la dinámica y la organización del Centro sin perder por ello su estatus específico como perteneciente al EOE. En este contexto, la Administración educativa fue implementando servicios informátizados de gestión documental y del expediente escolar (en Asturias, SAUCE, además de otros servicios de gestión complementarios en algunos casos, puramente de gestión otros). Con todo lo que este cambio de contexto implica de cambio de acceso a datos, nada ha impedido que se mantengan lo que aquí estamos llamado expedientes SEO y su funcionalidad para los propios EOE.

Es cierto que no siempre esta necesidad de la que deriva la pervivencia ha estado acompañada de un esperable esfuerzo de mantenimiento y mejora de la gestión. Y no me refiero sólo a la pervivencia de procedimientos basados en el papel, algunos de ellos mero vestigio de prácticas heredadas, otros consecuencia de la falta de alternativas digitales suficientemente funcionales y sencillas de implementar. Me refiero también a la falta de sistematicidad en los recursos y procedimientos de gestión, empezando por la muy deplorable ausencia criterios compartidos y explicitados de organización del contenido y siguiendo por la no menos incomprensible ausencia de interés (¿aparente?) por el desarrollo de procedimientos de aprovechamiento de toda esta riqueza documental, incluyendo la muy interesante abundancia de documentación antigua aun no digitalizada y menos explorada.

Diría yo que esta asuencia de actualización y aprovechameinto hace aun más destacable la persistencia de estas prácticas (por llamarlas de algún modo) que llevan a que, curso tras curso, los EOE incrementen el número de expedientes y el volumen de la documentación que contienen los creados en cursos posteriores. ¿Para qué tanto esfuerzo?, ¿qué hace que resulte de interés para los SEO perdurar en estas prácticas, aun a pesar de considerarlas una carga burocrática más?.

Varias podrían ser las respuestas, pero se me ocurre una muy simple y no siempre coherente con lo que muestra la realidad del uso de los expedientes: porque los SEO considerar los expedientes como la principal y más fiable fuente de información sobre un alumno y sobre las actuaciones desarrolladas con él por parte del Servicio. Y se considera así precisamente por nutrirse de la información que aportan, curso a curso, los propios SEO. Para que un "expediente" cumpla esta función, realmente no necesita estar inmaculadamente organizado, es suficiente con que esté actualizado.

Es por ello que el principal cuello de botella para la utilidad efectiva sea realmente un problema y esté sin resolver de modo totalmente satisfactorio. Me refiero a la incorporación efectiva al expediente de esas actuaciones "menores" que sólo quedan recogidas en el cuaderno de trabajo, notas cuyo estatus no siempre está claro, pero que normalmente se quedan en poder del OE o del PSC, aunque son el centro del muy serio intercambio de información que se da entre profesionales cuando se porduce un cambio de referente en la intervención. En algunos casos (PSC, fundamentalmente) se ha llegado a formas funcionales de informatización sistemática de esta recogida de datos (informes SISE, por ejemplo), pero ni están sistematizados ni dejan de suponer una sobrecarga de trabajo difícilmente asumible y, por ello, de escasa incidencia en los expedientes.

Pero salvo este nada menor problema pendiente de resolución, lo cierto es que en lo que al resto de la documentación se refiere es difícil que un mantenido y actualizado (aunque no necesariamente organizado) expediente SEO pueda ser superado en funcionalidad por los expedientes escolares de carácter administrativo o por las bases de datos oficiales tipo SAUCE. Funcionalidad para el seguimiento de la intervención, que es de lo que se trata en primer (y a veces único) término, aunque bien podría servir también para el análisis de la intervención y para el desarrollo de proyectos de investigación basados en la práctica.

Cierto que la carencia de estos dos últimos no se debe a la falta de documentación, y sí a la ausencia de propyectos (¿y de interés por ddesarrollarlos?), pero también lo es que la falta de organización y sistematización de la documentación de los expedientes SEO no ayuda precisamente. Tampoco lo hace para el uso básico (el seguimiento), pero aquí estas carencias son más fáciles de suplir con paciencia, tiempo y el saber hacer que proporcional la experiencia.

El objetivo de esta sección es precisamente proporcionar herramientas para facilitar que estas prácticas sean más viables, o cuanto menos menos costosas. Para ello empezaremos por aprender de lo que hay y de cómo está configurado para realizar algunas propuestas de mejora.

martes, 17 de marzo de 2026

Evaluación. Memoria

ENFEN. Fluidez verbal (VII)

El análisis de la entrada anterior permite que nos podamos replantear ahora la propuesta original de automatización de ENFEN-Fluidez.

Empezaré diciendo que el enfoque clínico es perfectamente válido en el ámbito profesional que le es propio, pero no en el educativo. Esto obliga a aclarar por qué utilizar herramientas de evaluación clínica como recursos de evaluación educativa, en qué circunstancias es admisible y cuales son los condicionantes que ese uso conlleva, si es que conlleva alguno.

La mayoría de los test clínicos (y la mayoría de los test empleados por los SEO lo son) están diseñados para la evaluación individual, lo que, en mi opinión, supone un hándicap como punto de partida, ya que este tipo de evaluación debe ser considerada una medida extraordinaria para nuestra intervención y sólo debemos optar por ella excepcionalmente y de forma justificada. Esto deriva del carácter contextual que debe tener toda intervención educativa para ser eficaz y justa; esto incluye nuestra intervención, sea ésta cual sea). También deriva de la constatación de los efectos negativos que para la inclusión tienen cierto tipo de intervenciones, una de ellas las actuaciones de evaluación individualizada por constituir una barrera a la presencia, con todo lo que de ella se deriva. Además hay que decir que la mayor parte de lo que pudiéramos obtener mediante la evaluación individualizada, lo podemos obtener mediante otras forma de trabajo, incluyendo la evaluación grupal contextualizada. La mayor parte, pero no todo.

Además en determinadas circunstancias y por razones de peso, la evaluación individualizada es necesaria, insustituible y está justificada, entre ellas, cuando...

  • ... no existen procedimientos alternativos que permitan obtener la misma información sin que sea necesario recurrir a ese tipo de actuación o los disponibles son muy costosos o complejos.
  • ... el proceso de evaluación está definido como constatación de hipótesis. Esto excluye la evaluación que se desarrolla en formatos descriptivos.
  • ... de los resultados que podemos obtener de la aplicación de determinadas pruebas que conllevan evaluación individualizada se derivan implicaciones relevantes para la intervención.

Además de estar justificada en esos casos la evaluación individualizada y la aplicación de pruebas originalmente clínicas, muchos de ellas lo son en función de para qué se emplean y cómo se analizan, pero permiten otros planteamiento y otros análisis compatible con los objetivos propios del enfoque educativo: facilitar el análisis causal de las dificultades de aprendizaje. Muchos de esos test se convierten en herramientas útiles para la evaluación educativa al ser reformulados y/o reinterpretados dentro de la transformación que de ellos hace en función de objetivos educativos.

En lo que sigue de entrada trataré de ejemplificar lo anterior en el desarrollo de la propuesta alternativa de automatización de ENFEN-Fluidez.

Dado que justificar el primero no ofrece mayor dificultad (hay pocos test que sean tan económicos y fáciles de aplicar y analizar como F1 y F2) y a la vez,en abstaracto, todas las del mundo; trataré de aterrizar empezando por replanear el enfoque de la elección de la prueba de forma coherente con el segundo de los criterios indicados antes: plantear la evaluación como trabajo en relación a una hipótesis como base para dar razón de ser al uso de ENFEN-Fluidez.

Aquí se produce un cambio radical de enfoque respecto a cómo se presupuso que sería el uso del test en el diseño del DocAp original: en ese caso el contexto venía definido en términos de procesamiento hacia adelante, convirtiendo ENFEN-Fluidez en un medio para objetivar la presencia de un déficit o, en su caso, constatar la normalidad de los resultados como ausencia del mismo. Ahora se plantea una forma de procesamiento hacia atrás en la que primero presumimos o constatamos (no entraremos en el cómo ni mediante qué medios) la presencia en el alumno de determinada problemática para la que ENFEN-Fluidez asumimos que aporta información relevante para la intervención educativa: para la categorización en términos de NEAE y/o para la identificación de necesidades educativas y/o para definición de estrategias de intervención educativa.

Me voy a tomar la libertad de no tratar aquí cómo constatamos ese déficit, ya que realmente no es un tema que sea necesario tratar en estos momentos. Tampoco lo voy a tratar las razones que avalan la idoneidad de ENFEN-Fluidez como recurso para la evaluación educativa de ciertas problemáticas, aunque realmente sí sería exigible una explicación detallada al respecto, pero en parte esta explicación se ha ido desgranando en entradas anteriores y conlleva más tiempo del que ahora dispongo y estoy dispuesto a dedicar. Diré que no carece de respaldo teórico la asociación de las dos dificultades que planteo con ENFEN-Fluidez, aunque otra cosa es que sea la única o la mejor opción.

El procedimiento de análisis de los datos se basa en el DocAp y lo desarrolla, aunque modificando alguno de sus planteamientos yajustándolo a los resultados obtenidos en nuestra pequeña muestra. Esto tiene importancia por dos razones:

  • Porque estoy asumiendo que es necesario constatar práctica y empíricamente la utilidad de un determinado recurso en función de objetivos propios y de la población y el contexto en que interviene el SEO.
  • Y porque se resalta la importancia de realizar este tipo de estudios como parte de las actuaciones de los SEO para la mejorar de su propia práctica.

Llegados a este punto estamos en condiciones de iniciar la construcción de nuestra propuesta de automatización, la cual consta de varias fases. Las primeras consisten en la recogida y el análisis de datos, las segundas tienen que ver con la elaboración del texto que sintetiza ese análisis y las terceras en el almacenamiento y devolución de información al usuario.

El primer bloque se incia (fase I) con la recopilación de datos de identificación de alumno: nombre, apellidos y edad como mínimos, aunque podemos ampliar el repertorio a centro escolar y curso o nivel. En realidad el único que realmente importa es el dato edad, ya que de él dependen procesos subsiguientes. Esto es importante para una cuestión que aquí y ahora no nos importa realmente: el tratamiento confidencial de los datos. dado que estamos trabajando en local, esa es una cuestión secundaria, cuyo tratamiento depende más de protocolos de custodia de los documentos que de con qué tecnologías se gestionan; pero en otros casos será una cuestión fundamental. De momento lo dejamos dicho.

El código de consta esta fase es el siguiente:


   
# Cuerpo principal o de ejecución

if __name__ == "__main__":

#Fase 1. Recoida de datos personales ------------------------------------------------------------
    
    print("--- DATOS PERSONALES DEL ALUMNO---")
    
# Solicitamos los datos al usuario
    nombre = input("Nombre: ")
    apellidos = input("Apellidos: ") 
    try: 	# Convertimos la edad a entero (int) para operar con el dato
    edad = int(input("Edad (sólo años): "))
    except ValueError:
        print("Error: Por favor, introduce un número válido para la edad (6 a 12 años).")
    centro_escolar = input("Centro escolar: ")
    curso = input("Curso: ")
	

Hasta aquí nada que decir, salvo la estructura try...except, que sirve para evitar algún error en un dato fundamental. Su conversión de string a integar, mediante la función int() se explica en el comentario.

La fase II consiste en identificar el motivo de uso de ENFEN-Fluidez, cuestión muy importante en esta propuesta y que conlleva cierta complejidad. Por ello he decidido desarrollarla mediante una función (def cod_hipotesis()) que recibe edad como parámetro y devuelve un código que identifica el tipo de hipótesis con la que vamos a trabajar en las fases siguientes.



def cod_hipotesis(edad):

    """
    Analiza la información disponible para plantear hipótesis para el análisis posterior y devuelve un código.
    Parámetro: edad (int)
    Retorna (return): string (Código de hipótesis de trabajo)
    """
    codigo = ""
    
    print("\n--- SEGUNDA FASE: ANÁLISIS DE INTERVENCIÓN ---")
    print("0. Ninguna de las siguientes")
    print("1. Hipótesis de TDAH")
    print("2. Dificultades de aprendizaje (Lectura)")
    opcion = input("Elija una opción (0 - 1 - 2): ")

#Lógica para hipótesis

    if opcion == "0":
        codigo = "0"
        return codigo
    else:
# --- Lógica para Hipótesis 1: TDAH ---
        if opcion == "1":
            codigo = "1"
            tiene_diag = input("¿Consta informe de especialista con diagnóstico o impresión diagnóstica de TDA-H? (s/n): ").lower() == 's'
            if tiene_diag:
                codigo += "A"  # Diagnóstico clínico informado por neuropediatra
            else:
                codigo += "B"  # Sintomatología observada en contexto familiar y/o escolar
                indicios_padres = input("¿Informan los padres de indicios compatibles con dificultades de atención y/o de hiperactividad  ? (s/n): ").lower() == 's'
                indicios_profe = input("¿Informa el profesorado de evidencias de dificultades de atención, hiperactividad o auto-regulación? (s/n): ").lower() == 's'
            
                if indicios_padres and indicios_profe:
                    codigo += "ab"
                elif indicios_padres:
                    codigo += "a"
                elif indicios_profe:
                    codigo += "b"
  
# --- Lógica para Hipótesis 2: Lectura ---
        elif opcion == "2":
            codigo = "2"
            if edad <= 7:
                codigo += "A"  # Para menores o iguales a 7 años
            else:
                codigo += "B"  # Para mayores de 7 años
            
        return codigo
  

Este código identifica la causa posible de las necesidades educativas en base a la (supuesta) información recogida en la anámnesis y el subtipo de la misma, en función de la fuente o de la edad. Posteriormente será utilizada para analizar los resultados en función del contexto que esa información ayuda a conformar.

La fase III consiste en recoger la puntuación que obtiene el alumno en las dos pruebas (F1 y F2). Aunque caben diferentes opciones, he decidido que sea recogida mediante input() desde el script principal, igual que los datos de identificación. De este modo forman parte de dicho script y pueden ser usados en la fase siguiente y en otras posteriores sin mayor dificultad.



#Fase 3. Obtención de la puntuación directa de F1 y F2

    print("\n--- RESULTADOS DE LA APLICACIÓN DE ENFEN-FLUIDEZ---")
    f1_pd = int(input(f"Resultado obtenido por {nombre} en F1: "))
    f2_pd = int(input(f"Resultado obtenido por {nombre} en F2: "))


La fase IV consiste en obtener la puntuación z derivada de la puntuación directa anterior. Para ello usamos una función (def calcular_z()), que contiene un diccionario de diccionarios con los estadísticos necesarios y recibe tres parámetros; la edad, el identificador de la prueba (factor) y la puntuación directa (puntuacion_directa).



def calcular_z(edad, factor, puntuacion_directa):

    baremos = {                                        # Diccionario con los estadísticos F1 y F2
        6:  {'f1': (5.28, 2.65),  'f2': (10.26, 3.81)},
        7:  {'f1': (6.65, 2.68),  'f2': (11.18, 3.34)},
        8:  {'f1': (8.66, 2.85),  'f2': (13.57, 3.95)},
        9:  {'f1': (9.25, 3.02),  'f2': (14.08, 3.87)},
        10: {'f1': (11.16, 3.09), 'f2': (16.79, 4.49)},
        11: {'f1': (11.73, 3.35), 'f2': (17.88, 4.72)},
        12: {'f1': (12.04, 3.19), 'f2': (17.81, 4.11)}
    }

    media, desviacion = baremos[edad][factor]       # Obtención de estadisticos en función de edad y prueba

    z = (puntuacion_directa - media) / desviacion    # Cálculo de Puntuación Z
    
    return round(z, 2)
    

´Primero obtenemos los estadísticos del diccionario mediante media, desviacion = baremos[edad][factor] de forma directa. Este procedimiento es adecuado en este caso porque controlamos directamente los datos de edad y factor y tenemos la seguridad de que no se va a producir error en el acceso a los datos. En otro caso sería más adecuado usar el método get(), como en el ejemplo siguiente, por ser más robusto y seguro:



datos_factor = baremos.get(edad, {}).get(factor)

if datos_factor:
    media, desviacion = datos_factor
else:
    print("Error: Edad o factor no encontrados")


Después se realiza el cálculo (z = (puntuacion_directa-media)/desviacion), que pasa (return round(z, 2)) a la variable del script desde la que se llama a la función ( f1_z = calcular_z (edad,"f1",f1_pd)).

La fase V sintentiza y codifica los resultados obtenidos, siendo la última del primer bloque. Esta codificación se ejecuta tambíén mediante una función y sirve para obtener una síntesis de datos que usaremos para construmir la base de datos de resultados sobre un documento csv que posteriormente servirá para el análisis de datos soporte de la invertigación-acción relativa la uso y la funcionalidad de ENFEN-Fluidez como herramienta de intervención del SEO.

Aunque no es estrictamente necesaria (y menos aun incorporarla a la base de datos), facilita la tarea poserior de análisis de datos, tanto grupales como individuales, así que he decidido implemantarla como parte del script.

Las categorías obedecen a criterios de utilidad descriptivo-analítica en función de los objetivos y el contexto en que aplicamos el recurso. Esto quiere decir que nos interesan determinados resultados en cuanto son significativos para establecer necesidades educativas en función del contexto hipotético en que nos movemos. Por ejemplo, si estamos trabajando con la hipótesis de incidencia de TDAH, nos interesa sólo si el sujeto obtiene puntuaciones inferiores a promedio, pero tiene importancia que dichas puntuaciones se den en ambos test o sólo en F1. Esto quiere decir que nos interesa saber si los valores z son iguales o inferiores a -1 Dt y si esto se produce en F1 y en F2 o sólo en F1 (sólo en F2 es irrelevante para el contexto TDAH). Esto es debido a que sólo déficit en F1 implica menor nivel de severidad que si la dificultad se observa también en F2.

En el segundo contexto (dificultad lectora) es de esperar discrepancia y F1 débil. Cualquier otro resultado es no confirmatorio, pero no determinante, por lo que resulta irrelevante para nuestros objetivos: no nos permite tomar decisiones de evaluación ni de intervención, que el lo mismo que sucede para un resultado dentro de normalidad (o superior, ya que unificamos ambos como N (normal) tanto en este contexto como en el de TDAH.

Esta es una forma de entender los resultados necesariamente restrictiva. No es la única y posiblemente tampoco la más correcta o la que mejores análisis produzca, pero sí es la que podemos usar en función del modo en que trabajamos con los datos: sólo determinados resultados (confirmatorios) son relevantes para nuestro análisis, en cuanto que están aceptados o confirmados por la teoría y la investigación. Si en su momento, de estas prácticas derivan nuevos soportes teóricos que permiten establecer otras hipótesis, determinados resultados pasarían a ser considerados relevantes.



def resulta_categ(f1_z, f2_z):
    
    # 1. Identificamos el grupo de cada factor True si es Bajo (B), False si es Normal (N)
    es_f1_bajo = f1_z <= -1
    es_f2_bajo = f2_z <= -1

    # 2. Lógica de la categorización
    
    if es_f1_bajo and es_f2_bajo:  				# Caso: Ambos Bajos
        return "EB"
    
    elif not es_f1_bajo and not es_f2_bajo:     # Caso: Ambos Normales
        return "EN"
    
    else:										# Caso: Desequilibrio (uno B y otro N)
        if es_f1_bajo:
            return "DF1d"
        else:
            return "DF2d"


Lo que estamos haciendo con esta función es categorizar los resultados dependiendo del nivel de desempeño, el cual toma como referencia el valor -1 Dt (esto permite que seas tú quien determine la referecnia cuantitativa que consideres más adecuada, aquí o en cualquier otro caso), derivando las dos primeras categorías de la pertenencia de ambos valores al mismo grupo (EB y EN) y las dos últimas por la discrepancia (DF1d, DF2d).

Una vez finalizada esta fase, entramos en el tercer bloque que afronta la recopilación de datos en un archivo csv y su publicación como informe individualizado.

Dentro de este bloque, la recopilación de resultados en un archivo csv constituye la fase VI y se concreta como función que recibe como parámetro la colección de datos recopilada en el script principal como diccionario (datos_archivar):


  datos_archivar = {
        'nombre': nombre,
        'apellidos': apellidos,
        'edad': edad,
        'centro': centro,
        'curso': curso,
        'codigo_h': codigo_h,
        'f1_pd': f1_pd,
        'f1_z': f1_z,
        'f2_pd': f2_pd,
        'f2_z': f2_z,
        'cat_z': cat_z
    }

Este es el parámetro datos que recibe la función def archivar_en_csv() mediante la cual se crear el archivo...



def archivar_en_csv(datos):
  
    # 1. Definir el nombre del archivo y la ruta (mismo directorio que el script)
    nombre_archivo = "bd_enfen_fluidez.csv"
    ruta_directorio = os.path.dirname(os.path.abspath(__file__))
    ruta_completa = os.path.join(ruta_directorio, nombre_archivo)
    
    # 2. Definir los encabezados del CSV
    encabezados = [
        'nombre',
        'apellidos',
        'edad',
        'centro',
        'curso',
        'codigo_h',
        'f1_pd',
        'f1_z',
        'f2_pd',
        'f2_z',
        'cat_z'
    ]

    # 3. Comprobar si el archivo ya existe para saber si escribir encabezados
    archivo_existe = os.path.isfile(ruta_completa)
    
    try:
        # Abrimos en modo 'a' (append/añadir) y newline='' para evitar líneas vacías
        with open(ruta_completa, mode='a', newline='', encoding='utf-8') as archivo:
            escritor = csv.DictWriter(archivo, fieldnames=encabezados)
            
            # Si el archivo es nuevo, escribimos la cabecera
            if not archivo_existe:
                escritor.writeheader()
            
            # Escribimos la fila con los resultados
            escritor.writerow(datos)
            
        print(f"✅ Datos archivados correctamente en: {nombre_archivo}")
        
    except Exception as e:
        print(f"❌ Error al guardar en CSV: {e}")

... y en la que, como contenidos fundamentales, primero se crea el encabezado del archivo csv mediante la lista encabezados[] y después se escribe el contenido mediante escritor.writerow(datos). En este documento se recopilarán todos los resultados de la aplicación del ENFEN-Fluidez, con idea de que sirva para realizar los estudios que se consideren convenientes mediante procedimientos de acceso al contenido de archivos de datos estructurados (por ejemplo).

Con la fase VII concluye este proyecto. En ella se construye el informe individualizado pero es suficientemente compleja como para que sea necesario sudividirla en varias partes. Esto facilita que se formule como función (principal) a la que se asocian varias funciones secundarias.

La función def generar_info (): también recibe como parámetro el contenido del conjunto datos pero los maneja de forma más compleja que la anterior, pudiendo diferenciarse varias partes en su desarrollo. Unas son asumidas directamente por la función y otras se derivan a funciones complementarias o secundarias.

En la primera de estas partes, además de crear el documento (doc = Document()) como archivo .docx (lo que supone cargar la biblioteca docx), creamos el título y copiamos los datos de identificación (por ejemplo: p.add_run(f"{datos['nombre']} {datos['apellidos']}\n"))

La segunda parte también es asumida por la función principal y consiste en explicar el concepto de Fluidez verbal, informar sobre ENFEN-Fluidez y sobre la utilidad de este (sub)test para la evaluación clínica y para la evaluación y la intervención educativa. Todo ello se realiza mediante un conjunto de instrucciones como la siguiente: p1 = doc.add_paragraph(), que contienen dentro del paréntesis el texto que deseamos sea escrito en el documento .docx.

La tercera parte del desarrollo del informe es responsabilidad de una función secundaria justifica_hipotesis (): que recibe como parámetro codigo_h desde la llamada a la subfunción que realizamos desde la función principal (parrafo_hipotesis = justifica_hipotesis(datos['codigo_h'])).



def justifica_hipotesis (codigo_h):

    # Caso para TDAH
    if codigo_h.startswith("1"):
        base = "La evaluación se fundamenta en la sospecha clínica de TDAH. "
        if "A" in codigo_h:
            detalle = "Al existir un diagnóstico previo de especialista, esta prueba sirve para cuantificar el impacto actual en las funciones ejecutivas."
        else:
            detalle = "Dada la sintomatología observada en casa y/o el colegio, se requiere objetivar la eficiencia del control atencional e inhibitorio."
        return base + detalle

    # Caso para Dificultades de Aprendizaje (Lectura)
    elif codigo_h.startswith("2"):
        base = "El motivo de evaluación son las dificultades en el proceso lector. "
        if "A" in codigo_h:
            detalle = "En edades tempranas (≤7 años), la fluidez verbal es un indicador crítico de la madurez léxica necesaria para la alfabetización."
        else:
            detalle = "En alumnos mayores de 7 años, se busca evaluar la automatización del acceso al léxico, clave para la comprensión lectora."
        return base + detalle

    return "Evaluación de cribado neuropsicológico general."
    

Esta función secundaria genera un texto específico basado en el código de hipótesis (pasado por parámetro) y en élla se diferencian las dos opciones que derivan de las dos categorías hipotéticas: TDAH o dificultades lectoras. Primero se extrae el primer elemento del código (v.g. if codigo_h.startswith("1"): y seguidamente el resto del contenido de dicho código. En función de ambos se va generando un texto u otro, que será lo que retorne la función a la principal (return base + detalle).

Se debe aclarar en este punto que la forma en que se concretan las hipótesis de trabajo no sólo son únicamente hipotéticas, además sólo lo son en el contexto previo de identificación de una determinada categoría de dificultades preeminentes derivadas de la anámnesis. Sin esta referencia carecen del contexto necesario para ser relevantes en el proceso de evaluación.

Una vez resuelta esta parte del informe, la que sigue se planea como parte de la función principal y se concreta como tabla que contiene los datos de ejecución de F1 y F2, incluyendo la puntuación directa (PD) y la puntuación típica calculada (Pz). En base a ella y a la codificación resultante de esos datos se planteará la parte final de esta función, la cual, dada su complejidad, se traslada a dos funciones secundarias que reciben (ambas) los mismos datos como parámetros ((codigo_h, cat_z)).

La primera función secundaria (analizar_resultados()) se encarga de realizar un breve análisis cualitativo de los resultados cuantitativos tomando como referencia la hipótesis de trabajo.



def analizar_resultados(codigo_h, cat_z):
  
    es_tdah = codigo_h.startswith("1")
    es_lectura = codigo_h.startswith("2")
    
# --- Casos para HIPÓTESIS TDAH ---
    if es_tdah:
        if cat_z == "EN":
            return "Los procesos de recuperación léxica y control inhibitorio evaluados se sitúan en niveles de normalidad. Es posible que ENFEN-Fluidez no permita, en este caso, objetivar  la incidencia del TDAH en los procesos requeridos en tareas de fluidez verbal."
        elif cat_z == "EB":
            return "Se observa un déficit global en fluidez que, en el marco del TDAH, sugiere dificultades importantes en la memoria de trabajo y en la velocidad de procesamiento."
        elif cat_z == "DF1d":
            return "Al ser F1 un marcador primario de TDAH por requerir mayor control inhibitorio y una búsqueda no rutinaria, esta disociación entre F1 y F2  se puede considerar compatible con TDAH, pudiendo interpretarse como indicador de un menor grado de severidad en términos de incidencia del trastorno en el procesamiento cognitivo requerido en tareas de Fluidez por comparación con resultados inferiores a promedio en F1 y F2."
        else: # Para DF2d
            return "Esta disociación es extremadamente infrecuente y sugiere dificultades de monitorización de la respuesta por inatención al resultar la tarea supuestamente poco relevante o novedosa."

# --- Casos para HIPÓTESIS LECTURA ---
    if es_lectura:
        if cat_z == "EN":
            return "Atendiendo a los resultados observados, la madurez léxica y el acceso al vocabulario se sitúan en niveles acordes a la edad cronológica del niño o niña. En este caso, las tareas de fluidez verbal posiblemente no permitan observar las causas de las dificutlades lectoras que presenta el alumno según los datos que constan en la anámnesis realidad."
        elif cat_z == "EB":
            return "El bajo rendimiento en ambas tareas de fluidez verbal indica una pobreza en el almacén léxico o una lentitud severa en la recuperación, lo que puede estar incidiendo negativamente en la decodificación y comprensión lectora."
        elif cat_z == "DF1d":
            return "El perfil sugiere que el acceso al léxico está comprometido en la ruta fonológica, lo que justifica la falta de automatización en la decodificación lectora e inciden negativamente en la fluidez."
        else: # Para DF2d
            return "El perfil sugiere que el acceso al léxico está comprometido en la ruta semántica, posiblemente por déficit general en el desarrollo del lenguaje derivado de distintas causas. No obstante esta manifestación de déficit es sumamente infrecuente, por lo que se debe interpretar con cautela."

    return "Perfil de cribado general sin hallazgos específicos vinculados a una hipótesis previa."


Como puedes ver no es una función demasiado compleja en términos de algoritmo, pero sí en cuanto al contenido. El que aquí se propone es es, una primera propuesta, que deberá ser revisado en función de los resultados del uso del test y que desde ya tú puedes adaptar en función de tus preferencias y conocimientos. Esto es totalmente coherente con el planteamiento de base: paradigma IA modelo experto. Y el experto eres tú.

La segunda función secundaria también ahonda en ese mismo planteamiento en dos sentidos: el mismo que en la anterior (tú decides el contenido) y como usuario del script: en este caso se proponen una colección de opciones de evaluación y de intervención, y es el OE quie selecciona aquellas que considera relevantes para el caso.



def propuestas (codigo_h, cat_z):
   
    opciones_validadas = {"evaluacion": [], "intervencion": []}
    
    # 1. Selección estricta del banco de datos según hipótesis
    if codigo_h.startswith("1"):  # MARCO TDAH
        banco_ev = [
            "Aplicar pruebas de atención sostenida y selectiva (ej. CPT, d2).",
            "Completar escalas de conducta para observación de impulsividad en aula.",
            "Realizar observación estructurada de la conducta en tareas de mesa."
        ]
        banco_int = [
            "Entrenamiento en autoinstrucciones para tareas de producción verbal.",
            "Uso de organizadores gráficos y tiempos de descanso tras tareas de carga atencional.",
            "Adaptación de materiales: fragmentar tareas largas en pasos cortos."
        ]
    elif codigo_h.startswith("2"):  # MARCO LECTURA
        banco_ev = [
            "Evaluación de procesos de decodificación y pseudopalabras (PROLEC-R / SE).",
            "Valoración de la velocidad de denominación (RAN/RAS).",
            "Evaluación de la conciencia fonológica y memoria fonológica de trabajo."
        ]
        banco_int = [
            "Refuerzo de la ruta fonológica mediante entrenamiento en conciencia fonémica.",
            "Lecturas repetidas y modelado para mejorar la prosodia y automatización.",
            "Uso de apoyos visuales y diccionarios de imágenes para reforzar el acceso léxico."
        ]
    else:
        # Si no hay hipótesis definida, retornamos listas vacías
        return opciones_validadas

    # 2. Interacción por CMD para validación directa
    
    print(f"\n--- VALIDACIÓN DE PROPUESTAS TECNICAS (Hipótesis: {codigo_h}) ---")
    print("Responda 's' para incluir la propuesta en el informe o cualquier otra tecla para omitirla.")

    print("\n[ BLOQUE: EVALUACIÓN ]")
    for prop in banco_ev:
        confirmar = input(f"¿Validar '{prop}'? (s/n): ").lower()
        if confirmar == 's':
            opciones_validadas["evaluacion"].append(prop)

    print("\n[ BLOQUE: INTERVENCIÓN ]")
    for prop in banco_int:
        confirmar = input(f"¿Validar '{prop}'? (s/n): ").lower()
        if confirmar == 's':
            opciones_validadas["intervencion"].append(prop)

    return opciones_validadas


Por varios motivos, esta función es significativamente más compleja que las anteriores desde diferentes puntos de vista: es evidente que lo es en cuanto al contenido, por lo que en esta propuesta no pretendo haber resuelto el proyecto, quedando pendente un análisis detallado del contenido presente y ausente. La segunda razón es que estas propuestas lo son en función de la hipótesis que deriva de la anámnesis, lo que hace que sean aun más necesaria la revisión que acabo de plantear.

Pero aun hay un tercer motivo de dificultad, en este caso "informática": esta función secundaria devuelve una colección de datos expresada como diccionario, motivo por el que la función primaria debe transformarlos para poder exponerlos como texto en el documento docx



#Acceso a función secundaria 3: propuestas (codigo_h, cat_z)

# 1. Llamada a la función
    dict_propuestas = propuestas(datos['codigo_h'], datos['cat_z'])

# 2. Preparar el texto para el informe. Creamos una lista de frases para luego unirlas
    lineas_informe = []

    doc.add_heading('Propuestas para la evaluación y para la intervención', level=1)
    
    if dict_propuestas["evaluacion"]:
        for item in dict_propuestas["evaluacion"]:
            lineas_informe.append(f"- {item}")
    
    if dict_propuestas["intervencion"]:
        for item in dict_propuestas["intervencion"]:
            lineas_informe.append(f"- {item}")

# 3. Unir todo en un solo string separado por saltos de línea
    texto_final = "\n".join(lineas_informe)

# 4. Añadir al documento de Word
    if texto_final:
        p_just = doc.add_paragraph(texto_final)
        p_just.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
    else:
        doc.add_paragraph("No se seleccionaron propuestas técnicas.")

Script final Para finalizar esta larga y compleja entrada te dejo a continuación acceso al script completo. Recuerda que deberás importar todas las bibliotecas que se precisan para su funcionamiento en caso de no tenerlas ya descargadas. Puedes identificarlas al inicio del script.


#Bibliotecas

import sys
import csv
import os
from docx import Document
from docx.shared import Pt
from docx.enum.text import WD_ALIGN_PARAGRAPH
from datetime import datetime

#FUNCIONES ========================================================================

#FUNCIÓN Definir hipótesis --------------------------------------------------------

def cod_hipotesis(edad):

    """
    Analiza la hipótesis clínica y devuelve un código identificador.
    Parámetro: edad (int)
    Retorna: string (Código de hipótesis de trabajo)
    """
    codigo = ""
    
    print("\n--- SEGUNDA FASE: ANÁLISIS DE INTERVENCIÓN ---")
    print("0. Ninguna de las siguientes")
    print("1. Hipótesis de TDAH")
    print("2. Dificultades de aprendizaje (Lectura)")
    opcion = input("Elija una opción (0 - 1 - 2): ")

#Lógica para hipótesis

    if opcion == "0":
        codigo = "0"
        return codigo
    else:
# --- Lógica para Hipótesis 1: TDAH ---
        if opcion == "1":
            codigo = "1"
            tiene_diag = input("¿Consta informe de especialista con diagnóstico o impresión diagnóstica de TDA-H? (s/n): ").lower() == 's'
            if tiene_diag:
                codigo += "A"  # Diagnóstico clínico informado por neuropediatra
            else:
                codigo += "B"  # Sintomatología observada en contexto familiar y/o escolar
                indicios_padres = input("¿Informan los padres de indicios compatibles con dificultades de atención y/o de hiperactividad  ? (s/n): ").lower() == 's'
                indicios_profe = input("¿Informa el profesorado de evidencias de dificultades de atención, hiperactividad o auto-regulación? (s/n): ").lower() == 's'
            
                if indicios_padres and indicios_profe:
                    codigo += "ab"
                elif indicios_padres:
                    codigo += "a"
                elif indicios_profe:
                    codigo += "b"
# --- Lógica para Hipótesis 2: Lectura ---
        elif opcion == "2":
            codigo = "2"
            if edad <= 7:
                codigo += "A"  # Para menores o iguales a 7 años
            else:
                codigo += "B"  # Para mayores de 7 años
            
        return codigo

# FUNCIÓN Cálculo de puntuaciones típicas (z) ------------------------------------------------------

def calcular_z(edad, factor, puntuacion_directa):

    baremos = {                                                      # Diccionario con los estadísticos F1 y F2
        6:  {'f1': (5.28, 2.65),  'f2': (10.26, 3.81)},
        7:  {'f1': (6.65, 2.68),  'f2': (11.18, 3.34)},
        8:  {'f1': (8.66, 2.85),  'f2': (13.57, 3.95)},
        9:  {'f1': (9.25, 3.02),  'f2': (14.08, 3.87)},
        10: {'f1': (11.16, 3.09), 'f2': (16.79, 4.49)},
        11: {'f1': (11.73, 3.35), 'f2': (17.88, 4.72)},
        12: {'f1': (12.04, 3.19), 'f2': (17.81, 4.11)}
    }

    media, desviacion = baremos[edad][factor]       # Obtención de estadisticos en función de edad y prueba

    z = (puntuacion_directa - media) / desviacion    # Cálculo de Puntuación Z
    
    return round(z, 2)

# FUNCIÓN Categorización resultados  z ----------------------------------------------

def resulta_categ(f1_z, f2_z):

    """
    Categorizamos el desempeño como EB, EN, DF1d, DF2d.
    """
    # 1. Identificamos el grupo de cada factor True si es Bajo (B), False si es Normal (N)
    es_f1_bajo = f1_z <= -1
    es_f2_bajo = f2_z <= -1

    # 2. Lógica de Categorización Acumulativa
    
    # Caso: Ambos Bajos
    if es_f1_bajo and es_f2_bajo:
        return "EB"
    
    # Caso: Ambos Normales
    elif not es_f1_bajo and not es_f2_bajo:
        return "EN"
    
    # Caso: Desequilibrio (uno B y otro N)
    else:
        if es_f1_bajo:
            return "DF1d"
        else:
            return "DF2d"

#FUNCIONES SECUNDARIAS a generar_info () ---------------------------------------------------------------------------

# FUNCIÓN SECUNDARIA para crear el párrafo justificativo del hipótesis

def justifica_hipotesis (codigo_h):
    """
    Genera un texto específico basado en el código de hipótesis.
    """
    # Caso para TDAH
    if codigo_h.startswith("1"):
        base = "la evaluación se fundamenta en la sospecha clínica de TDAH. "
        if "A" in codigo_h:
            detalle = "Al existir un diagnóstico previo de especialista, esta prueba sirve para cuantificar el impacto actual del TDAH en las funciones ejecutivas."
        else:
            detalle = "Dada la sintomatología observada en casa y/o en el colegio, se requiere objetivar la eficiencia del control atencional e inhibitorio, potencialmente afectado en caso de TDAH."
        return base + detalle

    # Caso para Dificultades de Aprendizaje (Lectura)
    
    elif codigo_h.startswith("2"):
        base = "el motivo de evaluación son las dificultades en el proceso lector. "
        if "A" in codigo_h:
            detalle = "En edades tempranas (≤7 años), la fluidez verbal es un indicador crítico de la madurez léxica necesaria para la alfabetización."
        else:
            detalle = "En alumnos mayores de 7 años, se busca evaluar hasta qué punto la automatización del acceso al léxico, clave para la comprensión lectora, puede verse lastrada por la persistencia de dificultades de decodificación."
        return base + detalle

    return "Evaluación de cribado neuropsicológico general."

# FUNCIÓN SECUNDARIA 2: Análisis cualitativo ligado a la hipótesis

def analizar_resultados(codigo_h, cat_z):
  
    es_tdah = codigo_h.startswith("1")
    es_lectura = codigo_h.startswith("2")
    
    # --- Casos para HIPÓTESIS TDAH ---
    if es_tdah:
        if cat_z == "EN":
            return "Los procesos de recuperación léxica y control inhibitorio evaluados se sitúan en niveles de normalidad. Es posible que ENFEN-Fluidez no permita, en este caso, objetivar  la incidencia del TDAH en los procesos requeridos en tareas de fluidez verbal."
        elif cat_z == "EB":
            return "Se observa un déficit global en fluidez que, en el marco del TDAH, sugiere dificultades importantes en la memoria de trabajo y en la velocidad de procesamiento."
        elif cat_z == "DF1d":
            return "Al ser F1 un marcador primario de TDAH por requerir mayor control inhibitorio y una búsqueda no rutinaria, esta disociación entre F1 y F2  se puede considerar compatible con TDAH, pudiendo interpretarse como indicador de un menor grado de severidad en términos de incidencia del trastorno en el procesamiento cognitivo requerido en tareas de Fluidez por comparación con resultados inferiores a promedio en F1 y F2."
        else: # Para DF2d
            return "Esta disociación es extremadamente infrecuente y sugiere dificultades de monitorización de la respuesta por inatención al resultar la tarea supuestamente poco relevante o novedosa."

    # --- Casos para HIPÓTESIS LECTURA ---
    if es_lectura:
        if cat_z == "EN":
            return "Atendiendo a los resultados observados, la madurez léxica y el acceso al vocabulario se sitúan en niveles acordes a la edad cronológica del niño o niña. En este caso, las tareas de fluidez verbal posiblemente no permitan observar las causas de las dificutlades lectoras que presenta el alumno según los datos que constan en la anámnesis realidad."
        elif cat_z == "EB":
            return "El bajo rendimiento en ambas tareas de fluidez verbal indica una pobreza en el almacén léxico o una lentitud severa en la recuperación, lo que puede estar incidiendo negativamente en la decodificación y comprensión lectora."
        elif cat_z == "DF1d":
            return "El perfil sugiere que el acceso al léxico está comprometido en la ruta fonológica, lo que justifica la falta de automatización en la decodificación lectora e inciden negativamente en la fluidez."
        else: # Para DF2d
            return "El perfil sugiere que el acceso al léxico está comprometido en la ruta semántica, posiblemente por déficit general en el desarrollo del lenguaje derivado de distintas causas. No obstante esta manifestación de déficit es sumamente infrecuente, por lo que se debe interpretar con cautela."

    return "Perfil de cribado general sin hallazgos específicos vinculados a una hipótesis previa."

# FUNCIÓN SECUNDARIA 3: Propuesta de actuaciones para conformidad del orientador

def propuestas (codigo_h, cat_z):
   
    opciones_validadas = {"evaluacion": [], "intervencion": []}
    
    # 1. Selección estricta del banco de datos según hipótesis
    if codigo_h.startswith("1"):  # MARCO TDAH
        banco_ev = [
            "Aplicar pruebas de atención sostenida y selectiva (ej. CPT, d2).",
            "Completar escalas de conducta para observación de impulsividad en aula.",
            "Realizar observación estructurada de la conducta en tareas de mesa."
        ]
        banco_int = [
            "Entrenamiento en autoinstrucciones para tareas de producción verbal.",
            "Uso de organizadores gráficos y tiempos de descanso tras tareas de carga atencional.",
            "Adaptación de materiales: fragmentar tareas largas en pasos cortos."
        ]
    elif codigo_h.startswith("2"):  # MARCO LECTURA
        banco_ev = [
            "Evaluación de procesos de decodificación y pseudopalabras (PROLEC-R / SE).",
            "Valoración de la velocidad de denominación (RAN/RAS).",
            "Evaluación de la conciencia fonológica y memoria fonológica de trabajo."
        ]
        banco_int = [
            "Refuerzo de la ruta fonológica mediante entrenamiento en conciencia fonémica.",
            "Lecturas repetidas y modelado para mejorar la prosodia y automatización.",
            "Uso de apoyos visuales y diccionarios de imágenes para reforzar el acceso léxico."
        ]
    else:
        # Si no hay hipótesis definida, retornamos listas vacías
        return opciones_validadas

    # 2. Interacción por CMD para validación directa
    
    print(f"\n--- VALIDACIÓN DE PROPUESTAS TECNICAS (Hipótesis: {codigo_h}) ---")
    print("Responda 's' para incluir la propuesta en el informe o cualquier otra tecla para omitirla.")

    print("\n[ BLOQUE: EVALUACIÓN ]")
    for prop in banco_ev:
        confirmar = input(f"¿Validar '{prop}'? (s/n): ").lower()
        if confirmar == 's':
            opciones_validadas["evaluacion"].append(prop)

    print("\n[ BLOQUE: INTERVENCIÓN ]")
    for prop in banco_int:
        confirmar = input(f"¿Validar '{prop}'? (s/n): ").lower()
        if confirmar == 's':
            opciones_validadas["intervencion"].append(prop)

    return opciones_validadas

#FUNCIÓN Archivo de datos en csv------------------------------------------------------------

def archivar_en_csv(datos):
  
    # 1. Definir el nombre del archivo y la ruta (mismo directorio que el script)
    nombre_archivo = "bd_enfen_fluidez.csv"
    ruta_directorio = os.path.dirname(os.path.abspath(__file__))
    ruta_completa = os.path.join(ruta_directorio, nombre_archivo)
    
    # 2. Definir los encabezados del CSV
    encabezados = [
        'nombre',
        'apellidos',
        'edad',
        'centro',
        'curso',
        'codigo_h',
        'f1_pd',
        'f1_z',
        'f2_pd',
        'f2_z',
        'cat_z'
    ]
    
    # 3. Comprobar si el archivo ya existe para saber si escribir encabezados
    archivo_existe = os.path.isfile(ruta_completa)
    
    try:
        # Abrimos en modo 'a' (append/añadir) y newline='' para evitar líneas vacías
        with open(ruta_completa, mode='a', newline='', encoding='utf-8') as archivo:
            escritor = csv.DictWriter(archivo, fieldnames=encabezados)
            
            # Si el archivo es nuevo, escribimos la cabecera
            if not archivo_existe:
                escritor.writeheader()
            
            # Escribimos la fila con los resultados
            escritor.writerow(datos)
            
        print(f"✅ Datos archivados correctamente en: {nombre_archivo}")
        
    except Exception as e:
        print(f"❌ Error al guardar en CSV: {e}")


#FUNCIÓN. Escribir informe individualizado ----------------------------------------------------

def generar_info (datos):

# Obtener la fecha actual para el nombre del archivo

    fecha_actual = datetime.now()
    anio = fecha_actual.year
    mes = fecha_actual.strftime('%m') # Formato de dos dígitos (01, 02...)
    
# Construir el nombre del archivo
    primer_nombre = datos['nombre'].split()[0]
    inicial_apellido = datos['apellidos'][0].upper()
    nombre_final = f"{datos['nombre']}{inicial_apellido}_infofluidez{anio}{mes}.docx"

# Gestión del directorio para almacenar los informes
    directorio_destino = "informes"
    if not os.path.exists(directorio_destino):
        os.makedirs(directorio_destino)
    ruta_final = os.path.join(directorio_destino, nombre_final) # Ruta completa
    doc = Document()    # Creación del documento

# Escritura del título
    titulo = doc.add_heading('Informe individualizado de evaluación de la fluidez verbal', 0)
    titulo.alignment = WD_ALIGN_PARAGRAPH.CENTER

# Datos de identificación

    doc.add_heading('Datos de identificación', level=1)
    p = doc.add_paragraph()
    p.add_run(f"Alumno: ").bold = True
    p.add_run(f"{datos['nombre']} {datos['apellidos']}\n")
    p.add_run(f"Curso: ").bold = True
    p.add_run(f"{datos['curso']} - {datos['centro']}\n")
    p.add_run(f"Fecha de emisión: ").bold = True
    p.add_run(f"{fecha_actual.strftime('%d/%m/%Y')}")

# Escritura de párrafo descriptivo ENFEN-Fluidez

    doc.add_heading('ENFEN-Fluidez. Descripción', level=1)

# Primer párrafo: Definición de fluidez verbal

    p1 = doc.add_paragraph(
    "La fluidez verbal es una tarea de producción del lenguaje que requiere el recurso a mecanismos de "
    "acceso al léxico para evocar rápidamente los conceptos verbales necesarios. Se trata de una tarea "
    "compleja en la que intervienen procesos cognitivos  que involucran al procesamiento lingüístico "
    "(memoria semántica y fonológica) como la capacidad de producción verbal controlada y programada, "
    "la organización de la respuesta, la estrategia de búsqueda léxica y la monitorización para evitar la "
    "emisión de respuestas ya dadas. También implica el procesamiento no lingüístico, en concreto, la atención, "
    "la memoria de trabajo, la velocidad de procesamiento, la inhibición y la flexibilidad mental."
    )
    p1.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY

# Segundo párrafo: ENFEN-Fluidez como recurso para el evaluación clínica (neuropsicológica)

    p2 = doc.add_paragraph(
    "ENFEN-Fluidez es un recurso pertinente para evaluar la fluidez verbal en población infantil (6 a 12 años). "
    "Desde el punto de vista neuropsicológico, esta prueba forma parte de ENFEN, primera batería adaptada "
    "al castellano que permite evaluar las funciones ejecutivas en niños de manera global. En concreto los resultados "
    "obtenidos con esta subprueba se consideran indicadores fiables de la eficiencia cognitiva del lóbulo frontal "
    "y del estatus neurocognitivo general del niño."
    )
    p2.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY

# Tercer párrafo: Utilidad de ENFEN-FLuidez para el ámbito escolar

    p3 = doc.add_paragraph(
    "A nivel escolar, ENFEN-Fluidez es un recurso apropiado para la detección de dificultades de "
    "aprendizaje (DA) y la identificación de necesidades específicas de apoyo educativo (NEAE), ya que "
    "contribuye a la evaluación de desarrollo madurativo global de los niños de 6 a 12 años y del nivel "
    "de desarrollo de su lenguaje expresivo (madurez léxica y habilidades fonológicas). Además es sensible "
    "a las dificultades asociadas al TDAH."
    )
    p3.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY

# Cuarto párrafo: Implicaciones para la intervención educativa

    p4 = doc.add_paragraph(
    "Por todo ello se considera que ENFEN-Fluidez es un recuros útil para plantear medidas educativas y "
    "para orientar la intervención especializada de apoyo."
    )
    p4.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY

# Escritura de párrafo justificativo del uso de ENFEN-Fluidez en relación a la hipótesis

    doc.add_heading('Motivación del uso de ENFEN-Fluidez', level=1)

# Llamada a la función secundaria 1 justifica_hipotesis()

    parrafo_hipotesis = justifica_hipotesis(datos['codigo_h'])

    p_just = doc.add_paragraph(f"En el caso de {datos['nombre']} " + parrafo_hipotesis)
    p_just.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY

# Continuamos con la Tabla de resultados...

# Tabla de resultados
    doc.add_heading('Resultados cuantitativos', level=1)
    table = doc.add_table(rows=1, cols=3)
    table.style = 'Light Grid Accent 1'
    
    hdr_cells = table.rows[0].cells
    hdr_cells[0].text = 'Subtest'
    hdr_cells[1].text = 'PD'
    hdr_cells[2].text = 'Puntuación Z'

    # Factor 1
    row1 = table.add_row().cells
    row1[0].text = 'Fluidez fonológica (F1)'
    row1[1].text = str(datos['f1_pd'])
    row1[2].text = str(datos['f1_z'])

    # Factor 2
    row2 = table.add_row().cells
    row2[0].text = 'Fluidez semántica (F2)'
    row2[1].text = str(datos['f2_pd'])
    row2[2].text = str(datos['f2_z'])

# Acceso a funciones secundarias ------------------------------------------------------

# Análisis
    doc.add_heading('Análisis de los resultados cuantitativos', level=1)

# Acceso a función secundaria 2: analizar_resultados ()

    parrafo_analisis_resultados = analizar_resultados (datos['codigo_h'], datos['cat_z'])

    p_just = doc.add_paragraph(parrafo_analisis_resultados)
    p_just.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY

#Acceso a función secundaria 3: propuestas (codigo_h, cat_z)

# 1. Llamada a la función
    dict_propuestas = propuestas(datos['codigo_h'], datos['cat_z'])

# 2. Preparar el texto para el informe. Creamos una lista de frases para luego unirlas
    lineas_informe = []
    doc.add_heading('Propuestas para la evaluación y para la intervención', level=1)
    
    if dict_propuestas["evaluacion"]:
        for item in dict_propuestas["evaluacion"]:
            lineas_informe.append(f"- {item}")
    
    if dict_propuestas["intervencion"]:
        for item in dict_propuestas["intervencion"]:
            lineas_informe.append(f"- {item}")

# 3. Unir todo en un solo string separado por saltos de línea
    texto_final = "\n".join(lineas_informe)

# 4. Añadir al documento de Word
    if texto_final:
        p_just = doc.add_paragraph(texto_final)
        p_just.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
    else:
        doc.add_paragraph("No se seleccionaron propuestas técnicas.")

# Guardar el informe final
    doc.save(ruta_final)
    print(f"\n✅ Proceso completado. Informe generado: {nombre_final}")

 
#==================================================================

# Cuerpo principal o de ejecución

#===================================================================

if __name__ == "__main__":

#Fase 1. Recogida de datos personales ------------------------------------------------------------
    
    print("--- DATOS PERSONALES DEL ALUMNO---")
    
# Solicitamos los datos al usuario
    nombre = input("Nombre: ")
    apellidos = input("Apellidos: ") 
    try: # Convertimos la entrada de edad a entero (int) para poder operar con ella
        edad = int(input("Edad (sólo años): "))
    except ValueError:
        print("Error: Por favor, introduce un número válido para la edad (6 a 12 años).")
    centro = input("Centro escolar: ")
    curso = input("Curso: ")
        
# Fase 2. Llamada a la función cod_hipotesis ------------------------------------------------------

    codigo_h = cod_hipotesis(edad)
    print(f"\n[SISTEMA] Evaluación completada para {nombre}.")
    print(f"[SISTEMA] Código generado: {codigo_h}")
    if codigo_h == "0":
        print(f"No procede aplicar ENFEN-Fluidez en la evaluación de {nombre}. Fin del proceso")
        sys.exit()  # Se interrumpe el script

#Fase 3. Obtención de la puntuación directa de F1 y F2

    print("\n--- RESULTADOS DE LA APLICACIÓN DE ENFEN-FLUIDEZ---")
    f1_pd = int(input(f"Resultado obtenido por {nombre} en F1: "))
    f2_pd = int(input(f"Resultado obtenido por {nombre} en F2: "))

#Fase 4. Obtención de puntuación z para F1 y para F2

    f1_z = calcular_z (edad,"f1",f1_pd)
    f2_z = calcular_z (edad,"f2",f2_pd)

#Fase 5. Categorización de los resultados

    cat_z = resulta_categ(f1_z, f2_z)

# Bloque 2. Fase 6. Guardar datos en csv

    datos_archivar = {
        'nombre': nombre,
        'apellidos': apellidos,
        'edad': edad,
        'centro': centro,
        'curso': curso,
        'codigo_h': codigo_h,
        'f1_pd': f1_pd,
        'f1_z': f1_z,
        'f2_pd': f2_pd,
        'f2_z': f2_z,
        'cat_z': cat_z
    }

    archivar_en_csv(datos_archivar)

    print (f"Se han archivado correctamente los datos de {nombre} en la base de datos de ENFEN-Fluidez")

# Fase 7. Informe individualizado

    print(f"A continuación se procede a generar el informe individualizado de {nombre}")
    
    generar_info (datos_archivar)

    print(f"Se ha generado el Informe Individualizado Fluidez de {nombre}")