Изменение столбцов кадра данных на основе функции предиката (dplyr :: mutate_if)

Я хотел бы использовать функцию mutate_if() dplyr для преобразования столбцов списка в столбцы кадра данных, но при попытке сделать это столкнусь с загадочной ошибкой. Я использую dplyr 0.5.0, purrr 0.2.2, R 3.3.0.

Базовая настройка выглядит так: у меня есть фрейм данных d, некоторые столбцы которого являются списками:

d <- dplyr::data_frame(
  A = list(
    list(list(x = "a", y = 1), list(x = "b", y = 2)),
    list(list(x = "c", y = 3), list(x = "d", y = 4))
  ),
  B = LETTERS[1:2]
)

Я хотел бы преобразовать столбец списков (в данном случае d$A) в столбец фреймов данных, используя следующую функцию:

tblfy <- function(x) {
  x %>%
    purrr::transpose() %>%
    purrr::simplify_all() %>%
    dplyr::as_data_frame()
}

То есть я бы хотел, чтобы столбец списка d$A был заменен списком lapply(d$A, tblfy), который

[[1]]
#  A tibble: 2 x 2
      x     y
  <chr> <dbl>
1     a     1
2     b     2

[[2]]
# A tibble: 2 x 2
      x     y
  <chr> <dbl>
1     c     3
2     d     4

Конечно, в этом простом случае я мог бы просто переназначить. Дело, однако, в том, что я хотел бы сделать это программно, в идеале с помощью dplyr, общеприменимым способом, который мог бы иметь дело с любым количеством столбцов списков.

Вот где я спотыкаюсь: когда я пытаюсь преобразовать столбцы списка в столбцы кадра данных с помощью следующего приложения

d %>% dplyr::mutate_if(is.list, funs(tblfy))

Я получаю сообщение об ошибке, которое не знаю, как интерпретировать:

Error: Each variable must be named.
Problem variables: 1, 2

Почему mutate_if() не работает? Как правильно применить его, чтобы получить желаемый результат?

Замечание

Комментатор указал, что функция tblfy() должна быть векторизована. Это разумное предложение. Но - если я не произвел некорректную векторизацию - это, похоже, не в корне проблемы. Подключив векторизованную версию tblfy(),

tblfy_vec <- Vectorize(tblfy)

в mutate_if() выходит из строя с ошибкой

Error: wrong result size (4), expected 2 or 1

Обновить

Получив некоторый опыт работы с мурлыканьем, я теперь считаю следующий подход естественным, хотя и несколько длинным:

d %>%
  map_if(is.list, ~ map(., ~ map_df(., identity))) %>%
  as_data_frame()

Это более или менее идентично решению @alistaire, приведенному ниже, но использует map_if(), соответственно. map() вместо mutate_if(), соотв. Vectorize().


person egnha    schedule 07.07.2016    source источник
comment
Итак, каков именно ожидаемый результат? Вы хотите заменить A из списка списков на список тибл?   -  person MrFlick    schedule 07.07.2016
comment
Ваша функция не векторизована, она принимает только один список. Посмотрите на tblfy(d$A). Произошла ошибка, потому что в d$A есть два списка. Вы не сравниваете яблоки с яблоками. В вашем lapply(d$A, tblfy) вы указываете своей функции по одному списку за раз, поэтому это работает. tblfy(d$A[[1]]) и tblfy(d$A[[2]]). В вашей функции dplyr вы предоставляете два списка. Измените tblfy, чтобы принимать более одного списка, или измените вызов dplyr. Или, как просит MrFlick, подумайте шире о том, что вы создаете.   -  person Pierre L    schedule 07.07.2016
comment
@MrFlick Я отредактировал вопрос, чтобы сделать желаемый результат явным. Это теперь ясно?   -  person egnha    schedule 07.07.2016
comment
@PierreLafortune Хорошее замечание. Я уже пробовал векторизацию, но все равно не получилось. См. Отредактированный вопрос. Предположительно я некорректно векторизирую. Но как? Как ни странно, когда я применяю tblfy_vec() непосредственно к d$A, я получаю список из 4, который совсем не соответствует моему пониманию, что векторизация создает функцию, которая работает со списком (или вектором) покомпонентно.   -  person egnha    schedule 07.07.2016
comment
Попробуйте вставить функцию применения. Либо карта, либо лаппи   -  person Pierre L    schedule 07.07.2016
comment
tblfy не работает для меня в образце набора данных. Проблема в том, что вам действительно нужно оценивать по строкам (или векторизовать) и обрабатывать ошибки, которые исходный код выдает, когда он не находится в функции. Что делает у меня работает, если вы можете превратить это в функцию: d %>% rowwise() %>% mutate(A = A %>% map_df(identity) %>% list()) %>% tidyr::unnest() Вы можете векторизовать, чтобы избежать rowwise, т.е. d %>% mutate(A = Vectorize(function(x) x %>% map_df(identity) %>% list())(A)) %>% tidyr::unnest()   -  person alistaire    schedule 07.07.2016


Ответы (2)


Исходная функция tblfy ошибается для меня (даже когда ее элементы связаны напрямую), поэтому давайте немного перестроим ее, добавив также векторизацию, которая позволяет нам избежать необходимого в противном случае предварительного вызова rowwise():

tblfy <- Vectorize(function(x){x %>% purrr::map_df(identity) %>% list()})

Теперь мы можем красиво использовать mutate_if:

d %>% mutate_if(purrr::is_list, tblfy)
## Source: local data frame [2 x 2]
## 
##                A     B
##           <list> <chr>
## 1 <tbl_df [2,2]>     A
## 2 <tbl_df [2,2]>     B

... и если мы разойдемся, чтобы посмотреть, что там,

d %>% mutate_if(purrr::is_list, tblfy) %>% tidyr::unnest()
## Source: local data frame [4 x 3]
## 
##       B     x     y
##   <chr> <chr> <dbl>
## 1     A     a     1
## 2     A     b     2
## 3     B     c     3
## 4     B     d     4

Пара заметок:

  • map_df(identity) кажется более эффективным при построении тибла, чем любая из альтернативных формулировок. Я знаю, что вызов identity кажется ненужным, но почти все остальное ломается.
  • Я не уверен, насколько широко будет полезен tblfy, поскольку он в некоторой степени зависит от структуры списков в столбце списка, которая может сильно различаться. Если у вас есть много подобной структуры, я полагаю, что это полезно.
  • Может быть способ сделать это с pmap вместо Vectorize, но я не могу заставить его работать с некоторыми беглыми попытками.
person alistaire    schedule 07.07.2016
comment
Спасибо, вот и все! Ваша версия tblfy() с использованием map_df() более лаконична, чем моя. Не думал о том, чтобы сделать это так. Фактически, глядя на исходный код для map_df(), можно понять, почему ваше решение работает, и, в частности, почему необходимо list() (что меня изначально озадачило): поскольку map_df на самом деле map, за которым следует bind_rows, исключение list() приведет к фрейму данных размер 4 х 2. - person egnha; 08.07.2016

Преобразование на месте без копирования:

library(data.table)

for (col in d) if (is.list(col)) lapply(col, setDF)

d
#Source: local data frame [2 x 2]
#
#                A B
#1 <S3:data.frame> A
#2 <S3:data.frame> B
person eddi    schedule 07.07.2016
comment
Не отвечает на исходный вопрос о mutate_if, но представляет собой очень хорошее альтернативное решение основной проблемы. data.table для меня в новинку. Спасибо! - person egnha; 08.07.2016