Convert a matrix to a list of column-vectors
Asked Answered
P

16

94

Say you want to convert a matrix to a list, where each element of the list contains one column. list() or as.list() obviously won't work, and until now I use a hack using the behaviour of tapply :

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

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

I'm not completely happy with this. Anybody knows a cleaner method I'm overlooking?

(for making a list filled with the rows, the code can obviously be changed to :

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

)

Pennyweight answered 25/7, 2011 at 17:8 Comment(2)
I wonder if optimized Rccp solution could be faster.Dulaney
With R 3.6 released years ago, this answer (using asplit) should be the accepted one.Kirbie
M
77

In the interests of skinning the cat, treat the array as a vector as if it had no dim attribute:

 split(x, rep(1:ncol(x), each = nrow(x)))
Moorer answered 25/7, 2011 at 23:17 Comment(6)
This is core of what tapply do. But it's simpler :). Probably slower but nice-looking solution will be split(x, col(x)) (and split(x, row(x)) respectively).Dulaney
I checked it. Equally fast will be split(x, c(col(x))). But it looks worse.Dulaney
split(x, col(x)) looks better - implicit coercion to vector is fine . . .Moorer
After much testing, this seems to work the fastest, especially with a lot of row or columns.Pennyweight
indeed, as underlined by @JorisMeys this solution is fastest. Benchmark with matrix 100000x150: mean time = 2.897655s with this solution and 3.096364s with tapply.Suppurative
Note that if x has column names then split(x, col(x, as.factor = TRUE)) will preserve the names.Initiatory
I
87

Gavin's answer is simple and elegant. But if there are many columns, a much faster solution would be:

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

The speed difference is 6x in the example below:

> 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 
Idellaidelle answered 25/7, 2011 at 19:41 Comment(2)
+1 Good point about relative efficiency of the various solutions. The best Answer thus far.Hymie
But I think in order to get the same results you need to do lapply(seq_len(nrow(x)), function(i) x[i,]) and then is slower.Marcusmarcy
M
77

In the interests of skinning the cat, treat the array as a vector as if it had no dim attribute:

 split(x, rep(1:ncol(x), each = nrow(x)))
Moorer answered 25/7, 2011 at 23:17 Comment(6)
This is core of what tapply do. But it's simpler :). Probably slower but nice-looking solution will be split(x, col(x)) (and split(x, row(x)) respectively).Dulaney
I checked it. Equally fast will be split(x, c(col(x))). But it looks worse.Dulaney
split(x, col(x)) looks better - implicit coercion to vector is fine . . .Moorer
After much testing, this seems to work the fastest, especially with a lot of row or columns.Pennyweight
indeed, as underlined by @JorisMeys this solution is fastest. Benchmark with matrix 100000x150: mean time = 2.897655s with this solution and 3.096364s with tapply.Suppurative
Note that if x has column names then split(x, col(x, as.factor = TRUE)) will preserve the names.Initiatory
M
30

data.frames are stored as lists, I believe. Therefore coercion seems best:

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

Benchmarking results are interesting. as.data.frame is faster than data.frame, either because data.frame has to create a whole new object, or because keeping track of the column names is somehow costly (witness the c(unname()) vs c() comparison)? The lapply solution provided by @Tommy is faster by an order of magnitude. The as.data.frame() results can be somewhat improved by coercing manually.

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
Malena answered 25/7, 2011 at 17:18 Comment(4)
Beaten by Gavin by 5 seconds. Darn you, "Are you a human" screen? :-)Malena
Luck of the draw I guess, I was just viewing this after @Joris snuck in ahead of me answering Perter Flom's Q. Also, as.data.frame() looses the names of the data frame, so data.frame() is a little nicer.Hymie
Equivalent of manual.coerce(x) could be unclass(as.data.frame(x)).Dulaney
Thanks Marek. That's about 6% faster, presumably because I can avoid using a function definition/call.Malena
S
25

Use asplit to convert a matrix into a list of vectors. Use the MARGIN argument to give the margins to split by. For a matrix 1 indicates rows, 2 indicates columns.

asplit(x, MARGIN = 1) # split into list of row vectors
asplit(x, MARGIN = 2) # split into list of column vectors
Stantonstanway answered 5/2, 2020 at 14:47 Comment(0)
H
17

Converting to a data frame thence to a list seems to work:

> 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
Hymie answered 25/7, 2011 at 17:18 Comment(0)
L
13

Using plyrcan be really useful for things like this:

library("plyr")

alply(x,2)

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

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

attr(,"class")
[1] "split" "list" 
Lili answered 27/7, 2011 at 7:48 Comment(0)
H
6

I know this is anathema in R, and I don't really have a lot of reputation to back this up, but I'm finding a for loop to be rather more efficient. I'm using the following function to convert matrix mat to a list of its columns:

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
}

Quick benchmark comparing with mdsummer's and the original solution:

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 
Hereinbefore answered 2/8, 2013 at 11:36 Comment(3)
Of course this drops column names, but it doesn't seem they were important in the original question.Hereinbefore
Tommy's solution is faster and more compact: system.time( lapply(seq_len(ncol(x)), function(i) x[,i]) ) user: 1.668 system: 0.016 elapsed: 1.693Hereinbefore
Trying to figure this out in a different context, doesn't work: stackoverflow.com/questions/63801018 .... looking for this: vec2 = castMatrixToSequenceOfLists(vecs);Happiness
D
6

The new function asplit() will be coming to base R in v3.6. Up until then and in similar spirit to the answer of @mdsumner we can also do

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

as per the docs of asplit(). As previously shown however, all split() based solutions are much slower than @Tommy's lapply/`[`. This also holds for the new asplit(), at least in its current form.

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
Deductible answered 1/2, 2019 at 16:15 Comment(0)
B
3

There's a function array_tree() in the tidyverse's purrr package that does this with minimum fuss:

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=1 to list by row instead. Works for n-dimensional arrays. It preserves names by default:

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

(this is a near word-for-word copy of my answer to a similar question here)

Brabble answered 12/2, 2019 at 14:49 Comment(0)
J
2

Under Some R Help site accessible via nabble.com I find:

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

as a valid solution and in my R v2.13.0 install this looks ok:

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

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

Can't say anythng about performance comparisons or how clean it is ;-)

Justifiable answered 25/7, 2011 at 17:18 Comment(2)
Interesting. I think this also works by coercion. c(as.data.frame(x)) produces identical behavior to as.list(as.data.frame(x)Malena
I think that this is so, because the members of the sample lists / matrix are of the same type, but I am not an expeRt.Justifiable
M
2

You could use apply and then c with do.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

And it looks like it will preserve the column names, when added to the matrix.

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
Mathre answered 31/8, 2014 at 23:10 Comment(5)
or unlist(apply(x, 2, list), recursive = FALSE)Drugge
Yep. You should add that as an answer @baptiste.Mathre
but that would require scrolling down to the bottom of the page! i'm way too lazy for thatDrugge
There's an "END" button on my machine... :-)Mathre
I think this can probably also be done by creating an empty list and filling it up. y <- vector("list", ncol(x)) and then something along the lines of y[1:2] <- x[,1:2], although it doesn't work that exact way.Mathre
J
2

convertRowsToList {BBmisc}

Convert rows (columns) of data.frame or matrix to lists.

BBmisc::convertColsToList(x)

ref: http://berndbischl.github.io/BBmisc/man/convertRowsToList.html

Janusfaced answered 23/2, 2018 at 3:24 Comment(0)
S
1

In the trivial case where the number of columns is small and constant, then I've found that the fastest option is to simply hard-code the conversion:

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
Solitaire answered 6/11, 2017 at 17:27 Comment(0)
V
1

There is also collapse::mrtl (row lists) and collapse::mctl (column lists). collapse is considerably faster.

collapse::mrtl(x)
collapse::mctl(x)

Benchmark:

x <- matrix(1:2e4, ncol = 100)
microbenchmark::microbenchmark(
  asplit = asplit(x, MARGIN = 2),
  collapse = mctl(x),
  split = split(x, rep(1:ncol(x), each = nrow(x))),
  lapply = lapply(seq_len(ncol(x)), function(i) x[,i])
)

# Unit: microseconds
#      expr     min       lq       mean   median       uq       max neval
#    asplit 281.301 318.8005  376.88597 370.4515  421.051   659.300   100
#  collapse  20.600  25.3510   46.51694  31.3510   41.351   157.401   100
#     split 748.001 935.1515 1144.62000 991.4010 1061.601 14922.301   100
#    lapply 139.401 152.0005  205.74196 165.3005  236.901  1319.701   100
Violent answered 5/5, 2023 at 15:10 Comment(0)
S
0

The simplest way to create a list that has the columns of a matrix mat as its elements is to use the fact that a data.frame object in R is internally represented as a list of the columns. Thus all that is needed is the following line

mat.list <- as.data.frame(mat)
Sepia answered 12/5, 2021 at 22:53 Comment(0)
S
0

A dplyr readable renewed approach for the same thing:

x <- matrix(1:10,ncol=2)
library(dplyr)
x %>% as_tibble() %>%
  as.list()

$V1
[1] 1 2 3 4 5

$V2
[1]  6  7  8  9 10
Spartan answered 30/3, 2022 at 0:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.