使用dplyr删除所有变量均为NA的行

52

我在处理一个看似简单的任务时遇到了问题:使用dplyr删除所有变量都是NA的行。我知道可以使用base R (Remove rows in R matrix where all data is NARemoving empty rows of a data file in R) 完成此任务,但我想知道是否有一种使用dplyr的简单方法。

示例:

library(tidyverse)
dat <- tibble(a = c(1, 2, NA), b = c(1, NA, NA), c = c(2, NA, NA))
filter(dat, !is.na(a) | !is.na(b) | !is.na(c))

上面的filter调用做了我想要的事情,但在我面临的情况下是不可行的(因为有大量变量)。我猜可以通过使用filter_并首先创建一个包含(长)逻辑语句的字符串来实现,但似乎应该有一种更简单的方法。

另一种方法是使用rowwise()do()

na <- dat %>% 
  rowwise() %>% 
  do(tibble(na = !all(is.na(.)))) %>% 
  .$na
filter(dat, na)

但那看起来不太好,虽然它能完成工作。还有其他的想法吗?


6
也许可以使用 dat %>% filter(rowSums(is.na(.)) != ncol(.)) 或者 dat %>% filter(rowMeans(is.na(.)) < 1) - David Arenburg
1
虽然,Hadley可能会建议使用长格式,例如 dat %>% mutate(indx = row_number()) %>% gather(var, val, -indx) %>% group_by(indx) %>% filter(sum(is.na(val)) != n()) %>% spread(var, val) - David Arenburg
@DavidArenburg 那会很好。我出于好奇做了一个快速的基准测试,比较了不同的方法,并将其添加到帖子中。 - hejseb
你可以将其作为答案发布,并在稍大的数据集上进行基准测试。如果有许多列,我猜Reduce方法的效率会变得不那么高效。 - David Arenburg
@DavidArenburg 我现在用的是40,大约有100,000行数据,但它仍然表现良好! - hejseb
显示剩余4条评论
10个回答

88

自dplyr 0.7.0新版本以来,已经存在了新的作用域过滤动词。使用filter_any,您可以轻松地过滤掉至少有一个非缺失列的行:

# dplyr 0.7.0
dat %>% filter_all(any_vars(!is.na(.)))

使用@hejseb的基准测试算法,该解决方案看起来与f4一样高效。

更新:

自dplyr 1.0.0版本以来,以上作用域动词已被取代。改为引入了跨越函数系列,它允许在多个(或全部)列上执行函数。现在通过至少有一个列不是NA来筛选行的方法如下:

# dplyr 1.0.0
dat %>% filter(if_any(everything(), ~ !is.na(.)))

7
据我看来,这是最直观的解决方案,可用于删除所有缺失值行。此外,值得一提的是,在您想检测所有缺失值行的情况下,必须使用all_vars()而不是any_vars(),例如 dat %>% filter_all(all_vars(is.na(.))) - Agile Bean
1
在dplyr 1.0中,filter_allany_vars都已被取代,而any_vars没有我所知道的替代品。 colwise vignette建议的选项是定义自己的辅助函数,例如rowAny <- function(x) rowSums(x) > 0,以便上述解决方案变为dat %>% filter(rowAny(across(everything(), ~ !is.na(.x)))) - Callum Savage
在dplyr 1.0中的另一种选择可能是像这样的东西:dat %>% rowwise() %>% filter(sum(is.na(c_across(everything()))) != ncol(.)) %>% ungroup(),虽然可能有更优雅的方法来实现这个。 - Callum Savage
1
“colwise”小册子(现在?)提到了以下方法:“dat%>%filter(if_any(everything(),〜!is.na(.x)))”,它“保留谓词对于至少一个所选列为真的行”。 (请参见下面shosaco的答案) - Thomas K
非常感谢!我只是不明白为什么我们可以在这里引用点“.”,而不需要匿名函数中的“.x”。 - Lenn

22

我建议在这里使用出色的清洁工包。Janitor非常易用:

janitor::remove_empty(dat, which = "rows")

17

基准测试

@DavidArenburg提出了一些替代方案。以下是它们的简单基准测试。

library(tidyverse)
library(microbenchmark)

n <- 100
dat <- tibble(a = rep(c(1, 2, NA), n), b = rep(c(1, 1, NA), n))

f1 <- function(dat) {
  na <- dat %>% 
    rowwise() %>% 
    do(tibble(na = !all(is.na(.)))) %>% 
    .$na
  filter(dat, na)
}

f2 <- function(dat) {
  dat %>% filter(rowSums(is.na(.)) != ncol(.))
}

f3 <- function(dat) {
  dat %>% filter(rowMeans(is.na(.)) < 1)
}

f4 <- function(dat) {
  dat %>% filter(Reduce(`+`, lapply(., is.na)) != ncol(.))
}

f5 <- function(dat) {
  dat %>% mutate(indx = row_number()) %>% gather(var, val, -indx) %>% group_by(indx) %>% filter(sum(is.na(val)) != n()) %>% spread(var, val) 
}

# f1 is too slow to be included!
microbenchmark(f2 = f2(dat), f3 = f3(dat), f4 = f4(dat), f5 = f5(dat))

Reducelapply似乎是最快的:

> microbenchmark(f2 = f2(dat), f3 = f3(dat), f4 = f4(dat), f5 = f5(dat))
Unit: microseconds
 expr        min          lq       mean      median         uq        max neval
   f2    909.495    986.4680   2948.913   1154.4510   1434.725 131159.384   100
   f3    946.321   1036.2745   1908.857   1221.1615   1805.405   7604.069   100
   f4    706.647    809.2785   1318.694    960.0555   1089.099  13819.295   100
   f5 640392.269 664101.2895 692349.519 679580.6435 709054.821 901386.187   100

使用更大的数据集 107,880 x 40:

dat <- diamonds
# Let every third row be NA
dat[seq(1, nrow(diamonds), 3), ]  <- NA
# Add some extra NA to first column so na.omit() wouldn't work
dat[seq(2, nrow(diamonds), 3), 1] <- NA
# Increase size
dat <- dat %>% 
  bind_rows(., .) %>%
  bind_cols(., .) %>%
  bind_cols(., .)
# Make names unique
names(dat) <- 1:ncol(dat)
microbenchmark(f2 = f2(dat), f3 = f3(dat), f4 = f4(dat))
< p > F5 太慢了,因此也被排除在外。 F4 似乎比以前表现得更好。

> microbenchmark(f2 = f2(dat), f3 = f3(dat), f4 = f4(dat))
Unit: milliseconds
 expr      min       lq      mean    median       uq      max neval
   f2 34.60212 42.09918 114.65140 143.56056 148.8913 181.4218   100
   f3 35.50890 44.94387 119.73744 144.75561 148.8678 254.5315   100
   f4 27.68628 31.80557  73.63191  35.36144 137.2445 152.4686   100

1
我在想,在f4中使用purrr函数是否会影响速度? filter(reduce(map., is.na), \+`) != ncol(.))`可以说是更加整洁。 - ClaytonJY
1
在我的机器上,使用相同的大钻石数据集,原始的 f4 和我上面建议的经过净化的版本之间的性能大致相同。 - ClaytonJY

9

从dyplr 1.0开始,colwise文档提供了一个类似的示例:

filter(across(everything(), ~ !is.na(.x))) #Remove rows with *any* NA

我们可以看到它使用与多个表达式一样的隐式"&逻辑",因此下面的小调整将选择所有NA行:
filter(across(everything(), ~ is.na(.x))) #Remove rows with *any* non-NA

但问题要求移除具有 全部 NA 的行。

  1. 我们可以使用前面提到的简单 setdiff 方法,或者
  2. 我们可以利用 across 返回逻辑 tibble 的事实,而 filter 有效地执行逐行 all()(即 &)操作。

例如:

rowAny = function(x) apply(x, 1, any)
anyVar = function(fcn) rowAny(across(everything(), fcn)) #make it readable
df %<>% filter(anyVar(~ !is.na(.x))) #Remove rows with *all* NA

或者:

filterout = function(df, ...) setdiff(df, filter(df, ...))
df %<>% filterout(across(everything(), is.na)) #Remove rows with *all* NA

甚至可以将上述两种方式结合起来,更直接地表达第一个例子:
df %<>% filterout(anyVar(~ is.na(.x))) #Remove rows with *any* NA

我认为,tidyverse中的filter函数需要一个描述“聚合逻辑”的参数。 它可以默认为“all”并保留原有行为,或允许使用“any”,这样我们就不需要编写像anyVar这样的辅助函数了。


1
谢谢,使用setdiff的filterout函数正常工作。只需小心,因为它也会删除任何重复行。为避免这种情况,我们可以使用dplyr中的anti_join函数。filterout = function(df, ...) anti_join(df, filter(df, ...)) - radhikesh93

6

dplyr 1.0.4 引入了 if_any()if_all() 函数:

dat %>% filter(if_any(everything(), ~!is.na(.)))

或者,更加详细地说:
dat %>% filter(if_any(everything(), purrr::negate(is.na)))

选取数据并保留所有包含非空值的行。


6

dplyr 1.0的解决方案简单,不需要使用辅助函数,只需在正确的位置添加否定即可。

dat %>% filter(!across(everything(), is.na))

1
更简短的写法:dat %>% filter(!across(everything(), is.na)) - mharinga
1
@mharinga 是的,我试图表达得更明确,但我会根据你的建议编辑答案。 - alex.franco
3
这并没有回答所询问的问题,而是删除了任何一列包含NA的行,而不仅仅是删除所有列都包含NA的行。 - Latrunculia

2
这里有另一种解决方案,使用purrr::map_lgl()tidyr::nest()
library(tidyverse)

dat <- tibble(a = c(1, 2, NA), b = c(1, NA, NA), c = c(2, NA, NA))

any_not_na <- function(x) {
  !all(map_lgl(x, is.na))
}


dat_cleaned <- dat %>%
  rownames_to_column("ID") %>%
  group_by(ID) %>%
  nest() %>%
  filter(map_lgl(data, any_not_na)) %>%
  unnest() %>%
  select(-ID)
## Warning: package 'bindrcpp' was built under R version 3.4.2

dat_cleaned
## # A tibble: 2 x 3
##       a     b     c
##   <dbl> <dbl> <dbl>
## 1    1.    1.    2.
## 2    2.   NA    NA

我怀疑这种方法无法与@hejseb答案中的基准相竞争,但我认为它很好地展示了nest %>% map %>% unnest模式的工作原理,用户可以逐行运行并弄清楚发生了什么。


1
你可以使用dplyr中的complete.cases函数,使用点(.)来指定前一个数据框在链式操作中。
library(dplyr)
df = data.frame(
    x1 = c(1,2,3,NA),
    x2 = c(1,2,NA,5),
    x3 = c(NA,2,3,5)
)
df %>%
   filter(complete.cases(.))

  x1 x2 x3
1  2  2  2

这就是我想要的,不知道其他人怎么想!谢啦。 - John

0

我一个简洁的解决方案,适用于dplyr 1.0.1,就是使用rowwise()

dat %>%
  rowwise() %>%
  filter(!all(is.na(across(everything())))) %>%
  ungroup()

非常类似于@Callum Savage在顶部帖子上的评论,但我第一次错过了它,并且没有使用sum()


0

(整洁宇宙 1.3.1)

data%>%rowwise()%>%
filter(!all(is.na(c_across(is.numeric))))

data%>%rowwise()%>%
filter(!all(is.na(c_across(starts_with("***")))))

1
目前你的回答不够清晰。请编辑并添加更多细节,以帮助其他人理解它如何回答所提出的问题。你可以在帮助中心找到有关如何撰写好答案的更多信息。 - Community

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