Shinyチュートリアル レッスン6: リアクティブな表現を使用する

この記事は約13分で読めます。

この記事はShiny公式チュートリアルを翻訳したものです。
公式サイト: https://shiny.posit.co/r/getstarted/shiny-basics/lesson1/index.html

チュートリアルの目次はこちら

shinyアプリは、驚くべきほど即座に高速に実行されます。しかし、アプリで大量の計算負荷の高い計算を実行する必要がある場合はどうなるでしょうか?

このレッスンでは、リアクティブな表現を使用してShinyアプリを効率化する方法を説明します。リアクティブな表現を使用すると、アプリのどの部分をいつ更新するかを制御できるため、アプリの速度を低下させる可能性がある不必要な計算を防ぐことができます。

始めるためには以下のことをしておきます:

  • 作業ディレクトリにstockVisという名前を付けた新しいフォルダーを作成します。
  • 次のファイルをダウンロードして、stockVis内に配置します: app.R, helpers.R
  • runApp("stockVis")でアプリを起動します。

stockVisはRのquantmodパッケージを使用するため、まだお持ちでない場合はinstall.packages("quantmod")でインストールする必要があります。

runApp("stockVis")

新たなアプリ: StockVis

StockVisアプリはティッカーシンボルで株価を検索し、結果を折れ線グラフで表示します。アプリを使用して、

  1. 調べる銘柄を選択してください
  2. 確認する日付の範囲を選択してください
  3. y 軸に株価をそのままプロットするか、株価の対数をプロットするかを選択します。
  4. インフレを考慮して価格を修正するかどうかを決定します。

“Adjust prices for inflation”というチェックボックスはまだ機能しないことに注意してください。このレッスンのタスクの1つは、このチェック ボックスを修正することです。

デフォルトでは、stockVisはSPYティッカー (S&P 500全体の指数) を表示します。別の銘柄を検索するには、Yahoo Finance が認識する銘柄記号を入力します。ここでYahooの銘柄記号を検索できます。一般的なシンボルには、GOOG (Google)、AAPL (Apple)、GS (Goldman Sachs) などがあります。

StockVisは、quantmodパッケージの2つの関数に大きく依存しています。

  1. Yahooファイナンスやセントルイス連邦準備銀行などのWeb サイトから金融データを R に直接ダウンロードするためにgetSymbols関数が使用されます。
  2. 価格を魅力的なチャートで表示するためにchartSeris関数を使用します。

また、stockVisはhelpers.RというRスクリプトにも依存しています。このスクリプトには、インフレに合わせて株価を調整する関数が含まれています。

チェックボックスと日付範囲

StockVis アプリは、いくつかの新しいウィジェットを使用します。

  • dateRangeInputで作成された日付範囲セレクター
  • checkboxInputで作られたいくつかのチェックボックス。チェックボックスウィジェットは非常にシンプルです。チェックボックスがチェックされている場合はTRUEを返し、チェックボックスがチェックされていない場合はFALSEを返します。

チェックボックスはuiオブジェクト内にlogadjustとして保存され,server関数内でinput$logおよびinput$adjustとして検索できます。ウィジェットとその値の使用方法を確認したい場合は、レッスン 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関数の再実行のたびに

  1. getSymbols関数を使用して Yahoo Finance からデータを再取得し
  2. 正しい軸でチャートを再描画します。

プロットを再描画するためにデータを再取得する必要がないため、これは良くありません。実際、データを頻繁に再取得すると、Yahoo ファイナンスから遮断されてしまいます (ボットのように見え始めるため)。しかし、さらに重要なのは、getSymbols関数の再実行は不必要な作業であり、アプリの速度が低下し、サーバーの帯域幅が消費される可能性があります。

リアクティブな表現

リアクティブな表現を使用すると、リアクティブな動作中に再実行される内容を制限できます。

リアクティブな表現は、ウィジェットの入力値を使用して出力値を返すRコードです。リアクティブな表現は、元のウィジェットが変更されるたびにこの値を更新します。

リアクティブな表現を作成するには、reactive関数を使用します。この関数は、(render*関数と同様に)中括弧{}で囲まれたRコードを受け取ります。

たとえば、次のリアクティブな表現は、stockVisのウィジェットを使用して Yahoo からデータを取得します。

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

このコードを実行すると、getSymbols関数が実行され、結果として価格データのデータフレームが返されます。dataInput関数を呼び出すことで、renderPlot関数の中で価格データにアクセスできます。

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

リアクティブな表現は、通常のR関数よりも少し賢いです。値をキャッシュし、その値がいつ古くなったかを認識します。これはどういう意味でしょうか?リアクティブな表現を初めて実行すると、コードの結果がコンピュータのメモリに保存されます。次にリアクティブな表現を呼び出すときは、計算を行わずにこの保存された結果を返すことができます (これにより、アプリが高速化されます)。

リアクティブな表現は、結果が最新である (変更されていない) ことがわかっている場合にのみ、保存された結果を返します。リアクティブな表現が結果が捨てられたことを学習した場合 (ウィジェットの値が変更されたため)、コードは再計算をします。次に、新しい結果を返し、新しい結果を保存します。リアクティブな表現は、この新しいコピーも期限切れになるまで使用します。

この挙動を要約してみましょう。

  • リアクティブな表現は、最初に実行したときに結果を保存します。
  • 次にリアクティブな表現が呼び出されたときに、保存された値が古くなったかどうか (つまり、依存するウィジェットが変更されたかどうか) がチェックされます。
  • 値が古い場合、リアクティブオブジェクトは値を再計算します (その後、新しい結果が保存されます)。
  • 値が最新の場合、リアクティブな表現は計算を行わずに保存された値を返します。

この動作を使用すると、Shiny がコードを不必要に再実行するのを防ぐことができます。以下の新しいStockVisアプリでリアクティブな表現がどのように機能するかを考えてみましょう。

server <- 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関数が再実行されます。

  1. renderPlot関数はdataInput関数を呼び出します
  2. dataInput関数はdatessymbウィジェットが変更されていないことを確認します
  3. dataInput関数はYahooからデータを再取得することなく、保存された株価のデータセットを返します。
  4. renderPlot関数は正しい軸でグラフを再描画します。

依存関係

ユーザーがsymbウィジェット内の銘柄記号を変更したらどうなるでしょうか?

これにより、renderPlotによって描画されたプロットは古くなりますが、renderPlotinput$symbを呼び出しません。shinyはinput$symbによってプロットが古くなったことに気づくでしょうか?

shinyはそれを知っており、プロットを描き直します。Shinyは、outputオブジェクトがどのリアクティブな表現に依存しているかを、どのウィジェットの入力が依存しているかを追跡するのと同様に,追跡します。 Shinyは、次の場合にオブジェクトを自動的に再構築します。

  • render*関数内のinputの値が変更される、または
  • renderPlot関数のリアクティブな表現が古くなる

リアクティブな表現は、input値をoutputオブジェクトに接続するチェーン内のリンクとして考えてください。outputオブジェクトは、チェーンの下流の任意の場所で行われた変更に反応します。 (リアクティブな表現は他のリアクティブな表現を呼び出すことができるため、長いチェーンを形成できます。)

リアクティブな表現は、reactiveまたはrender*関数内からのみ呼び出します。なぜでしょうか?これらのR関数のみが、警告なしで,変化する可能性があるリアクティブ出力を処理する機能を備えています。実際、Shinyでは、これらの関数の外でリアクティブな表現を呼び出すことができません。

ウォームアップ

“Adjust prices for inflation”の機能しないチェックボックスを修正するタイミングが来ました。ユーザーは、インフレに合わせて調整された価格と調整されていない価格を切り替えることができる必要があります。

helper.Rの中のadjust関数は、セントルイス連邦準備銀行によって提供される消費者物価指数データを使用して、過去の価格を現在の価格に変換します。しかし、これをアプリにどのように実装できるのでしょうか?

以下に解決策の 1 つを示しますが、これは理想的ではありません。その理由がわかりますか?もう一度言いますが、それはinput$logと関係があります。

server <- 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関数の中で呼び出されます。調整ボックスがチェックされている場合、通常のY軸スケールから記録された対数Y軸スケールに切り替えるたびに、アプリはすべての価格を再調整します。この再調整は不要な作業です。

Your turn

この問題を解決するには、新しいリアクティブな表現をアプリに追加します。リアクティブな表現は、dataInputの値を受け取り、データの調整された (または調整されていない) コピーを返す必要があります。

理解できたと思ったら、自分の解決策を以下の模範解答と比較してください。ユーザーが“Plot y axis on the log scale” をクリックしたときにアプリでどのような計算が行われ、どのような計算が行われないかを必ず理解してください。

server <- 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)
  })
}

これで、各入力を独自のリアクティブな表現またはrender*関数に分離しました。入力が変更された場合、古い式のみが再実行されます。

例題のフローを次に示します。

  • ユーザーが“Plot y axis on the log scale” をクリックします。
  • renderPlotを再実行します。
  • renderPlot関数はfinalInputを呼び出します。
  • finalInputdataInputinput$adjustをチェックします。
  • どちらも変更されていない場合は、finalInputは保存された値を返します。
  • いずれかが変更されていた場合は、finalInputは現在の入力値を使用して新しい値を計算します。新しい値をrenderPlotに渡し、将来のクエリのために新しい値を保存します。

要約

リアクティブな表現を使用してコードをモジュール化することで、アプリを高速化できます。

  • リアクティブな表現はinput値、または他のリアクティブな表現からの値を受け取り、新しい値を返します。
  • リアクティブな表現は結果を保存し、入力が変更された場合にのみ再計算します。
  • リアクティブな表現をreactive({})によって作成します。
  • 式の名前の後にかっこ「()」を付けてリアクティブな表現を呼び出します。
  • 他のリアクティブな表現またはrender*関数内からのみリアクティブな表現を呼び出します

洗練され効率化されたShinyアプリを作成できるようになりました。このチュートリアルの最後のレッスンでは、アプリを他の人と共有する方法を説明します。

コメント