Pull to refresh

«Оч.умелые ручки»: делаем Tableau/Qlik из R и «синей изоленты»

Reading time 4 min
Views 5.4K

Является продолжением предыдущих публикаций.


Естественно, что название является потешным, но, как хорошо известно, в каждой шутке есть доля правды. Сама тема возникла, когда в очередной сотый раз пришлось слышать настойчивое пожелание о том, что необходим «гибкий конструктор отчетов/графиков». После определенного момента проще взять и сделать, чем в очередной раз объяснять, что tidyverse покрывает все необходимые потребности.


Сама постановка задачи предельно проста: обеспечить графический интерфейс для рисования разнообразных графических представлений по произвольным табличным данным. Классическое решение представляет собой две связанные сущности:


  • интерфейс с большим-большим количеством менюшек и кнопочек, с множественными закулисными IF для управления взаимными состояниями этих элементов;
  • «гибкий плоттер» с большим количеством вложенных IF для отрисовки графиков в соотвествии со скормленным данными и положением кнопочек-ползунков, выставленных в UI.

С одной стороны делать «Yet Another Tableau» совершенно неинтересно. С другой стороны, постановка в стиле «сделать так, чтобы все было, но ничего не надо делать» — типичная задача для ТРИЗ.


В целом, после непродолжительных размышений было выработано решение, которое почти удовлетворяет последней постановке. Само Shiny приложение пока под NDA, свободно публикуемый прототип приведен на картинке.



Две ключевых идеи по упрощению задачи следующие (ничего нового, все уже придумано до нас):


  1. вместо статически заданного UI переходим к динамически генерируемому;
  2. используем интерпретатор R не только для исходного кода, но и внутри самого кода.

Идея 1. Динамический web-интерфейс


Вариант, когда все управляющие элементы статически заданы и меняется лишь их параметризация (название, состояние, списки, выбранные элементы ...) удобен на этапе дизайна. Все понятно, все очевидно, можно ручками пощупать. Но если допустимые состояния этих элементов очень сильно связано как с исходными данными для анализа (data.frame), так и с состоянием друг друга, мы попадаем в ситуацию весьма большого количества нетривиальных обработчиков событий по каждому элементу. Много-много запутанного кода.


Сделаем по-другому. Вместо UI элементов со сложным поведением раскидываем с помощью uiOutput placeholder-ы, в которые динамически рассчитываем и генерируем с помощью shiny::renderUI представление этого элемента. Все внешние параметры, требуемые для генерации элемента, трактуем как реактивные элементы (reactive). При этом все такие интерактивные элементы выступают в качестве «автономных агентов», которые смотрят на окружение и подстраиваются под него. Пользователь изменил состояние одного элемента — все зависимые стали пересчитывать по очереди свое состояние (мы явно не обрабатываем события, а используем реактивный подход shiny). При изменении их состояния могут возникнуть новые индуцированные изменения. И так, пока все не стабилизируется.


В результате, в коде остается только один обработчик (кнопка «Go»)
  observeEvent(input$gen_plot, { # код демонстрирует принцип

    escname <- function(x){
      # имена колонок надо закавычить
      # .....
    }

    point_code <- ""
    if(input$shape_type!="__NO_MAPPING__") {
      aes <- c("shape"=escname(input$aes_shape_col), "color"=escname(input$aes_color_col))
      point_code <- buildPointCode(fixed=c("shape"=input$shape_type, "color"=glue("'{input$plot_color}'")), aes=aes)
    }

    line_code <- ""
    if(input$line_type!="__NO_MAPPING__") {
      aes <- c("linetype"=escname(input$aes_linetype_col), "color"=escname(input$aes_color_col))
      line_code <- buildLineCode(fixed=c("linetype"=input$line_type, "color"=glue("'{input$plot_color}'")), aes=aes)
    }

    gcode <- glue("ggplot(data_df(), aes(x=`{input$x_axis_value}`, y=`{input$y_axis_value}`))\\
                  {point_code} {line_code} + xlab('{input$x_axis_label}')") %>%
      style_text(scope="spaces")

    plot_Rcode(gcode)
  })  

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


Идея 2. Повторное использование интерпретатора R


Многие любят тыкать в то, что R «медленный, потому что он — интерпретатор». Опуская необоснованность и голословность подобного утверждения (в детали ведь никто не желает опускаться), используем эту «слабость», как силу.


Вместо того, чтобы писать сложный «гибкий плоттер», который при генерации графики будет параметризированно процессить (welcome to Non-Standard-Evaluation!) исходные данные и учитывать все нюансы состояния UI, вместо него сделаем генератор tidyverse диалекта R кода (как строки), который при последующем программном исполнении (eval) будет генерировать требуемый график:


  output$staticPlot <- renderPlot({
    base::eval(parse(text=req(plot_Rcode)))
  })

Fin


Shiny прототип «а-ля Tableau» укладывается в 250 строк кода, включая UI часть, комментарии и множественную валидацию (ассерты) и стоит по лицензиям 0 рублей 0 копеек.


С наступающим 2018-м годом!


Предыдущая публикация — «R и Информационная безопасность. Как устранить противоречие интересов и запустить R на Linux в оффлайн-режиме».

Tags:
Hubs:
+8
Comments 6
Comments Comments 6

Articles