戯言日記

Rの話だと思ったら唐突にサバゲーが混じってくる何か。

DTの*_rows_selectedでデータテーブルからプロットを直接制御する(Shiny)

こちらはR Advent Calendar 2020 21日目の記事です。

qiita.com

8日目の記事でRアドカレに初参加した分際で2回目もやるってどうなのかとも思ったんですが、カレンダーが空いてるとひとまず予定突っ込みたくなる病気のせいで気が付いたら空いてる枠をクリックしてた()

という訳で、今回はShiny上で表示したデータテーブルの列をクリックで選択して、その列のデータでグラフを描く方法について書きます。
それと、Shinyを動作させられる状態でブログに貼る方法が分からないので、お手数ですがコードについては自身の環境で走らせてください(資料として画像は適宜貼ります)。


ちなみに今回のデータテーブルにはDTパッケージを利用する。

rstudio.github.io

そのままデータを渡すだけでも非常に分かりやすい&動的なデータテーブルを作ってくれる代物で、RMarkdownやShinyにおけるインタラクティブな表を記載する際に重宝する。
見た目も弄れるので、とりあえずデータテーブル=DTパッケージでもいいと思う。


で、このパッケージの強力な点は拡張機能が豊富なところ。
一例を挙げると、

  • 各行に対するフィルターや表示・非表示選択
  • 列のドラッグ&ドロップによる並び替え機能
  • 縦横のスクロールバーの実装1
  • 表示しているデータをcsvやpdfなどでダウンロードできるボタンの実装2

この辺りの機能が、関数内で少し記述を加えるだけで簡単に実装できてしまう。
複数機能を追加する場合はやや混乱しやすい3が、下記を参考にしていただければ何となく概要は掴めると思う。
日本語で解説しているページも多いので、調べてみると面白い。


iris %>% DT::datatable(
  rownames=FALSE,
  filter="top", #行のフィルターの実装
  extensions=c("Buttons", "ColReorder"), #ボタンと行の並び替えの実装
  options=list(autoWidth=TRUE, #表の幅の自動調整
               dom="RBlfrtip", #表の要素の順番(B=ボタン、t=表など)
               lengthMenu = c(10, 20, 50), #表示できる列数
               scrollX = TRUE, scrollY = "400px",
               scrollCollapse = TRUE, #スクロール機能
               buttons=c("colvis", "csv") #実装するボタンの種類
               )
)

f:id:doubtpad:20201220234116p:plain
こんな形のデータテーブルが出力できる


拡張機能が豊富すぎて逆に困るレベルのDT::datatableなのだが、あまり使っている事例を見かけない機能もある。 そのうちの1つが、Shinyで利用する場合に使える「データテーブル上で選択した行の順番が取得できる」機能。
どういうことかと言うと、これを使うことでデータテーブルから選択したデータをプロットしたり、そのデータを解析したり、動的な処理に活用できる。

rstudio.github.io

※上記ページの「2.1.1 Row Selection」参照

個人的には結構便利な機能なのだが、関数内で特に設定もせずに使える機能4だからなのか、Shiny限定の機能だからなのか、日本語で検索してもあまり引っ掛からない5

という訳で、機能を布教する目的で紹介させていただきたい。
ちなみに公式ではirisデータを使って「選択した行のプロットの色を変える」図を作っているので、少し違う角度で試してみる。

まずは適当にShinyでデータテーブルを用意。

library(shiny)
library(tidyverse)
library(DT)

ui <- fluidPage(
    titlePanel("Test"),
    mainPanel(
        DTOutput("dataTable")
    )
)

server <- function(input, output) {
    output$dataTable <- renderDT( diamonds )
}

shinyApp(ui = ui, server = server)


ちなみに、最初の表みたいに機能を拡張したい場合もほぼ同様に行える。

library(shiny)
library(tidyverse)
library(DT)

ui <- fluidPage(
    titlePanel("Test"),
    mainPanel(
        DTOutput("dataTable")
    )
)

server <- function(input, output) {
    output$dataTable <- renderDT(
        diamonds,
        rownames=FALSE, filter="top", extensions=c("Buttons"),
        options=list( autoWidth=TRUE, dom="RBlfrtip", buttons=c("colvis") )
        )
}

shinyApp(ui = ui, server = server)


本題の行選択についてやってみる。
データテーブルの行を選択すると、「何行目を選択したか」を数値ベクトルで返してくれるので、画面上に出してみる。

library(shiny)
library(tidyverse)
library(DT)

ui <- fluidPage(
    titlePanel("Test"),
    mainPanel(
        verbatimTextOutput("Rows"),
        DTOutput("dataTable")
    )
)

server <- function(input, output) {
    output$dataTable <- renderDT( diamonds, filter="top" )
    output$Rows <- renderPrint({ input$dataTable_rows_selected })
}

shinyApp(ui = ui, server = server)

f:id:doubtpad:20201220234800p:plain
選択した行の番号が出力されている


この行番号についてはデータテーブルに対するソートやフィルターには影響を受けない。
行を選択してからソートしてみても番号は変化しないし、適当にソートしてからデータテーブルの一番上の行を選択しても1行目とはならない。
この機能のおかげで、データテーブルをガンガン触りながら選択していくことが可能。


今回はdiamondsデータセットを例に、「ダイアモンドをカットと色で分けた場合のカラット数と値段の関係性」的なものを見れるようにしてみる。

  • データセットは必要な列のみ選択してグループ化、nest()した上でデータテーブルに表示
  • 各グループのデータ数も計算して追加
  • nest()で纏まったデータはlist()形式になるが、これをデータテーブル上に表示しようとすると表示がめちゃくちゃになるためrenderDT()には渡さない
  • 選択した行の番号を取得してグラフを描画する機能も追加

この辺を満たすようにコードを書いてみる。

library(shiny)
library(tidyverse)
library(DT)

dat <- diamonds %>%
    select(cut, color, carat, price) %>%
    group_by(cut, color) %>%
    mutate( cur_data() %>% tally() ) %>%
    nest(data=c(carat, price))

ui <- fluidPage(
    
    titlePanel("Test"),
    
    mainPanel(
        fluidRow(
            column(width=6, DTOutput("dataTable") ),
            column(width=6,
                   verbatimTextOutput("Rows"),
                   plotOutput("Fig", width = 400) )
        )
    )
)

server <- function(input, output) {
    
    output$dataTable <- renderDT( dat %>% select(-data), filter="top" )
    
    output$Fig <- renderPlot({
        dat[input$dataTable_rows_selected,] %>% unnest(data) %>%
            ggplot( aes(carat, price) ) + geom_point()
    })
    
    output$Rows <- renderPrint({ input$dataTable_rows_selected })
    
}

shinyApp(ui = ui, server = server)

f:id:doubtpad:20201220235020p:plain
選んだ行がプロットされる


なお、このままだと複数の行をクリックした時に全てのデータが1つのグラフにプロットされてしまう。

f:id:doubtpad:20201220235118p:plain
こんな感じ


そこでrenderDT()にselection="single"を加えると、1行ずつしか選択できなくなるため綺麗に動く。

library(shiny)
library(tidyverse)
library(DT)

dat <- diamonds %>%
    select(cut, color, carat, price) %>%
    group_by(cut, color) %>%
    mutate( cur_data() %>% tally() ) %>%
    nest(data=c(carat, price))

ui <- fluidPage(
    
    titlePanel("Test"),
    
    mainPanel(
        fluidRow(
            column(width=6, DTOutput("dataTable") ),
            column(width=6,
                   verbatimTextOutput("Rows")
                   , plotOutput("Fig", width = 400) )
        )
    )
)

server <- function(input, output) {
    
    output$dataTable <-
        renderDT( dat %>% select(-data), filter="top", selection="single" )
    
    output$Fig <- renderPlot({
        dat[input$dataTable_rows_selected,] %>% unnest(data) %>%
            ggplot( aes(carat, price) ) + geom_point()
    })
    
    output$Rows <- renderPrint({ input$dataTable_rows_selected })
    
}

shinyApp(ui = ui, server = server)


また、facet_grid()を使ってプロットを自動的に分割する方法でも対応できる。
こちらは複数行を比較できるので、例えば実験ごとのデータを見比べたりする際に便利。

library(shiny)
library(tidyverse)
library(DT)

dat <- diamonds %>%
    select(cut, color, carat, price) %>%
    group_by(cut, color) %>%
    mutate( cur_data() %>% tally() ) %>%
    nest(data=c(carat, price))

ui <- fluidPage(
    
    titlePanel("Test"),
    
    mainPanel(
        fluidRow(
            column(width=6, DTOutput("dataTable") ),
            column(width=6,
                   verbatimTextOutput("Rows"),
                   plotOutput("Fig", width = 400) )
        )
    )
)

server <- function(input, output) {
    
    output$dataTable <- renderDT( dat %>% select(-data), filter="top" )
    
    output$Fig <- renderPlot({
        dat[input$dataTable_rows_selected,] %>% unnest(data) %>%
            ggplot( aes(carat, price) ) + geom_point() +
            facet_grid(rows = vars(cut), cols = vars(color))
    })
    
    output$Rows <- renderPrint({ input$dataTable_rows_selected })
    
}

shinyApp(ui = ui, server = server)

f:id:doubtpad:20201220235343p:plain
因子で分割して表示できる


今回のようなグラフなら、例えばui.RにselectizeInput()を追加して、選択された行についてプロットする方法でも機能的には十分。
しかし、この方法ならデータテーブルとプロットをリンクすることで直感的な操作ができるため、データを感覚的に捉えやすくなる(と思う)。

「これが見たい!」と思った時に、対応する因子を確認して……選択肢から選んで……とやるより、見たい対象をデータテーブルでクリックするだけの方が思考にノイズが入らないので楽だと思う。


また、もし自分以外の人が使う可能性があるなら、直感的に操作できるUIにしておくことで説明がしやすくなる。
「データテーブルをクリックすればデータがプロットされます!」って言えば終わるとか強いと思う(小並感)。
あと作業量がそんなに変わらない割に何となく凄いことしているような雰囲気を出せるので、効果的に見せるという点でもオススメ。


ちなみに、列やセルに対する選択機能もあるため、応用すれば更に色々とできる。

library(shiny)
library(tidyverse)
library(DT)

nameList <- colnames(diamonds)

ui <- fluidPage(
    titlePanel("Test"),
    
    mainPanel(
        radioButtons("xAxis",
                     label="Factor",
                     choices = c("cut", "color", "clarity")),
        DTOutput("dataTable"),
        verbatimTextOutput("Rows"),
        plotOutput("Fig", width = 800)
    )
)

server <- function(input, output) {
    
    colNum <- reactive( input$dataTable_columns_selected )
    output$dataTable <-
        renderDT( diamonds, filter="top", selection=list(target="column") )
    
    output$Fig <- renderPlot({
        diamonds %>%
            ggplot(aes( !!sym(input$xAxis), !!sym(nameList[colNum()]) )) +
            geom_boxplot()
    })
    
    output$Rows <- renderPrint({ nameList[colNum()] })
}

shinyApp(ui = ui, server = server)

f:id:doubtpad:20201221000118p:plain
列ごとの選択→解析に便利


これも正直radioButtons()とかで実現できるけど、データテーブルで選んだ列が表示されるのはなかなか気持ちいい。

とりあえずShinyに触れ始めた人6は下のページを見ているだけでも夢が広がると思う。
rstudio.github.io

rstudio.github.io


DTパッケージのデータテーブルは他にもできることは多いので、ぜひ一度公式ページを眺めてみてください。

Enjoy!!



  1. Shinyでは使えないので注意(解説:https://rstudio.github.io/DT/extensions.html)。

  2. データテーブルに日本語が混ざっているとファイル内で文字化けするので、全て英語にしておく方が無難。

  3. 個人的にはoptionsのdomがやや厄介かも。行の並び替え("ColReorder")を実装する場合は大文字の"R"を追加する必要があったり、ボタンを実装した上で表の長さを変えるためのプルダウンメニューを表示するには"l"を追加したりと、単機能ごとの解説を読んでいるとハマる可能性はある(参考:https://datatables.net/reference/option/dom)。

  4. 正確には「行選択の場合は」特に設定もせずに使える。列やセルを選択するには設定が必要。

  5. 当社比。

  6. 筆者含む。