如何标记数据框中第一次出现的字符串以及之后的所有行?

How to flag the first occurrence of a character string in a data frame and all rows after?

假设我们从如下所示的数据框开始:

   ID STATE_1 STATE_2
1   1    NULL    NULL
2   1     FRY    NULL
3   1    NULL     CRY
4   1     FRY     CRY
5   1    NULL    NULL
6   1    NULL    NULL
7   1     FRY     CRY
8   1    NULL    NULL
9   5    NULL    NULL
10  5    NULL     CRY
11  5     FRY    NULL
12  5    NULL    NULL

复制如下:

ID <- c(1, 1, 1, 1, 1, 1, 1, 1, 5, 5, 5, 5)
STATE_1 <- c("NULL", "FRY", "NULL", "FRY", "NULL", "NULL", "FRY", "NULL", "NULL", "NULL", "FRY", "NULL")
STATE_2 <- c("NULL", "NULL", "CRY", "CRY", "NULL", "NULL", "CRY", "NULL", "NULL", "CRY", "NULL", "NULL")
df <- data.frame(ID, STATE_1, STATE_2)

如何添加一个 FLAG 列,从而对于每个 ID,一旦多个状态中的第一个被非 NULL 元素触发,则该 ID 的所有后续行都标记为第一个非 NULL状态?因此,例如,添加 FLAG 列后,输出将如下所示:

   ID STATE_1 STATE_2   FLAG   [Explanations for FLAG column elements]
1   1    NULL    NULL   NULL    No flag tripped for ID 1 (yet) so value is NULL
2   1     FRY    NULL    FRY    FRY tripped in STATE_1 so FRY applies for all subsequent rows for this ID
3   1    NULL     CRY    FRY    Ignore CRY since FRY happened first
4   1     FRY     CRY    FRY
5   1    NULL    NULL    FRY
6   1    NULL    NULL    FRY
7   1     FRY     CRY    FRY
8   1    NULL    NULL    FRY
9   5    NULL    NULL   NULL    No flag tripped for ID 5 (yet) so value is NULL
10  5    NULL     CRY    CRY    CRY tripped in STATE_2 so CRY applies for all subsequent rows for this ID
11  5     FRY    NULL    CRY    Ignore FRY since CRY happened first
12  5    NULL    NULL    CRY    But please read the following paragraph, because another possible scenario is omitted for sake of brevity

如果在上面的第 10 行中,对于 ID 5,同一行中同时出现了 FRY 和 CRY,那么 FRY 将控制所有后续的 ID 5 行。我没有在上面的示例中包括这种情况。所以有一个 STATE_1 胜过 STATE_2 的优先级,等等

在我的实际数据中有6个状态,而这个例子中有2个状态(STATE_1和STATE_2)。

我非常喜欢与 dplyr() 一起工作。

当我对此进行研究时,有几篇文章针对基于数字触发器的标记数据,但我找不到像我的数据中那样使用字符串触发器的任何解决方案。我希望有一个字符串解决方案,这样我就可以避免从字符串转换为数字变量的额外步骤。

是的,这可以用dplyr和字符串来完成。这是一种方法:

df %>%
  mutate(across(.cols = c("STATE_1", "STATE_2"), ~ na_if(., "NULL"))) %>%
  group_by(ID) %>%
  fill(STATE_1, STATE_2, .direction = "down") %>%
  mutate(flag = if_else(is.na(lag(coalesce(STATE_1, STATE_2))),
                        coalesce(STATE_1, STATE_2),
                        NA_character_)) %>%
  fill(flag, .direction = "down")
   ID STATE_1 STATE_2 flag
1   1    <NA>    <NA> <NA>
2   1     FRY    <NA>  FRY
3   1     FRY     CRY  FRY
4   1     FRY     CRY  FRY
5   1     FRY     CRY  FRY
6   1     FRY     CRY  FRY
7   1     FRY     CRY  FRY
8   1     FRY     CRY  FRY
9   5    <NA>    <NA> <NA>
10  5    <NA>     CRY  CRY
11  5     FRY     CRY  CRY
12  5     FRY     CRY  CRY

一步一步,这是这个解决方案的作用:

  • (将字符“NULL”值转换为NA。)
  • Group by ID 以便我们在每个ID内单独操作。
  • 使用fill()使用STATE_1STATE_2中的每个非空值来填充它下面的所有非空值,直到我们到达下一个非空值. (您也可以填写“向上”而不是向下;向下是默认方向,但为了清楚起见,我已将其明确包含在内。)
  • 创建 flag 字段。如果前一行中的两个状态都有空值(由lag()标识),那么我们想要触发一个新标志;使用 coalesce() 使 STATE_1 优先于 STATE_2。否则,我们不想要一面新旗帜;现在用 NA 填充。
  • 再次使用 fill() 向下填充标志:每个新标志都会填充其下方的行,直到我们得到一个新标志。

此过程也适用于您描述的场景,其中第 10 行有 STATE_1 的“FRY”:

df2 = df
df2$STATE_1[10] = "FRY"
df2 %>%
  mutate(across(.cols = -c("ID"), ~ na_if(., "NULL"))) %>%
  group_by(ID) %>%
  fill(STATE_1, STATE_2, .direction = "down") %>%
  mutate(flag = if_else(is.na(lag(coalesce(STATE_1, STATE_2))),
                        coalesce(STATE_1, STATE_2),
                        NA_character_)) %>%
  fill(flag, .direction = "down")
   ID STATE_1 STATE_2 flag
1   1    <NA>    <NA> <NA>
2   1     FRY    <NA>  FRY
3   1     FRY     CRY  FRY
4   1     FRY     CRY  FRY
5   1     FRY     CRY  FRY
6   1     FRY     CRY  FRY
7   1     FRY     CRY  FRY
8   1     FRY     CRY  FRY
9   5    <NA>    <NA> <NA>
10  5     FRY     CRY  FRY
11  5     FRY     CRY  FRY
12  5     FRY     CRY  FRY