Introducción a las aplicaciones web con Shiny

Un artículo dedicado a crear tu primer aplicación en R, de una manera sencilla.

Jorge Valente Hernández Castelán https://example.com/norajones (R-conomics)https://r-conomics.netlify.app
2022-06-01

Las aplicaciones web enfocadas a estadística

Es común que, como científicos de datos, estadístas, actuarios, economistas, matemáticos, etc, nos pidan conocimientos de programación y estadística las empresas de hoy en día, ya que su principal objetivo siempre será el volver más eficientes las cosas y mejorar sus ingresos, buscar áreas de oportunidades, etc. Las empresas suelen manejar sus datos con tableros de Power BI, Tableau o incluso excel en algunos casos, por lo que es común que te pidan únicamente la programación para poder analizar datos, transformar información, obtener estadísticos, encontrar correlaciones o generar pronósticos. Hacer estadística con R es algo sencillo, pero si queremos llevarlo al siguiente nivel y crear un programa que nos muestre esos resultados, de tal manera que cualquier persona pueda entenderlo, entonces lo idoneo sería crear una aplicación con la librería Shiny.

¿Por qué usar Shiny?

Esta librería no es precisamente nueva, lo que ha dado pie a su constante depuración, incremento y escalamiento de funciones, además de que se han creado muchas otras librerías que son compatibles con Shiny actualmente, mejorando las técnicas disponibles de integración y visualización de datos. Shiny es probablemente la librería más completa y compleja, debido a su interacción e integración con muchas otras.

Es importante mencionar que, a pesar de que es recomendable saber código HTML y CSS, no es un requisito indispensable para comenzar a trabajar con Shiny, ya que tiene funciones que crean este tipo de código automáticamente.

¿Hasta dónde puede llegar mi aplicación?

Una de las ventajas de Shiny es su enorme escalamiento. Este puede ser usado no solo por empresas pequeñas, si no también por aquellas industrias que manejan volúmenes considerables de información. Debo decir que el límite de una aplicación en Shiny prácticamente lo pone nuestra imaginación.

Primeros pasos en Shiny: Entendiendo la lógica

Ahora que terminamos con la breve introducción (creo que ya habrás notado lo mucho que me encanta esa paquetería) es momento de comenzar a trabajar con una nueva aplicación en shiny. Vamos paso a paso.

Estructura

Las aplicaciones se componen de dos principales partes: El UI y el Server. El UI es, básicamente, todo lo que ve el usuario, todos los botones, selectores, gráficos, mapas, textos y demás cosas que incluya nuestra aplicación. El Server por su parte es la zona en la que se alojan todos los códigos que hacen funcionar a la aplicación, todas las funciones necesarias para que cierto botón o selector nos devuelva un gráfico, un mapa, texto, etc. Debido a que todas las funciones y creaciones de gráficos se encuentran en el Server, es común que este último tenga muchos más códigos que el UI.

UI

Comenzando a darle una estructura a la aplicación, dentro del UI encontraremos diferentes funciones para crear una página en blanco, como fluidpage(), basicpage(), etc. En mi caso suelo utilizar bastante la primera. A partir del lienzo en blanco que hemos creado, podemos dividirlo en columnas con la función column(), la cual debe llevar un parámetro width para establecer el ancho de la columna, el cual debe ser un número del 1 al 12. Podemos jugar con los números para, por ejemplo, crear dos columnas de 6 y dividir la pantalla, o crear una columna de 8 y una de 4, etc. Ya es cuestión de gustos. Sabiendo lo anterior, nuestro UI generalmente se verá así:

library(shiny)

ui <- fluidPage(
  column(
    width = 6
  ),
  column(
    width = 12
  )
)

En esencia, el código del UI no son más que funciones que después se traducen a HTML. En el ejemplo anterior, su respectivo equivalente en HTML sería:

<div class="container-fluid">
  <div class="col-sm-6"></div>
  <div class="col-sm-12"></div>
</div>

Server

El server puede ser un poco más complicado de entender, pero nada del otro mundo. La idea es sencilla: El server va a recibir inputs o entradas de información y va a devolver outputs o salidas de información. Dicho de otra manera, el server recibirá la información de algún botón o selector que utilicemos en la aplicación y, consecuentemente nos devolverá un gráfico, texto, o lo que queramos implementar en nuestra app. El server como tal es una función de dos parámetros (a veces 3, dependiendo del renderizado, aunque eso lo veremos después), por lo que tendrá el siguiente aspecto general:

server <- function(input, output){
  
}

Y dentro de las llaves escribiremos los códigos necesarios para crear nuestros objetos.

Visualizando nuestra App

Al trabajar dentro de Rstudio tenemos diferentes formas de visualizar nuestra App en shiny. La más sencilla es llamar a nuestros objetos UI y Server dentro de la función ShinyApp(), de la siguiente manera:

shinyApp(ui = ui, server = server)

Lo cual automáticamente nos devolverá una pantalla con la app creada. Notese que ahora mismo nos devolverá una pantalla en blanco ya que no hemos creado ningún botón, cambiado colores, puesto gráficos, ni nada de eso. Por ahora sigue siendo un lienzo en blanco.

Introduciendo un gráfico sencillo

Para entender mejor la idea de Shiny, vamos a crear un gráfico sencillo el cual pueda cambiar dinámicamente dependiendo de las selecciones que hagamos. Podríamos utilizar un set de muestra de R, como por ejemplo el siguiente:

datos <- gapminder::gapminder

Ahora que tenemos la información necesaria, vamos a preguntarnos, ¿qué podemos hacer con ella? Lo más sencillo que se me ocurre ahora mismo es simplemente filtrar la expectativa de vida por continente para generar un gráfico de sus países. Primero veamos como hacer eso de manera normal, sin una app shiny de por medio.

Comenzamos seleccionando la información que necesitamos. En la tabla anterior podemos ver que tenemos datos por país, continente y año, por lo que, si quisieramos ver la expectiva de vida promedio por continente para el año 1952, por ejemplo, en una sesión normal de R normálmente haríamos algo así:

library(dplyr)
datos <- gapminder::gapminder

datos |> 
  filter(year == 1952) |> 
  group_by(continent) |> 
  summarise(`Expectativa de vida` = round(mean(lifeExp, na.rm = TRUE), digits = 2))
# A tibble: 5 x 2
  continent `Expectativa de vida`
  <fct>                     <dbl>
1 Africa                     39.1
2 Americas                   53.3
3 Asia                       46.3
4 Europe                     64.4
5 Oceania                    69.2

Pero, ¿y si quisieramos ver esos mismos datos para diferentes años o considerando solo ciertos países? Podríamos utilizar los mismos códigos filtrando para cada caso específico que queramos analizar, crear una función con los argumentos de los filtros, por ejemplo:

filtros <- function(year_sel = NULL,continent_sel = NULL ){
  
  x <- gapminder::gapminder |> 
  filter(year %in% year_sel & continent_sel %in% continent  ) |> 
  group_by(continent) |> 
  summarise(`Expectativa de vida` = round(mean(lifeExp, na.rm = TRUE), digits = 2))
  
  return(x)
  
}

filtros(1952, "Europe")
# A tibble: 5 x 2
  continent `Expectativa de vida`
  <fct>                     <dbl>
1 Africa                     39.1
2 Americas                   53.3
3 Asia                       46.3
4 Europe                     64.4
5 Oceania                    69.2

De hecho, el crear una función es prácticamente la lógica utilizada para las aplicaciones web, ya que pueden tener diferentes argumentos, pero por más agradable que se vea nuestra función en la consola de R, esta no es la manera más óptima de hacerlo si queremos ver una cantidad de escenarios considerablemente alta, sobre todo si queremos mostrar estos resultados a una tercera persona. En este punto es en el que combinamos las aplicaciones web hechas en shiny con nuestro código de R, ya que ahora si seremos capaces de mostrar esta información en diferentes ambientes.

Comencemos a pensar en una forma de visualizar esta información de la manera más agradable posible. Podríamos mostrar una tabla y un gráfico lado a lado, los cuales nos muestren los datos que que queremos con los filtros que queremos. El primer paso será crear una interfaz que se vea agradable para el usuario, comencemos con una columna que abarque todo el ancho de la pantalla, en la cual vamos a establecer los filtros con los que vamos a trabajar, después de ello podemos establecer otras dos columnas que abarquen media pantalla cada una, para introducir la tabla y el gráfico que queremos. Antes de pasar de lleno al código, quisiera ahondar un poco más en los filtros. Para filtrar la información tenemos múltiples recursos dentro de la librería shiny, como lo son botones, selectores, “sliders”, cuadros para introducir números, etc. Igualmente tenemos otras librerías como shinywidgets que nos dan botones o selectores con una vista más agradable. Por ahora, para no complicarnos demasiado, vamos a utilizar únicamente los widgets de la librería original de shiny, pero ¿cuáles usaremos exactamente? Para el país y/o continente bastaría un selector sencillo, pero para el año tenemos varías opciones. En nuestra tabla original podemos notar que no tenemos datos para todos los años, por lo que un sliderinput (barra que incluye una secuencia de números) podría no ser tan conveniente, un numericinput (cuadro en el que solo se pueden introducir carácteres numéricos), ya que prácticamente tendríamos que estar adivinando qué años tenemos disponibles. Veamos ejemplos con diferentes tipos de widgets, al final tú serás quien decida cuál es el más conveniente (aquí es conveniente hacer uso de la creatividad y pensar en lo más conveniente para tu caso particular).

Estableciendo el UI

Utilizando selectores

Para nuestro primer ejemplo, utilizaremos un selector simple para cada filtro. Nuestro código de UI se vería de la siguiente manera:

library(shiny)
library(echarts4r)
library(reactable)


fluidPage(
  column(width = 12, align = "center",
         selectInput("filt_country", "Paises disponibles:", 
                     choices = unique(datos$country) ),
         selectInput("filt_continent", "Continentes:",
                     choices = unique(datos$continent) ),
         selectInput("year_disp", "Años disponibles:",
                     choices = unique(datos$year))
         ),
  column(width = 6, reactableOutput("tabla")),
  column(width = 6, echarts4rOutput("grafico"))
)

Utilizando un numeric input

library(shiny)
library(echarts4r)
library(reactable)


fluidPage(
  column(width = 12, align = "center",
         selectInput("filt_country", "Paises disponibles:", 
                     choices = unique(datos$country) ),
         selectInput("filt_continent", "Continentes:",
                     choices = unique(datos$continent) ),
         numericInput("year_disp", "Años disponibles:",
                      value = 1957)
         ),
  column(width = 6, reactableOutput("tabla")),
  column(width = 6, echarts4rOutput("grafico"))
)

Utilizando un slider input

library(shiny)
library(echarts4r)
library(reactable)


fluidPage(
  column(width = 12, align = "center",
         selectInput("filt_country", "Paises disponibles:", 
                     choices = unique(datos$country) ),
         selectInput("filt_continent", "Continentes:",
                     choices = unique(datos$continent) ),
         sliderInput("year_disp", "Años disponibles:",
                      value = 1957, min = 1957,
                     max = 2022
                     )
         ),
  column(width = 6, reactableOutput("tabla_sum")),
  column(width = 6, echarts4rOutput("grafico_sum"))
)

Notese que utilizamos la función unique() para obtener los valores sin duplicados de cada columna. Ahora, es importante notar que el primer argumento de cada selector y el único argumento de los outputs, tanto del gráfico como de la tabla, es el ID, algo que será de suma importancia para nuestras aplicaciones, ya que con esto identificaremos a las tablas/gráficos que estemos renderizando, conectándolos con nuestros selectores para poder filtrar dinámicamente la información, más adelante vamos a ahondar en el ID, pero por ahora será necesario prestarle bastante atención. Ahora, los tres casos son prácticamente iguales, la elección aquí es tuya, dependiendo de las necesidades de tu aplicación o la forma en la que quieras mostra la información.

Estableciendo el servidor

Ahora que ya tenemos la página que será visualizada por el usuario, podemos comenzar a trabajar en el backend, es decir, las funciones de nuestra aplicación que harán que ésta funcione. Básicamente, utilizaremos múltiples funciones que se conecten a nuestra salida de gráfico y tablas. Comencemos con la tabla, la cuál es una salida o output, por lo que para poder llamarla, debemos crear un objeto output$id, el cuál en este caso, considerando que el id de la tabla es “tabla_sum”, sería output$tabla_sum. Generalmente, al trabajar con librerías para hacer tablas, gráficos, estas ya vienen con una función destinada a shiny, la cual suele comenzar con un render, en el caso específico de ésta tabla, utilizaremos la función renderReactable() junto a output$tabla_sum, lo cuál conectará la referencia con el id del UI. Dentro de la función para renderizar introduciremos los filtros dinámicos, pero eso lo veremos en un momento. El servidor de la aplicación debería verse de la siguiente manera para la tabla:

server <- function(input, output){
  
  output$tabla_sum <- renderReactable({
    
    gapminder::gapminder |> 
      group_by(continent) |>
      summarise(`Expectativa de vida` = round(mean(lifeExp, na.rm = TRUE), digits = 2)) |> 
      reactable()
  
  })
  
}

Nótese que la función renderReactable() acepta tener múltiples transformaciones de datos y creación de objetos dentro de ella (para ello se utilizan los corchetes), pero el objeto final de esta función debe ser necesariamente una tabla.

Ahora, no agregamos filtros en ese ejemplo, pero para agregarlos debemos considerar que los filtros son una entrada de datos, es decir, un input, por lo que tendrémos que hacer referencia a ellos de la siguiente manera: input$id_filtro. Para nuestro caso particular, vamos a conectar los id’s dentro de nuestra función de filtros por lo que nuestro resultado final será el siguiente:

server <- function(input, output){
  
  output$tabla_sum <- renderReactable({
    
    gapminder::gapminder |> 
      filter(year %in% input$year_disp & continent %in% input$filt_continent
             & country %in% input$filt_country
             ) |> 
      group_by(continent) |>
      summarise(`Expectativa de vida` = round(mean(lifeExp, na.rm = TRUE), digits = 2)) |> 
      reactable()
  
  })
  
}

Básicamente, la idea aquí es que la tabla se va a filtrar dependiendo de los datos que nosotros introduzcamos en los respectivos selectores, devolviendo al usuario esa tabla con su respectiva salida dinámica.

Para nuestro gráfico aplicaremos la misma lógica, por lo que el código se vería de la siguiente manera:

server <- function(input, output){
  
  output$grafico_sum <- renderEcharts4r({
    
    gapminder::gapminder |> 
      filter(year %in% input$year_disp & continent %in% input$filt_continent
             & country %in% input$filt_country
             ) |> 
      group_by(continent) |>
      summarise(`Expectativa de vida` = round(mean(lifeExp, na.rm = TRUE), digits = 2)) |> 
      arrange(desc(`Expectativa de vida`)) |> 
      e_charts(continent) |> 
      e_bar(`Expectativa de vida`)
  
  })
  
}

La lógica para el objeto que vayamos a crear, ya sea un gráfico, una tabla, un mapa, texto, etc, siempre será la misma. Introduciremos nuestros inputs mediante selectores, botones, etc, y el servidor nos devolverá el objeto basándose en nuestros inputs.

La app final

Finalmente, al juntar nuestros códigos, tanto del servidor como del UI, tendremos la siguiente aplicación:

library(shiny)
library(echarts4r)
library(reactable)


ui <- fluidPage(
  column(width = 12, align = "center",
         selectInput("filt_country", "Paises disponibles:", 
                     choices = unique(datos$country) ),
         selectInput("filt_continent", "Continentes:",
                     choices = unique(datos$continent) ),
         numericInput("year_disp", "Años disponibles:",
                      value = 1957)
         ),
  column(width = 6, reactableOutput("tabla")),
  column(width = 6, echarts4rOutput("grafico"))
  )

server <- function(input, output){
  
  output$grafico_sum <- renderEcharts4r({
    
    gapminder::gapminder |> 
      filter(year %in% input$year_disp & continent %in% input$filt_continent
             & country %in% input$filt_country
             ) |> 
      group_by(continent) |>
      summarise(`Expectativa de vida` = round(mean(lifeExp, na.rm = TRUE), digits = 2)) |> 
      arrange(desc(`Expectativa de vida`)) |> 
      e_charts(continent) |> 
      e_bar(`Expectativa de vida`)
  })
  
  
  output$tabla_sum <- renderReactable({
    
    gapminder::gapminder |> 
      filter(year %in% input$year_disp & continent %in% input$filt_continent
             & country %in% input$filt_country
             ) |> 
      group_by(continent) |>
      summarise(`Expectativa de vida` = round(mean(lifeExp, na.rm = TRUE), digits = 2)) |> 
      reactable()
  
  })
  
}

Y podemos correr nuestra aplicación de la siguiente manera:

shinyApp(ui = ui, server = server)

Nuestro resultado final será el siguiente:

Ciertamente, las aplicaciones web son un tema que requiere de práctica, pero a la larga se vuelven muy sencillas de manejar y entender, sin embargo, debemos considerar que cada vez tendremos programas más y más grandes, por lo que siempre es bueno tener separados los archivos del UI y Server, así como un archivo Global donde carguemos paquetes, datos y generemos información general que después trabajemos dinámicamente. En próximos tutoriales comenzaremos a ver prácticas recomendadas de programación para tener un código limpio en cuanto a aplicaciones, así como el trabajo mediante módulos, el cual será fundamental con programas grandes y complejos de entender.

Referencias

  1. Sievert, C. (2020). Interactive web-based data visualization with R, plotly, and shiny. CRC Press.

  2. Wickham, H. (2021). Mastering shiny. ” O’Reilly Media, Inc.”.

  3. Jak, S., Jorgensen, T. D., Verdam, M. G., Oort, F. J., & Elffers, L. (2021). Analytical power calculations for structural equation modeling: A tutorial and Shiny app. Behavior research methods, 53(4), 1385-1406.

  4. Li, D., Mei, H., Shen, Y., Su, S., Zhang, W., Wang, J., … & Chen, W. (2018). ECharts: a declarative framework for rapid construction of web-based visualization. Visual Informatics, 2(2), 136-146.

  5. DeqingLi, H., YiShen, S., WenliZhang, J., & MingZu, W. (2018). Echarts: A declarative framework for rapid construction of web-based visualization. Visual Informatics, 2(2), 136-146.

  6. Gackenheimer, C., & Paul, A. (2015). Introduction to React (Vol. 52). Apress.

  7. Banks, A., & Porcello, E. (2017). Learning React: functional web development with React and Redux. ” O’Reilly Media, Inc.”.

  8. Lawson, B., & Sharp, R. (2011). Introducing html5. New Riders.