UPDATE: 2022-12-15 20:15:35
ChatworkのAPIをRから操作するパッケージの制作途中で、あぁなるほど、そういうことになるのか、ということがあったので、解決策メモ。内容はas.data.frame(do.call(rbind,lapply))
が変なものを生成する、という下記の話。
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()
の結果が変。
<- as.data.frame(do.call(what = rbind, args = lapply(X = sample, FUN = function(x){`[`(x, c("message_is", "update_time"))})))
r1 <- as.data.frame(do.call(what = rbind, args = lapply(X = sample, FUN = function(x){`[[`(x, c("account"))})))
r2
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))
にするだけでも、狙っていた形に持っていける。
<- sample %>%
d ::map(`[[`, c("account")) %>%
purrr::map_dfr(.x = ., .f = function(x){dplyr::bind_rows(x)})
purrr
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の要素抽出
<- lapply(X = sample, FUN = function(x){`[[`(x, c("account"))})
tmp # リストの分解
<- unlist(tmp)
un_list <- names(un_list)
un_list_name # リストを分解した要素とそれに対応する名前のデータフレーム
<- data.frame(un_list, un_list_name)
d 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ごとのベクトルに変形しデータフレーム化
<- unstack(x = d, from = un_list_name)
df
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の要素抽出
<- lapply(X = sample, FUN = function(x){`[`(x, c("message_is", "update_time"))})
tmp <- unlist(tmp)
un_list <- names(un_list)
un_list_name <- data.frame(un_list, un_list_name)
d d
## un_list un_list_name
## 1 1 message_is
## 2 0 update_time
## 3 2 message_is
## 4 <NA> update_time
<- unstack(x = d, from = un_list_name)
df 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"
同じ要領でデータフレームに変換。
<- unstack(
d 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")
)
)
)
<- unstack(
d 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"
他にもっと妥当な方法ありそうだけど。