将三列数据框重塑为矩阵(从“长”格式到“宽”格式)

148

我有一个看起来像这样的data.frame

x a 1 
x b 2 
x c 3 
y a 3 
y b 3 
y c 2 

我希望将这个转换成矩阵形式,以便可以将其传递给热力图进行绘制。结果应该类似于:

    a    b    c
x   1    2    3
y   3    3    2
我尝试了reshape包中的cast函数,也尝试手动编写了一个函数来实现此功能,但似乎无法正确执行。

1
@AnandaMahto在这里也有一个很好的答案:https://dev59.com/YW3Xa4cB1Zd3GeqPg6Et#14515736 - Aaron left Stack Overflow
1
@Jaap,你是怎么决定关闭这个问题而不是其他问题的?我在这里的回答旨在成为对这个常见问题的权威回答,而且在我看来,比其他问题中的一堆答案更有用。 - Aaron left Stack Overflow
2
另一个问题有很多答案,每个答案都突出了特定的解决方案,并不比这个问题差。那么,我是如何做出决定的呢?很简单:另一个问题早先被提出并且有很多有价值的答案。 - Jaap
@Jaap:唉,这里的文化仍然让我感到困惑和沮丧。曾经“权威”的答案是首选,问题的时间也不重要。我想我需要保持“左派”(就像我的用户名一样)。 - Aaron left Stack Overflow
每隔一段时间,我会回来检查一下 Stack Overflow 是否有所改善...但目前还没有。 - Aaron left Stack Overflow
6个回答

217

有许多方法可以实现这个目标。本答案介绍了目前最常用的方法,并列举了一些旧的方法和其他类似问题答案中散落的各种方法。

tmp <- data.frame(x=gl(2,3, labels=letters[24:25]),
                  y=gl(3,1,6, labels=letters[1:3]), 
                  z=c(1,2,3,3,3,2))

使用tidyverse:

现在最新、最酷的做法是使用 tidyr 1.0.0 中的 pivot_wider。它返回一个数据框,这可能是大多数读者想要的。但对于热力图,您需要将其转换为真正的矩阵。

library(tidyr)
pivot_wider(tmp, names_from = y, values_from = z)
## # A tibble: 2 x 4
## x         a     b     c
## <fct> <dbl> <dbl> <dbl>
## 1 x       1     2     3
## 2 y       3     3     2

现在推荐的方法是使用tidyr中的spread函数,它同样返回一个数据框。

library(tidyr)
spread(tmp, y, z)
##   x a b c
## 1 x 1 2 3
## 2 y 3 3 2

使用reshape2:

通往tidyverse的首要步骤之一是使用reshape2包。

要获取矩阵,请使用acast

library(reshape2)
acast(tmp, x~y, value.var="z")
##   a b c
## x 1 2 3
## y 3 3 2

或者要得到一个数据框,使用dcast,像这样:Reshape data for values in one column

dcast(tmp, x~y, value.var="z")
##   x a b c
## 1 x 1 2 3
## 2 y 3 3 2

使用plyr:

在reshape2和tidyverse之间,出现了plyr,其中包含了 daply 函数,如此所示:https://dev59.com/UlrUa4cB1Zd3GeqPghvr#7020101

library(plyr)
daply(tmp, .(x, y), function(x) x$z)
##    y
## x   a b c
##   x 1 2 3
##   y 3 3 2

使用矩阵索引:

这种方法可能有点老式,但可以很好地演示矩阵索引的用法,在某些情况下非常有用。

with(tmp, {
  out <- matrix(nrow=nlevels(x), ncol=nlevels(y),
                dimnames=list(levels(x), levels(y)))
  out[cbind(x, y)] <- z
  out
})

使用 xtabs

xtabs(z~x+y, data=tmp)

使用稀疏矩阵:

Matrix包中还有一个sparseMatrix,如此处所示:R-通过列名将BIG表转换为矩阵

with(tmp, sparseMatrix(i = as.numeric(x), j=as.numeric(y), x=z,
                       dimnames=list(levels(x), levels(y))))
## 2 x 3 sparse Matrix of class "dgCMatrix"
##   a b c
## x 1 2 3
## y 3 3 2

使用reshape函数:

您也可以使用基本的R函数reshape,如此处所建议:将表格按列名转换为矩阵,不过您需要进行一些后续处理以删除额外的列并且正确获取名称(未展示)。

reshape(tmp, idvar="x", timevar="y", direction="wide")
##   x z.a z.b z.c
## 1 x   1   2   3
## 4 y   3   3   2

3
acast(tmp, x~y, value.var="z") 将会给出一个矩阵输出,其中 x 作为行名。 - mnel
你能否评论一下不同方法的优缺点? - Chris_Rands
2
在大多数小数据集中,主要考虑因素应该是编写清晰易懂且最不容易出现人为编码错误的代码,这对未来的分析师(包括你自己)都很重要。尽管这取决于你的优势和需求,但通常认为这是新的tidyverse软件包系列的优点之一。另一个考虑因素(虽然不是真正的优势/劣势)是您想要矩阵还是数据框作为结果;这个问题明确要求一个矩阵,您可以在答案中看到一些技术直接给出矩阵,而有些则给出数据框。 - Aaron left Stack Overflow
计算时间对于大数据集也可能是一个考虑因素,特别是当代码需要在多个数据集上重复执行多次时。我怀疑这部分取决于数据集的具体特征。如果这是您关心的问题,我建议您提出另一个关于针对您特定情况进行优化的问题;像这样的问题曾经是这个群体的热点。但我会重申我的前一点:优化用户体验(通常)比优化计算机更重要。 - Aaron left Stack Overflow

4

base R, unstack

unstack(df, V3 ~ V2)
#   a b c
# 1 1 2 3
# 2 3 3 2

这可能不是通用解决方案,但在这种情况下效果很好。

数据

df<-structure(list(V1 = structure(c(1L, 1L, 1L, 2L, 2L, 2L), .Label = c("x", 
"y"), class = "factor"), V2 = structure(c(1L, 2L, 3L, 1L, 2L, 
3L), .Label = c("a", "b", "c"), class = "factor"), V3 = c(1L, 
2L, 3L, 3L, 3L, 2L)), .Names = c("V1", "V2", "V3"), class = "data.frame", row.names = c(NA, 
-6L))

4

这个问题已经有些年头了,但也许还有一些人对替代答案感兴趣。

如果你不想加载任何包,你可以使用这个函数:

#' Converts three columns of a data.frame into a matrix -- e.g. to plot 
#' the data via image() later on. Two of the columns form the row and
#' col dimensions of the matrix. The third column provides values for
#' the matrix.
#' 
#' @param data data.frame: input data
#' @param rowtitle string: row-dimension; name of the column in data, which distinct values should be used as row names in the output matrix
#' @param coltitle string: col-dimension; name of the column in data, which distinct values should be used as column names in the output matrix
#' @param datatitle string: name of the column in data, which values should be filled into the output matrix
#' @param rowdecreasing logical: should the row names be in ascending (FALSE) or in descending (TRUE) order?
#' @param coldecreasing logical: should the col names be in ascending (FALSE) or in descending (TRUE) order?
#' @param default_value numeric: default value of matrix entries if no value exists in data.frame for the entries
#' @return matrix: matrix containing values of data[[datatitle]] with rownames data[[rowtitle]] and colnames data[coltitle]
#' @author Daniel Neumann
#' @date 2017-08-29
data.frame2matrix = function(data, rowtitle, coltitle, datatitle, 
                             rowdecreasing = FALSE, coldecreasing = FALSE,
                             default_value = NA) {

  # check, whether titles exist as columns names in the data.frame data
  if ( (!(rowtitle%in%names(data))) 
       || (!(coltitle%in%names(data))) 
       || (!(datatitle%in%names(data))) ) {
    stop('data.frame2matrix: bad row-, col-, or datatitle.')
  }

  # get number of rows in data
  ndata = dim(data)[1]

  # extract rownames and colnames for the matrix from the data.frame
  rownames = sort(unique(data[[rowtitle]]), decreasing = rowdecreasing)
  nrows = length(rownames)
  colnames = sort(unique(data[[coltitle]]), decreasing = coldecreasing)
  ncols = length(colnames)

  # initialize the matrix
  out_matrix = matrix(NA, 
                      nrow = nrows, ncol = ncols,
                      dimnames=list(rownames, colnames))

  # iterate rows of data
  for (i1 in 1:ndata) {
    # get matrix-row and matrix-column indices for the current data-row
    iR = which(rownames==data[[rowtitle]][i1])
    iC = which(colnames==data[[coltitle]][i1])

    # throw an error if the matrix entry (iR,iC) is already filled.
    if (!is.na(out_matrix[iR, iC])) stop('data.frame2matrix: double entry in data.frame')
    out_matrix[iR, iC] = data[[datatitle]][i1]
  }

  # set empty matrix entries to the default value
  out_matrix[is.na(out_matrix)] = default_value

  # return matrix
  return(out_matrix)

}

如何工作:

myData = as.data.frame(list('dim1'=c('x', 'x', 'x', 'y','y','y'),
                            'dim2'=c('a','b','c','a','b','c'),
                            'values'=c(1,2,3,3,3,2))) 

myMatrix = data.frame2matrix(myData, 'dim1', 'dim2', 'values')

myMatrix
>   a b c
> x 1 2 3
> y 3 3 2

3
tidyr 0.8.3.9000 开始,引入了一个名为 pivot_wider() 的新函数。它基本上是以前的 spread() 函数的升级版本(而且不再处于活跃开发状态)。来自 pivoting vignette

本文档介绍了新的 pivot_longer() 和 pivot_wider() 函数的使用。它们的目标是改进 gather() 和 spread() 的可用性,并结合其他包中的最新功能。

有段时间以来,很明显 spread() 和 gather() 的设计存在根本性问题。许多人认为这些名称不直观,很难记住哪个方向对应扩展,哪个方向对应收集。似乎也很难记住这些函数的参数,这意味着许多人(包括我!)每次都要查阅文档。

如何使用它(使用 @Aaron 提供的数据):
pivot_wider(data = tmp, names_from = y, values_from = z)

  x         a     b     c
  <fct> <dbl> <dbl> <dbl>
1 x         1     2     3
2 y         3     3     2

或者用“完整”的tidyverse风格:
tmp %>% 
 pivot_wider(names_from = y, values_from = z)

3
为了完整起见,这里有一个tapply()的解决方案。
with(d, tapply(z, list(x, y), sum))
#   a b c
# x 1 2 3
# y 3 3 2

数据

d <- structure(list(x = structure(c(1L, 1L, 1L, 2L, 2L, 2L), .Label = c("x", 
"y"), class = "factor"), y = structure(c(1L, 2L, 3L, 1L, 2L, 
3L), .Label = c("a", "b", "c"), class = "factor"), z = c(1, 2, 
3, 3, 3, 2)), class = "data.frame", row.names = c(NA, -6L))

1
来自tidyverse的tidyr包有一个非常好的函数可以实现这个功能。
假设你的变量从左到右依次命名为v1、v2和v3,数据框命名为dat:
dat %>% 
spread(key = v2,
       value = v3)

Ta da!

2
请查看@Aaron的答案。 - jogo
不知怎么的错过了他在最后讲解spread的部分。好发现,谢谢。 - Ahsen Majid
整洁数据解决方案现已移至顶部。 - Aaron left Stack Overflow

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接