Более быстрый способ чтения файлов фиксированной ширины в 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.
Ответы
Ответ 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)
Я не приурочил это, потому что я, очевидно, использую более медленную систему, чем вы и Ян: -)