RのDTパッケージを使うとJavaScriptのDataTablesライブラリ(DataTables | Table plug-in for jQuery)利用したインタラクティブなテーブルが作成できる。
しかし、JavaScriptのライブラリである都合上、Rでは可能なことが簡単には実現できないということもある。表題に挙げたNA
やInf
の扱いもその一つ。
NA
やInf
はnull
になる
まず、適当にNA
やInf
を含むデータフレームを作成する。
data <- tibble::tibble( x = c(1, 12, 112, NA, Inf, -Inf), y = c(1, 12, 112, NA, NA, NA), z = c(1, 12, 112, -999, 999, NA) ) data ## # A tibble: 6 × 3 ## x y z ## <dbl> <dbl> <dbl> ## 1 1 1 1 ## 2 12 12 12 ## 3 112 112 112 ## 4 NA NA -999 ## 5 Inf NA 999 ## 6 -Inf NA NA
これをDT::datatable()
で表示する。
library(DT) datatable(data, filter = "top")
このようにNA
もInf
も表示されない。これはデータが一旦JSONに変換されるが、JSONではNaN
やInfinity
を表現できないのでnull
で置換されて空で表示されるということらしい。なので当然だがソート時も正しく扱われない。
ただ、列xのようにInf
を含むとcolumn filterが効かなくなるので、単純にnull
だけかというとそうでもなさそうに見える。なお、後の例示でも出てくるのでfilter = "top"
を指定してcolumn filterを表示したが、これはDTパッケージの側で追加されている機能となっている(DataTablesでも同様のものを表示できるが、オプション1つでON/OFFできるレベルではなく、ある程度の実装が必要)。
NA
やInf
を表示したり、ソートで扱えるようにする方法はいくつか考えられる。ただ、いずれも一長一短という感じで、完全に問題がない方法は思いついていない。
DT.TOJSON_ARGS
オプションを使う
このオプションで以下のように指定すると、NA
やInf
が表示されるようになる。
options(DT.TOJSON_ARGS = list(na = "string"))
この方法には大きな欠点があり、上記のようにソートが文字列によるソートとなってしまう(上記スクリーンショットはx列でソートしている)。
ただ、Shinyを使ってサーバーサイド処理を行う場合はソートがRで行われるため、期待するようなソートになるらしい。これは試していないが、そうであれば特に欠点が無い方法のように思う。
na = NULL
で元の挙動に戻る。
options(DT.TOJSON_ARGS = list(na = NULL))
欠損値をすべてNA
やInfinity
に置き換える
やや乱暴ではあるが、出現する欠損値が1種類であれば、ある程度妥当な挙動となる。
欠損値を置き換えるには、options
引数の中で次のようにrender
にJavaScriptの関数を指定する。
datatable( data, filter = "top", options = list( columnDefs = list( list( targets = "_all", # 設定を適用するカラム。個別指定は数値で、"_all"はすべての列への指定となる。 type = "num", # この指定が無いと文字列でのソートになる。 render = JS( ' function(data, type, row, meta) { return data === null ? Infinity : data; } ' ) ) ) ) )
この場合、ソートも期待通りに動く。上記スクリーンショットではz列をソートしている。
なお、元データがInf
を含むとcolumn filterが動かないというのは変わらないので、この方法をとる場合はdatatable()
に渡す前にInf
をNA
に置換しておいたほうが良いだろう。
DT.TOJSON_ARGS
とrender
での置き換えを組み合わせる
DT.TOJSON_ARGS
でna = "string"
を指定するとNA
やInf
が表示されるが、JavaScriptでは解釈できないのでクライアントサイドの処理では文字列ソートになってしまうというのが課題だった。つまり、NaN
やInfinity
などのJavaScriptで解釈可能な形式に置き換えてやれば良い。
options(DT.TOJSON_ARGS = list(na = "string")) datatable( data, filter = "top", options = list( columnDefs = list( list( targets = "_all", type = "num", render = JS( ' function(data, type, row, meta) { return data === "NA" ? NaN : data === "Inf" ? Infinity : data === "-Inf" ? -Infinity : data; } ' ) ) ) ) )
この方法は概ね上手く動く。ただ、次のような欠点はある。
Inf
を含むとcolumn filterが動作しない問題は解決しない。DT.TOJSON_ARGS
の設定はすべての列に効いてしまうので、NA
等を表示したくない場合にも表示される。- 表示したくない列用に
render
を別途記述すれば良いが、多少手間がかかる。
- 表示したくない列用に
-Infinity
とNaN
の間のソート順序が気持ち悪い。- JavaScriptの仕様と思われるが、
(a, b) => b - a
のような比較関数を書いてコンソールで試した場合の結果とも異なるので良くわからない。
- JavaScriptの仕様と思われるが、
Inf
ではなく特定の値をInfinity
に置き換える
datatable( data, filter = "top", options = list( columnDefs = list( list( targets = "_all", type = "num", render = JS( ' function(data, type, row, meta) { return data === null ? NaN : data === 999 ? Infinity : data === -999 ? -Infinity : data; } ' ) ) ) ) )
先程の方法とほぼ同じだが、column filterが動作する。ただ、値の上限と下限は値を置き換える前の状態に依存してしまう。
置き換える前の値をデータ範囲に応じて違和感の無い値(例えば最大値+1など)に処理しておく手間をかけられるなら良いかもしれない。