# load packages ----------------------------------------------------------------
library(introexercises)
library(learnr)
library(gradethis)
library(flair)
library(dplyr)
library(ggplot2)
library(lubridate)
library(janitor)
library(fontawesome)
library(gtsummary)
library(scales)
# library(RMariaDB)        # connect to sql database

## set options for exercises and checking ---------------------------------------

## Define how exercises are evaluated 
gradethis::gradethis_setup(
  ## note: the below arguments are passed to learnr::tutorial_options
  ## set the maximum execution time limit in seconds
  exercise.timelimit = 60, 
  ## set how exercises should be checked (defaults to NULL - individually defined)
  # exercise.checker = gradethis::grade_learnr
  ## set whether to pre-evaluate exercises (so users see answers)
  exercise.eval = FALSE 
)

# ## event recorder ---------------------------------------------------------------
# ## see for details: 
# ## https://pkgs.rstudio.com/learnr/articles/publishing.html#events
# ## https://github.com/dtkaplan/submitr/blob/master/R/make_a_recorder.R
# 
# ## connect to your sql database
# sqldtbase <- dbConnect(RMariaDB::MariaDB(),
#                        user     = Sys.getenv("userid"),
#                        password = Sys.getenv("pwd"),
#                        dbname   = 'excersize_log',
#                        host     = "144.126.246.140")
# 
# 
# ## define a function to collect datos 
# ## note that tutorial_id is defined in YAML
#     ## you could set the tutorial_version too (by specifying version:) but use package version instead 
# recorder_function <- function(tutorial_id, tutorial_version, user_id, event, datos) {
#     
#   ## define a sql query 
#   ## first bracket defines variable names
#   ## values bracket defines what goes in each variable
#   event_log <- paste("INSERT INTO responses (
#                        tutorial_id, 
#                        tutorial_version, 
#                        date_time, 
#                        user_id, 
#                        event, 
#                        section,
#                        label, 
#                        question, 
#                        answer, 
#                        code, 
#                        correct)
#                        VALUES('", tutorial_id,  "', 
#                        '", tutorial_version, "', 
#                        '", format(Sys.time(), "%Y-%M%-%D %H:%M:%S %Z"), "',
#                        '", Sys.getenv("SHINYPROXY_PROXY_ID"), "',
#                        '", event, "',
#                        '", datos$section, "',
#                        '", datos$label,  "',
#                        '", paste0('"', datos$question, '"'),  "',
#                        '", paste0('"', datos$answer,   '"'),  "',
#                        '", paste0('"', datos$code,     '"'),  "',
#                        '", datos$correct, "')",
#                        sep = '')
# 
#     # Execute the query on the sqldtbase that we connected to above
#     rsInsert <- dbSendQuery(sqldtbase, event_log)
#   
# }
# 
# options(tutorial.event_recorder = recorder_function)
# hide non-exercise code chunks ------------------------------------------------
knitr::opts_chunk$set(echo = FALSE)
# datos prep --------------------------------------------------------------------
vig <- rio::import(system.file("dat/listado_vigilancia_limpio_20141201.rds", package = "introexercises"))

Introducción a R para Epidemiología Aplicada y Salud Pública

Bienvenido/a

Bienvenido al curso "Introducción a R para epidemiología aplicada", ofrecido por Epidemiología - una organización sin fines de lucro y el principal proveedor de formación, apoyo y herramientas de R para profesionales de primera línea de la salud pública.

knitr::include_graphics("images/logo.png", error = F)

Agrupar y resumir

Este ejercicio se centra en agrupar y resumir datos en tablas descriptivas.

Formato

Este ejercicio te guía a través de las tareas que debes realizar en RStudio en tu ordenador.

Obtener ayuda

Hay varias formas de obtener ayuda:

1) Busca la sección de ayuda (ver más abajo) 2) Pide ayuda al instructor/facilitador de tu curso en directo 3) Programa una llamada 1 a 1 con un instructor para "Tutoría del curso". 4) Publica una pregunta en La Comunidad de Applied Epi

Este es el aspecto que tendrá esa sección de ayuda:

r fontawesome::fa("lightbulb", fill = "gold") Haz clic para leer una pista

¡Aquí verás una pista útil!


r fontawesome::fa("check", fill = "red")Haz clic para ver una solución (¡pruébalo tú primero!)

linelist %>% 
  filter(
    edad > 25,
    distrito == "Bolo"
  )

Aquí tienes más explicaciones sobre por qué funciona la solución.


Cuestionario

Responder a las preguntas del cuestionario te ayudará a comprender el material. Las respuestas no se registran.

Para practicar, responde a las siguientes preguntas:

quiz(
  question_radio("¿Cuándo debería mirar las secciones de ayuda en rojo?",
    answer("Tras intentar escribir el código yo mismo", correct = TRUE),
    answer("Antes de intentar escribir el código", correct = FALSE),
    correct = "Revisar el código de ejemplo después de intentar escribirlo usted mismo puede ayudarle a mejorar",
    incorrect = "Por favor, intente completar el ejercicio usted mismo, o use las pistas disponibles, antes de mirar la respuesta."
  )
)
question_numeric(
 "¿Qué tan ansioso estás por comenzar este tutorial? En una escala del 1 (menos ansioso) al 10 (más ansioso).?",
 answer(10, message = "Intenta no preocuparte, ¡te ayudaremos a conseguirlo!", correct = T),
 answer(9, message = "Intenta no preocuparte, ¡te ayudaremos a conseguirlo!", correct = T),
 answer(8, message = "Intenta no preocuparte, ¡te ayudaremos a conseguirlo!", correct = T),
 answer(7, message = "Intenta no preocuparte, ¡te ayudaremos a conseguirlo!", correct = T),
 answer(6, message = "De acuerdo, lo conseguiremos juntos", correct = T),
 answer(5, message = "De acuerdo, lo conseguiremos juntos", correct = T),
 answer(4, message = "¡Me gusta tu confianza!", correct = T),
 answer(3, message = "¡Me gusta tu confianza!", correct = T),
 answer(2, message = "¡Me gusta tu confianza!", correct = T),
 answer(1, message = "¡Me gusta tu confianza!", correct = T),
 allow_retry = TRUE,
 correct = "Gracias por compartir.",
 min = 1,
 max = 10,
 step = 1
)

Licencia

Por favor, envía un correo electrónico a contact@appliedepi.org si tienes preguntas sobre el uso de estos materiales.

Objetivos de aprendizaje

Preparar el análisis

Abre el proyecto RStudio

r fontawesome::fa("window-restore", fill = "darkgrey") Haz doble clic en el archivo del proyecto RStudio "ebola" para abrirlo (como en el módulo anterior).

Vuelve a abrir tu script localizado en la carpeta "ebola_análisis.R" donde escribiste el código de limpieza del módulo anterior.

Añade los paquetes

Asegúrate de que los siguientes paquetes están incluidos en el directorio pacman::p_load() en el comando Cargar paquetes de tu script:

Sin embargo, asegúrate de que, cuando añadas paquetes, coloca el paquete {tidyverse} de último lugar en este comando. Esto es para que las funciones de {tidyverse} tengan prioridad sobre las funciones de otros paquetes con el mismo nombre.

Cuando ejecutes este comando, estos paquetes se instalarán (si estos todavía no están instalados) y se cargarán para su uso.

Recuerda que sólo debes tener un solo comando de pacman::p_load() en la parte superior de tu script.

r fontawesome::fa("check", fill = "red")Haz clic para ver la solución (¡pruébalo tú primero!)

pacman::p_load(
  rio,          # para cargar bases de datos
  here,         # para localizar archivos
  janitor,      # para limpieza
  lubridate,    # para limpieza de fechas  
  epikit,       # para crear categoría de edades
  gtsummary,    # para crear tablas
  scales,       # para añadir formato de porciento  
  flextable,    # para hacer tablas con formato
  tidyverse     # para manejo de datos y visualización
)


Ejecuta tu script R

Ejecuta todo el código de tu script. Si has terminado con éxito los módulos anteriores, el script hará lo siguiente:

Ahora puedes escribir más código en la parte inferior del script, utilizando el dataframe o base de datos vig.

Copia de seguridad

Si tu script R provoca demasiados errores, o no has terminado el último módulo, avisa a tu instructor. Puedes importar una versión limpia de la base de datos de vigilancia con el siguiente comando y utilizarla para este ejercicio:

vig <- import(here("datos", "limpios", "copia_seguridad", "listado_vigilancia_limpio_20141201.rds"))

Añade una nueva sección

Asegúrate de que tu script está bien comentado (#) para que sea fácil saber qué ocurre en cada parte del mismo.

Añade una nueva sección a tu script para las "Tablas resumen", justo encima del "Área de pruebas". Coloca el cursor donde deba empezar la nueva sección y pulsa Ctrl+Shift+R al mismo tiempo (o Cmd Mayús R en un Mac) para crear un nuevo encabezado de sección.

# Tablas resumen ----------------------------------------------

Recuerda que puedes navegar por tu script utilizando estas secciones con el botón "Esquema" (outline) situado en la esquina superior derecha del script R.

funciones del paquete {janitor}

A menudo, sólo queremos una tabla sencilla de los totales de una variable categórica o nominal, o simplemente saber "cuáles son los valores únicos" de una columna concreta.

La función tabyl() de {janitor} lo hace rápidamente tabulando los valores únicos de la variable que especificamos. Luego puedes personalizarla fácilmente (agregando porcentajes, totales por ejemplo), utilizando las funciones adorn_() del mismo paquete.

El uso de operador pipe

Los operadores pipe no sólo sirven para "limpiar". La función de estos conectores es pasar un comando a otro para partiendo desde una base de datos u objeto, como una cadena.

El hacer tablas con {janitor} puede requerir varios pasos, y puedes utilizar este operador %>% para pasar o conectar los comandos de un paso al siguiente (Ejemplo: Hago esto y "entonces" (operador pipe) hago esto).

Escribe un código en tu sección Tablas resumen que conecte el objeto vig con la función tabyl() y especifica la columna distrito en la función tabyl().

r fontawesome::fa("lightbulb", fill = "gold") Haz clic para leer una sugerencia

Escribre el nombre del objeto vig conectado con la función tabyl() y escribe la variable distrito entre paréntesis.


r fontawesome::fa("check", fill = "red")Haz clic para ver una solución (¡pruébalo tú primero!)

vig %>% 
  tabyl(distrito)


quiz(caption = "Cuestionario - tabyl()",
  question("¿Este comando es un comando para PRESENTAR o GUARDAR?",
    allow_retry = T,
    answer("Un comando para presentar",
           correct = T,
           message  = "Este es un comando presentación porque no se usó el operador de asignación '<-'. Los datos se convierten en una tabla y simplemente se presentan en la consola. No hay cambios permanentes en la base de datos."),
    answer("Un comando para guardar",
           correct = F,
           message = "Este es un comando presentación porque no se usó el operador de asignación '<-'. Los datos se convierten en una tabla y simplemente se presentan en la consola. No hay cambios permanentes en la base de datos."),
    answer("Un comando universal",
           correct = F,
           message = "No existe tal cosa como un comando universal")
    ),
  question("¿Cuál es el distrito con más casos registrados?",
    allow_retry = T,
    answer("Mountain Rural", correct = T),
    answer("West II"),
    answer("Central II"),
    answer("Manhattan")
  ),
  question("¿Cuántas filas faltan en el distrito (NA)?",
    allow_retry = T,
    answer("203"),
    answer("37"),
    answer("None"),
    answer(vig %>% filter(is.na(distrito)) %>% nrow(),
           correct = T)
  )
)

show_na = FALSE

Con la función tabyl() cuando en una variable con valores vacíos o NA, contiene por defecto una columna "porcentaje_válido" que muestra las proporciones calculadas al excluir los valores perdidos.

Ejecuta la función tabyl() pero especificando el argumento show_na = FALSE. ¿Cómo cambia la salida?

vig %>% 
  tabyl(distrito, show_na = FALSE)

¿Cómo sabrías ejecutar este argumento, o cuáles son los valores por defecto? Esta información está escrita en la documentación disponible de la función

En esta documentación, puedes ver que el valor por defecto para este argumento es TRUE pero que si lo ajustas a FALSE se eliminará NA valores de la tabla.

knitr::include_graphics("images/show_na_argument.png", error = F)

"Adornando" el formato de la tabla

El paquete {janitor} incluye una serie de funciones de soporte para el "adorno" en las que puedes canalizar en una tabla, que harán que el formato sea más presentable y detallado.

Añade las siguientes funciones al código, utilizando un operador pipe *en este orden*. Deja sus paréntesis vacíos, no necesitan parámetros. Observa cómo cambia la salida de tabla con cada función.

Ordenar

Actualmente, la tabla de distritos está ordenada alfabéticamente por el nombre del distrito. Cambia esto introduciendo al final la función arrange() del paquete {dplyr} y especificando que se ordene por la columna n de forma descendente.

Para especificar descendente, puedes envolver el nombre de la variable con desc() o poner un símbolo - (menos) delante del nombre de la variable.

r fontawesome::fa("check", fill = "red")Haz clic para ver una solución (¡pruébalo tú primero!)

vig %>% 
  tabyl(distrito) %>% 
  adorn_totals() %>% 
  adorn_pct_formatting() %>% 
  arrange(desc(n))

o

vig %>% 
  tabyl(distrito) %>% 
  adorn_totals() %>% 
  adorn_pct_formatting() %>% 
  arrange(-n)


Experimenta con el orden de los pasos... ¿Qué ocurre si añades adorn_totals() antes de ordenar por columnas n?

Reordena tus pasos para que las filas estén en orden descendente por la variable n y manteniendo la fila "Total" en la parte inferior.

r fontawesome::fa("check", fill = "red")Haz clic para ver la solución (¡pruébalo tú primero!)

vig %>% 
  tabyl(distrito) %>% 
  arrange(desc(n)) %>% 
  adorn_totals() %>% 
  adorn_pct_formatting()


Tabla de dos o más dimensiones (cruzada)

Ahora haz una tabulación cruzada del distrito y el hospital, colocando los nombres de ambas columnas en la columna tabyl() separados por una coma.

r fontawesome::fa("check", fill = "red")Haz clic para ver la solución (¡pruébalo tú primero!)

vig %>% 
  tabyl(distrito, hospital)


quiz(caption = "Cuestionario - tablas de hospitales",
  question("Según esta tabla cruzada, ¿En qué distrito probablemente se encuentre SMMH?",
    allow_retry = T,
    answer("West II"),
    answer("Central II"),
    answer("Mountain Rural", correct = T),
    answer("East I")
  ),
  question("¿Cómo afecta el argumento show_na = FALSE a esta tabulación cruzada?",
    allow_retry = T,
    answer("Sin efecto"),
    answer("Elimina NA de las filas"),
    answer("Elimina NA de filas y columnas", correct = T),
    answer("Elimina NA de las columnas")
  )
)

"Adorna" una tabla de dos o más dimensiones

Intenta añadir funciones de adorn_() al comando de la tabla con tabyl(). Observa cómo se comportan de forma diferente en las tablas cruzadas que en las tablas de una sola variable.

Ahora, intenta añadir estas funciones (sólo una cada vez):

Ejercicio final con tabyl()

Explora el objeto vig con la función tabyl().

Elige 2 tablas para guardar en tu script ebola_analysis.R en la sección "Tablas resumen que muestren tus nuevas habilidades sobre el uso de la función tabyl(). Elimina todas las demás tablas que hayas creado (o muévelas al "Área de pruebas" si quieres conservarlas como referencia).

funciones del paquete {dplyr}

Añade una sub-sección en tu sección "Tablas resumen" que se llame "Tablas con dplyr". Asegúrate de que tiene dos símbolos hash (#) para que aparezca correctamente el indentado en el esquema del script (ver el botón gris "Esquema" (Outline) en la esquina superior derecha del script).

# Tablas resumen ----------------------------------------------


## Tablas con janitor ----------------------------------------


## Tablas con dplyr ------------------------------------------

count()

La función count() del paquete {dplyr} proporciona una forma alternativa y sencilla de hacer tablas de frecuencia.

Prueba a ejecutar este código en tu sección "Tablas resumen":

## Tablas con dplyr
vig %>% 
  count(hospital)

¿Cómo se compara con la función tabyl() usando la misma variable?

Ahora, ajusta el comando para que, en lugar de presentar la tabla en la consola de R, la tabla se guarde como un nuevo objeto dataframe llamado hospital_recuento o conteo_hospital.

Una vez hecho esto, abre el objeto recién creado haciendo clic sobre él en el Entorno R, o ejecutando View(hospital_recuento) o View(conteo_hospital) .

Este paso es para mostrarte que, de hecho, ¡Estás creando otro objeto dataframe o base de datos! Podrías realizar análisis sobre este, o incluso exportarlo como un archivo csv o xlsx.

r fontawesome::fa("check", fill = "red")Haz clic para ver la solución (¡pruébalo tú primero!)

hospital_recuento <- vig %>% 
  count(hospital)




Ahora prueba a introducir 2 variables categóricas en la función count() como distrito y sexo (separados por una coma).

r fontawesome::fa("check", fill = "red")Haz clic para ver una solución (¡pruébalo tú primero!)

hospital_recuento <- vig %>% 
  count(hospital, sexo)




¿Qué aspecto tiene esta tabla? ¿Cómo se compara con la función de tabulación cruzada de tabyl()? ¿Cuáles podrían ser algunas ventajas o desventajas de este formato "largo" o vertical?

¿Cuál de los dos formatos se ajusta mejor a las directrices de "datos ordenados" explicadas en los Extras del módulo anterior? (Recuerda, en los datos ordenados, cada variable tiene su propia columna y cada observación su propia fila...)

NOTA: La estructura ordenada de los datos se trató en la sección Extras del 2º módulo sobre limpieza de datos, ¡Vuelve cuando tengas tiempo para revisar en profundidad el formato "datos ordenados"!

group_by() y summarise()

Las funciones group_by() y summarise() juntas son la herramienta más versátil para crear un nuevo objeto dataframe que contenga resultados de estadísticas.

Una gran ventaja de summarise() es la posibilidad de devolver resúmenes estadísticos más personalizados como median(), mean(), max(), min(), sd() (desviación estándar), percentiles y el número/porcentaje de filas que cumplen determinados criterios lógicos.

Resumen de estadísticas

Prueba este comando. Debería producir exactamente la misma tabla que hiciste con count().

vig %>% 
  group_by(hospital) %>% 
  summarise(n_casos = n())      # Cantidad de filas por grupo

Puede que te preguntes para que escribir el comando de esta forma, cuando podría escribir simplemente count()? La respuesta es que puedes añadir más líneas dentro de summarise() que crearán nuevas columnas del resumen calculadas.

Observa a continuación cómo se amplía el comando para crear 3 columnas de resumen, cada una con sus respectivos cálculos.

vig %>% 
  group_by(hospital) %>% 
  summarise(
    n_casos  = n(),                           # Cantidad de filas por grupo
    media_edad = mean(edad_anios, na.rm = T),    # Promedio de edad por grupo
    max_sintomas = max(fecha_sintomas, na.rm = T)   # Última fecha de inicio de sintomas por grupo
)

En las dos líneas que crean las variables o columnas media_edad y max_sintomas, se utilizan las funciones matemáticas mean() y max(). En estas funciones, el primer argumento es la columna de la base de datos original que se está resumiendo (por ejemplo edad_anios). A esto le sigue cualquier otro argumento relevante (por ejemplo na.rm = TRUE para la mayoría de las funciones matemáticas).

El argumento na.rm = TRUE

Hemos mencionado antes cómo en la mayoría de las funciones matemáticas debes incluir el argumento na.rm = TRUE. Esto se debe a que R quiere avisarte de cualquier valor que falte en el cálculo, y devolverá NA si hay alguno. Configurar na.rm = TRUE (NA "eliminar") desactiva este comportamiento.

Como experimento, vuelve a ejecutar temporalmente tu código anterior pero sin na.rm = T en el campo max_sintomas cálculo. ¿Qué cambia? ¿Ves cómo influye en el resultado el hecho de que falten valores de fecha_sintomas dentro del max() cálculo?

Criterios lógicos utilizando sum()

De forma similar a como utilizamos max() y mean() en summarise() puedes utilizar sum() para devolver el número de filas por grupo que cumplen criterios lógicos. La fila del base de datos original se "cuenta" si esta expresión lógica se evalúa como TRUE para esa fila. Por ejemplo

vig %>% 
  group_by(hospital) %>% 
  summarise(num_adultos = sum(edad_anios >= 18, na.rm = T))

Otros ejemplos son:

Observa el uso de na.rm = TRUE en sum() por las razones descritas anteriormente.

Observa también la diferencia entre sum() y summarise():

r fontawesome::fa("check", fill = "red")Haz clic para ver la solución (¡pruébalo tú primero!)

vig %>% 
  group_by(hospital) %>% 
  summarise(fem = sum(sexo == "mujer", na.rm = TRUE))

Puede que esto te resulte más claro de leer, escrito como

vig %>% 
  group_by(hospital) %>% 
  summarise(
    fem = sum(      # nombre de columna
      sexo == "mujer",   # columna usada para calcular la sumatoria
      na.rm = TRUE))     # remueve NAs para un calculo preciso


¿Qué cambiarías en el cálculo para resumir el número de casos masculinos?

¿Qué cambiarías para resumir el número de casos con sexo desconocido/ausente? Consejo: implica utilizar la función is.na()

Porcentajes

Cuando empieces a calcular estas columnas o variables, puede que quieras dividir una por otra, para crear un una proporción.

Una vez que hayas definido una columna o variable dentro de summarise() puedes hacer referencia a ella al final dentro del comando summarise(). Por ejemplo:

1) Calculas el número total de casos por hospital, y lo llamas n_casos 2) Calcula el número de casos masculinos por hospital, y llámalo hombres 3) Calcula la proporción de varones utilizando los dos

vig %>% 
  group_by(hospital) %>%                        # agrupar por hospital
  summarise(
    n_casos   = n(),                            # numero de casos por grupo
    hombres     = sum(sexo == "hombre", na.rm=T),    # conteo de masculinos por grupos
    hombres_pct = hombres / n_casos                 # porcentaje hombres del total, usando las dos columnas previamente creadas
  )

¿Qué aspecto tiene este porcentaje? ¿Es realmente un porcentaje? ¿O más bien una proporción (decimal)?

Hay una función que puedes utilizar para transformar rápidamente esta proporción en un porcentaje: es la función percent() del paquete {scales}.

Prueba ahora

vig %>% 
  group_by(hospital) %>%                      # agrupar por hospital
  summarise(
    n_casos   = n(),                          # numero de casos
    hombres     = sum(sexo == "hombre", na.rm=T),  # conteo de casos masculinos
    hombres_pct = percent(hombres / n_casos)      # porcentaje de casos masculinos
  )

Asegúrate de que tienes el paquete {scales} cargado (escrito en tu comando pacman al inicio del script).

Para más detalles, consulta el capítulo sobre Tablas descriptivas en el Manual de Epi.

Redondeo

Si necesitas redondear un número producido por summarise() envuélvelo en la función de R {base} round() y utiliza el argumento digits = para ajustar el número de decimales.

Prueba a ejecutar este código, con y sin el argumento función round()

vig %>% 
  group_by(hospital) %>% 
  summarise(
    media_edad = round(mean(edad_anios, na.rm = T), digits = 0)
  )

Al eliminar round() no olvides eliminar también la coma y digits = 0 que son el segundo argumento de esa función.

Desanidar las funciones y escribir el comando "más largo" con sangrías estratégicas puede ayudar a algunos programadores a entender qué argumentos pertenecen a cada función. Esto puede ser una preferencia personal.

vig %>% 
  group_by(hospital) %>% 
  summarise(
    media_edad = round(
      mean(
        edad_anios,
        na.rm = T),
      digits = 0
      )
  )

Estadísticas condicionales

Una de las opciones al momento de hacer un cálculo con la función de summarise() que puedes utilizar son los corchetes de subconjunto [ ]. Estos símbolos se pueden utilizar después de una columna o variable para aplicar un filtro según los criterios lógicos que escribas dentro.

Por ejemplo, colocado dentro de summarise() la declaración max_temp_fvr = max(temp[fiebre == "si"], na.rm = T) devolverá la temperatura máxima registrada en el grupo, pero sólo entre los casos que declararon tener fiebre.

Es un comando complicado: pregunta a tu facilitador si no lo entiendes.

Ejercicio final

Crea un objeto dataframe que resuma lo siguiente, para cada hospital:

Asegúrate de escribir este comando en tu script R, y ajusta tu comando para guardar este marco de datos en tu Entorno R como hospital_table para guardarlo más adelante.

r fontawesome::fa("check", fill = "red")Haz clic para ver la solución (¡pruébalo tú primero!)

hospital_table <- vig %>% 
     group_by(hospital) %>%                                     # Obtener los resultados por cada hospital
     summarise(
          n_casos   = n(),                                         # número de filas (casos)
          max_sintomas = max(fecha_sintomas, na.rm = T),                  # última fecha de aparición del caso
          menos_5    = sum(edad_anios <= 5, na.rm = T),              # número de niños menos de 5
          vomit_n   = sum(vomito == "si", na.rm=T),                # número de casos con vómitos
          vomit_pct = percent(vomit_n / n_casos),                  # porciento de casos con vómitos
          max_peso_hombres = max(peso_kg[sexo == "hombre"], na.rm = T)) # peso máximo para los hombres


Al hacer una tabla resumen compleja con {dplyr} asegúrate de leer el capítulo Manual de Epi ya que hay detalles que no hemos tenido tiempo de cubrir aquí.

¡Limpiemos ahora nuestro código! Mantén sólo una versión del hospital_table {dplyr}-en tu sección "Tablas resumen". Elimina el código {dplyr} de otras prácticas o muévelo al "Área de pruebas".

Tu sección "Tablas resumen" puede tener este aspecto:

# Tablas resumen ----------------------------------------------

## Tablas con {janitor} ----------------------------------------

vig %>% 
  tabyl(distrito, hospital)


vig %>% 
  tabyl(distrito) %>% 
  arrange(desc(n)) %>% 
  adorn_totals() %>% 
  adorn_pct_formatting()


## Tablas con {dplyr}------------------------------------------

hospital_table <- vig %>% 
     group_by(hospital) %>%                                     # Obtener los resultados por cada hospital
     summarise(
          n_casos   = n(),                                         # número de filas (casos)
          max_sintomas = max(fecha_sintomas, na.rm = T),                  # última fecha de aparición del caso
          menos_5    = sum(edad_anios <= 5, na.rm = T),              # número de niños menos de 5
          vomit_n   = sum(vomito == "si", na.rm=T),                # número de casos con vómitos
          vomit_pct = percent(vomit_n / n_casos),                  # porciento de casos con vómitos
          max_peso_hombres = max(peso_kg[sexo == "hombre"], na.rm = T))     # peso máximo para los hombres

¡Fíjate en lo clara y limpia que parece la sección!

{flextable}

Con el paquete {flextable} puedes convertir un objeto dataframe o exportarlo en una tabla con una mejor estética en formato HTML.

Esto es útil si creas una tabla con {janitor} o {dplyr} pero quieres incluirla en un informe o imprimirla como imagen PNG.

qflextable()

El comando más sencillo de {flextable} es qflextable() que significa "tabla flexible rápida". Convertirá una tabla en una imagen HTML, tras realizar algunos ajustes rápidos en relación de visualización y formato. Busca su aparición en el panel Visor de RStudio.

Añade el siguiente código a la sección "Tablas con dplyr" de tu código:

vig %>% 
     group_by(hospital) %>%                                     # Obtener los resultados por cada hospital
     summarise(
          n_casos   = n(),                                         # número de filas (casos)
          max_sintomas = max(fecha_sintomas, na.rm = T),                  # última fecha de aparición del caso
          menos_5    = sum(edad_anios <= 5, na.rm = T),              # número de niños menos de 5
          vomit_n   = sum(vomito == "si", na.rm=T),                # número de casos con vómitos
          vomit_pct = percent(vomit_n / n_casos),                  # porciento de casos con vómitos
          max_peso_hombres = max(peso_kg[sexo == "hombre"], na.rm = T)     # peso máximo para los hombres
  ) %>% 
  qflextable()

Revisa en el panel Visor de RStudio.

Prueba a hacer lo mismo con una das las tablas que creaste con las funciones del paquete {janitor} (por ejemplo, añade qflextable() al final con un operador pipe).

Hay muchos ajustes que puedes hacer en un objeto tabla flextable. Consulta el Capítulo Tablas de presentación del Manual de Epi R.

Aquí tienes un ejemplo de una tabla diferente, utilizada en nuestro Manual de Epi R:

knitr::include_graphics("images/flextable.png", error = F)

La sección Extras de este ejercicio contiene más práctica sobre el uso del paquete {flextable}.

Guardar una imagen

Una vez convertida tu tabla en objeto flextable puedes exportarla a Word, PowerPoint o HTML o como un archivo de imagen (PNG).

Para ello:

1) Guarda la tabla como un objeto con nombre (por ejemplo hospital_table) utilizando el operador de asignación <-

hospital_table <- vig %>% 
     group_by(hospital) %>%                                     # Obtener los resultados por cada hospital
     summarise(
          n_casos   = n(),                                         # número de filas (casos)
          max_sintomas = max(fecha_sintomas, na.rm = T),                  # última fecha de aparición del caso
          menos_5    = sum(edad_anios <= 5, na.rm = T),              # número de niños menos de 5
          vomit_n   = sum(vomito == "si", na.rm=T),                # número de casos con vómitos
          vomit_pct = percent(vomit_n / n_casos),                  # porciento de casos con vómitos
          max_peso_hombres = max(peso_kg[sexo == "hombre"], na.rm = T)) %>%     # peso máximo para los hombres
  qflextable()

Ten en cuenta que se trata de un comando de guardar no un comando presentar. Por lo tanto, para ver esta nueva tabla en el panel Visor, tienes que ejecutar el comando hospital_table.

2) En un comando separado escribe uno de las funciones del paquete {flextable} para exportar. No necesitas conectar los comandos de las tablas con estas funciones, son independientes.

Dentro del paréntesis de la función, indica primero el nombre del objeto flextable que has guardado (por ejemplo my_table). A continuación, introduce el argumento path = y proporciona el nombre de archivo deseado, entre comillas, en el que quieras guardar (incluyendo la extensión del archivo), por ejemplo:

# Guarda la el objeto tabla como un documento de word  "hospital_table.docx"
save_as_docx(hospital_table, path = "hospital_table.docx")

Como en el ejemplo anterior no se han especificado subcarpetas, el archivo se guardará en la carpeta raíz/principal de tu proyecto RStudio, la carpeta "ebola.

¿Qué debemos hacer para guardar la tabla en la subcarpeta "resultados" de la carpeta "ebola"? Recuerda el uso de la función here()

Actualiza tu comando de exportación hospital_table para que se guarde en la carpeta correcta:

# Guarda la el objeto tabla como un documento de word  "hospital_table.docx"
save_as_docx(hospital_table, path = here("resultados", "hospital_table.docx"))

Para guardar la tabla flextable como imagen PNG, necesitarás instalar Phantom JS (gratuito) para el uso de la función save_as_image() Puedes hacerlo instalando el paquete {webshot} (agregalo en el comando pacman), y luego ejecutando el comando webshot::install_phantomjs(). Considera la posibilidad de hacerlo después de terminar este ejercicio.

# Guarda la tabla como  hospital_table.png en la carpeta "resultados" dentro de la carpeta de tu proyecto
save_as_image(my_table, path = here("resultados", "hospital_table.png"))

funciones del paquete {gtsummary}

Ahora vamos a comenzar a aprender a usar la función tbl_summary() de {gtsummary} para hacer tablas presentables

Este paquete nos permite crear tablas listas para publicar con un código muy sencillo y breve. De hecho, tú puedes hacer ajustes más complejos y detallados en estas tablas, pero también es fácil obtener una tabla con mucha estética y con muy poco código.

Seleccionar columnas o variables

La diferencia con los métodos anteriores es que primero debes aplicar la función select() para seleccionar las columnas o varibles que quieres resumir en una tabla.

aplica al objeto vig con un operador pipe la función select() y escoge dos columnas y y luego con otro operador pipe conecta la función tbl_summary()

¿Qué se produjo para la(s) columna(s) categórica(s)?

¿Qué se produjo para la(s) columna(s) continua(s)?

Tablas estratificadas

También puedes añadir el parámetro by = en la función tbl_summary() y designar una columna para estratificar la tabla (en columnas). No olvides incluir esta columna en el select() ¡El comando anterior!

Intenta hacer una tabla que evalúe distrito, edad_cat, todas las columnas de síntomas, peso y altura

Ahora haz la misma tabla, estratificada por sexo.

r fontawesome::fa("check", fill = "red")Haz clic para ver la solución (¡pruébalo tú primero!)

vig %>% 
  select(distrito, edad_cat, fiebre, escalofrios, tos, dolor, vomito, peso_kg, alt_cm, sexo) %>% 
  tbl_summary(by = sexo)


Cambiar a formato flextable

Una nota importante con {gtsummary} es que no puedes exportar directamente la tabla a un documento Word. Para ello, conecta con un operador pipe la tabla con la función as_flex_table() de {flextable}. Esto permite que {gtsummary} transforme la tabla a un objeto flextable y poder ser exportado a un documento Word.

gt_a_flex <- vig %>% 
  select(distrito, edad_cat, fiebre, escalofrios, tos, dolor, vomito, peso_kg, alt_cm, sexo) %>% 
  tbl_summary(by = sexo) %>% 
  as_flex_table()

save_as_docx(gt_a_flex, "mi_tabla.docx")

Añadiendo valores p

{gtsummary} facilita la realización de pruebas estadísticas. Por ejemplo, la función add_p() se puede añadir si tienes la configuración adecuada de la tabla.

Lee más sobre las muchas formas de personalizar una tabla con {gtsummary} aquí.

Fin

¡Felicidades! ¡Has terminado el módulo sobre tablas resumen!

Extras

Más diversión con {flextable}

{flextable} es una herramienta muy útil para crear tablas listas para ser presentadas.

Empieza creando un nuevo script, llamado "ebola_flextable.R".

Ésta es la tabla que pretendemos crear:

knitr::include_graphics("images/table_final.png", error = F)

Introducción

Al igual que con el script "ebola_analyses.R", escribe alguna información descriptiva en la parte superior del script.

#############################################
# Ejemplo de Ebola con Flextable
# Seccion Extra
# Tu NOMBRE aqui
#############################################

Paquetes

A continuación, añade una sección y carga estos paquetes utilizando pacman::p_load():

r fontawesome::fa("check", fill = "red")Haz clic para ver una solución (¡pruébalo tú primero!)

# Load packages ----------------------------------------------------------------
pacman::p_load(
  rio,            # para importar/exportar
  here,           # para especificar rutas de archivos
  flextable,      # para crear tablas HTML
  scales,         # para funciones de ayuda con flextable
  officer,        # para funciones de ayuda para tablas con formato
  tidyverse)      # para la gestión, resumen y visualización de datos


Importación de datos

A continuación, crea una sección de importación e importa listado de casos de ébola depurada, que debes de tener guardadado en tu carpeta "ebola/datos/limpios" como "listado_vigilancia_limpio_20141201.rds". Como copia de seguridad, puedes utilizar la versión de la carpeta "ebola/datos/limpios/copia_seguridad".

r fontawesome::fa("check", fill = "red")Haz clic para ver una solución (¡pruébalo tú primero!)

# Importando los datos -------------------------------------------------------------

# El listado guardado creado por ti
vig <- import(here("datos", "limpios", "listado_vigilancia_limpio_20141201.rds"))    

# La versión de copia de seguridad
vig <- import(here("datos", "limpios", "copia_seguridad", "listado_vigilancia_limpio_20141201.rds"))    


Estilo de borde

A continuación, define el estilo del borde de la tabla en formato flextable mediante la función fp_border() del paquete {officer}. Utiliza la función para especificar el color del borde de la tabla (negro) y el ancho de la línea del borde (fijado en 1). Siéntete libre de jugar con esta función más adelante.

# define el estilo del borde de la tabla -----------------------------------------------------
border_style = officer::fp_border(color="black", width=1)

Creación de la tabla

Ahora empieza a crear tu tabla, con cambios paso a paso.

Primero, crea una tabla utilizando group_by() y summarise() que agrupe las filas del listado por hospital. Crea columnas de resumen que incluyan

r fontawesome::fa("lightbulb", fill = "gold") Haz clic para leer una sugerencia

Al objeto dataframe del listado aplica la función group_by() con un operador pipe y especifica que se agrupe por la columna hospital.

Luego con otro operador pipe conecta la función summarise() y dentro de ésta, especifica tres nuevas columna:

  • Para obtener el número de filas por grupo puedes utilizar n()
  • Para obtener el número los casos que vomitaron, utiliza sum() para contar los casos en los que vomito == "si" (!no olvides na.rm = TRUE!
  • Para obtener el porcentaje de casos que vomitaron, divide los casos con vómito por el número de casos.



r fontawesome::fa("check", fill = "red")Haz clic para ver la solución (¡pruébalo tú primero!)

# Crea la tabla -----------------------------------------------------------
vig %>% 
  group_by(hospital) %>%                           # obten la estadística de cada hospital
  summarise(
    n_casos   = n(),                               # numero de filas (casos)
    vomit_n   = sum(vomito == "si", na.rm=T),      # total de casos con vomito
    vomit_pct = percent(vomit_n / n_casos))        # proporción de casos con vomito


Incluir valores "perdidos"

En este curso hemos explicado que los valores que faltan se registran más adecuadamente en R como NA. Esto es cierto, pero ahora que estamos preparando el análisis para su presentación, podemos transformar estos valores en palabras más apropiadas. Aún así, este cambio debería producirse sólo en la tabla de salida, no en la base datos subyacente.

Añade la función mutate() antes group_by() y transforma la columna hospital con la función fct_na_value_to_level() del paquete {forcats} , añadiendo el valor explícito como "Desconocido".

Por último, canaliza la tabla con la función qflextable() y luego guarda la tabla como un objeto llamado vom_table.

r fontawesome::fa("lightbulb", fill = "gold") Haz clic para leer una sugerencia

Intenta buscar en la documentación de Ayuda la función fct_na_value_to_level()

r fontawesome::fa("check", fill = "red")Haz clic para ver una solución (¡pruébalo tú primero!)

# Create table -----------------------------------------------------------
vom_table <- vig %>% 
  mutate(hospital = fct_na_value_to_level(hospital, "Desconocido")) %>% # cambiar los valores NA a otro nombre
    group_by(hospital) %>%                         # obten la estadistica de cada hospital
  summarise(
    n_casos   = n(),                               # numero de filas (casos)
    vomit_n   = sum(vomito == "si", na.rm=T),      # total de casos con vomito
    vomit_pct = percent(vomit_n / n_casos)) %>%    # proporción de casos con vomito

  # convert to flextable
  qflextable()


Recuerda, cuando asignas y guardas un objeto con el operador de asignación <- para poder presentarlo o verlo debes de ejecutarlo (ejemplo escribir en la consola vom_table y presionar Enter) para ver la tabla actualizada `.

Encabezados de la tabla

¡Empecemos a dar formato la tabla para mejor presentación!

Primero, utiliza la función {flextable} función add_header_row() para añadir una fila de títulos sobre la fila actual. Aquí tienes el código que debes añadir a tu comando (con una tubería).

  add_header_row(
    top = TRUE,                           # un nuevo encabezado encima del encabezado anterior
    values = c("Hospital",                # valores para un nuevo encabezado
               "Total casos", 
               "Casos con vomito"),    # Este encabezado tendrá un ancho de dos columnas
    colwidths = c(1,1,2))                 # abacar las 4 columnas

Esta sección establece tres nuevos valores de encabezado (el texto) y cuántas de las cuatro columnas subyacentes debe abarcar cada uno. El último valor de encabezado debe abarcar dos columnas subyacentes.

r fontawesome::fa("exclamation", fill = "red") ¡Alerta! si añades una fila de encabezado con menos nombres de columna que el número de columnas subyacentes, verás un error como éste:

Error en inverse.rle(structure(list(lengths = colwidths, values = valores), : estructura 'rle' no válida

Ejecuta el comando vom_table y observa los cambios en tu visor de RStudio. Deberías ver el segundo encabezado y cómo los "Casos con vómitos" abarcan las dos columnas de la derecha.

Hagamos algunos ajustes manuales en los valores de encabezado. Pasa la tabla a este código:

  # Renombra los encabezados secundarios
  set_header_labels(n_casos   = "No.",                                         
                    vomit_n   = "No.",                           
                    vomit_pct = "Porcentaje del total")

Ejecuta el comando vom_table y observa los cambios en tu visor de RStudio.

Tu comando para la tabla anterior debería tener este aspecto

r fontawesome::fa("check", fill = "red")Haz clic para ver la solución (¡pruébalo tú primero!)

# Crear tabla -----------------------------------------------------------
vom_table <- vig %>% 

  mutate(hospital = fct_na_value_to_level(hospital, "Desconocido")) %>% # cambiar los valores NA a otro nombre

  group_by(hospital) %>%                                     # obtén la estadística de cada hospital
  summarise(
  n_casos   = n(),                               # numero de filas (casos)
    vomit_n   = sum(vomito == "si", na.rm=T),      # total de casos con vómito
    vomit_pct = percent(vomit_n / n_casos)) %>%    # proporción de casos con vómito

  # convierte a formato flextable
  qflextable() %>% 

  add_header_row(
    top = TRUE,                           # un nuevo encabezado encima del encabezado anterior
    values = c("Hospital",                # valores para un nuevo encabezado
               "Total casos", 
               "Casos con vómito"),    # Este encabezado tendrá un ancho de dos columnas
    colwidths = c(1,1,2))  %>%                # span the 4 columns

  # Re-label secondary headers
  set_header_labels(n_casos   = "No.",                                         
                    vomit_n   = "No.",                           
                    vomit_pct = "Porcentaje del total")


Formatear filas y columnas

A continuación, ajustaremos la alineación de los valores en las celdas y combinaremos algunos de los valores.

Para realizar ediciones específicas de formato en diferentes áreas (filas, columnas o celdas debemos comprender la sintaxis de localización de {flextable}. Muchas de las funciones de este paquete utilizan la siguiente sintaxis:

function(table, part = "", i = , j = )

Vamos a entenderla:

Puedes encontrar la lista completa de la función de formato de la tabla flextable aquí o revisar la documentación entrando en ?flextable. También puedes ver otro ejemplo trabajado en Manual de Epi R.

Introduce tu tabla en el siguiente código:

  # Centra y une celdas
  align(part = "all",
        align = "center",
        j = c(2:4)) %>%           # centraliza los valores en desde la segunda columna hasta la cuarta

  merge_at(i = 1:2,
           j = 1,
           part = "header")       # Une de forma vertical el título "Hospital"

Ejecuta el comando vom_table y observa los cambios en tu visor de RStudio.

Tus comandos de la tabla actualizada debería tener este aspecto

r fontawesome::fa("check", fill = "red")Haz clic para ver una solución (¡pruébalo tú primero!)

# Crea  una tabla -----------------------------------------------------------
vom_table <- vig %>% 

  mutate(hospital = fct_na_value_to_level(hospital, "Desconocido")) %>% # cambiar los valores NA a otro nombre

 group_by(hospital) %>%                                     # obten la estadistica de cada hospital
  summarise(
  n_casos   = n(),                               # numero de filas (casos)
    vomit_n   = sum(vomito == "si", na.rm=T),      # total de casos con vomito
    vomit_pct = percent(vomit_n / n_casos)) %>%    # proporción de casos con vomito

  # convierte a formato flextable
  qflextable() %>% 

  add_header_row(
    top = TRUE,                             # Nuevo encabezado encima del encabezado actual
    colwidths = c(1,1,2),                   # Extender el encabezado en el ancho de 4 columnas
    values = c("Hospital",                  # Valores para los nuevos encabezados
               "Total de casos", 
               "Cases con vómito")) %>%  # Esto expande el titulo al ancho de dos columnas

  # Nuevos nombres para los encabezados secundarios (de las columnas)
  set_header_labels(n_casos   = "No.",                                         
                    vomit_n   = "No.",                           
                    vomit_pct = "Porciento del total") %>% 

 # Alinea y une celdas
  align(part = "all",
        align = "center",
        j = c(2:4)) %>%           # centraliza los valores en desde la segunda columna hasta la cuarta

  merge_at(i = 1:2,
           j = 1,
           part = "header")       # Une de forma vertical el titulo "Hospital"


Ultimos ajustes

Por último, ajustaremos la fuente y el color de fondo de la tabla.

Añade el código siguiente a tu cadena comandos.

  # Cambia el color de fondo para columnas especificas
  bg(part = "body",         # Cuerpo de la tabla (excluyendo el cuerpo)
     j = 4,                 # 4ta columna
     bg = "gray95") %>%     # Color Gris

  # Cambia el color condicionalmente
  bg(j = 4,                 # 4ta columna
     i = ~ vomit_pct >= 55, # El ~muestra que que filas seran afectadas por la condicion declarada
     part = "body",         # parte de la tabla que será actualizada (el cuerpo por ejemplo- no el encabezado)
     bg = "orange") %>%     # El color del fondo

  # Resaltar el encabezado 
  bold(i = 1,           # la primera fila del encabezado
       bold = TRUE,
       part = "header") 

Filas de título y pie de página

La opción más sencilla es utilizar add_footer_lines() para añadir una celda grande que abarque la anchura de toda la tabla.

Añadir referencias en la tabla (con números en superíndice y referencias a pie de página) puede ser más complicado.

Añade una fila de pie de página a tu tabla que explique la fuente de datos y la lógica que hay detrás del coloreado condicional. Puntos extra si la creas con str_glue() para que parte del texto sea dinámico, ¡y ponlo en cursiva!

A continuación, utiliza la función inversa add_header_lines() para dar un título a tu tabla. ¡Puntos extra si luego alineas este título con el centro!

r fontawesome::fa("check", fill = "red")Haz clic para ver una solución (¡pruébalo tú primero!)

Introduce tu tabla en este código de la siguiente manera

  #añadir pie de pagina
  add_footer_lines(values = str_glue(
    "Casos notificados al {max_report_date}. Hospitales con >=55% con vómito en destacado.",
    max_report_date = max(vig$fecha_notifica, na.rm = T))) %>% 
  italic(part = "footer") %>% 

  # añadir titulo
  add_header_lines(values = "Casos con Vómito por hospital") %>% 
  align(part = "header", i = 1, align = "center")


Tu comando de tabla completa debería tener este aspecto

r fontawesome::fa("check", fill = "red")Haz clic para ver una solución (¡pruébalo tú primero!)

# Create table -----------------------------------------------------------
# Crea  una tabla -----------------------------------------------------------
vom_table <- vig %>% 

  mutate(hospital = fct_na_value_to_level(hospital, "Desconocido")) %>% # cambiar los valores NA a otro nombre

 group_by(hospital) %>%                                     # obten la estadistica de cada hospital
  summarise(
  n_casos   = n(),                               # numero de filas (casos)
    vomit_n   = sum(vomito == "si", na.rm=T),      # total de casos con vomito
    vomit_pct = percent(vomit_n / n_casos)) %>%    # proporción de casos con vomito

  # convierte a formato flextable
  qflextable() %>% 

  add_header_row(
    top = TRUE,                             # Nuevo encabezado encima del encabezado actual
    colwidths = c(1,1,2 ),                   # Extender el encabezado en el ancho de 4 columnas
    values = c("Hospital",                  # Valores para los nuevos encabezados
               "Total de casos", 
               "Cases con vómito")) %>%  # Esto expande el titulo al ancho de dos columnas

  # Nuevos nombres para los encabezados secundarios (de las columnas)
  set_header_labels(n_casos   = "No.",                                         
                    vomit_n   = "No.",                           
                    vomit_pct = "Porciento del total") %>% 

 # Centra y une celdas
  align(part = "all",
        align = "center",
        j = c(2:4)) %>%           # centraliza los valores en desde la segunda columna hasta la cuarta

  merge_at(i = 1:2,
           j = 1,
           part = "header") %>%       # Une de forma vertical el titulo "Hospital"
     # Cambia el color de fondo para columnas específicas 
    bg(part = "body",                 # Cuerpo de la tabla (excluyendo el cuerpo)
     j = 4,                           # 4ta columna
     bg = "gray95") %>%               # Color Gris

  # Cambia el color condicionalmente
  bg(j = 4,                 # 4ta columna
     i = ~ vomit_pct >= 55, # El ~muestra que que filas seran afectadas por la condicion declarada
     part = "body",         # parte de la tabla que sera actualizada (el cuerpo por ejemplo- no el encabezado)
     bg = "orange") %>%     # El color del fondo

  # Resaltar el encabezado 
  bold(i = 1,           # la primera fila del encabezado
       bold = TRUE,
       part = "header") %>%  

  #añadir pie de pagina
  add_footer_lines(values = str_glue(
    "Casos notificados al {max_report_date}. Hospitales con >=55% con vómito en destacado.",
    max_report_date = max(vig$fecha_notifica, na.rm = T))) %>% 
  italic(part = "footer") %>% 

  # añadir titulo
  add_header_lines(values = "Casos con Vomito por hospital") %>% 
  align(part = "header", i = 1, align = "center")


Guarda la tabla

Por último, guarda la tabla en un documento de Word, Powerpoint o la salida que prefieras. Recuerda que este comando no debe estar conectado por un operdador pipe al comando de creación de la tabla.

Este código debería funcionar para guardarla como Word en la carpeta "resultados" del proyecto RStudio.

# Save
save_as_docx(vom_table, path = here("resultados", "vom_table.docx"))


appliedepi/introexercises documentation built on April 22, 2024, 1:01 a.m.