DTでNA、Infを扱う

RのDTパッケージを使うとJavaScriptのDataTablesライブラリ(DataTables | Table plug-in for jQuery)利用したインタラクティブなテーブルが作成できる。

しかし、JavaScriptのライブラリである都合上、Rでは可能なことが簡単には実現できないということもある。表題に挙げたNAInfの扱いもその一つ。

NAInfnullになる

まず、適当にNAInfを含むデータフレームを作成する。

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")

このようにNAInfも表示されない。これはデータが一旦JSONに変換されるが、JSONではNaNInfinityを表現できないのでnullで置換されて空で表示されるということらしい。なので当然だがソート時も正しく扱われない。

ただ、列xのようにInfを含むとcolumn filterが効かなくなるので、単純にnullだけかというとそうでもなさそうに見える。なお、後の例示でも出てくるのでfilter = "top"を指定してcolumn filterを表示したが、これはDTパッケージの側で追加されている機能となっている(DataTablesでも同様のものを表示できるが、オプション1つでON/OFFできるレベルではなく、ある程度の実装が必要)。

NAInfを表示したり、ソートで扱えるようにする方法はいくつか考えられる。ただ、いずれも一長一短という感じで、完全に問題がない方法は思いついていない。

DT.TOJSON_ARGSオプションを使う

このオプションで以下のように指定すると、NAInfが表示されるようになる。

options(DT.TOJSON_ARGS = list(na = "string"))

この方法には大きな欠点があり、上記のようにソートが文字列によるソートとなってしまう(上記スクリーンショットはx列でソートしている)。

ただ、Shinyを使ってサーバーサイド処理を行う場合はソートがRで行われるため、期待するようなソートになるらしい。これは試していないが、そうであれば特に欠点が無い方法のように思う。

na = NULLで元の挙動に戻る。

options(DT.TOJSON_ARGS = list(na = NULL))

欠損値をすべてNAInfinityに置き換える

やや乱暴ではあるが、出現する欠損値が1種類であれば、ある程度妥当な挙動となる。

欠損値を置き換えるには、options引数の中で次のようにrenderJavaScriptの関数を指定する。

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()に渡す前にInfNAに置換しておいたほうが良いだろう。

DT.TOJSON_ARGSrenderでの置き換えを組み合わせる

DT.TOJSON_ARGSna = "string"を指定するとNAInfが表示されるが、JavaScriptでは解釈できないのでクライアントサイドの処理では文字列ソートになってしまうというのが課題だった。つまり、NaNInfinityなどの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を別途記述すれば良いが、多少手間がかかる。
  • -InfinityNaNの間のソート順序が気持ち悪い。
    • JavaScriptの仕様と思われるが、(a, b) => b - aのような比較関数を書いてコンソールで試した場合の結果とも異なるので良くわからない。

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など)に処理しておく手間をかけられるなら良いかもしれない。

参考