Mostrando entradas con la etiqueta csv. Mostrar todas las entradas
Mostrando entradas con la etiqueta csv. 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.

domingo, 1 de marzo de 2026

DATOS. Acceso a datos

CSV. Datos estructurados (II)

Módulo CSV. Nuevo registro de datos

En la entrada precedente aprendimos a acceder a un archivo .csv usando el módulo CSV; en la actual aprenderemos a añadir registros usando el mismo módulo. Además iremos directos al grano, digo, al script...



import csv
import os
from datetime import datetime

ruta_archivo = 'datos\libros3.csv'
NOMBRE_CAMPO_FECHA = 'fecha_lectura'

def sesion_carga_masiva():
    if not os.path.exists(ruta_archivo):
        print(f"❌ El archivo '{ruta_archivo}' no existe.")
        return

    try:
        # 1. Carga inicial para conocer la estructura y el último ID
        with open(ruta_archivo, mode='r', encoding='utf-8', newline='') as archivo:
            lector = csv.reader(archivo)
            cabecera = next(lector)
            datos_existentes = list(lector)
            
        # El ID inicial se basa en lo que ya hay en el archivo
        siguiente_id = len(datos_existentes) + 1
        campos_a_pedir = cabecera[1:-2]

        print(f"--- 🚀 Sesión iniciada. Para terminar escribe 'salir'.\n")

        while True:
            print(f"📝 Preparando Registro #{siguiente_id}:")
            datos_usuario = {}
            cancelar = False

            # 2. Bucle para pedir cada campo de la cabecera
            for columna in campos_a_pedir:
                valor = input(f"   {columna}: ").strip()
                
                if valor.lower() == 'salir':
                    cancelar = True
                    break
                datos_usuario[columna] = valor

            if cancelar: 
                break

            # 3. Procesamiento de Fecha y Validación
            fecha_str = datos_usuario.get(NOMBRE_CAMPO_FECHA)
            try:
                fecha_obj = datetime.strptime(fecha_str, "%d/%m/%Y")
                mes_auto = fecha_obj.month
                anio_auto = fecha_obj.year

                # 4. Construcción de la fila
                nueva_fila = [siguiente_id]
                for columna in campos_a_pedir:
                    nueva_fila.append(datos_usuario[columna])
                nueva_fila.extend([anio_auto,mes_auto])

                # 5. Guardado físico en el CSV
                with open(ruta_archivo, mode='a', encoding='utf-8', newline='') as archivo_escritura:
                    escritor = csv.writer(archivo_escritura)
                    escritor.writerow(nueva_fila)

                print(f"✅ Registro #{siguiente_id} guardado con éxito.\n")
                print("Escribe 'salir' para finalizar registro.\n")
                
                # Incrementamos el ID para el próximo libro de esta misma sesión
                siguiente_id += 1

            except ValueError:
                print(f"\n❌ ERROR: '{fecha_str}' no es una fecha válida (DD/MM/AAAA).")
                print("⚠️ Este registro no se guardó. Por favor, reinténtalo.\n")
                # No incrementamos el ID porque el registro falló

        print(f"\n--- Sesión finalizada. Se han añadido nuevos libros. ---")

    except Exception as e:
        print(f"❌ Error crítico en la sesión: {e}")

if __name__ == "__main__":
    sesion_carga_masiva()


Tomo como referencia el mismo conjunto de datos (libros3.csv) que nos sirvió para aprender a acceder y visualizar registros, pero esta vez planteo un procedimiento simple de escritura de registros mediante la función csv.writer(). Además de esta función, writerow() también es básica para el correcto funcionamiento del script, ya que es la que se encarga de añadir un uevo registro a nuestro documento.

En este caso utilizo una función (sesion_carga_masiva()), a la que llamo mediante el procedimiento if __name__ == "__main__":, aun pendiente de explicación. Todo llegará.

Aunque el usuario debe introducir la mayoría de los campos, algunos se implementan automáticamente. Tal es el caso del id del libro (siguiente_id = len(datos_existentes) + 1) y de los datos correspondientes al año y al mes de lectura, dereivados ambos del campo fecha_lectura (nueva_fila.extend([anio_auto,mes_auto])).

Dado que el script debe permitir la entrada múltiple de registros, se implementa un bucle (while True:) que lo hace posible, requiriendo al usuario la expresión 'salir' para cerrar el bucle y finalizar el script.

viernes, 27 de febrero de 2026

DATOS. Acceso a datos

CSV. Datos estructurados (I)

Módulo CSV. Acceso al archivo

Antes de nada una aclaración que parece necesaria: cuando hablamos de acceso a datos debemos distinguir entre lo que es acceder al contenido de un archivo y lo que implica acceder a un determinado dato o conjunto de datos que éste contiene. Lo primero implica hacer uso de determinadas tecnologías y lo segundo, además, la aplicación de determinados procesimientos.

En lo que se refiere a esta entrada cabe diferenciar esos procedimientos en el marco de una distinción que resulta de gran interés: la diferencia que implica acceder a datos estructurados y a datos no estructurados. Cuando hablamos, como ahora, de acceder a datos csv estamos planteando acceder a un tipo de documento (extensión .csv) que es de naturaleza estructurada, como lo son también los datos de una base de datos o los datos contenidos en una hoja de cálculo.

El formato .csv es uno de los formatos básicos para el trabajo con Python. Otro es el formato .txt, pero entre ambos existe una diferencia muy importante: los datos .csv están estructurados, mientras que los datos .txt no lo están. El acceso simple a ambos es sumamente sencillo desde Python, pero mientras que sigue siendo sencillo acceder a contenidos concretos de un archivo .csv, no lo es si trabajamos con un archivo .txt. Es este el motivo por el que (despues de hacerlo sobre directorios y archivos) nos planteamos ahora tratar el acceso a los archivos .csv y sus contenidos.

Un archivo .csv (comma-separated values) es una archivo de texto plano que contiene datos tabulares estructurados en filas y columnas separados por comas u otros delimitadores (punto y coma, tabulaciones).

Cada línea del archivo corresponde a una fila en la tabla, y cada valor de esa fila se separa por un delimitador (coma). Dada su simplicidad pueden ser leídos por casi cualquier hoja de cálculo o software de gestión de datos.

Python cuenta con herramientas para trabajar con archivos .csv, empezando por el módulo nativo CSV (que no requiere instalación mediante pip al ser nativo, aunuse ser importado) y siguiendo por Pandas y NumPy. Empezaremos por CSV

El módulo CSV cuenta con clases para leer y escribir datos tabulares en formato CSV y permite escribir y leer este tipo de archivo.

Existen dos enfoques de trabajo de este módulo en el tratamiento que da a los datos del archivo .csv: como listas o como diccionarios.

  • csv.reader(), enfoque lista en la lectura del archivo csv
  • csv.writer(), enfoque lista en la escribir en un archivo csv
  • csv.DictReader(), enfoque diccionario de la lectura de un archivo csv
  • csv.DictReader(), enfoque diccionario para la escritura en archivos csv

Veamos cómo se concretan ambos enfoques en el manejo de un mismo archivo csv.


import csv

#Acceso al archivo csv
ruta = 'datos\libros2.csv'
with open(ruta, mode='r', encoding='utf-8', newline='') as archivo:

# Creamos el lector enfocado a lista (csv.reader())
    lector = csv.reader(archivo, delimiter=',')
    
# Visualizamos la cabecera de la tabla csv  (mediante next())
    cabecera = next(lector)
    print(f"Listado de nombres de columnas (cabecera): {cabecera}")
    
# Iteramos sobre las filas mostrando las tres primeras columnas: id, autor y título
    for fila in lector:
          print(f"Id: {fila[0]}, Autor: {fila[1]}, Título {fila[2]}")


Esta es la forma más sencilla de acceder a un archivo csv. Es útil cuando conocemos la estructura de la tabla y el archivo tiene un tamaño manejable.

Es importante utilizar al fórmula with open() para facilitar el manejo del archivo csv, ya que nos permite abrirlo y cerrarlo de forma automática.

El acceso al contenido del archivo viene dada por la función csv.reader(), que, como vimos antes, nos permite capturar dicho contenido en una lista; primero a la cabecera (next()) y después a los datos mediante un bucle (for fila in lector:).

Sin embargo, esta forma de acceso tiene una limitación importante para el posterior acceso a los datos: es de recorrido único, por lo que deberemos repetir el procedimiento de acceso tantas veces como maniobras necesitemos realizar con el contenido del archivo csv.

En el script anterior la maniobra consistió en listar los registros y mostrarlos por pantalla (print(f"Id: {fila[0]}, Autor: {fila[1]}, Título {fila[2]}")) y en el siguiente accedemos a un registro determinado y dentro de él a una serie de campos empleado también un bucle y un contador para identificar el número de registro de deseamos visualizar (conta = 0), en este caso el nº 5 (if conta == 5)


#Mostramos un registro determinado y dentro de él dos campos.
#Para ello empleamos la iteración y nuestro conocimiento de la estructura de la tabla 

    for fila in lector:
        conta += 1
        if conta == 5:
            print(f"Autor: {fila[1]}, Título {fila[2]}")
            break 

Basándonos en los anterior, podemos crear un buscador sencillo de registros que funcione a petición del usuario. Simplemente se trata aplicar las funciones vistas antes, csv.reader() y otras, y poco más. Te dejo el script resultante de este propósito para que lo puedas usar en tus proyectos.

import csv

ruta_archivo = 'datos\libros3.csv'

def buscador_interactivo():
    try:
        # 1. Abrimos el archivo
        with open(ruta_archivo, mode='r', encoding='utf-8', newline='') as archivo:
            # Cargamos el lector
            lector = csv.reader(archivo)
            cabecera = next(lector)
            
            # Convertimos el lector a una lista para poder buscar varias veces. De no hacerlo sólo podríamos buscar un registro.
            # La consecuencia es un incremento de la carga de trabajo para la memoria del sistema (el contenido de la lista, ahora en memoria)
            datos = list(lector)
            total_registros = len(datos)

            print(f"--- 🔎 Buscador de registros cargado. ({total_registros} registros disponibles) ---")

            while True:
                # 2. Solicitar número al usuario
                entrada = input("\nIntroduce el número de registro a buscar (o 'salir' para terminar): ")

                if entrada.lower() == 'salir':
                    print("Gracias por usar nuestro buscador de registros.")
                    break

                # 3. Validar que sea un número
                try:
                    objetivo = int(entrada)
                except ValueError:
                    print("❌ Por favor, introduce un número válido.")
                    continue
                encontrado = False
                
                # 4. Buscar usando la función enumerate() sobre la lista de datos
                # 4.1. 'datos' es nuestro conjunto de filas.
                # 4.2. 'start=1' indica el inicio del conteo desde 1 (y no desde 0).
                # 4.3. Cada vuelta, 'i' recibe el número de fila y 'fila' los datos. 'i' es el contador automático de enumerate()
                for i, fila in enumerate(datos, start=1):
                    if i == objetivo:
                        print(f"\n✅ Registro #{i} encontrado:")
                        # Mostramos cada dato con su nombre de columna
                        for col, valor in zip(cabecera, fila):
                            print(f"   {col}: {valor}")
                        encontrado = True
                        break
                
                if not encontrado:
                    print(f"⚠️ El registro {objetivo} no existe. El rango es de 1 a {total_registros}.")

    except FileNotFoundError:
        print(f"❌ Error: No se encontró el archivo '{ruta_archivo}'.")

# Ejecutar el buscador
buscador_interactivo()


miércoles, 24 de septiembre de 2025

Datos. Python.


Biblioteca Camelot (II)

Extracción de tablas en pdf complejos



Aunque se trate de una forma bastante simple de entender la complejidad, cuando manejamos un pdf con varias tablas y/o con varias páginas, la cosa cambia. Ya no nos sirven procedimientos tan simples como el utilizado en la [entrada anterior].


Es cierto que esta mayor complejidad es relativa, pero lo es suficientemente como para que necesitemos formular script con Camelot diferentes del anterior. Aprovecharemos también esta entrada para variar un poco de objetivo, buscando ahora guardar nuestras tablas en un documento externo.

import camelot

# Especifica la ruta al archivo PDF
file_path = 'tablaSimple_c.pdf' 

# Usa el método lattice para extraer tablas
tables = camelot.read_pdf(file_path, flavor= 'lattice' , pages= '1-end' )

# Guarda las tablas en formato CSV
tables.export( 'tablas.csv' , f= 'csv' , compress= True ) 

# Imprime el contenido de todas las tablas 
for i, table in  enumerate (tables): 
    print ( f"Table {i + 1 } " ) # Numera la tabla
    print (table.df) #Y la imprime en consola 

El documento pdf con el que trabajamos en este caso está dividido en tres hojas y cada una de ellas contiene una tabla sencilla. El procedimiento, según se observa en el script, es, sin embargo un tanto diferente.

Ahora no es suficiente con indicar en el método camelot.read_pdf() como único parámetro la ruta del archivo (file_path), también es necesario establecer el número de páginas a estudiar (pages= '1-end') y el método empleado para extraer las tablas (flavor= 'lattice'). Veamos ambos parámetros con más detalle.

Cuando un documento tiene una única página, la función camelot.read_pdf() no precisa que se especifique el parámetro pages ya que asume por defecto esa condición, pero si omitimos el parámetro cuando el documento tiene más de una página, no se produce error, pero el script sólo trabaja con la primera página del documento y devuelve únicamente las tablas que contenga la página 1 del documento.

Por el contrario, si conocemos el documento y sabemos en qué páginas contiene tablas y en cuales no, usando el identificador de páginas de forma selectiva no ahorramos obtener datos que no nos interesan, ya que Camelot puede interpretar como tablas segmentos del documento que no lo son. Por ejemplo, si el parámetro  pages se concreta como pages= '1-end', el script registra las tablas de las hojas 1 al final del documento, lo que equivale a  pages= 'all' (todas las hojas); pero sí especifico  pages= '1-2' sólo trabajará con el intervalo de hojas 1 a 2 y si solicitamos  pages= '1,3' lo hará con las tablas de las hojas 1 y 3. También son posibles combinaciones de página e intervalo, como por ejemplo  pages= '1-5,4,7,12-end'.

Respecto al método de extracción de tablas, latice está diseñado para extraer tablas de contornos bien definidos, mientras que su alternativa (stream), más lento, permite trabajar con tablas de límites mal definidos y celdas separadas por espacios.

Este parámetro (flavor) no es necesario cuando las tablas son el contenido único de la página y además están bien definidas, pero sí lo son cuando el documento presenta mayor complejidad. Aunque no siempre incluirlo mejora los resultados.

El siguiente paso consiste en generar un archivo (en este caso uno por tabla y en formato csv) para guardar el resultado de la extracción, lo que en este caso conseguimos con la función export() (tables.export( 'tablas.csv' , f= 'csv' , compress= True ) ), donde especificamos el nombre del archivo ('tablas.csv'), el tipo de archivo (f= 'csv') y si se presentan comprimidos o no (compress= True). El resultado es una carpeta comprimida que contiene (en mi caso) tres documentos csv, cada uno con su correspondiente tabla.

Finalmente podemos imprimir por consola (Shell en realidad) cada una de esas tablas, cosa que ahora implica hacer uso de un bucle (for i, table in  enumerate (tables):) que escribe por pantalla el número de la tabla (según su posición en el texto) (print ( f"Table {i + 1 } " )) y su contenido (print (table.df)). El resultado de esta parte del script es la escritura en consola de las tres tablas de forma consecutiva. Podríamos optar por acceder a cualquiera de ellas sustituyendo el ciclo por una instrucción simplificada de escritura (vg. print(tables[0].df))

Bueno, pues en esta entrada hemos aprendido unas cuantas cosas más. Especialmente interesante poder guardar las tablas extraídas en un documento de fácil acceso y manejo.