UPDATE: 2022-12-15 20:15:35

はじめに

ChatworkのAPIをRから操作するパッケージの制作途中で、あぁなるほど、そういうことになるのか、ということがあったので、解決策メモ。内容はas.data.frame(do.call(rbind,lapply))が変なものを生成する、という下記の話。

APIが返すもの

APIが返すもののサンプルデータとして、こんなネストしたリストを返してくる。

library(tidyverse)
sample <- 
  list(
    list(message_is = "1", account = list(account_id = "X001", name = "X001_name"), update_time = 0),
    list(message_is = "2", account = list(account_id = "X002", name = "X002_name"), update_time = NA)
  )
sample
## [[1]]
## [[1]]$message_is
## [1] "1"
## 
## [[1]]$account
## [[1]]$account$account_id
## [1] "X001"
## 
## [[1]]$account$name
## [1] "X001_name"
## 
## 
## [[1]]$update_time
## [1] 0
## 
## 
## [[2]]
## [[2]]$message_is
## [1] "2"
## 
## [[2]]$account
## [[2]]$account$account_id
## [1] "X002"
## 
## [[2]]$account$name
## [1] "X002_name"
## 
## 
## [[2]]$update_time
## [1] NA

これをリストからデータフレーム化するために、as.data.frame(do.call(rbind,lapply))で変換しようとすると、something weirdが生成される。はじめからtidyverseライクなスタイルで書けばよかったんだけど、いっちょ前にbaseライクのほうがtidyverseのアップデートの影響も受ける可能性が低くなるかなーとか思ったのがそもそもの失敗だった。見た目は普通のデータフレームだし、クラスもデータフレームだけど、str()の結果が変。

r1 <- as.data.frame(do.call(what = rbind, args = lapply(X = sample, FUN = function(x){`[`(x, c("message_is", "update_time"))})))
r2 <- as.data.frame(do.call(what = rbind, args = lapply(X = sample, FUN = function(x){`[[`(x, c("account"))})))

list(r1,r2,str(r1),str(r2))
## 'data.frame':    2 obs. of  2 variables:
##  $ message_is :List of 2
##   ..$ : chr "1"
##   ..$ : chr "2"
##  $ update_time:List of 2
##   ..$ : num 0
##   ..$ : logi NA
## 'data.frame':    2 obs. of  2 variables:
##  $ account_id:List of 2
##   ..$ : chr "X001"
##   ..$ : chr "X002"
##  $ name      :List of 2
##   ..$ : chr "X001_name"
##   ..$ : chr "X002_name"
## [[1]]
##   message_is update_time
## 1          1           0
## 2          2          NA
## 
## [[2]]
##   account_id      name
## 1       X001 X001_name
## 2       X002 X002_name
## 
## [[3]]
## NULL
## 
## [[4]]
## NULL

as.data.frame()する前の段階の構造が原因っぽい。

do.call(rbind, lapply(X = sample, FUN = function(x){`[[`(x, c("account"))})) %>% class()
## [1] "matrix" "array"
do.call(rbind, lapply(X = sample, FUN = function(x){`[[`(x, c("account"))})) %>% str()
## List of 4
##  $ : chr "X001"
##  $ : chr "X002"
##  $ : chr "X001_name"
##  $ : chr "X002_name"
##  - attr(*, "dim")= int [1:2] 2 2
##  - attr(*, "dimnames")=List of 2
##   ..$ : NULL
##   ..$ : chr [1:2] "account_id" "name"

最初からこう書けばよかったのだが。dplyr::bind_rows()はそこらへんうまくやってくれるので、as.data.frame(do.call(bind_rows,lapply))にするだけでも、狙っていた形に持っていける。

d <- sample %>% 
  purrr::map(`[[`, c("account")) %>%
  purrr::map_dfr(.x = ., .f = function(x){dplyr::bind_rows(x)})

list(d,str(d))
## tibble [2 × 2] (S3: tbl_df/tbl/data.frame)
##  $ account_id: chr [1:2] "X001" "X002"
##  $ name      : chr [1:2] "X001_name" "X002_name"
## [[1]]
## # A tibble: 2 × 2
##   account_id name     
##   <chr>      <chr>    
## 1 X001       X001_name
## 2 X002       X002_name
## 
## [[2]]
## NULL

解法

ほかにもっと良い方法があるだろうけど、bind_rows()を使わない場合の現状の解法は下記。unstack()をうまく使えるように変形する。

# 必要な深さレベル2の要素抽出
tmp <- lapply(X = sample, FUN = function(x){`[[`(x, c("account"))})
# リストの分解
un_list <- unlist(tmp)
un_list_name <- names(un_list)
# リストを分解した要素とそれに対応する名前のデータフレーム
d <- data.frame(un_list, un_list_name)
d
##     un_list un_list_name
## 1      X001   account_id
## 2 X001_name         name
## 3      X002   account_id
## 4 X002_name         name
# unstack()でun_list_nameごとのベクトルに変形しデータフレーム化
df <- unstack(x = d, from = un_list_name)

list(df,str(df))
## 'data.frame':    2 obs. of  2 variables:
##  $ account_id: chr  "X001" "X002"
##  $ name      : chr  "X001_name" "X002_name"
## [[1]]
##   account_id      name
## 1       X001 X001_name
## 2       X002 X002_name
## 
## [[2]]
## NULL
# 必要な深さレベル1の要素抽出
tmp <- lapply(X = sample, FUN = function(x){`[`(x, c("message_is", "update_time"))})
un_list <- unlist(tmp)
un_list_name <- names(un_list)
d <- data.frame(un_list, un_list_name)
d
##   un_list un_list_name
## 1       1   message_is
## 2       0  update_time
## 3       2   message_is
## 4    <NA>  update_time
df <- unstack(x = d, from = un_list_name)
list(df,str(df))
## 'data.frame':    2 obs. of  2 variables:
##  $ message_is : chr  "1" "2"
##  $ update_time: chr  "0" NA
## [[1]]
##   message_is update_time
## 1          1           0
## 2          2        <NA>
## 
## [[2]]
## NULL

何がありがたいかというと、仮にAPIがこんなめちゃくちゃ嫌な構造で返してきても問題ない。

sample2 <- 
  list(
    list(
      list(
        list(x1="1",x2=list(x3=NA,x4="11"),y1="111"),
        list(x1="2",x2=list(x3="2",x4="22"),y1="222")
      ),
      list(
        list(x1="3",x2=list(x3="3",x4=NA),y1="333"),
        list(x1="4",x2=list(x3="4",x4="44"),y1="444")
      )
    ),
    list(
      list(
        list(x1="5",x2=list(x3="5",x4="55"),y1="555"),
        list(x1="6",x2=list(x3="6",x4="66"),y1=NA)
      ),
      list(
        list(x1=NA,x2=list(x3="7",x4="77"),y1="777"),
        list(x1="8",x2=list(x3="8",x4="88"),y1="888")
      )
    )
  )

sample2
## [[1]]
## [[1]][[1]]
## [[1]][[1]][[1]]
## [[1]][[1]][[1]]$x1
## [1] "1"
## 
## [[1]][[1]][[1]]$x2
## [[1]][[1]][[1]]$x2$x3
## [1] NA
## 
## [[1]][[1]][[1]]$x2$x4
## [1] "11"
## 
## 
## [[1]][[1]][[1]]$y1
## [1] "111"
## 
## 
## [[1]][[1]][[2]]
## [[1]][[1]][[2]]$x1
## [1] "2"
## 
## [[1]][[1]][[2]]$x2
## [[1]][[1]][[2]]$x2$x3
## [1] "2"
## 
## [[1]][[1]][[2]]$x2$x4
## [1] "22"
## 
## 
## [[1]][[1]][[2]]$y1
## [1] "222"
## 
## 
## 
## [[1]][[2]]
## [[1]][[2]][[1]]
## [[1]][[2]][[1]]$x1
## [1] "3"
## 
## [[1]][[2]][[1]]$x2
## [[1]][[2]][[1]]$x2$x3
## [1] "3"
## 
## [[1]][[2]][[1]]$x2$x4
## [1] NA
## 
## 
## [[1]][[2]][[1]]$y1
## [1] "333"
## 
## 
## [[1]][[2]][[2]]
## [[1]][[2]][[2]]$x1
## [1] "4"
## 
## [[1]][[2]][[2]]$x2
## [[1]][[2]][[2]]$x2$x3
## [1] "4"
## 
## [[1]][[2]][[2]]$x2$x4
## [1] "44"
## 
## 
## [[1]][[2]][[2]]$y1
## [1] "444"
## 
## 
## 
## 
## [[2]]
## [[2]][[1]]
## [[2]][[1]][[1]]
## [[2]][[1]][[1]]$x1
## [1] "5"
## 
## [[2]][[1]][[1]]$x2
## [[2]][[1]][[1]]$x2$x3
## [1] "5"
## 
## [[2]][[1]][[1]]$x2$x4
## [1] "55"
## 
## 
## [[2]][[1]][[1]]$y1
## [1] "555"
## 
## 
## [[2]][[1]][[2]]
## [[2]][[1]][[2]]$x1
## [1] "6"
## 
## [[2]][[1]][[2]]$x2
## [[2]][[1]][[2]]$x2$x3
## [1] "6"
## 
## [[2]][[1]][[2]]$x2$x4
## [1] "66"
## 
## 
## [[2]][[1]][[2]]$y1
## [1] NA
## 
## 
## 
## [[2]][[2]]
## [[2]][[2]][[1]]
## [[2]][[2]][[1]]$x1
## [1] NA
## 
## [[2]][[2]][[1]]$x2
## [[2]][[2]][[1]]$x2$x3
## [1] "7"
## 
## [[2]][[2]][[1]]$x2$x4
## [1] "77"
## 
## 
## [[2]][[2]][[1]]$y1
## [1] "777"
## 
## 
## [[2]][[2]][[2]]
## [[2]][[2]][[2]]$x1
## [1] "8"
## 
## [[2]][[2]][[2]]$x2
## [[2]][[2]][[2]]$x2$x3
## [1] "8"
## 
## [[2]][[2]][[2]]$x2$x4
## [1] "88"
## 
## 
## [[2]][[2]][[2]]$y1
## [1] "888"

同じ要領でデータフレームに変換。

d <- unstack(
  data.frame(
    unlist(sample2),
    names(unlist(sample2))
    )
  )
list(d,str(d))
## 'data.frame':    8 obs. of  4 variables:
##  $ x1   : chr  "1" "2" "3" "4" ...
##  $ x2.x3: chr  NA "2" "3" "4" ...
##  $ x2.x4: chr  "11" "22" NA "44" ...
##  $ y1   : chr  "111" "222" "333" "444" ...
## [[1]]
##     x1 x2.x3 x2.x4   y1
## 1    1  <NA>    11  111
## 2    2     2    22  222
## 3    3     3  <NA>  333
## 4    4     4    44  444
## 5    5     5    55  555
## 6    6     6    66 <NA>
## 7 <NA>     7    77  777
## 8    8     8    88  888
## 
## [[2]]
## NULL

基本的には、APIはリクエストに対して同じ構造でデータを返すはず…長さ(要素)が変わること「ない」はず、という前提ですが。下記のように突然y2が増えるとかないはず。

sample2 <- 
  list(
    list(
      list(
        list(x1="1",x2=list(x3=NA,x4="11"),y1="111"),
        list(x1="2",x2=list(x3="2",x4="22"),y1="222")
      ),
      list(
        list(x1="3",x2=list(x3="3",x4=NA),y1="333"),
        list(x1="4",x2=list(x3="4",x4="44"),y1="444")
      )
    ),
    list(
      list(
        list(x1="5",x2=list(x3="5",x4="55"),y1="555"),
        list(x1="6",x2=list(x3="6",x4="66"),y1=NA)
      ),
      list(
        list(x1=NA,x2=list(x3="7",x4="77"),y1="777"),
        list(x1="8",x2=list(x3="8",x4="88"),y1="888",y2="8888")
      )
    )
  )

d <- unstack(
  data.frame(
    unlist(sample2),
    names(unlist(sample2))
    )
  )

d
## $x1
## [1] "1" "2" "3" "4" "5" "6" NA  "8"
## 
## $x2.x3
## [1] NA  "2" "3" "4" "5" "6" "7" "8"
## 
## $x2.x4
## [1] "11" "22" NA   "44" "55" "66" "77" "88"
## 
## $y1
## [1] "111" "222" "333" "444" "555" NA    "777" "888"
## 
## $y2
## [1] "8888"

他にもっと妥当な方法ありそうだけど。