【问题标题】:Reading Excel file: How to find the start cell in messy spreadsheets?阅读 Excel 文件:如何在凌乱的电子表格中找到起始单元格?
【发布时间】:2017-08-31 17:36:30
【问题描述】:

我正在尝试编写 R 代码来从一堆旧电子表格中读取数据。数据的确切位置因工作表而异:唯一不变的是第一列是日期,第二列以“每月回报”作为标题。在本例中,数据从单元格 B5 开始:

如何使用 R 自动在 Excel 单元格中搜索我的 “月度回报” 字符串?

目前,我能想到的最好办法是从单元格 A1 开始上传 R 中的所有内容,并整理出结果(巨大的)矩阵中的混乱。我希望有一个更优雅的解决方案

【问题讨论】:

  • 文件是什么格式的? .xlsx.xls.csv。您有机会上传一个示例来测试解决方案吗?
  • @dww 这些文件大多是 .xls,但我希望 .xlsx 会变得更加普遍。由于害怕泄露机密信息,我无法上传真实示例而不将它们篡改得面目全非。

标签: r excel


【解决方案1】:

我还没有找到优雅的方法,但我对这个问题非常熟悉(从 FactSet PA 报告 -> Excel -> R 获取数据,对吗?)。我知道不同的报告有不同的格式,这可能会很痛苦。

对于格式稍有不同的烦人电子表格,我会执行以下操作。它不是最优雅的(它需要两次读取文件),但它可以工作。我喜欢阅读该文件两次,以确保列的类型正确,并且具有良好的标题。很容易弄乱列导入,所以我宁愿让我的代码读取文件两次,也不愿自己检查并清理列,如果从正确的行开始,read_excel 默认值非常好。

另外,值得注意的是,截至今天 (2017-04-20),readxl had an update。我安装了新版本,看看这是否会使这变得非常容易,但我不相信是这样,尽管我可能弄错了。

library(readxl)
library(stringr)
library(dplyr)

f_path <- file.path("whatever.xlsx")

if (!file.exists(f_path)) {
  f_path <- file.choose()
}

# I read this twice, temp_read to figure out where the data actually starts...

# Maybe you need something like this - 
#   excel_sheets <- readxl::excel_sheets(f_path)
#   desired_sheet <- which(stringr::str_detect(excel_sheets,"2 Factor Brinson Attribution"))
desired_sheet <- 1
temp_read <- readxl::read_excel(f_path,sheet = desired_sheet)

skip_rows <- NULL
col_skip <- 0
search_string <- "Monthly Returns"
max_cols_to_search <- 10
max_rows_to_search <- 10

# Note, for the - 0, you may need to add/subtract a row if you end up skipping too far later.
while (length(skip_rows) == 0) {
  col_skip <- col_skip + 1
  if (col_skip == max_cols_to_search) break
  skip_rows <- which(stringr::str_detect(temp_read[1:max_rows_to_search,col_skip][[1]],search_string)) - 0

}

# ... now we re-read from the known good starting point.
real_data <- readxl::read_excel(
  f_path,
  sheet = desired_sheet,
  skip = skip_rows
)

# You likely don't need this if you start at the right row
# But given that all weird spreadsheets are weird in their own way
# You may want to operate on the col_skip, maybe like so:
# real_data <- real_data %>%
#   select(-(1:col_skip))

【讨论】:

  • 是的,实际上我正在阅读 FactSet 归因报告!我喜欢阅读两次的想法:一次是为了找出东西在哪里,一次是让 readxl 发挥它的魔力。
  • @lebelinoz 哈哈哈 - 我觉得有必要回答,因为我一眼就知道来源,已经写了代码(相反,在这个问题上我把头撞在桌子上),正在等待在工作中更新的东西。这是一个很好的事件汇合。
  • 我投票给这个是赢家,因为它没有假设每月回报在哪里。我在我的示例中运行它,它适用于“月度回报”位于单元格 F3 和单元格 C5 中的工作表。在电子表格中工作的加分点实际上是在旁边的几个其他单元格中写有“月度回报”字样(我最初写这个问题时没有注意到),并将列排序/跳过为事后的想法(因为事实证明,日期并不总是在月报表的左边)。
  • @RafaelZayas 这太棒了!补充一点是否有帮助?假设“Monthly Returns”行与 MARCH-14 和“0.097”行之间还有 5、4 或 3 行(取决于文件)--怎么样!?!?
  • @JWalt - 如果这些行是空白的并且不要弄乱您的类型解析,那么过滤应该可以工作。如果有随机文本(例如,您要删除的辅助标题行,弄乱类型解析),那就更棘手了。我可能会进行第三次数据读取 - 第 1 次:找到第一个标题行。第 2 步:读取标题行(使用 n_max 参数。第 3 步:读取数据(从之前的第 1 和第 2 步跳过行),没有标题 (col_names=FALSE),然后将第二遍标题分配给列名. 这牺牲了效率/速度来换取易用性和自动类型解析,YMMV。
【解决方案2】:

好的,在为 xls 指定格式后,从 csv 更新为正确建议的 xls 加载。

library(readxl)
data <- readxl::read_excel(".../sampleData.xls", col_types = FALSE)

你会得到类似的东西:

data <- structure(list(V1 = structure(c(6L, 5L, 3L, 7L, 1L, 4L, 2L), .Label = c("", 
"Apr 14", "GROSS PERFROANCE DETAILS", "Mar-14", "MC Pension Fund", 
"MY COMPANY PTY LTD", "updated by JS on 6/4/2017"), class = "factor"), 
    V2 = structure(c(1L, 1L, 1L, 1L, 4L, 3L, 2L), .Label = c("", 
    "0.069%", "0.907%", "Monthly return"), class = "factor")), .Names = c("V1", 
"V2"), class = "data.frame", row.names = c(NA, -7L))

然后您可以动态过滤“每月收益”单元格并识别您的矩阵。

targetCell <- which(data == "Monthly return", arr.ind = T)
returns <- data[(targetCell[1] + 1):nrow(data), (targetCell[2] - 1):targetCell[2]]

【讨论】:

  • 为了扩展这一点,我会使用data &lt;- readxl::read_excel(".../sampleData.xlsx", col_types = FALSE),这样您就可以直接读取excel文件而不必担心将其转换为csv。查找目标单元格的其余代码应该是一样的。
  • @pdil 它实际上指定它是一个 xlsx 文件吗?一开始我也是这么想的,但实际上并没有具体说明还是我错过了什么?但是,如果他相应地指定问题,我当然会添加 excel 加载。
  • 考虑到图像中电子表格的格式(字体、百分比等),我认为是这种情况
【解决方案3】:


使用像 readxl 这样的通用包,如果您想享受自动类型转换,您必须阅读两次。我假设您对前面的垃圾行数有某种上限?在这里我假设是 10。我在一个工作簿中迭代工作表,但如果迭代工作簿,代码看起来非常相似。我会编写一个函数来处理单个工作表或工作簿,然后使用lapply()purrr::map()。该函数将封装跳过学习读取和“真实”读取。

library(readxl)

two_passes <- function(path, sheet = NULL, n_max = 10) {
  first_pass <- read_excel(path = path, sheet = sheet, n_max = n_max)
  skip <- which(first_pass[[2]] == "Monthly return")
  message("For sheet '", if (is.null(sheet)) 1 else sheet,
          "' we'll skip ", skip, " rows.")  
  read_excel(path, sheet = sheet, skip = skip)
}

(sheets <- excel_sheets("so.xlsx"))
#> [1] "sheet_one" "sheet_two"
sheets <- setNames(sheets, sheets)
lapply(sheets, two_passes, path = "so.xlsx")
#> For sheet 'sheet_one' we'll skip 4 rows.
#> For sheet 'sheet_two' we'll skip 6 rows.
#> $sheet_one
#> # A tibble: 6 × 2
#>         X__1 `Monthly return`
#>       <dttm>            <dbl>
#> 1 2017-03-14          0.00907
#> 2 2017-04-14          0.00069
#> 3 2017-05-14          0.01890
#> 4 2017-06-14          0.00803
#> 5 2017-07-14         -0.01998
#> 6 2017-08-14          0.00697
#> 
#> $sheet_two
#> # A tibble: 6 × 2
#>         X__1 `Monthly return`
#>       <dttm>            <dbl>
#> 1 2017-03-14          0.00907
#> 2 2017-04-14          0.00069
#> 3 2017-05-14          0.01890
#> 4 2017-06-14          0.00803
#> 5 2017-07-14         -0.01998
#> 6 2017-08-14          0.00697

【讨论】:

  • 谢谢。您假设“月度回报”将出现在第二列中,我需要一些更笼统的内容。
  • @lebelinoz 我假设您实际上是这个意思:“数据的确切位置因工作表而异:唯一不变的是第一列是日期,第二列有“每月回报”作为标题。"
【解决方案4】:

在这些情况下,了解数据的可能条件非常重要。我会假设您只想删除不符合表格的列和行。

我有这本 Excel 书:

我在左侧添加了 3 个空白列,因为当我在 R 中加载一列时,程序会忽略它们。那是为了确认 R 省略了左侧的空列。

首先:加载数据

library(xlsx)
dat <- read.xlsx('book.xlsx', sheetIndex = 1)
head(dat)

            MY.COMPANY.PTY.LTD            NA.
1             MC  Pension Fund           <NA>
2    GROSS PERFORMANCE DETAILS           <NA>
3 updated  by IG on 20/04/2017           <NA>
4                         <NA> Monthly return
5                       Mar-14         0.0097
6                       Apr-14          6e-04

第二:我添加了一些带有NA''值的列,以防您的数据包含一些

dat$x2 <- NA
dat$x4 <- NA
head(dat)

            MY.COMPANY.PTY.LTD            NA. x2 x4
1             MC  Pension Fund           <NA> NA NA
2    GROSS PERFORMANCE DETAILS           <NA> NA NA
3 updated  by IG on 20/04/2017           <NA> NA NA
4                         <NA> Monthly return NA NA
5                       Mar-14         0.0097 NA NA
6                       Apr-14          6e-04 NA NA

第三:当所有值为NA''时,删除列。我必须处理过去的那种问题

colSelect <- apply(dat, 2, function(x) !(length(x) == length(which(x == '' | is.na(x)))))
dat2 <- dat[, colSelect]
head(dat2)

            MY.COMPANY.PTY.LTD            NA.
1             MC  Pension Fund           <NA>
2    GROSS PERFORMANCE DETAILS           <NA>
3 updated  by IG on 20/04/2017           <NA>
4                         <NA> Monthly return
5                       Mar-14         0.0097
6                       Apr-14          6e-04

第四:只保留具有完整观察的行(这是我从你的例子中推测的)

rowSelect <- apply(dat2, 1, function(x) !any(is.na(x)))
dat3 <- dat2[rowSelect, ]
head(dat3)

   MY.COMPANY.PTY.LTD     NA.
5              Mar-14  0.0097
6              Apr-14   6e-04
7              May-14  0.0189
8              Jun-14   0.008
9              Jul-14 -0.0199
10             Ago-14 0.00697

最后,如果你想保留标题,你可以做这样的事情:

colnames(dat3) <- as.matrix(dat2[which(rowSelect)[1] - 1, ])

colnames(dat3) <- c('Month', as.character(dat2[which(rowSelect)[1] - 1, 2]))
dat3

    Month Monthly return
5  Mar-14         0.0097
6  Apr-14          6e-04
7  May-14         0.0189
8  Jun-14          0.008
9  Jul-14        -0.0199
10 Ago-14        0.00697

【讨论】:

    【解决方案5】:

    这就是我将如何解决它。

    第 1 步
    阅读 without 标头中的 excel 电子表格。

    第 2 步
    在这种情况下,查找字符串 Monthly return 的行索引

    第 3 步
    从确定的行(或列或两者)过滤,稍微美化一下就完成了。

    这是一个示例函数的样子。无论它在电子表格中的哪个位置,它都适用于您的示例。您可以尝试使用regex 使其更加健壮。

    功能定义:

    library(xlsx)
    extract_return <-  function(path = getwd(), filename = "Mysheet.xlsx", sheetnum = 1){
                           filepath = paste(path, "/", filename, sep = "")
                           input = read.xlsx(filepath, sheetnum, header = FALSE)
                           start_idx = which(input == "Monthly return", arr.ind = TRUE)[1]
                           output = input[start_idx:dim(input)[1],]
                           rownames(output) <- NULL
                           colnames(output) <- c("Date","Monthly Return")
                           output = output[-1, ]  
                           return(output)
                      }
    

    示例:

    final_df <- extract_return(
                    path = "~/Desktop", 
                    filename = "Apr2017.xlsx", 
                    sheetnum = 2)
    

    无论您有多少行或列,这个想法都是一样的。尝试一下,让我知道。

    【讨论】:

    • 谢谢,但我认为您假设月度回报在第二列。
    • 我没有做出这样的假设。通过列名,您可能会有这种感觉。如果您删除列名,或允许 R 设置默认名称,则每月返回可以是任何列。我的逻辑是在电子表格中找到每月回报,将其视为 df 的第一行,然后提取其下方的所有内容。提取后,它只是删除第一行。逻辑不看列位置。
    【解决方案6】:

    这是一个简洁的替代方案,可以避免上面讨论的多次读取问题。但是,在进行基准测试时,Rafael Zayas 的答案仍然胜出。

    library("tidyxl")
    library("unpivotr")
    library("tidyr")
    library("dplyr")
    
    tidy_solution <- function() {
        raw <- xlsx_cells("messyExcel.xlsx")
    
        start <- raw %>%
            filter_all(any_vars(. %in% c("Monthly return"))) %>%
            select(row, col)
    
        month.col <- raw %>%
            filter(row >= start$row + 1, col == start$col - 1) %>%
            pivot_wider(date, col)
    
        return.col <- raw %>%
            filter(row >= start$row + 1, col == start$col) %>%
            pivot_wider(numeric, col)
     
        output <- cbind(month.col, return.col)
     }
    
    # My Solution
                expr     min       lq     mean   median      uq     max neval
    tidy_solution() 29.0372 30.40305 32.13793 31.36925 32.9812 56.6455   100
    
    # Rafael's
                    expr     min      lq     mean   median       uq     max neval
    original_solution() 21.4405 23.8009 25.86874 25.10865 26.99945 59.4128   100
    

    【讨论】:

      【解决方案7】:
      grep("2014",dat)[1]
      

      这将为您提供带有年份的第一列。或者使用“-14”或任何你多年来拥有的东西。 类似的方式 grep("Monthly",dat)[1] 给你第二列

      【讨论】:

      • 这并没有真正回答问题。
      • 为什么不呢?更详细地说明您需要什么。
      • 你甚至没有读过这个问题。我在问如何在 R 中的 Excel 中自动执行任务(特别是如何在单元格中查找“月度回报”一词),而您告诉我如何根据字符串过滤数据,而没有任何参考Excel。
      • 如果你需要将excel sheet导入R,那就是一个,如果你需要解析这个sheet,那就完全是另一个了。学会准确说出您的需求,不要因为您无法解释您的需求而责备他人
      猜你喜欢
      • 2020-02-22
      • 1970-01-01
      • 1970-01-01
      • 2021-06-03
      • 1970-01-01
      • 2020-10-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多