Оптимизировать звонки, чтобы мутировать и суммировать? - PullRequest
2 голосов
/ 22 января 2020

У меня есть этот скрипт R:

rm(list = ls())

library(tidyr)
suppressWarnings(library(dplyr))
outFile = "zFinal.lua"

cat("\014\n")

cat(file = outFile, sep = "")

filea <- read.csv("csva.csv", strip.white = TRUE)
fileb <- read.csv("csvb.csv", strip.white = TRUE, sep = ";", header=FALSE)

df <-    
    merge(filea, fileb, by.x = c(3), by.y = c(1)) %>%
    subset(select = c(1, 3, 6, 2)) %>%
    arrange(ColA, ColB, V2) %>%
    group_by(ColA) %>%
    mutate(V2 = paste0('"', V2, "#", ColB, '"')) %>%
    summarise(ID = paste(V2, collapse = ", ", sep=";")) %>%
    mutate(ID = paste0('["', ColA, '"] = {', ID, '},')) %>%
    mutate(ID = paste0('\t\t', ID))

df <- df[c("ID")]

cat("\n\tmyTable = {\n", file = outFile, append = TRUE, sep = "\n")
write.table(df, append = TRUE, file = outFile, sep = ",", quote = FALSE, row.names = FALSE, col.names = FALSE)
cat("\n\t}", file = outFile, append = TRUE, sep = "\n")

# Done
cat("\nDONE.", sep = "\n")

Как видите, этот скрипт открывает csva.csv и csvb.csv.

Это csva.csv:

ID,ColA,ColB,ColC,ColD
2,3,100,1,1
3,7,300,1,1
5,7,200,1,1
11,22,900,1,1
14,27,500,1,1
16,30,400,1,1
20,36,900,1,1
23,39,800,1,1
24,42,700,1,1
29,49,800,1,1
45,3,200,1,1

И это csvb.csv:

100;file1
200;file2
300;file3
400;file4

Это выходной файл, который генерирует мой скрипт и файлы csv:

myTable = {

    ["3"] = {"file1#100", "file2#200"},
    ["7"] = {"file2#200", "file3#300"},
    ["30"] = {"file4#400"},

}

Этот выходной файл является именно тем, что Я хочу. Это прекрасно.

Вот что делает скрипт. Я не уверен, что могу объяснить это очень хорошо, поэтому, если я не справлюсь с этим, пропустите этот раздел.

Для каждой строки в csva.csv, если Col C (csva ) содержит число, которое содержится в столбце 1 (csvb), тогда выходной файл должен содержать строку, подобную этой:

["3"] = {"file1#100", "file2#200"},

Итак, в приведенном выше примере первая строка в ColA (csva) содержит число 3 и colB для этой строки равно 100. В csvb столбец 1 содержит 100, а столбец 2 содержит file1 # 100.

Поскольку csva содержит еще одно число 3 в ColA (последняя строка), это также обрабатывается и вывод в ту же строку.

Хорошо, так что мой сценарий действительно работает очень хорошо и выдает идеальный вывод. Проблема в том, что бегать слишком долго. csva и csvb в моем вопросе содержат всего несколько строк, поэтому вывод происходит мгновенно.

Однако данные, с которыми мне приходится работать в реальном мире, - csva - более 300 000 строк, а csvb - более 900 000 строк. , Таким образом, выполнение сценария занимает очень много времени (слишком долго, чтобы сделать его возможным). Он работает прекрасно, но для его запуска требуется слишком много времени.

Из-за постепенного комментирования строк кажется, что замедление происходит из-за мутации и суммирования. Без этих строк скрипт запускается примерно за 30 секунд. Но с мутированием и суммированием это занимает часы.

Я не слишком продвинут в R, поэтому как я могу заставить мой скрипт работать быстрее, возможно, улучшив мой синтаксис или предоставив более быстрые альтернативы для мутирования и суммирования?

Ответы [ 4 ]

2 голосов
/ 22 января 2020

Вот подход dplyr, который точно соответствует вашему. Реальные различия заключаются в том, что строки и столбцы удаляются с объекта как можно скорее, поэтому для перемещения требуется меньше багажа.

Я предполагаю, что на самом деле поможет с большими наборами данных. Пожалуйста, сообщите, каковы длительности до и после. Мне нравится, как вы сказали, какие звонки были самыми длинными; отчет о новых бутылках тоже помог бы.

Если это не достаточно быстро, следующий самый простой шаг, вероятно, это переход на sqldf (который использует SQLite под крышкой) или data .table . Оба требуют изучения другого синтаксиса (если вы уже не знаете sql), но в долгосрочной перспективе они могут стоить вашего времени.

# Pretend this info is being read from a file
str_a <-
"ID,ColA,ColB,ColC,ColD
2,3,100,1,1
3,7,300,1,1
5,7,200,1,1
11,22,900,1,1
14,27,500,1,1
16,30,400,1,1
20,36,900,1,1
23,39,800,1,1
24,42,700,1,1
29,49,800,1,1
45,3,200,1,1"

str_b <-
"100;file1
200;file2
300;file3
400;file4"


# Declare the desired columns and their data types.
#   Include only the columns needed.  Use the smaller 'integer' data type where possible.
col_types_a <- readr::cols_only(
  `ID`      = readr::col_integer(),
  `ColA`    = readr::col_integer(),
  `ColB`    = readr::col_integer(),
  `ColC`    = readr::col_integer()
  # `ColD`    = readr::col_integer() # Exclude columns never used
)
col_types_b <- readr::cols_only(
  `ColB`      = readr::col_integer(),
  `file_name` = readr::col_character()
)

# Read the file into a tibble
ds_a <- readr::read_csv(str_a, col_types = col_types_a)
ds_b <- readr::read_delim(str_b, delim = ";", col_names = c("ColB", "file_name"), col_types = col_types_b)

ds_a %>% 
  dplyr::select( # Quickly drop as many columns as possible; avoid reading if possible
    ID,
    ColB,
    ColA
  ) %>% 
  dplyr::left_join(ds_b, by = "ColB") %>% # Join the two datasets
  tidyr::drop_na(file_name) %>%           # Dump the records you'll never use
  dplyr::mutate(                          # Create the hybrid column
    entry = paste0('"', file_name, "#", ColB, '"')
  ) %>%
  dplyr::select(                          # Dump the unneeded columns
    -ID,
    -file_name
  ) %>% 
  dplyr::group_by(ColA) %>%               # Create a bunch of subdatasets
  dplyr::arrange(ColB, entry) %>%         # Sorting inside the group usually is faster?
  dplyr::summarise(
    entry = paste(entry, collapse = ", ", sep = ";")
  ) %>%
  dplyr::ungroup() %>%                    # Stack all the subsets on top of each other
  dplyr::mutate(                          # Mush the two columns
    entry = paste0('\t\t["', ColA, '"] = {', entry, '},')
  ) %>% 
  dplyr::pull(entry) %>%                  # Isolate the desired vector
  paste(collapse = "\n") %>%              # Combine all the elements into one.
  cat()

результат:

        ["3"] = {"file1#100", "file2#200"},
        ["7"] = {"file2#200", "file3#300"},
        ["30"] = {"file4#400"},
2 голосов
/ 22 января 2020

Вот более компактная версия вашего кода в базе R, которая должна обеспечивать повышение производительности.

(отредактировано в соответствии с данными, предоставленными wibeasley.)

ds_a$file_name <- ds_b$file_name[match(ds_a$ColB, ds_b$ColB)]
ds_a <- ds_a[!is.na(ds_a$file_name), -4]
ds_a <- ds_a[order(ds_a$ColB),]
ds_a$file_name <- paste0('"', ds_a$file_name, "#", ds_a$ColB, '"')
res <- tapply(ds_a$file_name, ds_a$ColA, FUN = paste,  collapse = ", ", sep=";")
res <- paste0("\t\t[\"", names(res), "\"] = {", res, "},", collapse = "\n")
cat("\n\tmyTable = {", res, "\t}", sep = "\n\n")

Вывод:

myTable = {

    ["3"] =  {"file1#100", "file2#200"},
    ["7"] =  {"file2#200", "file3#300"},
    ["30"] =  {"file4#400"},

}
0 голосов
/ 22 января 2020

Вот еще одно решение, которое использует производительность data.table, оставаясь в курсе ваших знаний о dplyr. Я не уверен, что есть много возможностей для улучшения всего за 10 секунд, но теоретически это могло бы помочь большим наборам данных, где стоимость создания индексов амортизируется в течение более длительного периода выполнения.

dtplyr * Пакет 1004 * переводит глаголы dplyr (которые вам знакомы) в синтаксис data.table. Это позволяет использовать ключи, что должно повысить производительность, особенно при объединении и группировании.

Функция dtplyr :: lazy_dt может помочь оптимизировать перевод dplyr-to-data.table.

Наконец, vroom заменяет readr, в основном из любопытства. Но это не зависит от других изменений, и кажется, что это никогда не было узким местом

col_types_a <- vroom::cols_only(
  `ID`      = vroom::col_integer(),
  `ColA`    = vroom::col_integer(),
  `ColB`    = vroom::col_integer(),
  `ColC`    = vroom::col_integer()
  # `ColD`    = vroom::col_integer() # Leave out this column b/c it's never used
)
col_types_b <- vroom::cols_only(
  `ColB`      = vroom::col_integer(),
  `file_name` = vroom::col_character()
)
ds_a <- vroom::vroom(str_a, col_types = col_types_a)
ds_b <- vroom::vroom(str_b, delim = ";", col_names = c("ColB", "file_name"), col_types = col_types_b)

# ds_a <- data.table::setDT(ds_a, key = c("ColB", "ColA"))
# ds_b <- data.table::setDT(ds_b, key = "ColB")

ds_a <- dtplyr::lazy_dt(ds_a, key_by = c("ColB", "ColA"))    # New line 1
ds_b <- dtplyr::lazy_dt(ds_b, key_by = "ColB")               # New line 2

ds_a %>% 
  dplyr::select( # Quickly drop as many columns as possible; avoid reading if possible
    ID,
    ColB,
    ColA
  ) %>%
  dplyr::inner_join(ds_b, by = "ColB") %>%                   # New line 3 (replaces left join)
  # tidyr::drop_na(file_name) %>%                            # Remove this line
  # dplyr::filter(!is.na(file_name)) %>%                     # Alternative w/ left join
  dplyr::mutate(
    entry = paste0('"', file_name, "#", ColB, '"')
  ) %>%
  dplyr::select( # Dump the uneeded columns
    -ID,
    -file_name
  ) %>% 
  dplyr::group_by(ColA) %>%
  dplyr::arrange(ColB, entry) %>%  # Sort inside the group usually helps
  dplyr::summarise(
    entry = paste(entry, collapse = ", ", sep=";")
  ) %>%
  dplyr::ungroup() %>% 
  dplyr::mutate(
    entry = paste0('\t\t["', ColA, '"] = {', entry, '},')
  ) %>% 
  dplyr::pull(entry) %>% # Isolate the desired vector
  paste(collapse = "\n") %>% 
  cat()
0 голосов
/ 22 января 2020

вместо этого вы можете попробовать загрузить свою таблицу в качестве таблицы данных. обычно data.tables работают быстрее, чем data.frames

library(data.table)
filea <- fread("csva.csv")

, просто проверьте, что это все еще data.table, прежде чем перейти к функции mutate (просто распечатайте ее, вы увидите очевидную разницу на data.frame).

...