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:

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.

Bibliografía

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:

    1. Interfaz del usuario (user interface)
    2. Función servidor (server function)
    3. Publicación de la app (app deployment)
Aclaraciones
  • En versiones antiguas de Shiny era necesario crear la interfaz y el servidor en archivos separados (ui.R y server.R respectivamente), aunque hoy en día esto es optativo.

  • Llamar a nuestro archivo de sentencias app.R no 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 bslib facilitan en gran medida nuestro trabajo y no debemos preocuparnos (demasiado) por aprender a utilizar estos lenguajes.
Paquete {bslib}
  • bslib proporciona un moderno conjunto de herramientas de interfaz de usuario para Shiny basado en Bootstrap. A diferencia de shiny (que utiliza Bootstrap 3) bslib utiliza la versión más moderna de Bootstrap (actualmente Bootstrap 5)

  • El paquete bslib es una dependencia de shiny, es decir, se instala al momento de instalar shiny.

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.

Importante

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 paquete FactoMineR:

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:

    1. Las cargas (loadings) asociadas a cada variable en cada una de las componentes halladas.
    2. El porcentaje de variancia explicada por cada componente.
    3. 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:

Cargas para las 11 Componentes
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
Variancia explicada por cada Componente
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:

    1. app.R (por ahora vacío) con codificación UTF-8
    2. queen.txt

  • Lo primero que podemos hacer dentro de app.R es leer los datos, definir interfaz y servidor vacíos, y hacer un llamado a la función shiny::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 bslib son bslib::page_fluid(), bslib::page_fixed(), bslib::page_fillable(), bslib::page_navbar() y bslib::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 de bslib::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 plano
    • shiny::htmlOutput() para texto con formato HTML
    • shiny::tableOutput() para tablas, matrices o conjuntos de datos
    • DT::DTOutput() para tablas creadas con el paquete DT
    • reactable::reactableOutput() para tablas creadas con el paquete reactable
    • shiny::plotOutput() para gráficos creados con el paquete ggplot2
    • plotly::plotlyOutput() para gráficos creados con el paquete plotly
    • echarts4r::echarts4rOutput() para gráficos creados con el paquete echarts4r
    • leaflet::leafletOutput() para mapas creados con el paquete leaflet

  • En nuestro caso usaremos DT::DTOutput() y reactable::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) y plotly::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:

    1. 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.

    2. 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 MiServidor el 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 servidor
    • DT::DTOutput() en la interfaz –> DT::renderDT() en el servidor
    • reactable::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 única
    • shiny::textInput(): entrada manual de texto
    • shiny::numericInput(): entrada de valores numéricos
    • shiny::dateInput(): entrada de fechas
    • shiny::fileInput(): permite subir un archivo desde mi PC para ser utilizado por la app
    • shiny::actionButton(): botón de acción para activar o desactivar cierta opción
    • shiny::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 shinyWidgets el 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 ACP
    • shiny::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$album nos permite acceder a los albumes seleccionados mientras que input$ejex e input$ejey nos permiten acceder al valor seleccionado para cada eje.

  • En esta sección utilizaremos input$album para 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 de input$ejex e input$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 de base_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 albumes Innuendo y Jazz; si definimos base_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:

    1. 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.

    2. Entrar a app.R, seleccionar todo el código y ejecutarlo.

    3. Usar la función shiny::runApp() en la consola de RStudio, eligiendo la ruta donde está ubicado el directorio que contiene la app, por ejemplo runApp("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)
)
Datos Importantes
  • 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 paquete shinyWidgets, el cual permite agregar una barra de búsqueda (liveSearch) en su argumento options.