Comunicación de Resultados: Shiny
Análisis Exploratorio de Datos | Licenciatura en Estadística | FCEyE | UNR
Introducción a Shiny
¿Qué es Shiny?
Shiny (W. Chang et al, 2021) es un paquete de R que permite construir aplicaciones web directamente desde RStudio.
Una característica importante de las aplicaciones web creadas mediante Shiny es que son dinámicas e interactivas. Esto implica que los usuarios pueden decidir qué datos ver, cómo verlos, y jugar con ellos.
Shiny puede instalarse como cualquier otro paquete de R:
install.packages("shiny")Usos comunes de Shiny
Actualmente Shiny es utilizado con una gran variedad de propósitos, en diversos ámbitos de aplicación. Algunos ejemplos son:
Empresas que desarrollan tableros de información (dashboards) para analizar en tiempo real cientos de indicadores, métricas y gráficas relativas a la situación actual de la compañía, por ejemplo: tablero para analizar tráfico aéreo según mes y aerolínea. En el mercado actual existen alternativas a Shiny, como por ejemplo Power BI o Tableau; si bien la preferencia entre uno u otro software dependerá del contexto, una búsqueda rápida en Google (‘shiny vs power bi’ o bien ‘shiny vs tableau’) nos demuestra que la discusión sigue abierta.
Organismos públicos que buscan digitalizar grandes volúmenes de información, evitando de esa manera recurrir a la impresión de miles de hojas de papel. Para poder comunicar resultados de manera eficiente, es necesario que el medio a través del cual circula la información sea flexible y de fácil acceso; por ejemplo, compartir los resultados de un Censo de Población y Hogares de manera física requiere editar varios libros con cientos de tablas y cuadros desagregados según múltiples variables (provincia, género, edad, condición de empleo, etc.). Mediante una Shiny app podemos almacenar toda esta información en un solo lugar, ahorrando así costos de impresión, envío, etc. Ejemplo: evolución de la cantidad de estudiantes, ingresantes y egresados/as en carreras de grado de la UNR. Otros ejemplos: https://appsilon.com/r-shiny-in-government-examples/.
Docentes que desarrollan apps interactivas con el objetivo de facilitar la enseñanza de conceptos mediante visualizaciones apropiadas. Ejemplo: visualización de distribuciones muestrales para la media, variancia y proporción.
Organizaciones que desean comunicar resultados de estudios o investigaciones de manera dinámica y actualizada en tiempo real. Algunos ejemplos son: seguimiento del avance de la pandemia de COVID-19 mediante referenciación geográfica, análisis de métricas relacionadas a deportistas, aplicaciones que permiten importar datasets sobre los cuales se ajustan modelos predefinidos, y un largo etcétera. Ejemplos: app para monitoreo de casos de COVID-19 y app para visualizar en vivo los tweets más relevantes con cierto hashtag.
Personas que arman aplicaciones sólo por diversión: nube de palabras de obras de Shakespeare, juego de la memoria con stickers de paquetes de R.
Para una muestra más completa de las posibilidades que ofrece Shiny, recomendamos visitar la galería de apps seleccionadas por RStudio y las apps ganadoras de la 3° edición del concurso Shiny Contest.
Además, en la página web oficial de Shiny existen numerosos ejemplos, machetes y tutoriales altamente recomendables para aquellas personas que estén dando sus primeros pasos con esta herramienta.
Aquellas personas interesadas en profundizar sus conocimientos sobre Shiny pueden consultar el muy recomendable libro Mastering Shiny (2021) escrito por Hadley Wickham, desarrollador del tidyverse y referente de Posit (empresa anteriormente conocida como RStudio). El libro se encuentra disponible para ser leído de manera gratuita en este link, y además, el código utilizado para generarlo puede consultarse en este repositorio público de GitHub.
Estructura de una Shiny App
A lo largo de este curso usaremos el término Shiny App para referirnos al conjunto de sentencias que generan la aplicación. Una Shiny App debe guardarse en un script de R, al cual llamaremos app.R.
Este archivo de sentencias estará conformado por 3 secciones bien diferenciadas:
- Interfaz del usuario (user interface)
- Función servidor (server function)
- Publicación de la app (app deployment)
En versiones antiguas de Shiny era necesario crear la interfaz y el servidor en archivos separados (
ui.Ryserver.Rrespectivamente), aunque hoy en día esto es optativo.Llamar a nuestro archivo de sentencias
app.Rno surge por un capricho, sino que es el nombre requerido por algunos servidores para poder ejecutar la aplicación de manera correcta cuando se encuentra disponible online. Si estamos trabajando con Shiny apps de manera local (en nuestra PC, sin publicación en web) podemos nombrar al script como más nos guste.Usaremos la notación
NombrePaquete::NombreFunción()para especificar explícitamente la función que estamos utilizando. Esta es una práctica recomendada para escribir código en R que mejora la claridad, evita conflictos y facilita el mantenimiento del código a largo plazo.
Interfaz
- La interfaz del usuario (user interface o ui, por sus siglas en inglés) se encarga de controlar el aspecto de la página web. Muchos de los conceptos necesarios para desarrollar una interfaz están vinculados a lenguajes de programación como HTML, CSS o JavaScript. Afortunadamente, las funciones del paquete
bslibfacilitan en gran medida nuestro trabajo y no debemos preocuparnos (demasiado) por aprender a utilizar estos lenguajes.
bslibproporciona un moderno conjunto de herramientas de interfaz de usuario para Shiny basado en Bootstrap. A diferencia deshiny(que utiliza Bootstrap 3)bslibutiliza la versión más moderna de Bootstrap (actualmente Bootstrap 5)El paquete
bslibes una dependencia deshiny, es decir, se instala al momento de instalarshiny.
Servidor
En la sección server se escribe el código de R que le indica a la app qué debe hacer y cómo debe funcionar. Aquí se incluyen generalmente la manipulación de datos, el ajuste de modelos, el armado de gráficos, etc.
En algunas ocasiones será recomendable realizar estas tareas por fuera del servidor para reducir tiempos de ejecución (más adelante hablaremos de este tema en mayor profundidad).
La versión más simple del server es una función con dos argumentos:
- input: almacena elementos de entrada tales como opciones elegidas por los/as usuarios/as a través de la interfaz.
- output: almacena elementos de salida para mostrar en la app: valores numéricos, textos, tablas, gráficos, mapas o cualquier tipo de resultado que deseemos visualizar.
Ejecución
La parte final de la aplicación es un llamado a la función
shiny::shinyApp(), cuyos dos argumentos principales son ui y server, es decir, los dos elementos definidos anteriormente.Ejecutar esta función da como resultado el lanzamiento de la aplicación, la cual podremos utilizar dentro de RStudio o usando nuestro navegador preferido (Google Chrome, Mozilla Firefox, Microsoft Edge, etc.).
Para ver la app fuera de RStudio debemos elegir la opción Run External al momento de la ejecución, o bien copiar y pegar en un navegador la dirección URL que se muestra en la consola mientras la app está activa.
Es importante destacar que, al seguir estos pasos, la aplicación sólo funcionará mientras la sesión de RStudio desde la cual se lanzó esté vigente. Más adelante veremos cómo subir nuestra aplicación a la web para que cualquier persona con conexión a Internet pueda acceder a ella.
A continuación plantearemos algunos objetivos específicos y desarrollaremos un camino paso a paso para construir nuestra primera Shiny App.
Datos: canciones de Queen
La popular aplicación Spotify registra numerosas variables para cada una de las canciones disponibles en su servicio de streaming, las cuales tratan de cuantificar ciertas características musicales.
Por ejemplo, a cada canción se le miden conceptos algo abstractos como “energía”, “positividad”, “instrumentalidad”, etc., además de otros mejor definidos, como duración, volumen o nivel de popularidad.
Estos datos pueden descargarse usando R mediante el paquete Rspotify, el cual se conecta a la API de Spotify para acceder a la información de cada artista, álbum y canción presente en la plataforma.
Para este curso hemos seleccionado canciones pertenecientes a la emblemática banda de rock británica Queen, formada en Londres en 1970.
- La base se encuentra almacenada en el archivo de texto plano
queen.txt, el cual cuenta con 152 registros (canciones) y 14 variables:
| Variable | Descripción |
|---|---|
| id | Código de identificación de la canción. |
| name | Nombre de la canción. |
| album | Álbum donde aparece la canción. |
| popularity | Nivel de popularidad en Spotify. |
| danceability | Mide qué tan “bailable” es la canción de acuerdo a características musicales como tempo, estabilidad rítmica, fuerza del pulso, etc. |
| energy | Mide intensidad y actividad, por ejemplo, canciones de heavy metal poseen valores altos de energía. |
| loudness | Volumen promedio en decibeles. |
| speechiness | Mide el nivel de oralidad: una canción muy “hablada” posee valores altos de esta variable. |
| acousticness | Mide el nivel de acústica de la canción. |
| instrumentalness | Mide qué tan instrumental es la canción. |
| liveness | Detecta si la canción fue grabada en vivo (recitales), en cuyo caso recibe un valor alto de esta variable. |
| valence | Mide el nivel de positividad: canciones con valores altos suenan alegres y eufóricas, mientras que canciones con valores bajos suenan tristes y apagadas. |
| tempo | Tempo promedio (pulsos/minuto). |
| duration_ms | Duración de la canción en milisegundos. |
- Podemos cargar los datos en R y darles una mirada rápida:
Código
datos <- readr::read_delim("../data/unidad05/queen.txt", delim = "\t")
dplyr::glimpse(datos)Rows: 152
Columns: 14
$ id <chr> "6aNP9GlBi3VHPXl7w3Qjr9", "5RYLa5P4qweEAKq5U1gdcK", "…
$ name <chr> "'39", "A Kind Of Magic", "A Winter's Tale", "Action …
$ album <chr> "A Night At The Opera", "A Kind Of Magic", "Made In H…
$ popularity <dbl> 45, 72, 40, 33, 38, 33, 82, 39, 62, 37, 41, 82, 58, 3…
$ danceability <dbl> 0.524, 0.670, 0.286, 0.727, 0.491, 0.307, 0.933, 0.78…
$ energy <dbl> 0.5710, 0.7760, 0.5650, 0.4780, 0.0765, 0.7960, 0.528…
$ loudness <dbl> -9.686, -5.874, -6.368, -6.121, -14.570, -8.066, -6.4…
$ speechiness <dbl> 0.0273, 0.0356, 0.0320, 0.0403, 0.0416, 0.0950, 0.161…
$ acousticness <dbl> 0.02710, 0.01840, 0.17900, 0.05640, 0.78700, 0.23300,…
$ instrumentalness <dbl> 0.00e+00, 2.94e-03, 0.00e+00, 2.62e-04, 3.06e-04, 2.0…
$ liveness <dbl> 0.1110, 0.1280, 0.3210, 0.0801, 0.1140, 0.3130, 0.163…
$ valence <dbl> 0.3730, 0.7030, 0.1320, 0.9090, 0.3050, 0.2310, 0.754…
$ tempo <dbl> 101.626, 130.128, 106.219, 166.364, 113.728, 181.620,…
$ duration_ms <dbl> 210800, 264253, 230160, 214813, 189587, 261653, 21465…
ACP
Si bien la base de datos presentada se puede explorar de diversas maneras, nos enfocaremos en una meta puntual: aplicar la técnica de Componentes Principales para reducir la dimensionalidad de los datos y al mismo tiempo buscar canciones con patrones de comportamiento similares.
El Análisis de Componentes Principales (ACP) puede llevarse a cabo en R de diversas maneras; en este curso aplicaremos la función
PCA()del paqueteFactoMineR:
Código
cp <- FactoMineR::PCA(
# matriz de datos: elegimos variables numéricas
X = dplyr::select(datos, dplyr::where(is.numeric)),
ncp = 11, # cantidad de componentes a retener
graph = FALSE # no mostrar los gráficos
)En particular, vamos a focalizarnos en tres aspectos del ACP:
- Las cargas (loadings) asociadas a cada variable en cada una de las componentes halladas.
- El porcentaje de variancia explicada por cada componente.
- El gráfico de los individuos proyectados sobre algún par de componentes.
A continuación mostramos estos resultados para el análisis llevado a cabo sobre la base completa:
| Variable | CP1 | CP2 | CP3 | CP4 | CP5 | CP6 | CP7 | CP8 | CP9 | CP10 | CP11 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| popularity | -0.0014 | 0.3128 | -0.6539 | 0.3325 | 0.4226 | 0.0709 | 0.3410 | -0.1953 | -0.1586 | 0.0146 | 0.0253 |
| danceability | 0.3310 | 0.8096 | -0.0955 | 0.0361 | 0.0042 | 0.0845 | -0.0597 | 0.3049 | 0.2010 | -0.2735 | 0.0747 |
| energy | 0.8767 | -0.2090 | 0.1042 | 0.1470 | -0.0669 | -0.1900 | 0.0159 | -0.1216 | 0.0002 | 0.0587 | 0.3100 |
| loudness | 0.7752 | -0.0448 | -0.2446 | 0.0906 | -0.0940 | -0.2809 | -0.0321 | -0.3331 | 0.3076 | -0.0393 | -0.1819 |
| speechiness | 0.4748 | -0.2219 | 0.2357 | 0.2339 | 0.5911 | 0.2990 | -0.4177 | 0.0013 | 0.0022 | -0.0056 | -0.0415 |
| acousticness | -0.8110 | 0.0381 | 0.0688 | -0.0209 | 0.1369 | 0.2161 | 0.0341 | -0.2376 | 0.4340 | 0.1011 | 0.1180 |
| instrumentalness | -0.3579 | -0.0806 | 0.3576 | 0.5266 | 0.2639 | -0.5298 | 0.1715 | 0.2635 | 0.1003 | 0.0105 | -0.0296 |
| liveness | 0.3346 | -0.3571 | 0.1658 | 0.5137 | -0.2855 | 0.4909 | 0.3492 | 0.1219 | 0.0798 | -0.0304 | -0.0502 |
| valence | 0.4882 | 0.7064 | 0.3065 | -0.0845 | 0.0096 | 0.0674 | 0.1011 | 0.0731 | 0.0100 | 0.3708 | -0.0619 |
| tempo | 0.3286 | -0.1402 | 0.4271 | -0.6048 | 0.3357 | 0.0123 | 0.4257 | -0.0764 | 0.0174 | -0.1534 | -0.0187 |
| duration_ms | 0.2533 | -0.4793 | -0.5712 | -0.3235 | 0.1425 | -0.0041 | 0.0493 | 0.4474 | 0.1739 | 0.1485 | 0.0097 |
| CP | Autovalor | % Variancia | % Acumulado |
|---|---|---|---|
| 1 | 3.0130 | 27.3905 | 27.3905 |
| 2 | 1.7321 | 15.7463 | 43.1368 |
| 3 | 1.3256 | 12.0510 | 55.1878 |
| 4 | 1.2156 | 11.0505 | 66.2383 |
| 5 | 0.8443 | 7.6750 | 73.9133 |
| 6 | 0.7897 | 7.1792 | 81.0925 |
| 7 | 0.6421 | 5.8372 | 86.9298 |
| 8 | 0.6089 | 5.5355 | 92.4652 |
| 9 | 0.3957 | 3.5972 | 96.0625 |
| 10 | 0.2744 | 2.4945 | 98.5570 |
| 11 | 0.1587 | 1.4430 | 100.0000 |
Paso I: Preliminares
Nuestro objetivo consiste en desarrollar una app que permita al usuario/a seleccionar:
- Qué álbum (o álbumes) incluir en el análisis de CP.
- Cuáles componentes graficar.
Ahora que ya tenemos un objetivo planteado, llegó el momento de construir una Shiny App desde cero. Una recomendación importante es guardar cada app en un directorio único, donde no haya aplicaciones creadas previamente ni otros archivos innecesarios.
En consecuencia, para empezar a armar la aplicación, comenzamos por crear un nuevo directorio y guardar allí dos archivos:
app.R(por ahora vacío) con codificación UTF-8queen.txt
- Lo primero que podemos hacer dentro de
app.Res leer los datos, definir interfaz y servidor vacíos, y hacer un llamado a la funciónshiny::shinyApp():
# Importación de datos y ajuste CP
datos <- readr::read_delim("queen.txt", delim = "\t")
cp <- FactoMineR::PCA(
# matriz de datos: elegimos variables numericas
X = dplyr::select(datos, dplyr::where(is.numeric)),
ncp = 11, # cantidad de componentes a almacenar
graph = FALSE # no mostrar los graficos
)
# Esqueleto de la Web
MiInterfaz <- bslib::page_fluid()
MiServidor <- function(input, output) {}
# Lanzamiento de la app
shiny::shinyApp(ui = MiInterfaz, server = MiServidor)- Al detectar la función
shiny::shinyApp(), RStudio reconoce que nuestro archivo es una aplicación web y agregará la opción Run App en la barra de herramientas:
Paso II: Interfaz
El segundo paso consiste en definir el aspecto de la página web. Algunas de las opciones disponibles en
bslibsonbslib::page_fluid(),bslib::page_fixed(),bslib::page_fillable(),bslib::page_navbar()ybslib::page_sidebar(), todas con diferentes estructuras predefinidas.Comenzaremos utilizando
bslib::page_fluid(), la cual permite una gran flexibilidad a la hora de diseñar la aplicación. Dado que estamos dando nuestros primeros pasos con Shiny, vamos a enfocarnos en construir una página web simple, conformada sólo por dos paneles; más adelante estudiaremos en mayor detalle diseños más complejos.Por costumbre, en el panel más angosto (sidebar) ubicaremos los comandos que nos permiten controlar el output; a su vez, en el panel principal (main panel) vamos a situar los resultados que queremos visualizar.
Podemos armar la estructura recién descripta usando la función
bslib::layout_sidebar()dentro debslib::page_fluid(). El esquema de la interfaz luce así:
Código
# Importación de datos y ajuste CP
datos <- readr::read_delim("queen.txt", delim = "\t")
cp <- FactoMineR::PCA(
# matriz de datos: elegimos variables numericas
X = dplyr::select(datos, dplyr::where(is.numeric)),
ncp = 11, # cantidad de componentes a almacenar
graph = FALSE # no mostrar los graficos
)
# User Interface
MiInterfaz <- bslib::page_fluid( # Estructura general de la web
# Título de la web
shiny::titlePanel("Mi Primera Shiny App - ACP sobre Canciones de Queen"),
bslib::layout_sidebar( # Función para crear paneles
sidebar = bslib::sidebar(), # Panel secundario
# Panel principal
)
)
# Servidor
MiServidor <- function(input, output) {}
# Lanzamiento de la app
shiny::shinyApp(ui = MiInterfaz, server = MiServidor)Paso III: Outputs
Ahora es el turno de definir lo que mostraremos en el panel principal. En general, cuando deseamos mostrar en Shiny algún elemento creado por nosotros/as, debemos utilizar alguna de las funciones de tipo output. Las más comunes son:
shiny::textOutput()para texto planoshiny::htmlOutput()para texto con formato HTMLshiny::tableOutput()para tablas, matrices o conjuntos de datosDT::DTOutput()para tablas creadas con el paqueteDTreactable::reactableOutput()para tablas creadas con el paquetereactableshiny::plotOutput()para gráficos creados con el paqueteggplot2plotly::plotlyOutput()para gráficos creados con el paqueteplotlyecharts4r::echarts4rOutput()para gráficos creados con el paqueteecharts4rleaflet::leafletOutput()para mapas creados con el paqueteleaflet
En nuestro caso usaremos
DT::DTOutput()yreactable::reactableOutput()para mostrar tablas con las cargas y autovalores del ACP respectivamente (observación: usamos ambos tipos de Output con fines didácticos; al momento de desarrollar una app deberán elegir uno u otro paquete) yplotly::plotlyOutput()para mostrar gráficos.Mediante el siguiente código le avisamos a R que dentro del panel principal habrá 4 objetos llamados tabla_cargas, tabla_pjevar, plot_scree y plot_indiv:
Código
# Importación de datos y ajuste CP
datos <- readr::read_delim("queen.txt", delim = "\t")
cp <- FactoMineR::PCA(
# matriz de datos: elegimos variables numericas
X = dplyr::select(datos, dplyr::where(is.numeric)),
ncp = 11, # cantidad de componentes a almacenar
graph = FALSE # no mostrar los graficos
)
# User Interface
MiInterfaz <- bslib::page_fluid(
shiny::titlePanel("Mi Primera Shiny App - ACP sobre Canciones de Queen"),
bslib::layout_sidebar(
sidebar = bslib::sidebar(),
DT::DTOutput("tabla_cargas"),
reactable::reactableOutput("tabla_pjevar"),
plotly::plotlyOutput("plot_scree"),
plotly::plotlyOutput("plot_indiv")
)
)
# Servidor
MiServidor <- function(input, output) {}
# Lanzamiento de la app
shiny::shinyApp(ui = MiInterfaz, server = MiServidor)Paso IV: Servidor
Ahora podemos ocuparnos de definir la sección server. Este bloque estará constituido por una función con dos argumentos: input y output:
el argumento input es una lista donde se guardan los valores que pueden cambiar según el deseo del usuario/a, en este caso álbums incluidos y componentes a graficar.
el argumento output es una lista donde se almacenan los resultados que dependen de los valores elegidos en input, en este caso las tablas y gráficos.
- Por ahora no nos preocuparemos por el argumento input; únicamente colocaremos dentro de la función
MiServidorel código necesario para generar los 4 objetos que deseamos mostrar:
Código
# Importación de datos y ajuste CP
datos <- readr::read_delim("queen.txt", delim = "\t")
cp <- FactoMineR::PCA(
# matriz de datos: elegimos variables numericas
X = dplyr::select(datos, dplyr::where(is.numeric)),
ncp = 11, # cantidad de componentes a almacenar
graph = FALSE # no mostrar los graficos
)
# User Interface
MiInterfaz <- bslib::page_fluid(
shiny::titlePanel("Mi Primera Shiny App - ACP sobre Canciones de Queen"),
bslib::layout_sidebar(
sidebar = bslib::sidebar(),
# Usamos bslib::card() para evitar superposición de las tablas
# Esto parece ser comportamiento inesperado de DT::DTOutput
bslib::card(DT::DTOutput("tabla_cargas"), height = "620px"),
reactable::reactableOutput("tabla_pjevar"),
plotly::plotlyOutput("plot_scree"),
plotly::plotlyOutput("plot_indiv")
)
)
# Servidor
MiServidor <- function(input, output) {
output$tabla_cargas <- DT::renderDT({
cp$var$coord |>
DT::datatable(options = list(pageLength = 11)) |>
DT::formatRound(columns = 1:11, digits = 4)
})
output$tabla_pjevar <- reactable::renderReactable({
cp$eig |>
reactable::reactable(
pagination = FALSE,
defaultColDef = reactable::colDef(
format = reactable::colFormat(digits = 2)
)
)
})
output$plot_scree <- plotly::renderPlotly({
gg_scree <- tibble::tibble(CP = 1:nrow(cp$eig),
PVE = cp$eig[,2]) |>
ggplot2::ggplot() +
ggplot2::aes(x = CP, y = PVE) +
ggplot2::geom_line(linewidth = 1) +
ggplot2::geom_point(size = 3, color = "red") +
ggplot2::scale_x_continuous(breaks = 1:nrow(cp$eig)) +
ggplot2::scale_y_continuous(name = "% Variancia Explicada") +
ggplot2::ggtitle("Scree Plot") +
ggplot2::theme_bw()
plotly::ggplotly(gg_scree)
})
output$plot_indiv <- plotly::renderPlotly({
individuos <- cp$ind$coord |>
dplyr::bind_cols(datos) |>
dplyr::mutate(name = stringr::str_wrap(name, 25)) |>
ggplot2::ggplot() +
ggplot2::aes(x = Dim.1, y = Dim.2, color = album, label = name) +
ggplot2::geom_hline(yintercept = 0, linewidth = 0.1) +
ggplot2::geom_vline(xintercept = 0, linewidth = 0.1) +
ggplot2::geom_point(alpha = 0) +
ggplot2::geom_text(size = 2, show.legend = FALSE) +
ggplot2::ggtitle("Gráfico de los individuos en las CP seleccionadas") +
ggplot2::theme_bw()
plotly::ggplotly(individuos)
})
}
# Lanzamiento de la app
shiny::shinyApp(ui = MiInterfaz, server = MiServidor)Importante: estructura del servidor
Estudiemos el formato de escritura empleado: dentro de la función que juega el rol de servidor debemos listar los elementos que deseamos que estén disponibles para ser incluidos en el resultado final de la app, siguiendo la estructura
output$NombreObjeto.Asociado a este nombre debe haber un llamado a una función de tipo render, las cuales son la contraparte de las funciones output mencionadas arriba. Ejemplos:
plotly::plotlyOutput()en la interfaz –>plotly::renderPlotly()en el servidorDT::DTOutput()en la interfaz –>DT::renderDT()en el servidorreactable::reactableOutput()en la interfaz –>reactable::renderReactable()en el servidor
Paso V: Widgets
Todavía no hemos otorgado a nuestra app la interactividad deseada: sólo muestra las primeras 2 componentes e incluye todos los álbums disponibles de Queen en la base, ya que así está programado en el código mostrado arriba.
Para lograr la interactividad será necesario incluir ciertos widgets (término traducido como “dispositivos” o “artilugios”) en nuestra app. Podemos pensar a un widget como un elemento prefabricado que nos da la posibilidad de transmitirle información a la app sobre lo que queremos. Los más comunes son:
shiny::checkboxGroupInput(): listado de opciones para marcar (multiple choice)shiny::radioButtons(): listado de opciones que admite una respuesta únicashiny::textInput(): entrada manual de textoshiny::numericInput(): entrada de valores numéricosshiny::dateInput(): entrada de fechasshiny::fileInput(): permite subir un archivo desde mi PC para ser utilizado por la appshiny::actionButton(): botón de acción para activar o desactivar cierta opciónshiny::sliderInput(): barra horizontal que permite elegir un valor numérico o un intervalo dentro de un rango determinado
Shiny ofrece una variada lista de posibilidades, las cuales podemos consultar en esta galería de widgets. Una alternativa recomendable es el paquete
shinyWidgetsel cual ofrece muchas otras opciones, las cuales vale la pena repasar en su página web. Más adelante hablaremos más sobre este paquete.Para continuar con el armado de nuestra app, existen dos widgets que serán particularmente útiles:
shiny::checkboxGroupInput()para poder elegir los discos de Queen a incluir en el ACPshiny::numericInput()para poder elegir las componentes a graficar (uno para cada eje)
A cada widget que creamos debemos asignarle un ID, el cual luego podrá ser usado para hacer referencia a los valores que cada uno de ellos tomen en determinado momento.
El código para generar los widgets puede incluirse directamente dentro del sidebarPanel presente en la interfaz de nuestra aplicación:
Código
# Datos
datos <- readr::read_delim("queen.txt", delim = "\t")
cp <- FactoMineR::PCA(
# matriz de datos: elegimos variables numericas
X = dplyr::select(datos, dplyr::where(is.numeric)),
ncp = 11, # cantidad de componentes a almacenar
graph = FALSE # no mostrar los graficos
)
# User Interface
MiInterfaz <- bslib::page_fluid(
shiny::titlePanel("Mi Primera Shiny App - ACP sobre Canciones de Queen"),
bslib::layout_sidebar(
sidebar = bslib::sidebar(
# Listado de Albums
shiny::checkboxGroupInput(
inputId = "album", # ID del widget
label = "Álbums a incluir", # Título a mostrar en la app
choices = sort(unique(datos$album)), # Opciones disponibles
selected = sort(unique(datos$album))[1:3] # Opciones seleccionadas al inicio
),
# Componente Eje X
shiny::numericInput(
inputId = "ejex", # ID del widget
label = "CP Eje X", # Título a mostrar en la app
value = 1, # Valor seleccionado inicialmente
min = 1, # Mínimo valor posible
max = 11 # Máximo valor posible
),
# Componente Eje Y
shiny::numericInput(
inputId = "ejey", # ID del widget
label = "CP Eje Y", # Título a mostrar en la app
value = 2, # Valor seleccionado inicialmente
min = 1, # Mínimo valor posible
max = 11 # Máximo valor posible
)
),
# Usamos bslib::card() para evitar superposición de las tablas
# Esto parece ser comportamiento inesperado de DT::DTOutput
bslib::card(DT::DTOutput("tabla_cargas"), height = "620px"),
reactable::reactableOutput("tabla_pjevar"),
plotly::plotlyOutput("plot_scree"),
plotly::plotlyOutput("plot_indiv")
)
)
# Servidor
MiServidor <- function(input, output) {
output$tabla_cargas <- DT::renderDT({
cp$var$coord |>
DT::datatable(options = list(pageLength = 11)) |>
DT::formatRound(columns = 1:11, digits = 4)
})
output$tabla_pjevar <- reactable::renderReactable({
cp$eig |>
reactable::reactable(
pagination = FALSE,
defaultColDef = reactable::colDef(
format = reactable::colFormat(digits = 4)
)
)
})
output$plot_scree <- plotly::renderPlotly({
gg_scree <- tibble::tibble(CP = 1:nrow(cp$eig),
PVE = cp$eig[,2]) |>
ggplot2::ggplot() +
ggplot2::aes(x = CP, y = PVE) +
ggplot2::geom_line(linewidth = 1) +
ggplot2::geom_point(size = 3, color = "red") +
ggplot2::scale_x_continuous(breaks = 1:nrow(cp$eig)) +
ggplot2::scale_y_continuous(name = "% Variancia Explicada") +
ggplot2::ggtitle("Scree Plot") +
ggplot2::theme_bw()
plotly::ggplotly(gg_scree)
})
output$plot_indiv <- plotly::renderPlotly({
individuos <- cp$ind$coord |>
dplyr::bind_cols(datos) |>
dplyr::mutate(name = stringr::str_wrap(name, 25)) |>
ggplot2::ggplot() +
ggplot2::aes(x = Dim.1, y = Dim.2, color = album, label = name) +
ggplot2::geom_hline(yintercept = 0, linewidth = 0.1) +
ggplot2::geom_vline(xintercept = 0, linewidth = 0.1) +
ggplot2::geom_point(alpha = 0) +
ggplot2::geom_text(size = 2, show.legend = FALSE) +
ggplot2::ggtitle("Gráfico de los individuos en las CP seleccionadas") +
ggplot2::theme_bw()
plotly::ggplotly(individuos)
})
}
# Lanzamiento de la app
shiny::shinyApp(ui = MiInterfaz, server = MiServidor)Paso VI: Reactividad
Cada uno de los widgets comunica al server el valor seleccionado por el usuario a través de la lista
input. Para acceder al valor definido por un usuario para un determinado widget usamos el input ID. En este caso,input$albumnos permite acceder a los albumes seleccionados mientras queinput$ejexeinput$ejeynos permiten acceder al valor seleccionado para cada eje.En esta sección utilizaremos
input$albumpara filtrar el conjunto de datos original de acuerdo a los albumes seleccionados por el usuario, obtendremos las CP a partir de la base filtrada y luego armaremos el gráfico según los valores deinput$ejexeinput$ejey.La base filtrada debe ser un elemento reactivo, es decir, un objeto de R cuyo valor no sea fijo, sino que se actualice de acuerdo a las opciones que seleccionemos en pantalla. Esto se logra mediante la función
shiny::reactive(). Dentro de nuestra función servidor podemos incluir el siguiente código:
base_filtrada <- shiny::reactive({
dplyr::filter(datos, album %in% input$album)
})Importante: cuando estamos utilizando elementos reactivos debemos llamarlos agregando un par de paréntesis al final de su nombre, por ejemplo,
base_filtrada()en vez debase_filtrada.El elemento reactivo definido arriba permite R actualice su valor a medida que el usuario de la app elige diferentes albumes. La lógica es simple: si definimos
base_filtrada <- dplyr::filter(datos, album %in% c("Innuendo", "Jazz")), luego la app usará siempre las canciones que pertenecen a los albumesInnuendoyJazz; si definimosbase_filtrada <- shiny::reactive({dplyr::filter(datos, album %in% input$album)}), el valor cambiará siempre que el usuario modifique manualmente el input asociado.Dado que los objetos de R que dependen de valores reactivos también son reactivos, el objeto donde guardamos el resultado del ACP también es reactivo (depende de la base de datos, que a su vez depende de los discos elegidos). Todos estos detalles requieren que modifiquemos el código original. Por ejemplo, lo que antes era:
cp <- FactoMineR::PCA(
X = dplyr::select(datos, dplyr::where(is.numeric)),
ncp = 11,
graph = FALSE
)ahora se convierte en:
cp <- shiny::reactive({
FactoMineR::PCA(
X = dplyr::select(base_filtrada(), dplyr::where(is.numeric)),
ncp = 11,
graph = FALSE
)
})Aclaración: el concepto de reactividad es un tema complejo que requiere de cierto tiempo para ser comprendido plenamente. Si bien la reactividad es un elemento fundamental de cualquier aplicación Shiny, por una cuestión de tiempo disponible no profundizaremos demasiado en este tópico. Aquellas personas interesadas en aprender más sobre el tema pueden consultar el capítulo “Mastering reactivity” del ya mencionado libro Mastering Shiny.
Paso VII: Ejecución
- ¡Llegamos al último paso! Una vez que ya tenemos listas la interfaz y el servidor, sólo falta agregar la sentencia que permite ejecutar la aplicación:
shiny::shinyApp(ui = MiInterfaz, server = MiServidor)Esta línea de código debe figurar siempre dentro del archivo app.R, respetando los nombres asignados a la interfaz y al server.
Existen 3 maneras diferentes de lanzar la aplicación:
Apretar el botón Run App en la barra de herramientas de RStudio, donde podemos elegir si queremos ejecutarla dentro de RStudio o en un navegador externo.
Entrar a
app.R, seleccionar todo el código y ejecutarlo.Usar la función
shiny::runApp()en la consola de RStudio, eligiendo la ruta donde está ubicado el directorio que contiene la app, por ejemplorunApp("C:/Mis Documentos/MiApp").
- Uniendo el código presentado en cada paso, llegamos a este resultado final:
Código
# Datos
datos <- readr::read_delim("queen.txt", delim = "\t")
# User Interface
MiInterfaz <- bslib::page_fluid(
shiny::titlePanel("Mi Primera Shiny App - ACP sobre Canciones de Queen"),
bslib::layout_sidebar(
sidebar = bslib::sidebar(
# Listado de Albums
shiny::checkboxGroupInput(
inputId = "album", # ID del widget
label = "Álbums a incluir", # Título a mostrar en la app
choices = sort(unique(datos$album)), # Opciones disponibles
selected = sort(unique(datos$album))[1:3] # Opciones seleccionadas al inicio
),
# Componente Eje X
shiny::numericInput(
inputId = "ejex", # ID del widget
label = "CP Eje X", # Título a mostrar en la app
value = 1, # Valor seleccionado inicialmente
min = 1, # Mínimo valor posible
max = 11 # Máximo valor posible
),
# Componente Eje Y
shiny::numericInput(
inputId = "ejey", # ID del widget
label = "CP Eje Y", # Título a mostrar en la app
value = 2, # Valor seleccionado inicialmente
min = 1, # Mínimo valor posible
max = 11 # Máximo valor posible
)
),
# Usamos bslib::card() para evitar superposición de las tablas
# Esto parece ser comportamiento inesperado de DT::DTOutput
bslib::card(DT::DTOutput("tabla_cargas"), height = "620px"),
reactable::reactableOutput("tabla_pjevar"),
plotly::plotlyOutput("plot_scree"),
plotly::plotlyOutput("plot_indiv")
)
)
#Servidor
MiServidor <- function(input, output) {
base_filtrada <- shiny::reactive({
dplyr::filter(datos, album %in% input$album)
})
cp <- shiny::reactive({
FactoMineR::PCA(
# matriz de datos: elegimos variables numericas
X = dplyr::select(base_filtrada(), dplyr::where(is.numeric)),
ncp = 11, # cantidad de componentes a almacenar
graph = FALSE # no mostrar los graficos
)
})
output$tabla_cargas <- DT::renderDT({
cp()$var$coord |>
DT::datatable(options = list(pageLength = 11)) |>
DT::formatRound(columns = 1:11, digits = 4)
})
output$tabla_pjevar <- reactable::renderReactable({
cp()$eig |>
reactable::reactable(
pagination = FALSE,
defaultColDef = reactable::colDef(
format = reactable::colFormat(digits = 4)
)
)
})
output$plot_scree <- plotly::renderPlotly({
gg_scree <- tibble::tibble(CP = 1:nrow(cp()$eig),
PVE = cp()$eig[,2]) |>
ggplot2::ggplot() +
ggplot2::aes(x = CP, y = PVE) +
ggplot2::geom_line(linewidth = 1) +
ggplot2::geom_point(size = 3, color = "red") +
ggplot2::scale_x_continuous(breaks = 1:nrow(cp()$eig)) +
ggplot2::scale_y_continuous(name = "% Variancia Explicada") +
ggplot2::ggtitle("Scree Plot") +
ggplot2::theme_bw()
plotly::ggplotly(gg_scree)
})
output$plot_indiv <- plotly::renderPlotly({
individuos <- cp()$ind$coord |>
dplyr::bind_cols(base_filtrada()) |>
dplyr::mutate(name = stringr::str_wrap(name, 25)) |>
dplyr::select(album,
name,
x = paste0("Dim.", input$ejex),
y = paste0("Dim.", input$ejey)) |>
ggplot2::ggplot() +
ggplot2::aes(x = x, y = y, color = album, label = name) +
ggplot2::geom_hline(yintercept = 0, linewidth = 0.1) +
ggplot2::geom_vline(xintercept = 0, linewidth = 0.1) +
ggplot2::geom_point(alpha = 0) +
ggplot2::geom_text(size = 2, show.legend = FALSE) +
ggplot2::ggtitle("Gráfico de los individuos en las CP seleccionadas") +
ggplot2::labs(x = paste0("CP", input$ejex), y = paste0("CP", input$ejey)) +
ggplot2::theme_bw()
plotly::ggplotly(individuos)
})
}
# Lanzamiento de la app
shiny::shinyApp(ui = MiInterfaz, server = MiServidor)- Es interesante notar que se implementaron algunos cambios en el servidor, para adaptarnos a la reactividad definida en el paso anterior; prestemos especial atención al proceso de selección de componentes dentro del código del gráfico de individuos:
dplyr::select(
album,
name,
x = paste0("Dim.", input$ejex),
y = paste0("Dim.", input$ejey)
)Si lanzamos la app desde RStudio, es necesario detenerla para poder seguir utilizando R. Esto se logra apretando el botón rojo “STOP” ubicado en el sector superior derecho de la consola.
En la versión final del código de la app, algunas sentencias se incorporan dentro de la función server y otras fuera de ella. La recomendación a seguir es incluir en el server sólo los outputs y aquellos objetos que sean reactivos (o que dependan de objetos reactivos), y dejar fuera cualquier otro. Gracias a esto evitamos sobrecargar y/o enlentecer la app, ya que los objetos definidos fuera del server se evalúan una única vez, mientras que aquellos definidos dentro del server son re-evaluados constantemente.
Se recomienda crear un esquema que replique la estructura principal de la app, ya que esto ayuda a comprender mejor su funcionamiento. En nuestro caso, el esquema está compuesto por tres inputs (album, ejex y ejey) y cuatro outputs (tabla_cargas, tabla_pjevar, plot_scree y plot_indiv).
El orden de los pasos seguidos en este tutorial no es necesariamente fijo, y dependerá en gran medida de las características de la app que estemos desarrollando. Algunas apps simples quizás no necesiten reactividad, widgets o gráficos, mientras que aquellas apps más complejas seguramente requieran de un ida y vuelta retroalimentado entre los pasos 2 a 6, hasta que alcancemos el resultado deseado.
Una vez que hayamos adquirido cierta experiencia en la construcción de apps, resulta una buena idea construir en primer lugar la interfaz con sus widgets incluidos (pasos 2 y 5), para luego dedicarle tiempo al servidor y el contenido (pasos 3, 4 y 6). Obviamente esta es sólo una sugerencia, y el proceso de creación de una app debe responder a las necesidades y gustos de su respectivo/a autor/a.
Trabajo en Equipo
Intentar replicar la siguiente app, en la cual se muestra un histograma para alguna de las variables numéricas presentes en la base.
Detalles a considerar:
- Se pueden elegir múltiples álbums
- El título del gráfico debe mostrar los nombres de los álbums seleccionados
- El histograma utiliza siempre 10 barras
- Ayuda: el widget empleado en ambos casos es
pickerInput()del paqueteshinyWidgets, el cual permite agregar una barra de búsqueda (liveSearch) en su argumentooptions.





