¿Cómo convertir una matriz en una lista de vectores de columna en R?

80

Supongamos que desea convertir una matriz en una lista, donde cada elemento de la lista contiene una columna. list()o as.list()obviamente no funcionará, y hasta ahora uso un truco usando el comportamiento de tapply:

x <- matrix(1:10,ncol=2)

tapply(x,rep(1:ncol(x),each=nrow(x)),function(i)i)

No estoy completamente feliz con esto. ¿Alguien conoce un método más limpio que estoy pasando por alto?

(para hacer una lista llena de filas, el código obviamente se puede cambiar a:

tapply(x,rep(1:nrow(x),ncol(x)),function(i)i)

)

Joris Meys
fuente
1
Me pregunto si la solución Rccp optimizada podría ser más rápida.
Marek

Respuestas:

68

Con el fin de despellejar al gato, trate la matriz como un vector como si no tuviera un atributo tenue:

 split(x, rep(1:ncol(x), each = nrow(x)))
mdsumner
fuente
9
Este es el núcleo de lo que tapplyhacemos. Pero es más simple :). Probablemente una solución más lenta pero atractiva será split(x, col(x))(y split(x, row(x))respectivamente).
Marek
Yo lo revisé. Igualmente rápido será split(x, c(col(x))). Pero parece peor.
Marek
2
split (x, col (x)) se ve mejor: la coerción implícita al vector está bien. . .
mdsumner
2
Después de muchas pruebas, esto parece funcionar más rápido, especialmente con muchas filas o columnas.
Joris Meys
2
Tenga en cuenta que si xtiene nombres de columna split(x, col(x, as.factor = TRUE)), los conservará.
banbh
73

La respuesta de Gavin es simple y elegante. Pero si hay muchas columnas, una solución mucho más rápida sería:

lapply(seq_len(ncol(x)), function(i) x[,i])

La diferencia de velocidad es 6x en el siguiente ejemplo:

> x <- matrix(1:1e6, 10)
> system.time( as.list(data.frame(x)) )
   user  system elapsed 
   1.24    0.00    1.22 
> system.time( lapply(seq_len(ncol(x)), function(i) x[,i]) )
   user  system elapsed 
    0.2     0.0     0.2 
Tommy
fuente
2
+1 Buen comentario sobre la eficiencia relativa de las distintas soluciones. La mejor respuesta hasta ahora.
Gavin Simpson
Pero creo que para obtener los mismos resultados necesitas hacer lapply (seq_len (nrow (x)), function (i) x [i,]) y luego es más lento.
skan
26

los data.frames se almacenan como listas, creo. Por tanto, la coerción parece mejor:

as.list(as.data.frame(x))
> as.list(as.data.frame(x))
$V1
[1] 1 2 3 4 5

$V2
[1]  6  7  8  9 10

Los resultados de la evaluación comparativa son interesantes. as.data.frame es más rápido que data.frame, ya sea porque data.frame tiene que crear un objeto completamente nuevo o porque hacer un seguimiento de los nombres de las columnas es de alguna manera costoso (atestigua la comparación c (unname ()) vs c () )? La solución de solapamiento proporcionada por @Tommy es más rápida en un orden de magnitud. Los resultados de as.data.frame () se pueden mejorar un poco coaccionando manualmente.

manual.coerce <- function(x) {
  x <- as.data.frame(x)
  class(x) <- "list"
  x
}

library(microbenchmark)
x <- matrix(1:10,ncol=2)

microbenchmark(
  tapply(x,rep(1:ncol(x),each=nrow(x)),function(i)i) ,
  as.list(data.frame(x)),
  as.list(as.data.frame(x)),
  lapply(seq_len(ncol(x)), function(i) x[,i]),
  c(unname(as.data.frame(x))),
  c(data.frame(x)),
  manual.coerce(x),
  times=1000
  )

                                                      expr     min      lq
1                                as.list(as.data.frame(x))  176221  183064
2                                   as.list(data.frame(x))  444827  454237
3                                         c(data.frame(x))  434562  443117
4                              c(unname(as.data.frame(x)))  257487  266897
5             lapply(seq_len(ncol(x)), function(i) x[, i])   28231   35929
6                                         manual.coerce(x)  160823  167667
7 tapply(x, rep(1:ncol(x), each = nrow(x)), function(i) i) 1020536 1036790
   median      uq     max
1  186486  190763 2768193
2  460225  471346 2854592
3  449960  460226 2895653
4  271174  277162 2827218
5   36784   37640 1165105
6  171088  176221  457659
7 1052188 1080417 3939286

is.list(manual.coerce(x))
[1] TRUE
Ari B. Friedman
fuente
Derrotado por Gavin por 5 segundos. Maldita sea, la pantalla "¿Eres un humano"? :-)
Ari B. Friedman
1
Suerte del sorteo, supongo, estaba viendo esto después de que @Joris se coló delante de mí respondiendo la Q de Perter Flom. Además, as.data.frame()pierde los nombres del marco de datos, por lo que data.frame()es un poco mejor.
Gavin Simpson
2
Equivalente de manual.coerce(x)podría ser unclass(as.data.frame(x)).
Marek
Gracias Marek. Eso es aproximadamente un 6% más rápido, presumiblemente porque puedo evitar usar una definición / llamada de función.
Ari B. Friedman
16

La conversión a un marco de datos de ahí a una lista parece funcionar:

> as.list(data.frame(x))
$X1
[1] 1 2 3 4 5

$X2
[1]  6  7  8  9 10
> str(as.list(data.frame(x)))
List of 2
 $ X1: int [1:5] 1 2 3 4 5
 $ X2: int [1:5] 6 7 8 9 10
Gavin Simpson
fuente
12

Usar plyrpuede ser realmente útil para cosas como esta:

library("plyr")

alply(x,2)

$`1`
[1] 1 2 3 4 5

$`2`
[1]  6  7  8  9 10

attr(,"class")
[1] "split" "list" 
Sacha Epskamp
fuente
6

Sé que esto es un anatema en R, y realmente no tengo mucha reputación para respaldar esto, pero encuentro que un bucle for es bastante más eficiente. Estoy usando la siguiente función para convertir el tapete de matriz en una lista de sus columnas:

mat2list <- function(mat)
{
    list_length <- ncol(mat)
    out_list <- vector("list", list_length)
    for(i in 1:list_length) out_list[[i]] <- mat[,i]
    out_list
}

Comparación rápida de referencia con mdsummer y la solución original:

x <- matrix(1:1e7, ncol=1e6)

system.time(mat2list(x))
   user  system elapsed 
  2.728   0.023   2.720 

system.time(split(x, rep(1:ncol(x), each = nrow(x))))
   user  system elapsed 
  4.812   0.194   4.978 

system.time(tapply(x,rep(1:ncol(x),each=nrow(x)),function(i)i))
   user  system elapsed 
 11.471   0.413  11.817 
alfymbohm
fuente
Por supuesto, esto elimina los nombres de las columnas, pero no parece que fueran importantes en la pregunta original.
alfymbohm
2
La solución de Tommy es más rápido y más compacto:system.time( lapply(seq_len(ncol(x)), function(i) x[,i]) ) user: 1.668 system: 0.016 elapsed: 1.693
alfymbohm
Tratar de resolver esto en un contexto diferente, no funciona: stackoverflow.com/questions/63801018 .... buscando esto:vec2 = castMatrixToSequenceOfLists(vecs);
mshaffer
5

La nueva función asplit()llegará a la base R en la v3.6. Hasta entonces y en espíritu similar a la respuesta de @mdsumner también podemos hacer

split(x, slice.index(x, MARGIN))

según los documentos de asplit(). Sin embargo, como se mostró anteriormente, todas las split()soluciones basadas son mucho más lentas que las de @ Tommy lapply/`[`. Esto también es válido para lo nuevo asplit(), al menos en su forma actual.

split_1 <- function(x) asplit(x, 2L)
split_2 <- function(x) split(x, rep(seq_len(ncol(x)), each = nrow(x)))
split_3 <- function(x) split(x, col(x))
split_4 <- function(x) split(x, slice.index(x, 2L))
split_5 <- function(x) lapply(seq_len(ncol(x)), function(i) x[, i])

dat <- matrix(rnorm(n = 1e6), ncol = 100)

#> Unit: milliseconds
#>          expr       min        lq     mean   median        uq        max neval
#>  split_1(dat) 16.250842 17.271092 20.26428 18.18286 20.185513  55.851237   100
#>  split_2(dat) 52.975819 54.600901 60.94911 56.05520 60.249629 105.791117   100
#>  split_3(dat) 32.793112 33.665121 40.98491 34.97580 39.409883  74.406772   100
#>  split_4(dat) 37.998140 39.669480 46.85295 40.82559 45.342010  80.830705   100
#>  split_5(dat)  2.622944  2.841834  3.47998  2.88914  4.422262   8.286883   100

dat <- matrix(rnorm(n = 1e6), ncol = 1e5)

#> Unit: milliseconds
#>          expr       min       lq     mean   median       uq      max neval
#>  split_1(dat) 204.69803 231.3023 261.6907 246.4927 289.5218 413.5386   100
#>  split_2(dat) 229.38132 235.3153 253.3027 242.0433 259.2280 339.0016   100
#>  split_3(dat) 208.29162 216.5506 234.2354 221.7152 235.3539 342.5918   100
#>  split_4(dat) 214.43064 221.9247 240.7921 231.0895 246.2457 323.3709   100
#>  split_5(dat)  89.83764 105.8272 127.1187 114.3563 143.8771 209.0670   100
nbenn
fuente
4

Úselo asplitpara convertir una matriz en una lista de vectores

asplit(x, 1) # split into list of row vectors
asplit(x, 2) # split into list of column vectors
Daniel Freeman
fuente
3

Hay una función array_tree()en el purrrpaquete de tidyverse que hace esto con un mínimo de esfuerzo:

x <- matrix(1:10,ncol=2)
xlist <- purrr::array_tree(x, margin=2)
xlist

#> [[1]]
#> [1] 1 2 3 4 5
#>  
#> [[2]]
#> [1]  6  7  8  9 10

Use margin=1para listar por fila en su lugar. Funciona para matrices de n dimensiones. Conserva los nombres por defecto:

x <- matrix(1:10,ncol=2)
colnames(x) <- letters[1:2]
xlist <- purrr::array_tree(x, margin=2)
xlist

#> $a
#> [1] 1 2 3 4 5
#>
#> $b
#> [1]  6  7  8  9 10

(esta es una copia casi palabra por palabra de mi respuesta a una pregunta similar aquí )

wjchulme
fuente
2

En el sitio de ayuda de Some R accesible a través de nabble.com , encuentro:

c(unname(as.data.frame(x))) 

como una solución válida y en mi R v2.13.0 instalar esto se ve bien:

> y <- c(unname(as.data.frame(x)))
> y
[[1]]
[1] 1 2 3 4 5

[[2]]
[1]  6  7  8  9 10

No puedo decir nada sobre las comparaciones de rendimiento o lo limpio que es ;-)

Diletante
fuente
2
Interesante. Creo que esto también funciona por coerción. c(as.data.frame(x))produce un comportamiento idéntico aas.list(as.data.frame(x)
Ari B. Friedman
Creo que esto es así, porque los miembros de las listas / matriz de muestra son del mismo tipo, pero no soy un experto.
Dilettant
2

Podrías usar applyy luego ccondo.call

x <- matrix(1:10,ncol=2)
do.call(c, apply(x, 2, list))
#[[1]]
#[1] 1 2 3 4 5
#
#[[2]]
#[1]  6  7  8  9 10

Y parece que conservará los nombres de las columnas cuando se agreguen a la matriz.

colnames(x) <- c("a", "b")
do.call(c, apply(x, 2, list))
#$a
#[1] 1 2 3 4 5
#
#$b
#[1]  6  7  8  9 10
Rich Scriven
fuente
5
ounlist(apply(x, 2, list), recursive = FALSE)
baptiste
Sí. Deberías agregar eso como respuesta @baptiste.
Rich Scriven
1
¡pero eso requeriría desplazarse hasta el final de la página! Soy demasiado vago para eso
baptiste
Hay un botón "FIN" en mi máquina ... :-)
Rich Scriven
Creo que esto probablemente también se puede hacer creando una lista vacía y llenándola. y <- vector("list", ncol(x))y luego algo parecido a y[1:2] <- x[,1:2], aunque no funciona de esa manera exacta.
Rich Scriven
1

En el caso trivial en el que el número de columnas es pequeño y constante, he descubierto que la opción más rápida es simplemente codificar la conversión:

mat2list  <- function (mat) lapply(1:2, function (i) mat[, i])
mat2list2 <- function (mat) list(mat[, 1], mat[, 2])


## Microbenchmark results; unit: microseconds
#          expr   min    lq    mean median    uq    max neval
##  mat2list(x) 7.464 7.932 8.77091  8.398 8.864 29.390   100
## mat2list2(x) 1.400 1.867 2.48702  2.333 2.333 27.525   100
ms609
fuente