使用cut()函数为数据集添加区间类时返回NA值的问题排查
问题重现
你希望基于以下数据集,为每行的Position字段添加对应LimAm到LimAv的区间类:
df <- structure(list(Position = c(1925.12260901036, 2463.70794121719, 1282.23738714798), ID = c(11, 12, 24), LimAm = c(39943.8043856595, 40482.3897178663, 39300.9191637971), LimAv = c(40443.8043856595, 40982.3897178663, 39800.9191637971)), row.names = c(NA, -3L), class = c("tbl_df", "tbl", "data.frame"))
但执行以下代码后,class列全为NA:
df %>% mutate(class=cut(Position,c(LimAm, LimAv),include.lowest = FALSE))
输出结果:
# A tibble: 3 × 5 Position ID LimAm LimAv class <dbl> <dbl> <dbl> <dbl> <fct> 1 1925. 11 39944. 40444. NA 2 2464. 12 40482. 40982. NA 3 1282. 24 39301. 39801. NA
原因解析
1. 数值本身不匹配
所有Position的取值(1925、2464、1282)都远小于对应行的LimAm(39944、40482、39301),自然不在(LimAm, LimAv]这个区间内,返回NA是符合逻辑的结果。
2. cut()函数的用法误解
cut()的第二个参数breaks要求是全局统一的分割点向量,而非逐行对应的区间。当你传入c(LimAm, LimAv)时,它会把所有行的LimAm和LimAv合并成一个全局分割序列(比如示例中是c(39943.8,40482.39,39300.92,40443.8,40982.39,39800.92)),然后将每个Position放到这个全局分割的区间中,而非逐行匹配自身的LimAm和LimAv。
之前代码能正常运行是巧合——当时的Position刚好落在全局分割后的某个区间里,新增数据后全局分割区间变化,或者Position不再匹配,就出现了NA。
正确解决办法
要实现逐行判断Position是否在对应行的(LimAm, LimAv]区间,需要用逐行处理的方式,比如结合rowwise()或者purrr包的map2():
方法1:用rowwise()逐行处理
library(dplyr) df %>% rowwise() %>% # 先判断是否在区间内,再生成区间标签或NA mutate(class = ifelse(Position > LimAm & Position <= LimAv, paste0("(", round(LimAm, 1), ";", round(LimAv, 1), "]"), NA_character_)) %>% ungroup()
如果想保留cut()生成的因子型结果,也可以在rowwise()内调用cut():
df %>% rowwise() %>% mutate(class = cut(Position, breaks = c(LimAm, LimAv), include.lowest = FALSE)) %>% ungroup()
方法2:用purrr::map2()逐行生成结果
library(dplyr) library(purrr) df %>% mutate(class = map2_chr(LimAm, LimAv, ~{ ifelse(Position > .x & Position <= .y, paste0("(", round(.x, 1), ";", round(.y, 1), "]"), NA_character_) }))
补充说明
如果之前的数据集能正常运行,大概率是当时的Position数值落在了全局分割后的某个区间,或者你误将逐行区间的逻辑用在了全局分割上。正确的逐行区间匹配必须明确处理每行的独立区间,而不是依赖cut()的全局分割特性。
内容的提问来源于stack exchange,提问作者C. Guff

