【问题标题】:Direct way of telling ifelse to ignore NA告诉 ifelse 忽略 NA 的直接方法
【发布时间】:2017-06-07 10:56:04
【问题描述】:

正如here 所解释的,当ifelse(test, yes, no) 中的测试条件为NA 时,评估也是NA。因此以下返回...

df <- data.frame(a = c(1, 1, NA, NA, NA ,NA),
                 b = c(NA, NA, 1, 1, NA, NA),
                 c = c(rep(NA, 4), 1, 1))
ifelse(df$a==1, "a==1", 
    ifelse(df$b==1, "b==1", 
        ifelse(df$c==1, "c==1", NA)))
#[1] "a==1" "a==1" NA     NA     NA     NA    

... 而不是想要的

#[1] "a==1" "a==1" "b==1" "b==1"  "c==1" "c==1" 

按照 Cath 的建议,我可以通过正式指定测试条件不应包含 NA 来规避此问题:

ifelse(df$a==1 &  !is.na(df$a), "a==1", 
    ifelse(df$b==1 & !is.na(df$b), "b==1", 
        ifelse(df$c==1 & !is.na(df$c), "c==1", NA)))

但是,正如 akrun 还指出的那样,随着列数的增加,此解决方案变得相当冗长。


解决方法是首先将所有 NAs 替换为 data.frame 中不存在的值(例如,在本例中为 2):

df_noNA <- data.frame(a = c(1, 1, 2, 2, 2 ,2),
                 b = c(2, 2, 1, 1, 2, 2),
                 c = c(rep(2, 4), 1, 1))

ifelse(df_noNA$a==1, "a==1", 
    ifelse(df_noNA$b==1, "b==1", 
        ifelse(df_noNA$c==1, "c==1", NA)))
#[1] "a==1" "a==1" "b==1" "b==1"  "c==1" "c==1" 

但是,我想知道是否有更直接的方法来告诉ifelse 忽略 NAs?还是为&amp; !is.na写一个函数是最直接的方式?

ignorena <- function(column) {
        column ==1 & !is.na(column)
}
ifelse(ignorena(df$a), "a==1", 
    ifelse(ignorena(df$b), "b==1", 
        ifelse(ignorena(df$c), "c==1", NA)))
#[1] "a==1" "a==1" "b==1" "b==1"  "c==1" "c==1" 

【问题讨论】:

    标签: r if-statement na


    【解决方案1】:

    您可以使用%in% 代替== 来忽略NAs。

    ifelse(df$a %in% 1, "a==1", 
           ifelse(df$b %in% 1, "b==1", 
                  ifelse(df$c %in% 1, "c==1", NA)))
    

    不幸的是,与原来的相比,这并没有带来任何性能提升,而 @arkun 的解决方案大约快了 3 倍。

    solution_original <- function(){
      ifelse(df$a==1 &  !is.na(df$a), "a==1", 
             ifelse(df$b==1 & !is.na(df$b), "b==1", 
                    ifelse(df$c==1 & !is.na(df$c), "c==1", NA)))
    }
    
    solution_akrun <- function(){
      v1 <- names(df)[max.col(!is.na(df)) * NA^!rowSums(!is.na(df))]
      i1 <- !is.na(v1)
      v1[i1] <- paste0(v1[i1], "==1")
    }
    
    solution_mine <- function(x){
      ifelse(df$a %in% 1, "a==1", 
             ifelse(df$b %in% 1, "b==1", 
                    ifelse(df$c %in% 1, "c==1", NA)))
    }
    set.seed(1)
    df <- data.frame(a = sample(c(1, rep(NA, 4)), 1e6, T),
                     b = sample(c(1, rep(NA, 4)), 1e6, T),
                     c = sample(c(1, rep(NA, 4)), 1e6, T))
    microbenchmark::microbenchmark(
      solution_original(),
      solution_akrun(),
      solution_mine()
    )
    ## Unit: milliseconds
    ##                expr      min       lq     mean   median       uq       max neval
    ## solution_original() 701.9413 839.3715 845.0720 853.1960 875.6151 1051.6659   100
    ##    solution_akrun() 217.4129 242.5113 293.2987 253.2144 387.1598  564.3981   100
    ##     solution_mine() 698.7628 845.0822 848.6717 858.7892 877.9676 1006.2872   100
    

    受此启发:R: Dealing with TRUE, FALSE, NA and NaN

    编辑

    根据@arkun 的评论,我重新制定了基准并修改了声明。

    【讨论】:

    • 我使用的是 OP 提供的 6 行数据。
    • 做到了。见编辑。事实证明,您的解决方案更快。但是,我还发现当一行中的所有值都是 NAs 时,您的解决方案不会返回收集输出(您的解决方案中的值是 c==3,这不是 OP 想要的)。如果您提供修复,我可以重新运行基准测试。
    • 这对我来说是一个简单的修复,但我认为 OP 的示例并没有显示出这种模式
    • 虽然 %in% 在这种情况下有效,但我一般不建议这样做,因为当你有 vector.a %in% vector.b 时,当两个向量都有 NA 时,你会得到 NA - 这可能不是你想要的。
    【解决方案2】:

    dplyr::case_when 是级联 ifelse 调用的便捷替代方案:

    library(dplyr)
    
    df <- data.frame(a = c(1, 1, NA, NA, NA ,NA),
                     b = c(NA, NA, 1, 1, NA, NA),
                     c = c(rep(NA, 4), 1, 1))
    
    df %>% mutate(equals = case_when(a == 1 ~ 'a==1', 
                                     b == 1 ~ 'b==1', 
                                     c == 1 ~ 'c==1'))
    #>    a  b  c equals
    #> 1  1 NA NA   a==1
    #> 2  1 NA NA   a==1
    #> 3 NA  1 NA   b==1
    #> 4 NA  1 NA   b==1
    #> 5 NA NA  1   c==1
    #> 6 NA NA  1   c==1
    

    它像ifelse 一样级联,所以如果第一个条件为真,即使第二个和第三个条件也为真,也会返回第一个结果。如果都不为真,则返回NA

    set.seed(47)
    df <- setNames(as.data.frame(matrix(sample(c(1, NA), 30, replace = TRUE), 10)), letters[1:3])
    
    df %>% mutate(equals = case_when(a == 1 ~ 'a==1', 
                                     b == 1 ~ 'b==1', 
                                     c == 1 ~ 'c==1'))
    #>     a  b  c equals
    #> 1  NA  1  1   b==1
    #> 2   1 NA NA   a==1
    #> 3  NA  1 NA   b==1
    #> 4  NA NA  1   c==1
    #> 5  NA NA NA   <NA>
    #> 6  NA NA  1   c==1
    #> 7   1  1  1   a==1
    #> 8   1  1  1   a==1
    #> 9  NA  1 NA   b==1
    #> 10 NA  1 NA   b==1
    

    而且速度很快:

    set.seed(47)
    df <- setNames(as.data.frame(matrix(sample(c(1, NA), 3 * 1e5, replace = TRUE), ncol = 3)), letters[1:3])
    
    microbenchmark::microbenchmark(
        original = {
            ifelse(df$a == 1 &  !is.na(df$a), "a==1", 
                   ifelse(df$b == 1 & !is.na(df$b), "b==1", 
                          ifelse(df$c == 1 & !is.na(df$c), "c==1", NA)))},
        akrun = {
            v1 <- names(df)[max.col(!is.na(df)) * NA^!rowSums(!is.na(df))]
            i1 <- !is.na(v1)
            v1[i1] <- paste0(v1[i1], "==1")
        },
        amatsuo_net = {
            ifelse(df$a %in% 1, "a==1", 
                   ifelse(df$b %in% 1, "b==1", 
                          ifelse(df$c %in% 1, "c==1", NA)))
        },
        alistaire = {
            df %>% mutate(equals = case_when(a == 1 ~ 'a==1', 
                                             b == 1 ~ 'b==1', 
                                             c == 1 ~ 'c==1'))
        }
    )
    #> Unit: milliseconds
    #>         expr      min       lq      mean    median        uq       max neval
    #>     original 81.19896 86.11843 110.93882 123.92463 128.58037 171.11026   100
    #>        akrun 27.50351 30.99127  38.98353  32.67991  34.64947  77.98958   100
    #>  amatsuo_net 83.75744 88.54095 109.22226 110.40066 129.02168 170.92911   100
    #>    alistaire 16.57426 18.91951  21.73293  19.29925  24.30350  33.83180   100
    

    【讨论】:

    • 我不知道为什么你的第一段代码不再运行了。
    • @B.Davis 对我来说一切都一样。你遇到了什么错误?
    • Error in mutate_impl(.data, dots) : object 'a' not found
    • dplyr_0.5.0R version 3.3.2 (2016-10-31) 但我还没有尝试更新
    • @B.Davis 当case_when 首次被引入时,它使用基本语义而不是 dplyr 样式的 NSE,因此您必须编写 .$a。它在某个时候完全集成,因此在当前版本中,任何一种样式都可以使用。
    【解决方案3】:

    我们可以在没有嵌套ifelse 循环的情况下更有效地做到这一点。对于第一个数据集,我们为非 NA 元素创建一个逻辑矩阵 (!is.na(df)),获取最大值的列索引,即每一行的 TRUE,使用该索引获取列名,paste 和 @987654324 @

    paste0(names(df)[max.col(!is.na(df))], "==1")
    #[1] "a==1" "a==1" "b==1" "b==1" "c==1" "c==1"
    

    如果存在只有 NA 的行

    v1 <- names(df)[max.col(!is.na(df)) * NA^!rowSums(!is.na(df))]
    i1 <- !is.na(v1)
    v1[i1] <- paste0(v1[i1], "==1")
    

    第二个数据集因为没有NA,我们可以直接和1比较得到一个逻辑矩阵,步骤和之前一样

    paste0(names(df_noNA)[max.col(df_noNA == 1)], "==1")
    #[1] "a==1" "a==1" "b==1" "b==1" "c==1" "c==1"
    

    【讨论】:

    • 这是否意味着除了指定ifelse 应该忽略NAs 的每个条件之外,没有直接的方法告诉ifelse 忽略NAs?
    • @Flo 你需要!is.na
    • @akroun 感谢您的澄清和 +1 的有效解决方案!我接受了 amatsuo_net 的回答,因为它提供了“一种更直接的方式来告诉 ifelse 忽略 NAs”。
    猜你喜欢
    • 2011-12-17
    • 2012-11-15
    • 2011-06-20
    • 2018-07-16
    • 1970-01-01
    • 2012-11-30
    • 2018-10-07
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多