Mi primera shinyapp: un planificador semanal de comidas
Pocas cosas más aburridas que pensar todas las semanas qué comer. Para evitarme el tedio, hice una app que me ayude.
Hacía por lo menos dos años que tenía la idea de aprender Shiny, pero nunca encontraba la excusa para armar una app de algo que realmente necesitara. Y hace unas semanas, cuando estaba sufriendo con la tarea de tener que pensar todos los días qué comer, me iluminé: una app de planificación de comidas semanal, ese tenía que ser el proyecto con el cual finalmente aprendiera a hacer una shinyApp. ¡Manos a la obra!
¿Qué es y cómo funciona una Shiny App?
Shiny es un paquete de R que permite crear aplicaciones web, que se pueden hostear como sitios web autónomos o se pueden incrustar en otros documentos como RMarkdown.
Desde la opción de nuevo archivo en RStudio, elegimos la opción Shiny Web App
que nos va a abrir un script llamado app.R
. Allí vamos a encontrar una app modelo que nos va a servir para entender cómo funciona esta lógica. Antes de seguir, te recomiendo que hagas click en Run App
y que juegues un poco con ella.
El código está comentado (en inglés) y es bastante claro, pero vamos a revisar sus partes. A grandes rasgos, toda shinyApp tiene tres partes:
El ui que es la interfaz de usuario, es decir, todo lo que define lo que el usuario va a manipular o con lo que va a interactuar (por ejemplo, el input que le puede dar).
El server que es lo que efectivamente se está ejecutando. Es la parte más parecida a un script de R normal.
La función
shinyApp()
que conecta las partes anterior.
En el caso de este ejemplo, esta shinyApp grafica un histograma sobre la frecuencia de espera (en minutos) hasta la próxima erupción de Old Faithful, un géiser ubicado en el parque Yellowstone en Wyoming (sí, el del Oso Yogi). El usuario (nosotros) puede controlar el ancho de las columnas del histograma para ver las frecuencias expresadas en distinta cantidad de minutos.
Veamos la parte de ui:
ui <- fluidPage(
# Título de la app
titlePanel("Old Faithful Geyser Data"),
# Barra lateral con control deslizante
sidebarLayout(
sidebarPanel(
sliderInput("bins",
"Number of bins:",
min = 1,
max = 50,
value = 30)
),
# Define el gráfico que se muestra como resultado
mainPanel(
plotOutput("distPlot")
)
)
)
Toda la sección de ui está contenida dentro de ui <- fluidPage()
que es la función que especifica un diseño de página fluida que se adaptará a distintos tipos de pantalla. Dentro de esa función aparecerán los distintos elementos que el usuario verá en la shinyApp.
El primer de esos elementos es el título con la función titlePanel
, cuyo argumento obligatorio es el título de la app, pero que también acepta el título de la ventana (windowTitle = "..."
).
El segundo de los objetos define que la app tendrá un diseño con la barra lateral (sidebarLayout()
) y con un panel principal. Con la función sidebarPanel()
definimos todo lo que aparecerá uno abajo del otro en la barra lateral izquierda, que en este caso solo es un control deslizante (sliderInput()
) que el usuario modificará para definir lla cantidad de barras (bins
) que tendrá el histograma. El segundo elemento dentro de la diagramación es el mainPanel()
: todo lo que aparezca dentro de esta función será el contenido del panel principal, a la derecha de la barra lateral. En este caso, se mostrará el resultado (output) del gráfico (plot), por eso la función es plotOutput()
, que toma como argumento el objeto distPlot
. ¿Por qué ese argumento? Lo veremos en un momento.
En resumen, esta sección nos muestra el título (titlePanel()
), una sección de barra lateral (siderbarLayout()
) donde habrá un elemento interactivo (sliderInput()
) que le permitirá al usuario elegir la cantidad de barras que tendrá el histograma resultante, y un panel principal (mainPanel()
) que define el tipo de resultado que habrá en el centro de la pantalla (en este caso, el objeto distPlot
).
Veamos qué tenemos en la sección server:
# Define la lógica del servidor para crear el histograma
server <- function(input, output) {
output$distPlot <- renderPlot({
# genera las barras a partir de input$bins de la sección ui
x <- faithful[, 2]
bins <- seq(min(x), max(x), length.out = input$bins + 1)
# crea el histograma con la cantidad especificada de barras
hist(x, breaks = bins, col = 'darkgray', border = 'white',
xlab = 'Waiting time to next eruption (in mins)',
main = 'Histogram of waiting times')
})
}
La sección server gestiona la operación que se realiza para mostrar el resultado buscado, por eso tiene la sintaxis de una función. Toma dos argumentos, el input
que es el valor definido por el usuario, y el output
que es el objeto resultado (estos dos elementos son fijos). Y dentro del código de la función, se construye el objeto output$distPlot
que estaba definido en la sección de ui; a su vez, este ejecuta la función renderPlot()
que contiene el código para crear el histograma. Este código no tiene nada de raro, pero fíjense en el argumento length.out = input$bins + 1
: allí se llama nuevamente al objeto input
que es el definido por el usuario.
Finalmente, la ShinyApp termina con una línea de código que unifica ambas partes:
# Corre la app
shinyApp(ui = ui, server = server)
Si bien la estructura general parece sencilla, tiene un alto grado de complejidad. Por suerte, el sitio web de Posit tiene una serie de tutoriales para aprender más sobre cómo funciona esta herramienta.
Ahora sí, mi ShinyApp
Como les comenté más arriba, mi primera ShinyApp tuvo el desafío de ayudarme a decidir qué comer. Soy una acumuladora serial de recetas, pero las tengo todas desorganizadas. Armar esta app me obligó a organizarlas en una tabla, que tiene principalmente cuatro columnas: el nombre del plato, la categoría (principal, acompañamiento, postre), tipo de contenido (omnívoro, vegetariano, vegano) y el link a la receta. Tiene también otras columnas que se encuentran en preparación.
Sin más nada que agregar, el código de mi app es el siguiente:
# Setup -------------------------------------------------------------------
#library(shiny)
library(tidyverse)
library(tibble)
base_comidas <- read.csv("base_comidas.csv", stringsAsFactors = TRUE)
# Define UI
ui <- fluidPage(
# Application title
titlePanel("Planificador de comida"),
# Sidebar para elegir tipo de comida
sidebarLayout(
sidebarPanel(
selectInput(inputId = "tipo_comida",
label = "Tipos de comida:",
choices = levels(base_comidas$Categoría),
selected = "Elija un tipo de comida"),
checkboxGroupInput(inputId = "contenido",
label = "¿Cómo desea que sea su comida?",
choices = levels(base_comidas$Contenido)),
checkboxGroupInput(inputId = "dias",
label = "¿Para cuántos días quiere planificar?",
choices = c("Lunes", "Martes", "Miércoles",
"Jueves", "Viernes", "Sábado",
"Domingo")),
checkboxGroupInput(inputId = "momento",
label = "¿Qué comidas quiere planificar?",
choices = c("Almuerzo", "Cena")),
radioButtons(inputId = "repeticiones",
label = "¿Quiere repetir las comidas?",
choices = c("Sí" = "TRUE",
"No" = "FALSE")),
actionButton("plan_button", "Armá el plan")
),
# Mostrar la comida elegida
mainPanel(
dataTableOutput(outputId ="comida")
)
)
)
# Define server logic
server <- function(input, output) {
observeEvent(input$plan_button, {
# Obtener la elección de comida
eleccion <- base_comidas %>%
filter(Categoría == as.character(input$tipo_comida) &
Contenido == as.character(input$contenido)) %>%
select(Nombre)
# Generar todas las combinaciones de días y momentos
combinaciones <- expand.grid(dias = input$dias, momento = input$momento)
# Elegir comida al azar para cada combinación
comidas_seleccionadas <- combinaciones %>%
mutate(comida = sample_n(eleccion, size = nrow(combinaciones), replace = as.logical(input$repeticiones)))
# Crear data frame de usuario
comidas_user <- comidas_seleccionadas %>%
pivot_wider(values_from = "comida",
names_from = "momento") %>%
rename(Días = dias)
# Devolver tabla
output$comida <- renderDataTable(comidas_user, options = list(searching = FALSE,
paging = FALSE,
info = FALSE))
})
}
# Run the application
shinyApp(ui = ui, server = server)
Pueden ver la versión online del planificador aquí. Hay varias cuestiones que me gustaría mejorar; por ejemplo, pensar varias comidas que tengan un mismo ingrediente principal es fundamental para reducir la cantidad de compras y de tiempo de preparación de las comidas. Me gustaría también incluir una variable de “tiempo de preparación” o “complejidad de preparación”, para poder diferenciar la disponibilidad de cada día. Y, finalmente, sí: tengo que trabajar en el aspecto estético.
Como siempre, recordá que podés suscribirte a mi blog para no perderte ninguna actualización, y si te quedó alguna consulta no dudes en contactarme. Y, si te gusta lo que hago, podés invitarme un cafecito desde Argentina o un kofi.