Я создаю довольно много приложений R Shiny, и одно из наиболее распространенных применений - обеспечение динамической фильтрации базовых данных, чтобы вы могли настраивать диаграмму или таблицу на основе определенного подмножества интересов.

Обычно мы жестко кодируем параметры фильтрации в меню боковой панели некоторой формы и определяем раскрывающиеся списки ввода для интересующих полей. Например, если мы фильтруем набор данных mtcars, мы могли бы предложить список выбора ввода, подобный этому, на стороне пользовательского интерфейса:

shiny::selectInput('cyl', "Select no of cylinders", choices = mtcars$cyl %>% unique() %>% sort())

а затем мы можем отфильтровать mtcars, используя input$cyl на стороне сервера, например:

df <- shiny::reactive({
   mtcars %>% dplyr::filter(cyl == input$cyl)
})

Я работал над созданием более общей структуры, которая обеспечивает полностью гибкую, динамическую систему фильтрации (похожую на фильтрацию в электронной таблице MSExcel), позволяющую пользователю выбирать любое поле, которое он хочет фильтровать, и любые значения для этого поля, которые они заинтересованы. Я также хотел настроить его так, чтобы он мог применяться в общем к любому набору данных, требуя от пользователя только указания набора данных и подмножества имен столбцов, для которых разрешена фильтрация.

Ссылочная прозрачность блестящих входов

Для этого нам нужно обойти нестандартные проблемы оценки. В приведенном выше примере mtcars cyl обрабатывается иначе, чемinput$cyl. cyl является ссылочно прозрачным в контексте mtcars, но input$cyl передается как входная переменная, и его значение указывается в кавычках. Так, например, если мы выберем input$cyl = 6, это будет интерпретироваться так, как мы ожидали в dplyr:

df <- mtcars %>% dplyr::filter(cyl == '6')

Итак, это работает нормально, но что, если бы мы также хотели выбрать столбец в качестве блестящего ввода? Например, вы можете захотеть, чтобы ваш пользователь мог выбирать из 20 или 30 различных столбцов для фильтрации. Возможно, мы сможем создать input$col для выбранного столбца для фильтрации и input$val для выбранного значения следующим образом:

df <- shiny::reactive({
   mtcars %>% dplyr::filter(input$col == input$val)
})

Что ж, нет, мы не можем, потому что input$col передается как значение в кавычках и не является ссылочно прозрачным.

Вот где вопросы могут быть действительно полезны и могут позволить вам абстрагироваться от ваших возможностей фильтрации (а также от ваших группировок, выбора и всех других прекрасных вещей, которые нам нравятся в dplyr).

Подумайте о настройке следующей простой функции, которая позволит вам выбрать как столбец, так и значение, по которому вы хотите выполнить фильтрацию:

filter1_by <- function(df, fcol1, fv1) {
  
  filter_var1 <- dplyr::quo(fcol1)
  df %>% filter_at(vars(!!filter_var1), all_vars(. == fv1))
}

Эта простая маленькая функция позволяет нам обрабатывать выбор столбца (fcol1) отдельно от выбора значения (fval1) с помощью filter_at(). fcol1 теперь фиксируется как выражение в кавычках вместе со своим окружением, что делает его ссылочно прозрачным, а затем мы просто используем оператор !! (bangbang), чтобы удалить кавычки, чтобы dplyr мог принять имя столбца без кавычек, но ссылочно прозрачным.

Излишне говорить, что это можно расширить, чтобы фильтровать любое количество входных данных, выбранных из любого количества столбцов. Например, функция, позволяющая использовать три фильтра, будет выглядеть так:

filter3_by <- function(df, fcol1, fv1, fcol2, fv2, fcol3, fv3) {
  filter_var1 <- dplyr::quo(fcol1)
  filter_var2 <- dplyr::quo(fcol2)
  filter_var3 <- dplyr::quo(fcol3)
df %>% 
     filter_at(vars(!!filter_var1), all_vars(. == fv1)) %>% 
     filter_at(vars(!!filter_var2), all_vars(. == fv2)) %>%
     filter_at(vars(!!filter_var3), all_vars(. == fv3))
)

Построим полный пример

Приступим к созданию полного примера с использованием mtcars. Допустим, мы хотим, чтобы пользователь мог выбрать до трех столбцов для фильтрации из любого из доступных столбцов в mtcars.

Сначала мы создаем вектор из имен столбцов mtcars, который мы будем использовать в качестве исходного выбора:

fields <- colnames(mtcars)

Давайте настроим три функции для фильтрации по одному, двум или трем столбцам:

# filter on 1 columns
filter1_by <- function(df, fcol1, fv1) {
  filter_var1 <- dplyr::quo(fcol1)
df %>% 
     filter_at(vars(!!filter_var1), all_vars(. == fv1))
)
# filter on 2 columns
filter2_by <- function(df, fcol1, fv1, fcol2, fv2) {
  filter_var1 <- dplyr::quo(fcol1)
  filter_var2 <- dplyr::quo(fcol2)
df %>% 
     filter_at(vars(!!filter_var1), all_vars(. == fv1)) %>% 
     filter_at(vars(!!filter_var2), all_vars(. == fv2))
)
# filter on 3 columns
filter3_by <- function(df, fcol1, fv1, fcol2, fv2, fcol3, fv3) {
  filter_var1 <- dplyr::quo(fcol1)
  filter_var2 <- dplyr::quo(fcol2)
  filter_var3 <- dplyr::quo(fcol3)
df %>% 
     filter_at(vars(!!filter_var1), all_vars(. == fv1)) %>% 
     filter_at(vars(!!filter_var2), all_vars(. == fv2)) %>%
     filter_at(vars(!!filter_var3), all_vars(. == fv3))
)

Теперь давайте создадим структуру блестящих входов, позволяющую фильтровать по крайней мере один столбец, но до трех столбцов, используя флажки, чтобы позволить пользователю решить, нужны ли ему дополнительные фильтры:

shiny::mainPanel(
  # select first filter column from fields vector 
  shiny::selectInput("filter1", "Select filter column 1:", 
                     choices = fields),
  # reference a uiOutput that will offer values for first column
  shiny::uiOutput("filter1choice"),
  # offer a checkbox to allow user to select a second filter
  shiny::checkboxInput("filter2req", "Add second filter?"),
  # set further conditional panels to appear in the same fashion
  shiny::conditionalPanel(condition = 'input.filter2req', 
                          shiny::uiOutput("filter2eval"),
                          shiny::uiOutput("filter2choice"),
                          shiny::checkboxInput("filter3req", 
                                               "Add third filter?")),
  shiny::conditionalPanel(condition = 'input.filter3req & 
                                       input.filter2req', 
                  shiny::uiOutput("filter3eval"),
                  shiny::uiOutput("filter3choice"))
 
)

Теперь нам нужно построить uiOutputs, которые заполняются в соответствии с выбранными столбцами фильтра и реагируют на то, что уже было выбрано:

# vector of picklist values for the first selected filter 
choicevec1 <- reactive({
    mtcars %>%  dplyr::select(input$filter1) %>% unique() %>% dplyr::arrange_(input$filter1)
})
# renders the picklist for the first selected filter
output$filter1choice <- renderUI(
  selectizeInput("filter1val", "Select filter 1 condition:", choices = choicevec1(), multiple = TRUE)
)
# second column chosen from all remaining fields
output$filter2eval <- renderUI({
  selectInput("filter2", "Select filter criteria 2:", choices = sort(fields[fields != input$filter1]))
})
# vector of picklist values for the second selected filter
choicevec2 <- reactive({
    filter1_by(mtcars, input$filter1, input$filter1val) %>% 
                       dplyr::select(input$filter2) %>% 
                       unique() %>% 
                       dplyr::arrange_(input$filter2)
})
# renders picklist for filter 2
output$filter2choice <- renderUI(
  selectizeInput("filter2val", "Select filter 2 condition:", choices = choicevec2(), multiple = TRUE)
)
# third column selected from remaining fields
output$filter3eval <- renderUI({
  selectInput("filter3", "Select filter criteria 3:", choices = sort(fields[!fields %in% c(input$filter1, input$filter2)]))
})
# vector of picklist values for third selected column
choicevec3 <- reactive({
    filter2_by(mtcars, input$filter1, input$filter1val, 
               input$filter2, input$filter2val) %>% 
               dplyr::select(input$filter3) %>% 
               unique() %>% 
               dplyr::arrange_(input$filter3)
})
# render picklist for filter 3
output$filter3choice <- renderUI(
  selectizeInput("filter3val", "Select filter 3 condition:", choices = choicevec3(), multiple = TRUE)
)

Так что это все, что нам нужно сделать со стороны пользовательского интерфейса.

На стороне сервера нам теперь просто нужно определить нужный нам фильтр на основе выбранных входных данных:

filtered_mtcars <- reactive({
  # case when all three filters are used
  if (input$filter3req & input$filter2req) {
    filter3_by(mtcars, input$filter1, input$filter1val, 
               input$filter2, input$filter2val,
               input$filter3, input$filter3val) 
  } else if (input$filter2req) {
  # case when two filters are used
    filter2_by(mtcars, input$filter1, input$filter1val, 
               input$filter2, input$filter2val) 
  } else {
  # case when only one filter is used   
    filter1_by(mtcars, input$filter1, input$filter1val)
  }
})

Теперь вы можете просто отображать свой реактивный фрейм данных filtered_mtcars() или выполнять с ним какие-либо манипуляции.

Использование этого метода на практике и дальнейшее развитие

Эти функции легко переносятся в любое разрабатываемое вами приложение, для которого требуется такая гибкая фильтрация. Вы можете легко расширить выбор фильтров, просто написав дополнительные функции filter4_by, filter5_by и т. Д. Вы также можете легко настроить возможность выбора нескольких значений в раскрывающихся списках, используя selectizeInput() и заменяя == на %in% внутри filter_at().

См. Здесь, чтобы увидеть действительно простое демонстрационное приложение, которое я создал, показывающее количество медалей на Олимпийских играх, которое позволяет использовать до трех фильтров. Код Github для этого находится здесь.

Вы можете помочь улучшить это? Вместо того, чтобы писать все эти отдельные функции filter1_by, filter2_by и т. Д., В идеале нам понадобится только одна функция фильтра, как показано ниже:

filter_by <- function (df, ...) {
  filter_conditions <- quos(...)
  df %>% dplyr::filter(!!!filter_conditions)
}

где входы - это списки условий. Эти входные данные потребуют некоторой настройки, чтобы справиться с нестандартной оценкой, но я еще не придумал, как их следует сформулировать.

Дай мне знать, если ты взломаешь это.

Изначально я был чистым математиком, затем стал психометриком и специалистом по анализу данных. Я увлечен применением всех этих дисциплин к сложным человеческим вопросам. Я также помешан на программировании и большой поклонник японских ролевых игр. Найдите меня в LinkedIn или Twitter.