我如何将一个变量中的列表追加到R数据框中特定行的列表中?

3
我创建了一个 RMarkdown,用于检查错误。它会输出 print 语句,指定错误和需要更正的行数(这将检查下面的 df 中的错误)。我创建了另一个数据框(在下面的示例中是 df.index),用于跟踪每列(在 df 中)需要修正的行。本质上,我需要添加一列来存储需要为 df 中每个列进行纠正的行列表。然后,随着我执行更多的错误检查,我需要将条目附加到 df.index 中给定行中的列表,并在新创建的 summary 数据框中的 rows 列中添加新的列表到其他行。
我查看了几十个关于列表的 SO 条目,但找不到一个好的答案。下面是我尝试过的内容,我使用此简要示例展示它。该代码可以运行并给出我想要的输出。但它非常冗长,可能很难让我项目团队的其他成员阅读和理解。 简要示例 数据
library(dplyr)

# Dataframe that contains the dataset that I'm checking for errors.
df <-
  structure(
    list(
      `1.1.` = c("Andrew", "Max", "Sylvia", NA, "1",
                 NA, NA, "Jason"),
      `1.2.` = c(1, 2, 2, NA, NA, 5, 3, NA),
      `1.3.` = c(
        "cool",
        "amazing",
        "wonderful",
        "okay",
        NA,
        "sweet",
        "chocolate",
        "fine"
      )
    ),
    class = "data.frame",
    row.names = c(NA, -8L)
  )


# Dataframe that contains the column numbers and names, which will be used to create a summary of what rows need to be changed for each column.
df.index <-
  structure(list(
    number = c("1.1.", "1.2.", "1.3."),
    name = c("name",
             "number", "category")
  ),
  class = "data.frame",
  row.names = c(NA, -3L))

我尝试过的方法

obs <- "1.1."

na.index <- which(is.na(df$`1.1.`))

summary <- df.index %>%
  dplyr::mutate(rows = ifelse(number == obs, list(na.index), NA))

# Check to see if there are any numeric values in this character column. Adding 6 just to have a duplicate for this example.
na.index2 <-
  c(which(!is.na(as.numeric(
    as.character(df$`1.1.`)
  ))), 6)

# Append new list from na.index2 to the existing list in row 1 (or 1.1.), and keep only the unique values, excluding NAs.
summary <- summary %>%
  dplyr::mutate(rows = ifelse(number == obs, list(unique(na.omit(
    unlist(append(rows, list(na.index2)))
  ))), NA))

# Column 1.2. in df.
obs <- "1.2."

na.index3 <- which(df$`1.2.` > 2)

summary <- summary %>%
  dplyr::mutate(rows = ifelse(number == obs, list(na.index3), rows))

na.index4 <- which(df$`1.2.` == 2)

summary <- summary %>%
  dplyr::mutate(rows = ifelse(number == obs, list(unique(na.omit(
    unlist(append(rows[2], list(na.index4)))
  ))), rows))

# Column 1.3. in df.
obs <- "1.3."

na.index5 <- which(df$`1.3.` == "okay")

summary <- summary %>%
  dplyr::mutate(rows = ifelse(number == obs, list(na.index5), rows))

输出(也是预期输出)

summary

  number     name       rows
1   1.1.     name 4, 6, 7, 5
2   1.2.   number 6, 7, 2, 3
3   1.3. category          4

我得到了以上示例中所有正确的行,但一定有更简单的方法来做到这一点,而且不需要创建obs并在附加列表时指定行号(例如rows[2])。
正如您所看到的,不是每一列都具有相同的错误检查。因此,我希望能够很容易地将一个列表添加到summary中的rows列中,就像在每个类别(如1.2.1.3.等)的类似检查中一样,同时能够附加额外的列表(如此处所示)。
2个回答

3
我们可以遍历'df'的列,获取NA和仅为数字的元素(str_detect)的索引并放入一个list中。然后使用`pivot_longer`将其重塑成“长”格式,并与'df.index'数据进行连接。
library(dplyr)
library(stringr)
library(tidyr)
out <-  df %>% 
    summarise(across(everything(), ~ {
       tmp <- df.index$name[match(cur_column(), df.index$number)]
       if(tmp == 'name') list(which(is.na(.)|str_detect(., 
          '^-?[0-9.]+$'))) else if(tmp == "number") 
             list(which(is.na(.)|str_detect(., '\\D'))) else 
          list(which(is.na(.)))})) %>%
    pivot_longer(everything(), names_to = 'number', values_to = 'rows') %>% 
    right_join(df.index) %>%
    select(names(df.index), everything())

-输出

> out
# A tibble: 3 x 3
  number name     rows     
  <chr>  <chr>    <list>   
1 1.1.   name     <int [4]>
2 1.2.   number   <int [3]>
3 1.3.   category <int [1]>

> out$rows
[[1]]
[1] 4 5 6 7

[[2]]
[1] 4 5 8

[[3]]
[1] 5

更新

根据最新的检查,只需修改else ifelse中的条件即可。

out <-  df %>% 
    summarise(across(everything(), ~ {
       tmp <- df.index$name[match(cur_column(), df.index$number)]
       if(tmp == 'name') list(which(is.na(.)|str_detect(., 
          '^-?[0-9.]+$'))) else if(tmp == "number") 
             list(which(. >= 2)) else 
          list(which(. %in% 'okay'))})) %>%
    pivot_longer(everything(), names_to = 'number', values_to = 'rows') %>% 
    right_join(df.index) %>%
    select(names(df.index), everything())

-输出
> out$rows
[[1]]
[1] 4 5 6 7

[[2]]
[1] 2 3 6 7

[[3]]
[1] 4

2

我会为每个需要检查的项目创建一个函数。然后,我会设计一个框架来将这些函数应用到感兴趣的列上。以下是一种实现方式。

# your check for numbers
check_1 <- function(x) {
  is_number <- !is.na(as.numeric(as.character(x)))
  is_na <- is.na(x)
  
  which(is_number | is_na)
}

# some random check ("fine" is an error!)
check_2 <- function(x) {
  which(x == "fine")
}

我会创建一个检查列表。在这里,元素是列的名称,而值是该函数。
all_checks <- list("1.1." = check_1,
                   "1.3." = check_2)

现在,您可以map 每个检查,并将其应用于您想要的列。
check_results <- imap(all_checks, ~ .x(df[[.y]]))
# > check_results
# $`1.1.`
# [1] 4 5 6 7
# 
# $`1.3.`
# [1] 8

如果您愿意,您可以使用这些结果来创建摘要表格。或者,只需保留它并将其用作列表。

map_dfr(check_results, ~ tibble(rows = paste(.x, collapse = ", ")), .id = "number") %>% 
  left_join(df.index, by = "number") %>% 
  relocate(number, name, rows)
# # A tibble: 2 x 3
#   number name     rows      
#   <chr>  <chr>    <chr>     
# 1 1.1.   name     4, 5, 6, 7
# 2 1.3.   category 8  

这样做的好处是现在可以很容易地查看所有检查,并添加更多/更新。甚至可以将多个检查添加到同一列中。结果check_results列表可以具有相同名称的多个元素。如果您想仅按列标记所有错误而不是按检查分解,则只需要做一些额外的工作来折叠它们。

# apply check_1 to more columns
all_checks <- list("1.1." = check_1,
                   "1.2." = check_1,
                   "1.3." = check_2)

check_results <- imap(all_checks, ~ .x(df[[.y]]))
# > check_results
# $`1.1.`
# [1] 4 5 6 7
# 
# $`1.2.`
# [1] 1 2 3 4 5 6 7 8
# 
# $`1.3.`
# [1] 8

跟踪编辑

如果你想添加更多选项,这个功能是可以扩展的。例如,以下是如何将额外参数传递给检查函数。

# this nor takes the column as x, but some other y and z as well
check_3 <- function(x, y, z) {
  message("passed in y:\t", y)
  message("passed in z:\t", z)
  
  x_criteria <- x == "Max"
  
  which(x_criteria)
}

您可以将您的all_checks列表制作得尽可能复杂,并从相关列表元素中提取信息。
# now we have a list with the function and parameters
all_checks <- list("1.1." = list(fn = check_3, pars = list(z = 5, y = 10)),
                   "1.3." = list(fn = check_2))

# we use exec to execute the function with the parameter list
imap(all_checks, ~ exec(.x$fn, x = df[[.y]], !!!.x$pars))
# passed in y:  10
# passed in z:  5
# $`1.1.`
# [1] 2
#
# $`1.3.`
# [1] 8

这对于我的情况非常好!我正在处理的数据集大约有100列和数百行数据;我还必须进行约30个不同的错误检查。因此,我认为这是更好的组织方式(并且更容易为其他人所遵循),以便更容易跟踪。如果没有那么多列和更少的错误检查,另一个解决方案也很好。谢谢! - AndrewGB
1
我经常使用R进行大量的ETL工作,并且自己也有一些类似的东西。我喜欢有一个列表,可以轻松查看所有内容。另外,您还可以为不同类型的检查、适用于多个列(从数据框中读取)的规则等设置单独的列表。随着您的操作越来越多,您可以开始构建适合自己的框架。最终会变得非常强大。 - user10917479
太好了,谢谢!是的,我将要检查的一些错误会更加复杂,因为其中一些检查将取决于其他列中的内容,所以在处理这些问题时一定要记住分开的列表想法。 - AndrewGB
快速跟进:如何以最简单的方式将附加参数传递给您应用的每个函数?例如,check_1具有超过x的参数,例如check_1 <- function(x, y, z)...。您如何在imap中传递它们? - AndrewGB
1
@AndyBrown 我在结尾处添加了一个修订。简而言之,您可以将all_checks转换为具有一定结构的嵌套列表,并根据需要取出列表元素。诀窍是如何使用imap最有效地完成此操作。 - user10917479
谢谢!我有一个后续问题,关于在尝试应用于整个数据框时尝试类似操作时遇到的问题,我无法弄清楚如何将其与此集成,并想知道您是否可以看一下。但是我已经为此创建了一个新的问题:https://stackoverflow.com/questions/68737808/how-to-pass-column-names-into-a-function-when-using-an-apply-or-map-function-in - AndrewGB

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