Mostrando entradas con la etiqueta Análisis de datos. Mostrar todas las entradas
Mostrando entradas con la etiqueta Análisis de datos. Mostrar todas las entradas

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}") 


sábado, 7 de marzo de 2026

Evaluación. Memoria

ENFEN. Fluidez verbal (VI)

Además de para ejercitarnos en el análisis de datos, la entrada anterior nos ha servido para considerar nuestra muestra de resultados ENFEN-Fluidez como razonablemente conforme a norma. Ahora la continuación de aquel análisis nos servirá para comprobar la viabilidad de nuestro planteamiento de cómo y par qué usar ENFE-Fluidez en la evaluación psicopedagógica de los SEO.

A partir de este momento, este análisis se centra en el rendimiento del alumando de nuestra muestra en relación al esperado para la media del grupo normativo, motivo por el que se emplea la puntuación z. Se trata de confirmar si la diferencia F1-F2 se mantiene dentro de lo esperado o es superior. En este segundoa caso (y sólo en él) se considera posible un análisis complementario en los términos en que se difinió éste en función del signo de la diferencia (F1>F2 vs F2>F1).

El primer paso en el análisis es comparar las puntuaciones 'z' de F1 Y de F2 para determinar la existencia o no de discrepancias y, en su caso, el tamaño de estas discrepancias. Originalmente esta cuestión se planteó en campo Discrepancia con tres posibles valores: Nula, Moderada o Severa, pero ahora nos interesa sólo el contraste Nula vs. No nula. Después se analiza la posición de cada sujeto en relación al rendimiento esperado. La tabla que sigue muestra, en síntesis el resultado de este doble análisis.

Discrepancia N Alto Promedio Bajo
NULA 23 1 19 3
Moderada 5

Estos resultados muestran que ENFEN-Fluidez es fundamentalmente lo que dice ser: una prueba para evaluar la memoria verbal, una vez descontamos la incidencia del criterio semántico en la conformación del lexicón. La diferencia 23 vs. 5 no deja lugar a dudas al respecto, especialmente si tenemos en cuenta las características del alumnado de esta pequeña muestra. Lo que resulta hasta cierto punto sorprendente es la fortaleza de esta capacidad ante contextos negativos como son los que caraterizan a nuestra muestra, en la que sólo 8 alumnos se encuentran por debajo del rendimiento normativo medio, bien en F1, bien en F2 o en ambas pruebas, a pesar de ser todos ellos alumnos con necesidades específicas de apoyo educativo, la mayoría con necesidades educativas especiales y un grupo importante de ellos también con condiciones socio-familiares deprimidas.

Estos 8 casos se dividen en dos grupos de peso similar respecto a las dos dificultades observadas: 3 presentan un rendimiento global (F1 y F2) inferior a promedio y 5 presenta discrepancia entre F1 y F2, siendo esta discrepancia mayoritariamente (4/5) por F1-Débil por encima de la diferencia normativa. Como ya dijimos, nos planeamos que estas dos formas de manifestarse la dificultad indican bajo rendimiento en memoria verbal la primera y posible incidencia de factores relativos al proceso de aprendizaje la segunda.

Empezando por la primera dificultad, y contrariamente a lo planteado incialmente, ahora debemos considerar que también aquí se presentan implicaciones para la evaluación: este déficit (de memoria) exige confirmación, lo que sugiere, en principio y por no salirse del constructo, que es conveniente aplicar otra prueba de memoria de trabajo: WISC-Dígitos podría ser un buen candidato por robusted, simplicidad, similitud y complementariedad respecto a ENFEN-Fluidez.

A partir de aquí se podrían considerar diferentes escenarios en el análisis de los resultados y en las propuestas de intervención educativa y clínica; pero estas son cuestiones para otra propuesta. En esta nos quedemos en la recomendación para el proceso de evaluación: si un sujeto se sitúa en la categoría déficit en Fluidez, se recomienda una evaluación complementaria de la memoria secuencial mediante WISC-Dígitos.

El segundo resultado observado exige varios análisis, ya que también son varias las posibles implicaciones:

  • Que el déficit sea por F1-Débil
  • Que lo sea por F2-Débil
  • Que lo sea por la fortaleza de F-Fuerte: posición de F-Fuerte en valor +1z, frente a F-Débil normal

De primeras descartamos que la tercera sea indicador de dificultad de algún tipo, por lo que este resultado pasará a ser considerado como una forma más de normalidad.

De las dos que restan sólo la primera se confirma empíticamente: mayoritariamente (4/5) la discrepancia negativa lo es por F1-Débil (siendo F1 <=-1z). podríamos considerar estos resultados una forma acentuada de la discrepancia "normal" F2>F1; pero esto sólo desplaza el abordaje de explicación causal: ¿por qué en determinados casos la diferencia "normal" se acentúa superando los límites de dicha "normalidad"?. Deberemos volver a la hipótesis de un débil desarrollo de las capacidades de las habilidades metafonológicas, con implicaciones indirectas en el aprendizaje de la lectura. F1 se conformaría así como un indicador fiable de dificultades metafonológicas con posibles implicaciones en el aprendizaje lector por la ruta fonológica.

Desde la perspectiva de análisis que ahora se confirma relevante incidiríamos en la confirmación del déficit metafonológico mediante evaluación complementaria y, en segundo lugar, en el estudio de un posible déficit lector que, en función de la edad del alumno, se puede concretar como evaluación mediante PROLEC-R (Palabras-vs-Pseudopalabras) o como análisis del expediente escolar.

Para finalizar una breve, pero necesaria, referencia a la excepción 1/5 deribada de F2-Débil: se trata de un alumno que obtiene la misma PD en F1 y en F2, lo que en si mismo ya es excepcional en ENFEN-Fluidez. Debido a ello, parece recomendable confirmar que se trata de un resultado representativo del funcionamiento del sujeto.

Llegar a esta solución es relevante por sus implicaciones: la posiblidad F1>F2 que incialmente consideramos como una de las posibles hipótesis de trabajo, ahora proponemos tratarla como excepcional y "sospechosa". Esto supone que los datos empíricos no permiten sostener F2-Debil<=-1z como indicador de déficit semático; frente a ello, la evidencia de incidencia negativa del contexto sobre el desarrollo del lenguaje está plenamente demostrada en la literatura y es evidente que con nuestros resultados no estamos autorizados para negar esa evidencia; es por ello que lo que corresponde es considerar F2 como no pertinente para abordar este tipo de cuestiones.

Esta conclusión nos obliga a ser muy cautos respecto a la aceptación de determiandos plantamientos, por muy verosímiles que parezcan, mientras no estén sustentados en estudios específicos, a ser posible basados en datos empíricos. Sírvanos esto para ENFEN-Fluidez y para el resto de las pruebas.

martes, 3 de marzo de 2026

Evaluación. Memoria

ENFEN. Fluidez verbal (V)

Finalizaba la entrada anterior comprometiendo en el futuro el análisis de los datos que recogíamos con el script Python que ella contenía. Empiezo ésta retomando esa promesa en el marco del acceso a los datos estructurados mediante el módulo CSV (por ejemplo, aquí).

El conjunto de datos con el que me propongo trabajar se ajusta al empleo de esos procedimientos y al actual nivel de nuestro conocimiento sobre el tratamiento de datos, incluyendo que no es necesaria la limpieza de datos y el análisis a realizar es muy sencillo en cuanto a sus objetivos: nos limitaremos a la descripción de los datos.

Los datos de que disponemos son pocos, pero suficientes para nuestro propósito actual: analizar el funcionamiento de la prueba como recurso de automatización de la evaluación en el marco paradigma "experto" de la IA.

Empezaremos por describir los datos disponibles y por comprobar si su funcionamiento general se ajusta al observado por los datos normativos, concretamente...

  • si aumenta el rendimiento en F1 y F2 en función de la edad,
  • si se aprecia diferencia de rendimiento sistemáticamente favorable a F2
  • y si se aprecia relevancia de la variable género.

Estas tres comprobaciones son necesarias para confirmar que nuestra pequeña muestra no pertenece a una población diferente a la la muestra normativa. Las dos primeras cuestiones fueron tratadas en la entrada en la que se analizaron los datos muestrales, pero no la última. En ENFEN-Fluidez (y en las demás pruebas ENFEN), se da a entender que la variable sexo es irrelevante, por lo que en ningún momento se hace referencia a sus efectos. Explícitamente en estos mismos términos se expresan las fuentes consultadas: se aprecian diferencias en función de la edad y el nivel educativo, pero no del sexo. No obstante, pretendo comprobar que en nuestro caso tampoco se aprecian esas diferencias.

Empezaremos por analizar la incidencia de la variable edad en los resultados, entendiendo que, en nuestra práctica, edad y nivel de escolarización son equivalentes.

Nuestra muestra está formada por 28 alumnos y alumnas, número insuficiente para dotarlo de la mínima representatividad, pero suficiente para comprobar las similitudes y diferencias repecto al grupo normativo.

En nuestros datos están representadas las edades 6 a 12 años, a excepción del colectivo 10 años. La tabla que sigue contiene esta información.

Edad (años) N %
6 años 3 10,71
7 años 7 25,00
8 años 8 28,57
9 años 4 14,29
10 años 0 0
11 años 3 10,71
12 años 3 10,71

Los grupos etarios no están equitativamente representados, predominando los grupos etarios 7 y 8 años, pero el resto cuenta con un número mínimo, suficiente para realizar algunos cálculos que nos ayuden en los análisis posteriores. El primero de estos análisis es la evolución de las PD en F1 y F2 en las diferentes edades, a fin de contatar en nuestra muestra la misma tendencia quese observa en el grupo normativo.

Los gráficos anterior muestra las similitudes y las diferencias en la evolución de F1 y F2 como promedio etario entre nuestros datos y los normativos. En ellos podemos observar comportamiento tendencialmente similar entre ambos, aunque también algunas diferencias de interés en otro contexto de análisis, que no en este, en el que lo importante para nuestros objetivos actuales, es que ambos grupos muestran la misma tendencia evolutiva: a mayor edad, mayor rendimiento, tanto en F1 como en F2.

Lo único que no se ajusta a esta observación es el comportamiento de nuestro grupo 12 años en F1, que es marcadamente descendente respecto a 11 años y contradice lo esperado a nivel normativo, en el que se observa un incremento moderado respecto al grupo etario anterior. Es posible que la explicación de este comportamiento esté en la pecularidad de nuestra muestra, que se concretamente se acentúa en este subgrupo de edad: estamos ante una pequeña muestra de alumnos con necesidades educativas que, en concreto y por el momento en que se recogieron los datos, entra dentro de la condición de alumnado con antecedentes de importantes dificultades escolares, incluyendo la repetición de curso.

La segunda cuestión que nos interesa es la diferencia de enrdimiento entre F1 y F2, que es favorable para F2 en todos los grupos etarios en el baremos y también en nuestra muestra. Únicamente se observa un caso en que ambas puntuaciones son iguales, pero ninguno en el que F1 sea superior a F2. En resumen, nuestro grupo tiene en esto el mismo comportamiento que el normativo.

Para finalizar estudiamos el comportamiento de la variable sexo. Nuestra muestra está formada por un número muy similar de niños (15) y de niñas (13), incluyendo un reparto equitativo ambos subgrupos por edades. Realizados los cálculos pertinentes, los valores medios de F1, F2 y de la diferencia F1-F2 en niños y en niñas muestran gran similitud, como podemos observar en esta tabla.

Género Md en F1 Md en F2 Diferencia F1-F2
Niños 7,07 12,40 -5,33
Niñas 7,23 12,38 -5,15

No parece necesarios más cálculos para concluir que no existen diferencias en los resultados que obtienen niños y niñas, confirmándose también en nuestra muestra la irrelevancia estadística del género en el rendimiento en tareas de fluidez verbal.

Como resumen de estos análisis realizados podemos decir que nuestra muestra tiene el mismo comportamiento que el grupo normativo de ENFEN-Fluidez. Esto nos permite realizar análisis de niveles de rendimiento en los términos planteados en entradas anteriores. Estos análisis se expondrán en otra entrada.

viernes, 27 de febrero de 2026

Datos

Fases del proceso

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

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

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

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

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

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

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

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

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

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

viernes, 20 de febrero de 2026

DATOS. Acceso a datos

Transcripción de audios

También los audios no interesan como fuentes de datos, pero la transcripción "manual" de una grabación de audio para convertirla en texto editable, aun en el caso de grabaciones cortas, lleva una cantidad ingente de tiempo. Posiblemente esta sea una de las razones, si no la de mayor peso, por la cual no es frecuente que los SEO utilicen este soporte como sistema de recogida de datos. Por suerte, actualmente disponemos de recursos para automatizar la transcripción de audio a texto de forma funcional y fiable, on-line y en local.

Hace tiempo que existen on-line recursos para realizar esta transformación, pero no nos garantizan satisfactoriamente la confidencialidad, y además pueden suponer costes económicos o limitaciones de algún tipo. La consecuencia es que recurrir a estos medios no siempre (pocas veces diría yo) pueden considerarse una opción real. Aquí es donde las opciones "en local" se vuelven herramientas de gran interés... Pero presentan ciertas complicaciones y algunas limitaciones.

La principal limitación es que algunas de ellas están pensadas precisamente para hacer posible el proceso inverso: convertir texto a audio, de modo que sea factible generar recuros de utilidad para implementar determinadas pautas DUA, pensadas para garantizar la accesibilidad de los contenidos, por ejemplo, a invidentes.

El uso de alternativas IA puede cubrir también ese tipo de necesidades. Un ejemplo de ello es la funcionalidad de conversoón a audio disponible en NotebookLM; pero en esta aplicación no contamos con herramientas de conversión audio-a-terxto y estamos hablando precisamente de un servicio-en-local.

Sí existen, no obstante, alternativas que corren en local y que son fiables en sus resultados, posibles en cuanto a exigencia de recursos de memoria y procesamiento y confiables en términos de garantía de confidencialidad. Esto último a consecuencia precisamente de correr-en-local.

No puedo afirmar que Whisper sea la única opción que cumpla estas condiciones, pero sí que las cumple y que funciona aceptablemente bien ante demandas reales, comprobadas personalmente. Cierto es que esta biblioteca necesita una instalación que no se resuelve únicamente con el consabido pip install, pero que tampoco conlleva una complejidad excesiva. En todo lo anterior me estoy refiriendo a Windows, donde se necesita, además, acceso a FFmpeg, lo que requeire la previa instalación de Chocolatey desde PowerShell.

No es que ese proceso resulte especialmente complicado ni arriesgado, pero no me responsabilizo de asesorate al respecto. Te recomiento que consultes el tema con quien consideres pertinente. Yo lo hice con la Web y con IA-Gemini; y he de decir que con éxito.

En esta entrada me limito a exponer el resultado en forma de script Python, de cuyo buen funcionamiento doy fe en un sistema no especialmente potente ni dispone de GPU. El que sigue es el script resultante de pelearme durante un rato con IA-Gemini 3.



import whisper
import os

def transcribir_a_unidad_d_y_pantalla(ruta_audio_input):
    # 1. Configuración de nombres y rutas
    nombre_archivo = os.path.basename(ruta_audio_input)
    nombre_sin_extension = os.path.splitext(nombre_archivo)[0]
    ruta_salida_txt = f"D:/{nombre_sin_extension}_transcripcion.txt"

    try:
        if not os.path.exists(ruta_audio_input):
            print(f"❌ Error: No se encontró el archivo en: {ruta_audio_input}")
            return

        # 2. Carga del modelo
        print("Cargando modelo Whisper...")
        modelo = whisper.load_model("base")

        # 3. Transcripción
        print(f"Procesando: {nombre_archivo}...")
        resultado = modelo.transcribe(ruta_audio_input, language="es", fp16=False)      
        
        texto_final = resultado["text"]

        # 4. Mostrar por pantalla (NUEVO)
        print("\n" + "="*50)
        print("TEXTO TRANSCRITO:")
        print("="*50)
        print(texto_final)
        print("="*50 + "\n")

        # 5. Guardar en Unidad D:
        if os.path.exists("D:/"): #Si no dispones de esta unidad o deseas utilizar otra dirección, debes especificarlo AQUÍ
            with open(ruta_salida_txt, "w", encoding="utf-8") as f:
                f.write(texto_final)
            print(f"✅ Archivo guardado con éxito en: {ruta_salida_txt}")
        else:
            print("❌ Error: La unidad D: no está disponible.")

    except Exception as e:
        print(f"❌ Ocurrió un error inesperado: {e}")

# --- VARIABLE DE RUTA ---
mi_variable_ruta = "AQUÍ LA RUTA DE TU AUDIO" #No te olvides concretar la ruta de tu audio

if __name__ == "__main__":
    transcribir_a_unidad_d_y_pantalla(mi_variable_ruta)

Este script bien merece un análisis detallado, cosa que no toca en esta entrada, pero no podía esperar a proporcionártelo por si te interesa para desarrollar un sistema de registro sistemático de actuaciones o para convertir a texto información sensible.

Como te dije, lo he probado personalmente con diferentes tipos de archivos de sonido (archivos wav creados con Audacity y archivos de audio generados con NotebookLM), incluso con archivos de vídeo (mp4 generado con NotebookLM) y el resultado es muy satisfactorio. Pueden observarse algunos errores, pero son mínimos y no comprometen la fidelidad del contenido.

Cierto que para poder usar este script necesitas resolver primero las cuestiones de adaptación de tu sistema (al menos si trabajas con Windows), según indiqué. POr propia experiencia supongo que no te resultará complicado.

La mayor limitación es, no obstante, que esta herramienta sólo resuelve el problema de la transcripción de voz_a_texto, no el acceso al contenido textual resultante, pero esta es una batalla que no podríamos librar sin antes pelear la presente.

Nota

El código mostrado en esta entrada ha sido desarrollado usando IA Gemini 3 y posteriormente revisado y adaptado por el autor.

DATOS. Acceso a datos

Recuento de directorios y archivos

Para completar el tratamiento de los directorios y de los archivos como datos, en esta entrada retomaremos su recuento pero esta vez haciendo uso de la biblioteca pathlib. De este modo dispondremos de un recurso con funcionalidad similar a la que ofrece os, pero más actualizado. Sólo nos interesa aquí como alternativa, por lo que me limitaré a unos mínimos en el tratamiento de esta cuestión.

Empezaré por presentar un script que resume los procedimientos vistos en las dos entradas anteriores de esta misma subsección: el recuento en profundidad de directorios y archivos de forma diferenciada y la cuantificación de los archivos en función de su extensión.


from pathlib import Path
from collections import Counter

#---- FUNCIÓN-------------------------------------------------------------

def analizar_directorio(ruta_entrada, archivo_salida):
    # Convertimos la cadena de texto en un objeto Path
    path_raiz = Path(ruta_entrada)

    if not path_raiz.exists() or not path_raiz.is_dir():
        print(f"Error: La ruta '{ruta_entrada}' no es un directorio válido.")
        return

    # Inicializamos contadores
    total_directorios = 0
    total_archivos = 0
    extensiones = Counter()

    # rglob('*') busca de forma recursiva en todos los niveles
    for elemento in path_raiz.rglob('*'):
        if elemento.is_dir():
            total_directorios += 1
        elif elemento.is_file():
            total_archivos += 1
            # .suffix devuelve la extensión con el punto (ej: .txt)
            ext = elemento.suffix.lower() if elemento.suffix else "Sin extensión"
            extensiones[ext] += 1

    # Construcción del informe de resultados
    informe = []
    informe.append("=" * 50)
    informe.append(f"INFORME DE ANÁLISIS RECURSIVO")
    informe.append(f"Directorio origen: {path_raiz.absolute()}")
    informe.append("=" * 50)
    informe.append(f"Total de directorios encontrados: {total_directorios}")
    informe.append(f"Total de archivos encontrados:    {total_archivos}")
    informe.append("-" * 50)
    informe.append(f"{'EXTENSIÓN':<20} | {'CANTIDAD':<10}")
    informe.append("-" * 50)

    # Ordenar extensiones por frecuencia (de mayor a menor)
    for ext, cuenta in extensiones.most_common():
        informe.append(f"{ext:<20} | {cuenta:<10}")
    
    informe.append("=" * 50)

    # Convertir la lista del informe en un solo string
    texto_final = "\n".join(informe)

    # Mostrar por pantalla
    print(texto_final)

    # Guardar en archivo TXT
    try:
        # Pathlib también permite escribir archivos directamente
        Path(archivo_salida).write_text(texto_final, encoding='utf-8')
        print(f"\n[ÉXITO] El informe se ha guardado en: {archivo_salida}")
    except Exception as e:
        print(f"\n[ERROR] No se pudo escribir el archivo: {e}")

# --- USO DE LA FUNCIÓN -------------------------------------------------------

mi_directorio = "D:/BasesDatosTest" 
nombre_txt = "analisis_pathlib.txt"

analizar_directorio(mi_directorio, nombre_txt)

Además de la diferencia esperada en cuanto importación de librerías (pathlib frente a os, y Counter de la biblioteca collections), las diferencias claves respecto a los script basados en os radica en el uso de determindas funciones específicas de pathlib:

  • la función Path.rglob('*') simplifica la búsqueda recursiva ("r" viene de "recursive"). Al usar el comodín *, pedimos que busque todo lo que hay dentro del directorio, con independencia de los niveles de anidamiento que pueda presentar su estructura.
  • Counter es un diccionario especializado que automatiza el conteo de las exteniones de los archivos (extensiones = Counter()).
  • El atributo.suffix devuelve directamente la extensión del archivo (elemento.suffix.lower()). Mediante .lower unificamos el formato de la extensión a minúsculas evitando así posibles duplicidades.

Aunque es una cuestión secundaria en este script, interesa destacar también la forma en que se genera aquí el archivo .txt, ya que supone una mejora respecto al modo básico de proceder: Path.write_text() es una forma abreviada de escribir archivos de texto sin necesidad de abrir y cerrar flujos manualmente.

Nota

El código mostrado en esta entrada ha sido desarrollado usando IA Gemini 3 y posteriormente adaptado por el autor.

jueves, 19 de febrero de 2026

DATOS. Acceso a datos

Recuento de archivos

Si en la entrada temáticamente anterior tratamos al directoiro como dato, en esta haremos lo mismo con el archivo: también el recuento de archivos puede ser objeto de análisis, para lo cual, es de gran utilidad saber utilizar bibliotecas-Python como os o pathlib.

De momento, en esta entrada trataremos el tema de un modo similar a cómo lo hicimos en la anterior (ya citada): aprenderemos a identificar específicamente los archivos en dos niveles de profundidad respecto a la estructura de directorios y a diferenciar los archivos en función de su extensión. Esto último constituye el paso previo a acceder a los archivos propiamente dichos en busca de información contenida en ellos; tema que nos lleva a la segunda fase del proceso de acceso a datos.

El script que sigue es la formulación más sencilla de nuestro actual objetivos: identificar los archivos existentes en un directorio dado, mostrarlos por pantalla y cuantificarlos. Los comentario incluídos en el script facilitan la comprensión de su funcionamiento.



import os

#0. Establecemos la ruta (directorio) sobre el que trabajar 
ruta_archivos = "C:/BasesDatosTest"

if not os.path.exists(ruta_archivos): #Comprobamos que la ruta existe
    print("La ruta especificada no existe.")

contador = 0 #Creamos el contador de archivos

# 1. Iteramos sobre cada elemento dentro del directorio buscando los archivos
for elemento in os.listdir(ruta_archivos): # Construimos la ruta completa del elemento
    ruta_completa = os.path.join(ruta_archivos, elemento)
    if os.path.isfile(ruta_completa): # Comprobamos que el elemento es un archivo (no directorio)
        contador += 1 # incrementamos el contador de archivos
        print(elemento) # e imprimimos por pantalla el nombre de cada archivo encontrado

#2. Informamos del resultado (número de archivos encontrados)
print(f"Número total de archivos en el directorio: ", contador)


Lo que hace que este script identifique los archivos (no los directorios) es la función os.path.isfile(), que permite especificar que estamos buscando archivos. Recuerda que la búsqueda de directorios requería la función os.path.isdir().

Dentro de la sencillez y limitación de este script está que únicamente nos da acceso a la raiz del directorio, por lo que el resultado sólo es fiable si el directorio no contiene subdirectorios (directorios secundarios o anidados). En ese caso es de esperar que existan archivos dentro de ellos, con lo que este recuento no sería fiable. Para resolver este problema usaremos un segundo script.


import os

#0. Directorio a estudiar
ruta_raiz = "D:/BasesDatosTest" 

if not os.path.exists(ruta_raiz): #Control simple de error
    print ("Error: La ruta proporcionada no existe.")

total_archivos = 0 #Contador de archivos

# 1. Recuento recursivo de archivos mediante os.walk()).
for ruta_actual, subcarpetas, archivos in os.walk(ruta_raiz): # os.walk() recorre TODOS los (sub)directorios
    print(archivos) #Mostramos la lista de archivos que contiene cada directorio
    cantidad_en_esta_carpeta = len(archivos)
    total_archivos += cantidad_en_esta_carpeta
    print(ruta_actual, "  || ",cantidad_en_esta_carpeta," archivos") #Imprime el recuento de archivos en el (sub)directorio
        
#2. Informe final de resultados
print("*" * 150)
print(f"CONTEO FINALIZADO.")
print("-" * 25)
print(f"Total de archivos (incluyendo subdirectorios): ",  total_archivos )
print("*" * 150)

Este segundo script recorre cada uno de los subdirectorios del principal mediante la función os.walk() obteniendo los archivos existentes en la lista archivos. Hemos creado instrucciones print() que facilitan información del listado de archivos de cada (sub)directorio en print(archivos)) y del recuento de los archivos que contienen en print(ruta_actual, " || ",cantidad_en_esta_carpeta," archivos"), pero esta información no es estrictamente necesaria, pudiendo ser comentadas ambas líneas. Lo realmente importante en cuanto a resultados lo encontramos a partir del comentario #2. Informe final de resultados.

Para finalizar esta entrada vamos a consisderar una tercera cuestión en relación a los archivos como datos: la tipología de archivos, identificada con su extensión. Se trata de un tipo de dato que puede resultar muy relevante según en qué tipo de investigación, ya que se relaciona con cuestiones como la importancia del tipo de herramienta informática (servicio, si se prefiere) que predomina en determinadas actuaciones. En estos contextos, analizar el peso del tipo de archivo es una forma de operativizar esas variables de forma sencilla. También contamos con recursos en Python para obtener este tipo de información.



import os

#Función. INICIO -----------------------------------------------------

def analizar_y_guardar(ruta_raiz, archivo_salida):
    
#A. Control básico de error
    if not os.path.exists(ruta_raiz):
        print("Error: La ruta no existe.")
        return

#B. Para almacenar contenido
    total_general = 0
    conteo_extensiones = {}
    informe_texto = "" # Aquí acumularemos todo lo que irá al TXT

#C. Informe. Encabezado del informe
    linea_div = "-" * 70 + "\n"
    encabezado = f"INFORME DE DIRECTORIO: {ruta_raiz}\n"
    columnas = f"{'DIRECTORIO':<55} | {'ARCHIVOS':<10}\n"
    
    informe_texto += encabezado + linea_div + columnas + linea_div
    print(encabezado + linea_div + columnas + linea_div, end="")

#D. Recorrido del directorio-base en profundidad (os.walk()) para capturar el total de archivos 
    for ruta_actual, _, archivos in os.walk(ruta_raiz):
        cantidad = len(archivos)
        total_general += cantidad
        
#E. Registro (sub)directorio por (sub)directorio
        linea_carpeta = f"{ruta_actual:<55} | {cantidad:<10}\n"
        informe_texto += linea_carpeta
        print(linea_carpeta, end="")

#F. Procesamiento de las extensiones de los archivos (no de su nombre o raiz)
        for nombre in archivos:
            _, ext = os.path.splitext(nombre)
            ext = ext.lower() if ext else "Sin extensión"
            conteo_extensiones[ext] = conteo_extensiones.get(ext, 0) + 1

#G. Resumen Final de extensiones (categorías + frecuencias)
    resumen_global = "\n" + "="*40 + "\n"
    resumen_global += "RESUMEN GLOBAL DE EXTENSIONES\n"
    resumen_global += "="*40 + "\n"
    resumen_global += f"Total de archivos analizados: {total_general}\n"
    resumen_global += "-" * 40 + "\n"
    resumen_global += f"{'EXTENSIÓN':<15} | {'CANTIDAD':<10}\n"
    resumen_global += "-" * 40 + "\n"

    for ext, cant in sorted(conteo_extensiones.items(), key=lambda x: x[1], reverse=True):
        resumen_global += f"{ext:<15} | {cant:<10}\n"
    
    resumen_global += "="*40 + "\n"
    
#H. Informe. Unimos todo el contenido
    informe_texto += resumen_global
    print(resumen_global)

#I. Informe. Guardado del archivo de informe
    try:
        with open(archivo_salida, "w", encoding="utf-8") as f:
            f.write(informe_texto)
        print(f"\n[ÉXITO] Informe guardado en: {archivo_salida}")
    except Exception as e:
        print(f"\n[ERROR] No se pudo guardar el archivo: {e}")

# Función. FINAL--------------------------------------------------------

#Llamada a o ejecución de la función ---------------------------------

mi_ruta = "D:/BasesDatosTest"
nombre_informe = "informe_del_analisis.txt"

analizar_y_guardar(mi_ruta, nombre_informe)


Este script contiene al anterior y añade la funcionalidad de identificar el tipo de archivo por su extensión. Pero es de mayor complejidad que el anterior no sólo por ese motivo: implica el uso de una función y la creación de un informe permanente mediante la creación de un archivo .txt.

Vayamos por partes, y la primera y más importante se refiere al recuento de las extensiones de los archivos, que podemos ver tras el comentario F (Procesamiento de las extensiones...). En este conjunto de instrucciones destaca la siguiente: _, ext = os.path.splitext(nombre), en la que la función os.path.splitext() divide la ruta en dos partes: la raíz y la extensión: como lo que nos interesa es la extensión, la raiz se sustitye por _. Si lo que nos interesara fuera la raiz o nombre del archivo, la instrucción se presentaría de este modo: nombre_raiz, _ = os.path.splitext().

La segunda tiene que ver con el uso de la función analizar_y_guardar() que, como vemos tiene dos parámetros, los mismos que encontramos en la llamada a la función la final del script (analizar_y_guardar(mi_ruta, nombre_informe)). Sobre el uso de funciones en Python tenderemos que volver en otro momento.

Finalmente, a diferencia de los dos script anteriores, en este hemos optado por generar un archivo que guarde tota la información en un formato básico (.txt), lo que nos servirá como elemento de relación con el posterior desarrollo de esta sección del blog; cosa que queda para próximas entradas en las que trabajaremos con el acceso al contenido de losa archivos, emepezando precisamente por los más accesibles: precisamente los archivos de texto plano.

Nota

El código mostrado en esta entrada ha sido desarrollado usando IA Gemini 3.
Cuando consideré necesario fui moldeando la respuesta de la IA hasta obtener el script deseado y finalmente modiqué el contenido del script para adaptarlo a mis objetivos, por ejemplo modificando el código inicial para eliminar una función cuando el uso de funciones no me pareción pertinente.

miércoles, 18 de febrero de 2026

DATOS. Acceso a datos.

El directorio como dato

Podemos trabajar con directorios como para de la gestión documental, pero también podemos considerar el directorio como un tipo más de dato, sujeto, en consecuencia, a procesos de análisis. En ambos casos es necesario disponer de recursos para el manejo de directorios. Bibliotecas como os y pathlib de Python nos los proporcionan.

En esta entrada nos plantearemos el tratamiento de los directorios como datos, por lo que enfocaremos los procedimientos de uso de las funciones que ofrecen esas librerías al logro de este objetivo, con independencia de otros tratamientos posibles que se abordan en otras publicaciones de este blog. No se puede descartar que parte de lo que se diga aquí no haya sido tratado en ellas; y aunque el enfoque sea diferente (pensado más en la creación y manipulación de archivos), las instrucciones y los procedimientos de trabajo no tienen por qué serlo en lo fundamental.

Centrándonos ahora en lo que aquí nos interesa, nos situaremos ante circunstancias en las que, por diferentes razones (suponemos ahora que en el marco de un proyecto de análisis de datos) necesitamos conocer la estructura de (sub)directorios de un conjunto documental (en su lugar y momento trabajaremos sobre un proyecto real que materializa esta hipótesis). En este momento, ese conocimiento (referido a los directorios) se convierte en nuestro objetivo. Herramientas como las contenidas en el módulo os nos sirven para identificar las rutas (relativas o absolutas) y de generar el listado (base de la cuantificación) de los (sub)directorios implicados.

A modo de ejemplo, veamos una concreción de lo anterior como script:



import os

# Define la ruta del directorio
directorio = "D:/BasesDatosTest"

# Obtiene una lista con los nombres de los archivos y carpetas
contenido = os.listdir(directorio)

#Lee el listado de componentes del directorio (serviría como ruta relativa)

print(f'Colección de elementos del directorio \n {contenido} \n')

#Recorre el directorio y muestra las rutas absolutas

print('Rutas absolutas de archivo \n')

for elemento in contenido:
    print(directorio + '/' + elemento + '\n')

#Recorre el directorio, cuenta los elementos y muestra los componentes y cuántos son

n_elem = 0

print('Listado de documentos y subdirectorios componentes \n')
 
for elemento in contenido:
    n_elem = n_elem +1
    print (f'Elemento número {str(n_elem)} -> {elemento}')

print(f'\n TOTAL elementos del directorio {str(n_elem)}')

Analizo acontinuación el contexto, el código y el resultado que obtenemos con él.

Empezando por el contexto, se presupone que nuestro objetivo es identificar la estructura de elementos de un determinado directorio ubicado en la unidad D: (directorio = "D:/BasesDatosTest"), a fin de conocer su composición (en principio a nivel cuantitativo).

Con una instrucción (contenido = os.listdir(directorio), este script nos permite obtener el contenido del directorio solicitado, incluyendo los (sub)directorios y los archivos, lo que excede lo que necesitamos en este momento, pero que nos sirve como referencia para afinar el logro de nuestro objetivo. Lo que sigue no es otra cosa que diferentes formas de visualizar ese contenido.

No me detengo en explicar esas formas, ya que lo que realmente nos interesa es identificar de forma diferenciada los (sub)directorios existentes, sin atender (en estos momentos) a los archivos que también contiene el directorio principal.

Además también debemos tener en cuenta que este script sólo nos muestra el contenido inmediato de ese directorio principal, no el contendio de los subdirectorios, dato que también nos puede interesar en nuestro actual análisis de los directorios como contenido.

Vayamos por partes. Primero nos centraremos en aislar los subdirectorios para no mezclarlos con los archivos:


import os

# 0. Identificamos la ruta del directorio a estudiar
directorio = "D:/BasesDatosTest"

# 1. Guardamos el elemento sólo si es un directorio
directorios = [] #Creamos la lista para contener los directorios

for elemento in os.listdir(directorio): #Recorremos el iterable resultante de la función listdir()
    ruta_completa = os.path.join(directorio, elemento)  # Unimos la ruta base con el nombre del elemento
    if os.path.isdir(ruta_completa): # Añadimos cada subdirectorio encontrado (isdir()) a la lista de directorios append())
        directorios.append(elemento)

# 2. Identificamos el número de elementos de la lista directorios con la función len()
total_directorios = len(directorios)

# 3. Mostramos los resultados
print(f' Los subdirectorios encontrados son los siguientes: {directorios} \n') #Nombres de los (sub)directorios

print(f'El número total de directorios en "{directorio}" es: {total_directorios}') #Número de (sub)directorios (principales)

La función principal, la que nos resuelve el problema de identificar sólo los directorios (excluyendo los archivos) es isdir(), cuya función es evidente: identificar en el contenido del directorio obtenido por la función listdir() aquellos elementos que son directorios, excluyendo los elementos que no lo son. Otra función (len()), aplicada sobre la lista directorios (len(directorios)) nos permite contar el número de elementos (directorios).

El script anterior nos ha permitido identificar los directorios existente en la raiz de nuestro directorio de referencia, pero no sabemos si esos (sub)directorios cuentan, a su vez con subdivisiones (subdirectorios de segundo nivel), lo cual en determinados estudios puede ser un dato de interés. Si queremos profundizar en detalle, deberemos aplicar otro procedimiento, como el propuesto en este último script.


import os

# 0. Identificamos la ruta del directorio a estudiar
ruta_base = "D:/BasesDatosTest"

# Establecemos los contadores para el conteo de elementos
carpetas_principales = []
conteo_por_principal = {} # Diccionario para guardar {NombrePrincipal: TotalSubdirs}
total_subdirectorios_global = 0

print(f"--- DESGLOSE DE DIRECTORIOS EN: {ruta_base} ---\n")

# 1. Obtenemos los directorios de primer nivel
elementos_raiz = [f for f in os.listdir(ruta_base) if os.path.isdir(os.path.join(ruta_base, f))] #Obsérvese el uso de las funciones listdir() e isdir()

for principal in elementos_raiz:    #Este ciclo permite dar contenido a la lista mediante append()
    ruta_principal = os.path.join(ruta_base, principal)
    carpetas_principales.append(principal)
    
# 2. Para cada principal, recorremos recursivamente sus subdirectorios
    conteo_subdirs_rama = 0
    subdirectorios_lista = []
    
    for root, dirs, files in os.walk(ruta_principal):
        for d in dirs:
            conteo_subdirs_rama += 1
            ruta_relativa = os.path.relpath(os.path.join(root, d), ruta_base) # Guardamos la ruta relativa para que el listado sea legible
            subdirectorios_lista.append(ruta_relativa)
    
    conteo_por_principal[principal] = conteo_subdirs_rama # Guardamos el conteo y mostramos el listado de esta rama
    total_subdirectorios_global += conteo_subdirs_rama
    
    print(f"Directorio Principal: [{principal}]")
    if subdirectorios_lista:
        for s in subdirectorios_lista:
            print(f"  └── {s}")
    else:
        print("  └── (Sin subdirectorios)")
    print(f"  > Total subdirectorios en esta rama: {conteo_subdirs_rama}\n")

# 3. Resumen final del recuento de contenidos 
print("="*50)
print("RESUMEN DEL RECUENTO")
print("="*50)
print(f"Total de Directorios Principales: {len(carpetas_principales)}")
print(f"Total Global de Subdirectorios:   {total_subdirectorios_global}")
print(f"Total de carpetas (Suma total):    {len(carpetas_principales) + total_subdirectorios_global}")

Lo que este script añade al anterior es el acceso a los (sub)directorios y la visualización de su contenido (sólo de los subdirectorios de segundo, tercer... nivel (ver punto 2 del script). Para esto es fundamental el uso de la función os.walk() en (os.walk(ruta_principal)).

A fin de facilitar la obtención de datos, hemos incorporado al script un sistema de recuento que resumen la información obtenidas (punto 3 del script)

Nota

El código mostrado en esta entrada ha sido desarrollado usando Gemini-IA a modo de auxiliar de programación.
La respuesta de la IA fue moldeada mediante prompt sucesivos hasta obtener el funcionamiento deseado.
Finalmente, el contenido del script (instrucciones y comentarios) y su funcionamiento en local fue objeto de revisión, comprobación y modificación por parte del autor de esta entrada.