Date Редакция Категория comp Теги R / Shiny / учебник

Перевод-пересказ руководства.

Приложения Shiny хороши тем, что запускаются быстро, практически мгновенно. Но что если вашему приложению нужно выполнить много медленных вычислений?

Этот урок покажет вам, как оптимизировать приложения Shiny, содержащие реактивные выражения. Реактивные выражения позволят вам управлять тем, какие именно части вашего приложения будут выполняться, реагируя на обновления виджетов, что позволит избежать ненужной работы.

Для начала:

  • создайте новую папку с именем stockVis в своём рабочем каталоге;
  • загрузите следующие файлы и поместите их в stockVis: ui.R, server.R и helpers.R;
  • запустите приложение с помощью runApp("stockVis").

StockVis использует пакет расширения R quantmod, так что нужно установить его с помощью

install.packages("quantmod")

если он у вас ещё не установлен.

runApp("stockVis")

Новое приложение: stockVis

Приложение stockVis ищет цены на акции по тикерам и отображает результаты в виде графика. Приложение позволяет:

  • выбрать акцию для наблюдения;
  • указать диапазон времени наблюдения;
  • выбрать, указывать ли по оси Y цену на акцию или логарифм цены;
  • задать, следует ли корректировать цены с учетом инфляции.

stockVis1.png

Заметим, что флажок (check box) "Adjust prices for inflation" пока не работает, и одна из задач урока — исправить это положение.

По умолчанию stockVis отображает тикер SPY соответствующий индексу S&P 500. Чтобы найти котировки других акций, введите символ акции, принятый в Yahoo! Finance. Вы можете найти список подобных символов здесь. Некоторые популярные символы: GOOG (Google), AAPL (Apple) и GS (Goldman Sachs).

Приложение StockVis использует две функции из пакета quantmod:

  • usesgetSymbols — для загрузки финансовых данных прямо в R из сайтов, подобных Yahoo! Finance и Federal Reserve Bank of St. Louis.
  • chartSeries — для отображения цен на графике.

StockVis также зависит от скрипта helpers.R, содержащего функцию, которая вносит в цены на акции поправку на инфляцию.

Флажки и диапазоны данных

В приложении stockVis используется несколько новых виджетов:

  • селектор диапазона данных (date range selector), созданный с помощью функции dateRangeInput, и
  • пара флажков checkboxInput. Флажки устроены просто: они возвращают TRUE, если флажок включен, и FALSE, если флажок не установлен.

В скрипте ui.R эти флажки названы log и adjust. Это означает, что вы можете просмотреть их значения input$log и input$adjust в скрипте server.R. Если вам нужно вспомнить как использовать виджеты и их значения, загляните в Урок 3 и Урок 4.

Оптимизируем вычисления

В приложении stockVis есть проблема.

Посмотрим, что произойдет при нажатии "Plot y axis on the log scale": значение input$log изменится, в результате будут заново пересчитаны все команды внутри renderPlot:

output$plot <- renderPlot({
  data <- getSymbols(input$symb, src = "yahoo", 
    from = input$dates[1],
    to = input$dates[2],
    auto.assign = FALSE)

  chartSeries(data, theme = chartTheme("white"), 
    type = "line", log.scale = input$log, TA = NULL)
})

Каждый раз при выполнении renderPlot:

  • он заново получает данные от Yahoo! Finance с помощью getSymbols, и
  • он перерисовывает график с нужным видом оси.

Это не слишком хорошо, поскольку для повторного построения графика вовсе не нужно вновь получать данные. Кроме того, Yahoo! Finance может заблокировать доступ, если ваше приложение будет слишком часто к нему обращаться (потому что оно будет выглядеть как бот). Но главное: повторный вызов getSymbols — это лишнее действие, которое только напрасно нагружает сервер, и может замедлить работу приложения.

Реактивные выражения reactive

Вы можете ограничить число перезапусков, вызванных изменениями виджетов, используя реактивные выражения.

Реактивное выражение — это выражение на R, которое использует входные данные виджета и возвращает значение. Реактивное выражение будет обновлять это значение всякий раз, когда изменяется значение виджета.

Для создания реактивных выражений используется функция reactive, которая принимает на вход выражение, заключенное в фигурные скобки (так же, как и функции render*).

Вот, например, как выглядит реактивное выражение, использующее виджеты stockVis для выборки данных из Yahoo:

dataInput <- reactive({
  getSymbols(input$symb, src = "yahoo", 
    from = input$dates[1],
    to = input$dates[2],
    auto.assign = FALSE)
})

При выполнении dataInput будет вызываться getSymbols и возвращать результаты в виде таблицы с данными о ценах. Вы можете построить график ценовых данных, вызывая dataInput в renderPlot:

output$plot <- renderPlot({    
  chartSeries(dataInput(), theme = chartTheme("white"), 
    type = "line", log.scale = input$log, TA = NULL)
})

Реактивные выражения устроены немного "умнее" обычных функций R. Они кэшируют свои значения и знают, когда их те устаревают. Что это значит? При первом запуске реактивного выражения, оно сохранит результат выполнения в памяти компьютера. В следующий раз, когда вы вызовете реактивное выражение, оно может вернуть этот сохранённый результат, не делая никаких вычислений (что сделает ваше приложение более быстрым).

Реактивное выражение будет возвращать сохранённый результат до тех пор, пока этот результат остаётся актуальным. Если реактивному выражению стало известно, что результат устарел (потому что изменилось состояние виджета), то это выражение будет пересчитано. Тогда оно вернёт новый результат, сохранит его копию и будет использовать эту копию, пока она в свою очередь не устареет.

Сформулируем алгоритм поведения реактивных выражений.

  • Реактивное выражение сохраняет результат, полученный из него при первом запуске.
  • Во время следующего вызова реактивное выражение проверяет, не устарело ли сохраненное значение (т. е. не изменилось ли состояние виджета, от которого это значение зависит).
  • Если значение устарело, реактивное выражение пересчитает его (и сохранит новый результат).
  • Если значение не устарели, реактивное выражение вернет сохранённое значение, не выполняя никаких вычислений.

Вы можете использовать это поведение, чтобы предотвратить повторное выполнение кода в Shiny.

Рассмотрим, как реактивное выражение будет работать в новой версии приложения stockVis. Изменения в коде приведены ниже.

# server.R

library(quantmod)
source("helpers.R")

shinyServer(function(input, output) {

  dataInput <- reactive({
    getSymbols(input$symb, src = "yahoo", 
      from = input$dates[1],
      to = input$dates[2],
      auto.assign = FALSE)
  })

  output$plot <- renderPlot({    
    chartSeries(dataInput(), theme = chartTheme("white"), 
      type = "line", log.scale = input$log, TA = NULL)
  })
})

При нажатии "Plot y axis on the log scale", изменяется input$log, что вызывает запуск renderPlot. Сейчас

  • renderPlot вызывает dataInput();
  • dataInput проверяет, не изменились ли виджеты dates и symb;
  • dataInput возвращает сохранённый им ранее набор данных с ценами на акции, не скачивая заново данные из Yahoo!
  • renderPlot построит новый график с заданным пользователем видом оси координат.

Зависимости

Что произойдёт, если пользователь изменит символ акции в виджете symb?

График, построенный функцией renderPlot, устареет. Но ведь renderPlot больше не вызывает input$symb! Узнает ли Shiny, что input$symb изменился и график нужно перестроить?

Да, Shiny узнает и заново построит график. Shiny отслеживает, от каких реактивных выражений зависит объект output, точно также как и то, от каких значений виджетов он зависит. Shiny автоматически перестроит объект, если

  • входное значение в render*-функции объекта изменилось или
  • значения реактивных выражений в render*-функции объекта устарели.

Думайте о реактивных выражениях, как о звеньях цепи, связывающей значения input с объектами output. Объекты в output будет реагировать на любые изменения, сделанные в других звеньях цепочки. (Цепь может оказаться весьма длинной, поскольку одни реактивные выражения могут вызывать другие реактивные выражения).

Вызывать реактивные выражения можно только из функций reactive или render*. Почему? Потому что только эти функции R приспособлены иметь дело с реактивными выражениями, значения которых может изменится без предупреждения. На практике, Shiny помешает вам вызвать реактивные выражения откуда бы ни было, кроме указанных выше двух типов функций.

Разминка

Настало время наладить работу флажка "Adjust prices for inflation". Пользователь должен иметь возможность переключаться между ценами с поправкой на инфляции и ценами без такой поправки.

Функция adjust в helpers.R использует данные Consumer Price Index, предоставляемые Федеральным резервным банком Сент-Луиса, чтобы преобразовать исторические данные по ценам к ценам сегодняшнего дня. Но как это можно реализовать в приложении?

Ниже представлен один из вариантов решения, хотя и не идеальный. Посмотрим, сможете ли вы определить в чём загвоздка. Да, это снова связано с input$log.

# server.R

library(quantmod)
source("helpers.R")

shinyServer(function(input, output) {

  dataInput <- reactive({
    getSymbols(input$symb, src = "yahoo", 
        from = input$dates[1],
        to = input$dates[2],
        auto.assign = FALSE)
  })

  output$plot <- renderPlot({   
    data <- dataInput()
    if (input$adjust) data <- adjust(dataInput())

    chartSeries(data, theme = chartTheme("white"), 
        type = "line", log.scale = input$log, TA = NULL)
  })
})

adjust вызывается внутри renderPlot. Если флажок adjust установлен, то приложение будет корректировать все цены при каждом переключении из обычного масштаба оси y в логарифмический. Такая перестройка — это лишняя работа.

Ваша очередь

Исправьте эту проблему, добавив новое реактивное выражение в приложение. Реактивное выражение должно принимать значение dataInput и возвращать откорректированную (или неоткорректированную) копию данных.

Когда вы решите, что справились с этим, сравните своё решение с ответом, приведенным ниже. Убедитесь, что вы понимаете, какие расчеты будут производится в вашем приложении, а какие не будут, когда пользователь нажмет "Plot y axis on the log scale".

# server.R

library(quantmod)
source("helpers.R")


shinyServer(function(input, output) {

  dataInput <- reactive({  
      getSymbols(input$symb, src = "yahoo", 
          from = input$dates[1],
          to = input$dates[2],
          auto.assign = FALSE)
  })

  finalInput <- reactive({
    if (!input$adjust) return(dataInput())
    adjust(dataInput())
  })

  output$plot <- renderPlot({
    chartSeries(finalInput(), theme = chartTheme("white"), 
        type = "line", log.scale = input$log, TA = NULL)
  })
})

Теперь вы смогли изолировать каждый ввод (input), поместив его в собственное реактивное выражение или в функцию семейства render*. Если значения на входе изменятся, то повторно вычисляться будут только выражения, значения которых устарели.

Вот как примерно будет выглядеть работа с приложением:

  • пользователь нажимает виджет "Plot y axis on the log scale";
  • выполняется renderPlot;
  • renderPlot вызывает finalInput;
  • finalInput проверяет значения dataInput и input$adjust;
  • если они не изменились, finalInput возвращает сохранённое им ранее значение;
  • если что-то изменилось, finalInput вычисляет новое значение, в зависимости от текущих значений входных параметров. Он передаёт это значение renderPlot и сохраняет его, чтобы использовать при следующих запросах.

Резюме

Вы можете сделать ваши приложения более быстрыми, используя модульную организацию кода и реактивные выражения.

  • реактивное выражение принимает входные значения или значения, переданные ему другими реактивными выражениями и возвращает некоторое значение;
  • реактивные выражения сохраняют вычисленные ими значения до тех пор, пока не получать новые значения входных параметров;
  • создать реактивное выражение можно функцией reactive({ });
  • вызвать реактивное выражение можно по его имени, за которым следуют скобки ();
  • вызвать реактивное выражение можно только: 1) из другого реактивного выражения; 2) из render*-функции.

Теперь вы сможете создавать на Shiny достаточно продвинутые и быстрые приложения. Последний урок из данного руководства покажет вам как поделиться своим приложением с другими.



Комментарии

comments powered by Disqus