Как свести список к списку без принуждения?

Я пытаюсь достичь функциональности, аналогичной unlist, за исключением того, что типы не приводятся к вектору, а вместо этого возвращается список с сохраненными типами. Например:

flatten(list(NA, list("TRUE", list(FALSE), 0L))

должен вернуться

list(NA, "TRUE", FALSE, 0L)

вместо того

c(NA, "TRUE", "FALSE", "0")

который будет возвращен unlist(list(list(NA, list("TRUE", list(FALSE), 0L)).

Как видно из приведенного выше примера, сглаживание должно быть рекурсивным. Есть ли в стандартной библиотеке R функция, которая выполняет это, или, по крайней мере, какая-то другая функция, которую можно использовать для простой и эффективной реализации этого?

ОБНОВЛЕНИЕ: я не знаю, ясно ли это из вышеизложенного, но не списки не должны сглаживаться, т.е. flatten(list(1:3, list(4, 5))) должен возвращать list(c(1, 2, 3), 4, 5).


person eold    schedule 15.11.2011    source источник
comment
Что должно flatten( list(1:3, list(1:3, 'foo')) ) вернуть?   -  person Tommy    schedule 16.11.2011
comment
list(c(1, 2, 3), c(1, 2, 3), 'foo'). Объяснение: 1:3 не является списком, поэтому его не следует сводить.   -  person eold    schedule 16.11.2011
comment
purrr::flatten выглядит как текущая передовая практика (согласно ответу @Aurèle)   -  person geotheory    schedule 13.09.2017


Ответы (6)


Интересная нетривиальная задача!

ГЛАВНОЕ ОБНОВЛЕНИЕ Со всем, что произошло, я переписал ответ и удалил несколько тупиков. Я также приурочил различные решения к разным случаям.

Вот первое, довольно простое, но медленное решение:

flatten1 <- function(x) {
  y <- list()
  rapply(x, function(x) y <<- c(y,x))
  y
}

rapply позволяет перемещаться по списку и применять функцию к каждому элементу листа. К сожалению, с возвращаемыми значениями он работает точно так же, как unlist. Поэтому я игнорирую результат rapply и вместо этого добавляю значения к переменной y, выполняя <<-.

Выращивание y таким способом не очень эффективно (оно квадратично по времени). Так что, если есть много тысяч элементов, это будет очень медленно.

Более эффективный подход заключается в следующем, с упрощениями от @JoshuaUlrich:

flatten2 <- function(x) {
  len <- sum(rapply(x, function(x) 1L))
  y <- vector('list', len)
  i <- 0L
  rapply(x, function(x) { i <<- i+1L; y[[i]] <<- x })
  y
}

Здесь я сначала узнаю длину результата и заранее выделяю вектор. Затем я заполняю значения. Как видите, это решение намного быстрее.

Вот версия отличного решения @ JoshO'Brien, основанного на Reduce, но расширенного, чтобы обрабатывать произвольную глубину:

flatten3 <- function(x) {
  repeat {
    if(!any(vapply(x, is.list, logical(1)))) return(x)
    x <- Reduce(c, x)
  }
}

А теперь пусть битва начнется!

# Check correctness on original problem 
x <- list(NA, list("TRUE", list(FALSE), 0L))
dput( flatten1(x) )
#list(NA, "TRUE", FALSE, 0L)
dput( flatten2(x) )
#list(NA, "TRUE", FALSE, 0L)
dput( flatten3(x) )
#list(NA_character_, "TRUE", FALSE, 0L)

# Time on a huge flat list
x <- as.list(1:1e5)
#system.time( flatten1(x) )  # Long time
system.time( flatten2(x) )  # 0.39 secs
system.time( flatten3(x) )  # 0.04 secs

# Time on a huge deep list
x <-'leaf'; for(i in 1:11) { x <- list(left=x, right=x, value=i) }
#system.time( flatten1(x) ) # Long time
system.time( flatten2(x) )  # 0.05 secs
system.time( flatten3(x) )  # 1.28 secs

... Итак, мы наблюдаем, что решение Reduce быстрее, когда глубина мала, и решение rapply быстрее, когда глубина большая!

На предмет корректности вот несколько тестов:

> dput(flatten1( list(1:3, list(1:3, 'foo')) ))
list(1L, 2L, 3L, 1L, 2L, 3L, "foo")
> dput(flatten2( list(1:3, list(1:3, 'foo')) ))
list(1:3, 1:3, "foo")
> dput(flatten3( list(1:3, list(1:3, 'foo')) ))
list(1L, 2L, 3L, 1:3, "foo")

Непонятно, какой результат желаем, но я склоняюсь к результату от _15 _...

person Tommy    schedule 15.11.2011
comment
Я придумал что-то похожее на ваше обновление, но, возможно, менее сложное: y <- vector("list", sum(rapply(x,length))); i <- 1 затем rapply(x, function(z) {y[[i]] <<- z; i <<- i+1}). Это примерно так же быстро, как ваше обновленное решение. - person Joshua Ulrich; 15.11.2011
comment
Глупый, да, это намного проще - я не думал, что y[[i]] <<- z сработает, поэтому даже не пробовал! - person Tommy; 15.11.2011
comment
@Tommy - Я только что украл вашу последнюю версию Flatten, добавив строку, которая учитывает угловой случай, который вы указали. Надеюсь, вы не против, и не стесняйтесь редактировать свою версию соответствующим образом. Спасибо! - person Josh O'Brien; 15.11.2011
comment
+1 - Не знаю, как я еще не проголосовал за этот пост. Это должно поднять вас на первое место, чтобы ваши отличные сравнения получили максимальную видимость. Кроме того, я определенно предпочитаю вывод flatten2. - person Josh O'Brien; 16.11.2011
comment
Спасибо. Вы можете удалить flatten1. Он не только самый медленный, но и не сохраняет не-списки (т.е. 1: 5 сглаживается, а не должен). - person eold; 16.11.2011
comment
@Tommy: вы можете придумать решение, которое сохраняет имена в именованном списке? - person user443854; 25.01.2013
comment
Одна проблема с большинством этих методов состоит в том, что NULL элементы удаляются незаметно. - person Jeroen; 05.11.2013
comment
@Tommy, извините за отсутствие атрибуции в моей функции пакета rlist::list.flatten для использования вашей реализации flatten2. Вы заслуживаете полной признательности на странице github.com/renkun. -ken / rlist / blob / master / R / list.flatten.R и спасибо за вашу идею! - person Kun Ren; 31.07.2015

Для списков, которые содержат всего несколько вложений, вы можете использовать Reduce() и c(), чтобы сделать что-то вроде следующего. Каждое приложение c() удаляет один уровень вложенности. (Полное общее решение см. ниже в разделе "РЕДАКТИРОВАНИЕ".)

L <- (list(NA, list("TRUE", list(FALSE), 0L)))
Reduce(c, Reduce(c, L))
[[1]]
[1] NA

[[2]]
[1] "TRUE"

[[3]]
[1] FALSE

[[4]]
[1] 0



# TIMING TEST
x <- as.list(1:4e3)
system.time(flatten(x))   # Using the improved version    
# user  system elapsed 
# 0.14    0.00    0.13 
system.time(Reduce(c, x))
# user  system elapsed 
# 0.04    0.00    0.03 

РЕДАКТИРОВАТЬ Ради удовольствия, вот версия решения @JoshO'Brien, созданная @ Tommy, которая действительно работает для уже плоских списков. ДАЛЬНЕЙШЕЕ РЕДАКТИРОВАНИЕ. Теперь @ Tommy решил и эту проблему, но более чистым способом. Я оставлю эту версию на месте.

flatten <- function(x) {
    x <- list(x)
    repeat {
        x <- Reduce(c, x)
        if(!any(vapply(x, is.list, logical(1)))) return(x)
    }
}

flatten(list(3, TRUE, 'foo'))
# [[1]]
# [1] 3
# 
# [[2]]
# [1] TRUE
# 
# [[3]]
# [1] "foo"
person Josh O'Brien    schedule 15.11.2011
comment
+1 за хорошее использование Reduce! ... Но вроде не работает flatten(list(3, TRUE, 'foo')) - person Tommy; 15.11.2011
comment
Меня больше беспокоит рекурсивная реализация, чтобы работать со списками непостоянной глубины. Есть ли функция, которая может использоваться для определения того, сглаживается ли список? - person eold; 15.11.2011
comment
@leden - Вы можете проверить, является ли список плоским, с помощью !any(sapply(L, class)=="list"), который будет оцениваться как TRUE для полностью плоских списков. - person Josh O'Brien; 15.11.2011
comment
@leden - Я добавил вариант, который это делает. - person Tommy; 15.11.2011
comment
@Tommy - Почему тебе пришлось пойти дальше и испортить совершенно элегантное решение;). Если в функцию могут быть переданы уже плоские списки, вам нужно либо: (а) заранее проверить этот случай; или (b) превентивно обернуть каждый переданный ему список, например: Reduce(c, list(x)), где в вашем примере x <- list(3, TRUE, 'foo'). - person Josh O'Brien; 15.11.2011
comment
@ JoshO'Brien - ну, я попросил тебя опубликовать лучшее решение, не так ли? ;-). Предложение: просто переместите предложение if перед Reduce и пропустите x<-list(x) - person Tommy; 15.11.2011
comment
@ ДжошО'Брайен не стал бы !any(vapply(L, is.list, logical(1))) лучше? - person hadley; 16.11.2011
comment
@hadley - Вы имеете в виду комментарий на четыре комментария выше вашего, где я предложил !any(sapply..., верно? (В тексте сообщения я использовал лучшую идиому, которую вы (и Томми) предложили). - person Josh O'Brien; 16.11.2011
comment
@ttmaccer - Отличный улов. Держу пари, что формулировка Reduce() никогда не будет быстрее, чем ваша do.call() конструкция, а иногда она будет НАМНОГО медленнее. Спасибо. - person Josh O'Brien; 15.08.2012

Как насчет этого? Он основан на решении Джоша О'Брайена, но выполняет рекурсию с помощью цикла while вместо использования unlist с recursive=FALSE.

flatten4 <- function(x) {
  while(any(vapply(x, is.list, logical(1)))) { 
    # this next line gives behavior like Tommy's answer; 
    # removing it gives behavior like Josh's
    x <- lapply(x, function(x) if(is.list(x)) x else list(x))
    x <- unlist(x, recursive=FALSE) 
  }
  x
}

Сохранение закомментированной строки дает такие результаты (что предпочитает Томми, и я тоже).

> x <- list(1:3, list(1:3, 'foo'))
> dput(flatten4(x))
list(1:3, 1:3, "foo")

Вывод моей системы с использованием тестов Томми:

dput(flatten4(foo))
#list(NA, "TRUE", FALSE, 0L)

# Time on a long 
x <- as.list(1:1e5)
system.time( x2 <- flatten2(x) )  # 0.48 secs
system.time( x3 <- flatten3(x) )  # 0.07 secs
system.time( x4 <- flatten4(x) )  # 0.07 secs
identical(x2, x4) # TRUE
identical(x3, x4) # TRUE

# Time on a huge deep list
x <-'leaf'; for(i in 1:11) { x <- list(left=x, right=x, value=i) }
system.time( x2 <- flatten2(x) )  # 0.05 secs
system.time( x3 <- flatten3(x) )  # 1.45 secs
system.time( x4 <- flatten4(x) )  # 0.03 secs
identical(x2, unname(x4)) # TRUE
identical(unname(x3), unname(x4)) # TRUE

РЕДАКТИРОВАТЬ: Что касается глубины списка, возможно, что-то вроде этого сработает; он рекурсивно получает индекс для каждого элемента.

depth <- function(x) {
  foo <- function(x, i=NULL) {
    if(is.list(x)) { lapply(seq_along(x), function(xi) foo(x[[xi]], c(i,xi))) }
    else { i }
  }
  flatten4(foo(x))
}

Это не очень быстро, но вроде работает нормально.

x <- as.list(1:1e5)
system.time(d <- depth(x)) # 0.327 s

x <-'leaf'; for(i in 1:11) { x <- list(left=x, right=x, value=i) }
system.time(d <- depth(x)) # 0.041s

Я представил, что это будет использоваться таким образом:

> x[[ d[[5]] ]]
[1] "leaf"
> x[[ d[[6]] ]]
[1] 1

Но вы также можете подсчитать, сколько узлов находится на каждой глубине.

> table(sapply(d, length))

   1    2    3    4    5    6    7    8    9   10   11 
   1    2    4    8   16   32   64  128  256  512 3072 
person Aaron left Stack Overflow    schedule 15.11.2011
comment
+1 за продолжение продления этого. Вот если бы у нас был способ быстро оценить глубину списков ... Есть идеи? - person Josh O'Brien; 16.11.2011
comment
@ ДжошО'Брайен: См. Редактирование для понимания глубины. Это работает, но не очень хорошо. Какие-либо предложения? - person Aaron left Stack Overflow; 16.11.2011
comment
Привет, Аарон. Хорошее решение, но я согласен, что оно не идеальное. Было бы неплохо найти что-то, что всегда работало быстрее, чем в худшем случае flatten4 тайминги. У меня две мысли: интересно, есть ли уже у филогенетиков что-то подобное в пакете, и люди, которые работают с синтаксическими анализаторами, могут сделать это в мгновение ока. - person Josh O'Brien; 16.11.2011
comment
Я поиграл несколько минут со строкой, полученной из deparse(L), то есть "list(NA, list(\"TRUE\", list(FALSE), 0L))", но понял, что я над головой / у меня нет времени. Моя основная идея состояла в том, чтобы прогнать его один раз, считая каждое вхождение подстроки list( как +1, а все совпадающие правые части ) как -1. max(cumsum()) или что-то подобное даст вам максимальную глубину. Похоже на разумный подход с возможно чудовищным регулярным выражением, необходимым для реализации! Это может быть хорошим вопросом для кого-то из нас в какой-то момент ... - person Josh O'Brien; 16.11.2011
comment
Спасибо. Я думаю, что это пока лучшее решение. - person eold; 16.11.2011
comment
+1. Это решает еще одну проблему: другие функции также выводят списки векторов, только эта. (Решение Томми не делает этого, когда векторы и списки находятся в глубине = 2) - person Michael Schubert; 12.04.2014
comment
если вы измените оба экземпляра is.list в flatten4 на function(x) !is.data.frame(x) & is.list(x), то это будет работать и для фреймов данных, которые в настоящее время не делают ни один из ответов flatten4(list(1:3, list(1:3, 'foo'), TRUE, 'hi', list(head(mtcars), list(tail(mtcars))))) работает как мечта - person rawr; 10.11.2015

Отредактировано для устранения недостатка, указанного в комментариях. К сожалению, это делает его еще менее эффективным. Ну что ж.

Другой подход, хотя я не уверен, что он будет более эффективным, чем все, что предлагал @Tommy:

l <- list(NA, list("TRUE", list(FALSE), 0L))

flatten <- function(x){
    obj <- rapply(x,identity,how = "unlist")
    cl <- rapply(x,class,how = "unlist")
    len <- rapply(x,length,how = "unlist")
    cl <- rep(cl,times = len)
    mapply(function(obj,cl){rs <- as(obj,cl); rs}, obj, cl, 
        SIMPLIFY = FALSE, USE.NAMES = FALSE)
}

> flatten(l)
[[1]]
[1] NA

[[2]]
[1] "TRUE"

[[3]]
[1] FALSE

[[4]]
[1] 0
person joran    schedule 15.11.2011
comment
Да, он немного (~ 3x) медленнее, но +1 за интересное решение! - person Tommy; 15.11.2011
comment
Хм. Я не справляюсь с flatten( list(1:3, list(1:3, 'foo')) ) - person Tommy; 16.11.2011
comment
@Tommy Хороший улов. Я отредактировал, чтобы решить эту проблему, хотя, к сожалению, это ухудшит производительность, чем раньше. - person joran; 16.11.2011

purrr::flatten достигает этого. Хотя это не рекурсивно (по замыслу).

Так что нанесение его дважды должно сработать:

library(purrr)
l <- list(NA, list("TRUE", list(FALSE), 0L))
flatten(flatten(l))

Вот попытка рекурсивной версии:

flatten_recursive <- function(x) {
  stopifnot(is.list(x))
  if (any(vapply(x, is.list, logical(1)))) Recall(purrr::flatten(x)) else x
}
flatten_recursive(l)
person Aurèle    schedule 05.10.2016

Вы также можете использовать rrapply в rrapply-пакете (расширенная версия base-rapply), установив how = "flatten":

library(rrapply)

rrapply(list(NA, list("TRUE", list(FALSE), 0L)), how = "flatten")
#> [[1]]
#> [1] NA
#> 
#> [[2]]
#> [1] "TRUE"
#> 
#> [[3]]
#> [1] FALSE
#> 
#> [[4]]
#> [1] 0

Время вычисления

Ниже приведены некоторые контрольные сроки для функций flatten2 и flatten3 в ответе Томми для двух больших вложенных списков:

flatten2 <- function(x) {
  len <- sum(rapply(x, function(x) 1L))
  y <- vector('list', len)
  i <- 0L
  rapply(x, function(x) { i <<- i+1L; y[[i]] <<- x })
  y
}

flatten3 <- function(x) {
  repeat {
    if(!any(vapply(x, is.list, logical(1)))) return(x)
    x <- Reduce(c, x)
  }
}

## large deeply nested list (1E6 elements, 6 layers)
deep_list <- rrapply(replicate(10, 1, simplify = F), classes = c("list", "numeric"), condition = function(x, .xpos) length(.xpos) < 6, f = function(x) replicate(10, 1, simplify = F), how = "recurse")

system.time(flatten2(deep_list))
#>    user  system elapsed 
#>   1.715   0.012   1.727
## system.time(flatten3(deep_list)), not run takes more than 10 minutes
system.time(rrapply(deep_list, how = "flatten"))
#>    user  system elapsed 
#>   0.105   0.016   0.121

## large shallow nested list (1E6 elements, 2 layers)
shallow_list <- lapply(replicate(1000, 1, simplify = F), function(x) replicate(1000, 1, simplify = F))

system.time(flatten2(shallow_list))
#>    user  system elapsed 
#>   1.308   0.040   1.348
system.time(flatten3(shallow_list))
#>    user  system elapsed 
#>   5.246   0.012   5.259
system.time(rrapply(shallow_list, how = "flatten"))
#>    user  system elapsed 
#>    0.09    0.00    0.09
person Joris C.    schedule 06.07.2020