使用 dplyr 删除所有变量均为 NA 的行
Remove rows where all variables are NA using dplyr
我在执行一项看似简单的任务时遇到了一些问题:使用 dplyr 删除 所有 变量 NA
的所有行。我知道它可以使用 base R (Remove rows in R matrix where all data is NA and Removing 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)
但这看起来不太好,尽管它完成了工作。其他想法?
基准测试
@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))
使用 Reduce
和 lapply
似乎是最快的:
> 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))
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
这是另一个使用 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
模式如何工作以及用户如何 运行 方面做得很好通过它 line-by-line 来弄清楚发生了什么。
自 dplyr 0.7.0 新版本以来,存在范围过滤动词。使用 filter_any 您可以轻松过滤至少有一个非缺失列的行:
# dplyr 0.7.0
dat %>% filter_all(any_vars(!is.na(.)))
使用@hejseb 基准测试算法,该解决方案似乎与 f4 一样高效。
更新:
自 dplyr 1.0.0 起,上述作用域动词已被取代。相反,引入了 across 函数族,它允许在多个(或所有)列上执行一个函数。过滤至少一列不是 NA 的行现在看起来像这样:
# dplyr 1.0.0
dat %>% filter(if_any(everything(), ~ !is.na(.)))
从dyplr 1.0开始,colwise小插图给出了一个类似的例子:
filter(across(everything(), ~ !is.na(.x))) #Remove rows with *any* NA
我们可以看到它使用与多个表达式相同的隐式“& 逻辑”filter
。所以下面的小调整选择所有 NA 行:
filter(across(everything(), ~ is.na(.x))) #Remove rows with *any* non-NA
但问题要求逆集:删除具有 all NA 的行。
- 我们可以使用前面的方法做一个简单的
setdiff
,或者
- 我们可以利用
across
returns 逻辑小标题和 filter
有效地执行 row-wise 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
或者将上面2个结合起来更直接的表达第一个例子:
df %<>% filterout(anyVar(~ is.na(.x))) #Remove rows with *any* NA
在我看来,tidyverse filter
函数将受益于描述 'aggregation logic' 的参数。它可以默认为“全部”并保留行为,或者允许“任何”,这样我们就不需要编写 anyVar
-like 辅助函数。
使用dplyr 1.0的解决方案很简单,不需要辅助函数,只需要在合适的地方加一个否定即可。
dat %>% filter(!across(everything(), is.na))
我建议在这里使用很棒的看门人包。看门人很user-friendly:
janitor::remove_empty(dat, which = "rows")
dplyr 1.0.4 引入了 if_any()
和 if_all()
功能:
dat %>% filter(if_any(everything(), ~!is.na(.)))
或者,更详细一点:
dat %>% filter(if_any(everything(), purrr::negate(is.na)))
“获取数据并保留任何条目为非 NA 的所有行”
我在 dplyr 1.0.1 中使用的一个巧妙的解决方案是使用 rowwise()
dat %>%
rowwise() %>%
filter(!all(is.na(across(everything())))) %>%
ungroup()
与@Callum Savage 在顶部的评论非常相似 post 但我在第一遍时错过了它,并且没有 sum()
(tidyverse 1.3.1)
data%>%rowwise()%>%
filter(!all(is.na(c_across(is.numeric))))
data%>%rowwise()%>%
filter(!all(is.na(c_across(starts_with("***")))))
您可以使用 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
我在执行一项看似简单的任务时遇到了一些问题:使用 dplyr 删除 所有 变量 NA
的所有行。我知道它可以使用 base R (Remove rows in R matrix where all data is NA and Removing 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)
但这看起来不太好,尽管它完成了工作。其他想法?
基准测试
@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))
使用 Reduce
和 lapply
似乎是最快的:
> 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))
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
这是另一个使用 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
模式如何工作以及用户如何 运行 方面做得很好通过它 line-by-line 来弄清楚发生了什么。
自 dplyr 0.7.0 新版本以来,存在范围过滤动词。使用 filter_any 您可以轻松过滤至少有一个非缺失列的行:
# dplyr 0.7.0
dat %>% filter_all(any_vars(!is.na(.)))
使用@hejseb 基准测试算法,该解决方案似乎与 f4 一样高效。
更新:
自 dplyr 1.0.0 起,上述作用域动词已被取代。相反,引入了 across 函数族,它允许在多个(或所有)列上执行一个函数。过滤至少一列不是 NA 的行现在看起来像这样:
# dplyr 1.0.0
dat %>% filter(if_any(everything(), ~ !is.na(.)))
从dyplr 1.0开始,colwise小插图给出了一个类似的例子:
filter(across(everything(), ~ !is.na(.x))) #Remove rows with *any* NA
我们可以看到它使用与多个表达式相同的隐式“& 逻辑”filter
。所以下面的小调整选择所有 NA 行:
filter(across(everything(), ~ is.na(.x))) #Remove rows with *any* non-NA
但问题要求逆集:删除具有 all NA 的行。
- 我们可以使用前面的方法做一个简单的
setdiff
,或者 - 我们可以利用
across
returns 逻辑小标题和filter
有效地执行 row-wiseall()
(即 &)这一事实。
例如:
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
或者将上面2个结合起来更直接的表达第一个例子:
df %<>% filterout(anyVar(~ is.na(.x))) #Remove rows with *any* NA
在我看来,tidyverse filter
函数将受益于描述 'aggregation logic' 的参数。它可以默认为“全部”并保留行为,或者允许“任何”,这样我们就不需要编写 anyVar
-like 辅助函数。
使用dplyr 1.0的解决方案很简单,不需要辅助函数,只需要在合适的地方加一个否定即可。
dat %>% filter(!across(everything(), is.na))
我建议在这里使用很棒的看门人包。看门人很user-friendly:
janitor::remove_empty(dat, which = "rows")
dplyr 1.0.4 引入了 if_any()
和 if_all()
功能:
dat %>% filter(if_any(everything(), ~!is.na(.)))
或者,更详细一点:
dat %>% filter(if_any(everything(), purrr::negate(is.na)))
“获取数据并保留任何条目为非 NA 的所有行”
我在 dplyr 1.0.1 中使用的一个巧妙的解决方案是使用 rowwise()
dat %>%
rowwise() %>%
filter(!all(is.na(across(everything())))) %>%
ungroup()
与@Callum Savage 在顶部的评论非常相似 post 但我在第一遍时错过了它,并且没有 sum()
(tidyverse 1.3.1)
data%>%rowwise()%>%
filter(!all(is.na(c_across(is.numeric))))
data%>%rowwise()%>%
filter(!all(is.na(c_across(starts_with("***")))))
您可以使用 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