删除数据框中所有值均为NA的列

206

我有一个数据框,其中一些列包含NA值。

如何删除所有行都包含NA值的列?

14个回答

202

试一下这个:

df <- df[,colSums(is.na(df))<nrow(df)]

4
创建一个与旧对象相同大小的对象会导致在大型对象上出现内存问题。最好使用函数来减小对象的大小。下面的答案使用Filter或使用data.table将有助于减少内存使用量。 - mtelesha
4
似乎无法使用非数值列。 - verbamour
1
似乎无法处理单行数据框。 - gaspar
2
另一种选择可能是df [colSums(!is.na(df))> 0],即使只剩下1列也会返回一个data.frame并且仅在2个而不是3个位置上使用df。(摘自当前已删除的帖子。) - GKi
1
@GKi 另一个选项是设置 drop = FALSE 以避免丢失维度: df <- df[, colSums(is.na(df)) < nrow(df), drop = FALSE] - ismirsehregal
显示剩余4条评论

131
到目前为止,提供的两种方法在处理大型数据集时会失败,因为它们会创建一个与df大小相同的对象is.na(df),并且存在其他内存问题。以下是两种更加高效的内存和时间使用方法:
一种使用Filter的方法:
Filter(function(x)!all(is.na(x)), df)

同时,我们可以使用 data.table 的方法(以实现更高的时间和内存效率)

library(data.table)
DT <- as.data.table(df)
DT[,which(unlist(lapply(DT, function(x)!all(is.na(x))))),with=F]

使用大数据的示例(30列,100万行)

big_data <- replicate(10, data.frame(rep(NA, 1e6), sample(c(1:8,NA),1e6,T), sample(250,1e6,T)),simplify=F)
bd <- do.call(data.frame,big_data)
names(bd) <- paste0('X',seq_len(30))
DT <- as.data.table(bd)

system.time({df1 <- bd[,colSums(is.na(bd) < nrow(bd))]})
# error -- can't allocate vector of size ...
system.time({df2 <- bd[, !apply(is.na(bd), 2, all)]})
# error -- can't allocate vector of size ...
system.time({df3 <- Filter(function(x)!all(is.na(x)), bd)})
## user  system elapsed 
## 0.26    0.03    0.29 
system.time({DT1 <- DT[,which(unlist(lapply(DT, function(x)!all(is.na(x))))),with=F]})
## user  system elapsed 
## 0.14    0.03    0.18 

7
非常好。你可以使用 data.frame 做同样的事情。这里没有什么真正需要 data.table 的东西。关键是 lapply,它避免了 is.na(df) 执行整个对象的复制。+10 分为指出这一点。 - Matt Dowle
1
你会如何使用 data.frame 进行操作呢?@matt-dowle - s_a
10
@s_a, bd1 <- bd[, unlist(lapply(bd, function(x) !all(is.na(x))))] 可以翻译为:将数据框bd中所有非空列提取出来,生成新的数据框bd1。具体实现是通过对bd使用lapply函数,判断每一列是否全部为空值is.na(x),再通过unlist函数将结果转换为向量形式进行子集索引。 - mnel
6
@mnel,我认为你需要删除 function(x) 后面的 , —— 顺便说一句,谢谢你提供的示例。 - Thieme Hennis
1
你能用 := 还是 set() 更快地完成吗? - skan

101

更新

您现在可以使用带有where选择器的selectselect_if已被取代,但在dplyr 1.0.2中仍然可用。(感谢@mcstrother提供此信息)。

library(dplyr)
temp <- data.frame(x = 1:5, y = c(1,2,NA,4, 5), z = rep(NA, 5))
not_all_na <- function(x) any(!is.na(x))
not_any_na <- function(x) all(!is.na(x))

> temp
  x  y  z
1 1  1 NA
2 2  2 NA
3 3 NA NA
4 4  4 NA
5 5  5 NA

> temp %>% select(where(not_all_na))
  x  y
1 1  1
2 2  2
3 3 NA
4 4  4
5 5  5

> temp %>% select(where(not_any_na))
  x
1 1
2 2
3 3
4 4
5 5

旧回答

dplyr现在有一个select_if动词,在这里可能会有帮助:

> temp
  x  y  z
1 1  1 NA
2 2  2 NA
3 3 NA NA
4 4  4 NA
5 5  5 NA

> temp %>% select_if(not_all_na)
  x  y
1 1  1
2 2  2
3 3 NA
4 4  4
5 5  5

> temp %>% select_if(not_any_na)
  x
1 1
2 2
3 3
4 4
5 5

5
来到这里是寻找 dplyr 的解决方案,结果没有失望。谢谢! - Andrew Brēza
我发现这个问题是它会删除具有大多数但不是全部值缺失的变量。 - MBorg
10
select_if在dplyr中已经被取代,因此最近的语法中,最后两行将是temp %>% select(where(not_all_na)) -- 尽管 select_if仍可在dplyr 1.0.2 中使用。如果您不想在单独的行上定义函数,则也可以使用temp %>% select(where(~!all(is.na(.x)))) - mcstrother
1
@mcstrother 谢谢 - 这是对我的回答非常有帮助的更新。如果你想自己回答,我很乐意撤销编辑。 - zack
我找不到 not_any_na。这是从哪里来的?我已经加载了 dplyr..... - Sky Scraper
@SkyScraper是在提供的代码中定义的一个函数。 - zack

33
有点晚了,但你也可以使用 janitor 包。这个函数将删除所有为 NA 的列,并且可以更改为删除所有为 NA 的行。
df <- janitor::remove_empty(df, which = "cols")

19

使用purrr软件包的另一种选项:

library(dplyr)

df <- data.frame(a = NA,
                 b = seq(1:5), 
                 c = c(rep(1, 4), NA))

df %>% purrr::discard(~all(is.na(.)))
df %>% purrr::keep(~!all(is.na(.)))

17
另一种方法是使用apply()函数。 如果您有数据框,
df <- data.frame (var1 = c(1:7,NA),
                  var2 = c(1,2,1,3,4,NA,NA,9),
                  var3 = c(NA)
                  )

然后,您可以使用apply()来查看哪些列满足您的条件,因此您可以简单地使用与Musa答案相同的子集操作,只是使用apply方法。

> !apply (is.na(df), 2, all)
 var1  var2  var3 
 TRUE  TRUE FALSE 

> df[, !apply(is.na(df), 2, all)]
  var1 var2
1    1    1
2    2    2
3    3    1
4    4    3
5    5    4
6    6   NA
7    7   NA
8   NA    9

3
我本以为这会更快,因为使用colSum()函数的解决方案看起来做了更多的工作。但在我的测试集上(之前有1614个变量的213个观测值,之后变为1377个变量),它需要的时间正好是之前的3倍。(但因为这是一个有趣的方法,所以还是值得一试的加一分。) - Darren Cook

8
一个老问题,但我认为我们可以用更简单的data.table解决方案来更新@mnel的好答案:
DT[, .SD, .SDcols = \(x) !all(is.na(x))]

我正在使用 R>=4.1 中可用的新的 \(x) lambda 函数语法,但真正关键的是通过 .SDcols 传递逻辑子集。

速度是等效的。

microbenchmark::microbenchmark(
  which_unlist = DT[, which(unlist(lapply(DT, \(x) !all(is.na(x))))), with=FALSE],
  sdcols       = DT[, .SD, .SDcols = \(x) !all(is.na(x))],
  times = 2
)
#> Unit: milliseconds
#>          expr      min       lq     mean   median       uq      max neval cld
#>  which_unlist 51.32227 51.32227 56.78501 56.78501 62.24776 62.24776     2   a
#>        sdcols 43.14361 43.14361 49.33491 49.33491 55.52621 55.52621     2   a

8
df[sapply(df, function(x) all(is.na(x)))] <- NULL

4
您可以使用Janitor包中的remove_empty函数。
library(janitor)

df %>%
  remove_empty(c("rows", "cols")) #select either row or cols or both

此外,还有另一种dplyr的方法。
 library(dplyr) 
 df %>% select_if(~all(!is.na(.)))

或者

df %>% select_if(colSums(!is.na(.)) == nrow(df))

如果您只想排除/保留一定数量的缺失值列,这也是非常有用的,例如:

 df %>% select_if(colSums(!is.na(.))>500)

1
一个方便的 base R 选项是 colMeans()
df[, colMeans(is.na(df)) != 1]

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