Подтвердить что ты не робот

Более быстрый способ чтения файлов фиксированной ширины в R

Я работаю с большим количеством файлов с фиксированной шириной (т.е. без разделительного символа), которые мне нужно прочитать в R. Таким образом, обычно существует определение ширины столбца для синтаксического анализа строки в переменных. Я могу использовать read.fwf для чтения данных без проблем. Однако для больших файлов это может занять длинное время. Для недавнего набора данных это заняло 800 секунд для чтения в наборе данных с ~ 500 000 строк и 143 переменных.

seer9 <- read.fwf("~/data/rawdata.txt", 
  widths = cols,
  header = FALSE,
  buffersize = 250000,
  colClasses = "character",
  stringsAsFactors = FALSE))

fread в пакете data.table в R отлично подходит для решения большинства проблем чтения данных, за исключением того, что он не анализирует файлы фиксированной ширины. Тем не менее, я могу читать каждую строку в виде одной символьной строки (~ 500 000 строк, 1 столбец). Это занимает 3-5 секунд. (Мне нравится data.table.)

seer9 <- fread("~/data/rawdata.txt", colClasses = "character",
               sep = "\n", header = FALSE, verbose = TRUE)

На SO есть множество хороших сообщений о том, как анализировать текстовые файлы. См. Предложение JHoward here, чтобы создать матрицу начального и конечного столбцов и substr для анализа данных. См. Предложение GSee здесь для использования strsplit. Я не мог понять, как сделать эту работу с этими данными. (Кроме того, Майкл Смит сделал несколько предложений в списке рассылки data.table с участием sed, которые были выше моей способности реализовать.) Теперь, используя fread и substr(), я могу сделать все это примерно через 25-30 секунд. Обратите внимание, что при принуждении к таблице данных в конце происходит кусок времени (5 секунд?).

end_col <- cumsum(cols)
start_col <- end_col - cols + 1
start_end <- cbind(start_col, end_col) # matrix of start and end positions
text <- lapply(seer9, function(x) {
        apply(start_end, 1, function(y) substr(x, y[1], y[2])) 
        })
dt <- data.table(text$V1)
setnames(dt, old = 1:ncol(dt), new = seervars)

Мне интересно, можно ли это улучшить? Я знаю, что я не единственный, кто должен читать файлы с фиксированной шириной, поэтому, если это можно сделать быстрее, это сделает загрузку даже более крупных файлов (с миллионами строк) более терпимыми. Я попытался использовать parallel с mclapply и data.table вместо lapply, но это ничего не изменило. (Вероятно, из-за моей неопытности в R.) Я полагаю, что функцию Rcpp можно было бы написать, чтобы сделать это очень быстро, но это выходит за рамки моего набора навыков. Кроме того, я не могу использовать приложение и применять соответствующим образом.

Моя реализация data.table(с цепочкой magrittr) принимает одно и то же время:

text <- seer9[ , apply(start_end, 1, function(y) substr(V1, y[1], y[2]))] %>% 
  data.table(.)

Может ли кто-нибудь сделать предложения, чтобы улучшить скорость этого? Или это примерно так же хорошо, как и получается?

Вот код для создания подобной таблицы данных внутри R (а не для привязки к фактическим данным). Он должен иметь 331 символ и 500 000 строк. Существуют пробелы для имитации отсутствующих полей в данных, но это НЕ данные, разделенные пробелами. (Я читаю необработанные данные SEER, если кто-то заинтересован.) Также включая ширину столбцов (cols) и имена переменных (seervars), если это помогает кому-то другому. Это фактические определения столбцов и переменных для данных SEER.

seer9 <-
  data.table(rep((paste0(paste0(letters, 1000:1054, " ", collapse = ""), " ")),
                 500000))

cols = c(8,10,1,2,1,1,1,3,4,3,2,2,4,4,1,4,1,4,1,1,1,1,3,2,2,1,2,2,13,2,4,1,1,1,1,3,3,3,2,3,3,3,3,3,3,3,2,2,2,2,1,1,1,1,1,6,6,6,2,1,1,2,1,1,1,1,1,2,2,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,7,5,4,10,3,3,2,2,2,3,1,1,1,1,2,2,1,1,2,1,9,5,5,1,1,1,2,2,1,1,1,1,1,1,1,1,2,3,3,3,3,3,3,1,4,1,4,1,1,3,3,3,3,2,2,2,2)
seervars <- c("CASENUM", "REG", "MAR_STAT", "RACE", "ORIGIN", "NHIA", "SEX", "AGE_DX", "YR_BRTH", "PLC_BRTH", "SEQ_NUM", "DATE_mo", "DATE_yr", "SITEO2V", "LATERAL", "HISTO2V", "BEHO2V", "HISTO3V", "BEHO3V", "GRADE", "DX_CONF", "REPT_SRC", "EOD10_SZ", "EOD10_EX", "EOD10_PE", "EOD10_ND", "EOD10_PN", "EOD10_NE", "EOD13", "EOD2", "EOD4", "EODCODE", "TUMOR_1V", "TUMOR_2V", "TUMOR_3V", "CS_SIZE", "CS_EXT", "CS_NODE", "CS_METS", "CS_SSF1", "CS_SSF2", "CS_SSF3", "CS_SSF4", "CS_SSF5", "CS_SSF6", "CS_SSF25", "D_AJCC_T", "D_AJCC_N", "D_AJCC_M", "D_AJCC_S", "D_SSG77", "D_SSG00", "D_AJCC_F", "D_SSG77F", "D_SSG00F", "CSV_ORG", "CSV_DER", "CSV_CUR", "SURGPRIM", "SCOPE", "SURGOTH", "SURGNODE", "RECONST", "NO_SURG", "RADIATN", "RAD_BRN", "RAD_SURG", "SS_SURG", "SRPRIM02", "SCOPE02", "SRGOTH02", "REC_NO", "O_SITAGE", "O_SEQCON", "O_SEQLAT", "O_SURCON", "O_SITTYP", "H_BENIGN", "O_RPTSRC", "O_DFSITE", "O_LEUKDX", "O_SITBEH", "O_EODDT", "O_SITEOD", "O_SITMOR", "TYPEFUP", "AGE_REC", "SITERWHO", "ICDOTO9V", "ICDOT10V", "ICCC3WHO", "ICCC3XWHO", "BEHANAL", "HISTREC", "BRAINREC", "CS0204SCHEMA", "RAC_RECA", "RAC_RECY", "NHIAREC", "HST_STGA", "AJCC_STG", "AJ_3SEER", "SSG77", "SSG2000", "NUMPRIMS", "FIRSTPRM", "STCOUNTY", "ICD_5DIG", "CODKM", "STAT_REC", "IHS", "HIST_SSG_2000", "AYA_RECODE", "LYMPHOMA_RECODE", "DTH_CLASS", "O_DTH_CLASS", "EXTEVAL", "NODEEVAL", "METSEVAL", "INTPRIM", "ERSTATUS", "PRSTATUS", "CSSCHEMA", "CS_SSF8", "CS_SSF10", "CS_SSF11", "CS_SSF13", "CS_SSF15", "CS_SSF16", "VASINV", "SRV_TIME_MON", "SRV_TIME_MON_FLAG", "SRV_TIME_MON_PA", "SRV_TIME_MON_FLAG_PA", "INSREC_PUB", "DAJCC7T", "DAJCC7N", "DAJCC7M", "DAJCC7STG", "ADJTM_6VALUE", "ADJNM_6VALUE", "ADJM_6VALUE", "ADJAJCCSTG")

UPDATE: LaF выполнил все чтение всего за 7 секунд из файла .txt. Возможно, есть еще более быстрый способ, но я сомневаюсь, что что-то может сделать заметно лучше. Удивительный пакет.

27 июля 2015 г. Обновление Просто хотел предоставить небольшое обновление. Я использовал новый пакет readr, и я смог прочитать весь файл за 5 секунд, используя readr:: read_fwf.

seer9_readr <- read_fwf("path_to_data/COLRECT.TXT",
  col_positions = fwf_widths(cols))

Кроме того, обновленная функция stringi:: stri_sub не менее чем в два раза быстрее, чем base:: substr(). Таким образом, в приведенном выше коде, который использует fread для чтения файла (около 4 секунд), а затем применяется для разбора каждой строки, извлечение 143 переменных заняло около 8 секунд с помощью stringi:: stri_sub по сравнению с 19 для base:: substr. Таким образом, fread plus stri_sub все еще занимает около 12 секунд. Неплохо.

seer9 <-  fread("path_to_data/COLRECT.TXT",     
  colClasses = "character", 
  sep = "\n", 
  header = FALSE)
text <- seer9[ , apply(start_end, 1, function(y) substr(V1, y[1], y[2]))] %>% 
  data.table(.)

Обновление за 10 декабря 2015 года:

См. также ниже от @MichaelChirico, который добавил несколько отличных тестов и пакет iotools.

4b9b3361

Ответ 1

Вы можете использовать пакет LaF, который был написан для обработки больших файлов с фиксированной шириной (также слишком большой для вставки в память). Чтобы использовать его, сначала нужно открыть файл, используя laf_open_fwf. Затем вы можете проиндексировать результирующий объект, как обычный нормальный кадр данных, чтобы прочитать нужные вам данные. В приведенном ниже примере я прочитал весь файл, но вы также можете прочитать определенные столбцы и/или строки:

library(LaF)
laf <- laf_open_fwf("foo.dat", column_widths = cols, 
  column_types=rep("character", length(cols)),
  column_names = seervars)
seer9 <- laf[,]

Ваш пример с использованием 5000 строк (вместо 500 000) занял 28 секунд, используя read.fwf и 1,6 секунды, используя LaF.

Дополнение Ваш пример с использованием 50 000 строк (вместо 500 000) занял 258 секунд, используя read.fwf и 7 секунд, используя LaF на моей машине.

Ответ 2

Теперь, когда есть (между этим и другим важным вопросом об эффективном чтении файлов с фиксированной шириной), справедливое количество опций в предложении для чтения в таких файлов, я думаю, что некоторый бенчмаркинг является подходящим.

Для сравнения я буду использовать следующий файл на большой стороне (400 МБ). Это всего лишь куча случайных символов со случайными полями и ширинами:

set.seed(21394)
wwidth = 400L
rrows = 1000000

#creating the contents at random
contents = 
  write.table(replicate(rrows, paste0(sample(letters, wwidth, replace = TRUE),
                                      collapse = "")), file="testfwf.txt",
              quote = FALSE, row.names = FALSE, col.names = FALSE)

#defining the fields & writing a dictionary
n_fields = 40L
endpoints = unique(c(1L, sort(sample(wwidth, n_fields - 1L)), wwidth + 1L))
cols = ist(beg = endpoints[-(n_fields + 1L)],
             end = endpoints[-1L] - 1L)

dict = data.frame(column = paste0("V", seq_len(length(endpoints)) - 1L)),
                  start = endpoints[-length(endpoints)] - 1,
                  length = diff(endpoints))

write.csv(dict, file = "testdic.csv", quote = FALSE, row.names = FALSE)

Я сравню пять методов, упомянутых между этими двумя потоками (я добавлю некоторые другие, если авторы захотят): базовая версия (read.fwf), передает результат от in2csv до fread (@AnandaMahto), Hadley new readr (read_fwf), используя LaF/ffbase (предложение @jwijffls) и улучшенную (оптимизированную) версию, предложенную автором вопроса (@MarkDanese) fread с stri_sub из stringi.

Вот код сравнения:

library(data.table)
library(stringi)
library(readr)
library(LaF); library(ffbase)
library(microbenchmark)

microbenchmark(times = 5L,
               utils = read.fwf("testfwf.txt", diff(endpoints), header = FALSE),
               in2csv = 
                 fread(paste("in2csv -f fixed -s",
                             "~/Desktop/testdic.csv",
                             "~/Desktop/testfwf.txt")),
               readr = read_fwf("testfwf.txt", fwf_widths(diff(endpoints))),
               LaF = {
                 my.data.laf = 
                   laf_open_fwf('testfwf.txt', column_widths=diff(endpoints),
                                column_types = rep("character", 
                                                   length(endpoints) - 1L))
                 my.data = laf_to_ffdf(my.data.laf, nrows = rrows)
                 as.data.frame(my.data)},
               fread = fread(
                 "testfwf.txt", header = FALSE, sep = "\n"
                 )[ , lapply(seq_len(length(cols$beg)),
                             function(ii) 
                               stri_sub(V1, cols$beg[ii], cols$end[ii]))])

И вывод:

# Unit: seconds
#    expr       min        lq      mean    median        uq       max neval cld
#   utils 423.76786 465.39212 499.00109 501.87568 543.12382 560.84598     5   c
#  in2csv  67.74065  68.56549  69.60069  70.11774  70.18746  71.39210     5 a  
#   readr  10.57945  11.32205  15.70224  14.89057  19.54617  22.17298     5 a  
#     LaF 207.56267 236.39389 239.45985 237.96155 238.28316 277.09798     5  b 
#   fread  14.42617  15.44693  26.09877  15.76016  20.45481  64.40581     5 a  

Итак, кажется, что readr и fread + stri_sub довольно конкурентоспособны как самые быстрые; встроенный read.fwf является явным неудачником.

Обратите внимание, что реальное преимущество readr заключается в том, что вы можете предварительно указать типы столбцов; с помощью fread вам придется набирать текст впоследствии.

EDIT: добавление некоторых альтернатив

В предложении @AnandaMahto я включаю еще несколько вариантов, в том числе тот, который кажется новым победителем! Чтобы сэкономить время, я исключил самые медленные варианты выше в новом сравнении. Здесь новый код:

library(iotools)

microbenchmark(times = 5L,
               readr = read_fwf("testfwf.txt", fwf_widths(diff(endpoints))),
               fread = fread(
                 "testfwf.txt", header = FALSE, sep = "\n"
                 )[ , lapply(seq_len(length(cols$beg)),
                             function(ii) 
                               stri_sub(V1, cols$beg[ii], cols$end[ii]))],
               iotools = input.file("testfwf.txt", formatter = dstrfw, 
                                    col_types = rep("character",
                                                    length(endpoints) - 1L), 
                                    widths = diff(endpoints)),
               awk = fread(paste(
                 "awk -v FIELDWIDTHS='", 
                 paste(diff(endpoints), collapse = " "), 
                 "' -v OFS=', ' '{$1=$1 \"\"; print}' < ~/Desktop/testfwf.txt", 
                 collapse = " "), header = FALSE))

И новый вывод:

# Unit: seconds
#     expr       min        lq      mean    median        uq       max neval cld
#    readr  7.892527  8.016857 10.293371  9.527409  9.807145 16.222916     5  a 
#    fread  9.652377  9.696135  9.796438  9.712686  9.807830 10.113160     5  a 
#  iotools  5.900362  7.591847  7.438049  7.799729  7.845727  8.052579     5  a 
#      awk 14.440489 14.457329 14.637879 14.472836 14.666587 15.152156     5   b

Итак, он выглядит как iotools как очень быстрый, так и очень последовательный.

Ответ 3

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

Преобразование вашего плоского файла в csv

Сначала загрузите рассматриваемый инструмент.

Вы можете загрузить двоичный файл из каталога bin, если вы находитесь в OS X Mavericks (где я его скомпилировал), или скомпилируйте его, перейдя в src и используя clang++ csv_iterator.cpp parse.cpp main.cpp -o flatfileparser.

Для анализатора плоских файлов нужны два файла: CSV-заголовочный файл, в котором каждый пятый элемент указывает ширину переменной (опять же, это связано с моим чрезвычайно конкретным приложением), которую вы можете создать с помощью:

cols = c(8,10,1,2,1,1,1,3,4,3,2,2,4,4,1,4,1,4,1,1,1,1,3,2,2,1,2,2,13,2,4,1,1,1,1,3,3,3,2,3,3,3,3,3,3,3,2,2,2,2,1,1,1,1,1,6,6,6,2,1,1,2,1,1,1,1,1,2,2,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,7,5,4,10,3,3,2,2,2,3,1,1,1,1,2,2,1,1,2,1,9,5,5,1,1,1,2,2,1,1,1,1,1,1,1,1,2,3,3,3,3,3,3,1,4,1,4,1,1,3,3,3,3,2,2,2,2)
writeLines(sapply(c(-1, cols), function(x) paste0(',,,,', x)), '~/tmp/header.csv')

и скопируйте полученный ~/tmp/header.csv в тот же каталог, что и ваш flatfileparser. Переместите плоский файл в тот же каталог, и вы можете запустить его в своем плоском файле:

./flatfileparser header.csv yourflatfile

который произведет yourflatfile.csv. Добавьте заголовок, который вы указали выше, вручную, используя трубопровод (>> от Bash).

Быстрое чтение в CSV файле

Использовать экспериментальный файл hreadley, передав имя файла fastread::read_csv, что дает data.frame. Я не думаю, что он поддерживает файлы fwf, хотя он уже в пути.

Ответ 4

Я не уверен, какую ОС вы используете, но это работало довольно просто для меня в Linux:

Шаг 1. Создайте команду для awk, чтобы преобразовать файл в csv

Вы можете сохранить его в реальном CSV файле, если вы планируете использовать данные и в другом программном обеспечении.

myCommand <- paste(
  "awk -v FIELDWIDTHS='", 
  paste(cols, collapse = " "), 
  "' -v OFS=',' '{$1=$1 \"\"; print}' < ~/rawdata.txt", 
  collapse = " ")

Шаг 2. Используйте fread непосредственно для этой команды, которую вы только что создали.

seer9 <- fread(myCommand)

Я не приурочил это, потому что я, очевидно, использую более медленную систему, чем вы и Ян: -)