Forma más rápida de leer archivos de ancho fijo

Trabajo con muchos archivos de ancho fijo (es decir, sin carácter separador) que necesito leer en R. Por lo tanto, generalmente hay una definición del ancho de la columna para analizar la cadena en variables. Puedo usar read.fwf para leer los datos sin problemas. Sin embargo, para archivos grandes, esto puede llevar mucho tiempo. Para un conjunto de datos reciente, esto tomó 800 segundos para leer en un conjunto de datos con ~ 500,000 filas y 143 variables.

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

fread en el paquete data.table en R es increíble para resolver la mayoría de los problemas de lectura de datos, excepto que no analiza los archivos de ancho fijo. Sin embargo, puedo leer cada línea como una cadena de caracteres individuales (~ 500,000 filas, 1 columna). Esto toma 3-5 segundos. (Me encanta data.table)

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

Hay una serie de buenas publicaciones sobre SO sobre cómo analizar archivos de texto. Vea la sugerencia de JHoward aquí , para crear una matriz de columnas de inicio y final, y substr para analizar los datos. Vea la sugerencia de GSee aquí para usar strsplit . No pude encontrar la manera de hacerlo funcionar con esta información. (Además, Michael Smith hizo algunas sugerencias en la lista de correo de data.table que involucraba a sed que estaban más allá de mi capacidad de implementación. ) Ahora, usando fread y substr() puedo hacer todo en unos 25-30 segundos. Tenga en cuenta que la coerción a una tabla de datos al final toma un tiempo (5 segundos?).

 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) 

Lo que me pregunto es si esto puede mejorarse más. Sé que no soy el único que tiene que leer archivos de ancho fijo, por lo que si se pudiera hacer más rápido, sería más tolerable cargar archivos aún más grandes (con millones de filas). Traté de usar el parallel con mclapply y data.table lugar de con lapply , pero eso no cambió nada. (Probablemente debido a mi inexperiencia en R.) Imagino que se podría escribir una función de Rcpp para hacer esto muy rápido, pero eso está más allá de mi conjunto de habilidades. Además, es posible que no esté utilizando lapply y aplique de forma adecuada.

Mi implementación data.table (con magrittr encadenamiento) toma el mismo tiempo:

 text % data.table(.) 

¿Alguien puede hacer sugerencias para mejorar la velocidad de esto? ¿O es esto tan bueno como se pone?

Aquí hay un código para crear una tabla de datos similar dentro de R (en lugar de vincularla con datos reales). Debería tener 331 caracteres y 500,000 filas. Hay espacios para simular campos faltantes en los datos, pero NO son datos delimitados por espacios. (Estoy leyendo datos SEER brutos, en caso de que alguien esté interesado). También se incluyen los anchos de columna (cols) y los nombres de variables (servadores) en caso de que esto ayude a otra persona. Estas son las definiciones reales de columnas y variables para los datos 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") 

ACTUALIZACIÓN: LaF hizo toda la lectura en menos de 7 segundos desde el archivo .txt sin formato. Tal vez haya una manera aún más rápida, pero dudo que algo pueda mejorar apreciablemente. Paquete increíble.

Actualización del 27 de julio de 2015 Solo quería proporcionar una pequeña actualización de esto. Usé el nuevo paquete readr, y pude leer todo el archivo en 5 segundos usando readr :: read_fwf.

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

Además, la función stringi :: stri_sub actualizada es al menos dos veces más rápida que base :: substr (). Entonces, en el código anterior que usa fread para leer el archivo (aproximadamente 4 segundos), seguido de aplicar para analizar cada línea, la extracción de 143 variables tomó aproximadamente 8 segundos con stringi :: stri_sub en comparación con 19 para base :: substr. Entonces, fread plus stri_sub todavía tiene solo 12 segundos para ejecutarse. No está mal.

 seer9 <- fread("path_to_data/COLRECT.TXT", colClasses = "character", sep = "\n", header = FALSE) text % data.table(.) 

10 de diciembre de 2015 actualización:

Consulte también la respuesta a continuación de @MichaelChirico, que ha agregado algunos puntos de referencia excelentes y el paquete iotools.

Ahora que hay (entre esta y la otra pregunta importante sobre la lectura efectiva de archivos de ancho fijo) una buena cantidad de opciones sobre la oferta para leer en dichos archivos, creo que es apropiado realizar algunas evaluaciones comparativas.

Usaré el siguiente archivo en el lado grande (400 MB) para comparar. Es solo un grupo de caracteres aleatorios con campos y anchos definidos al azar:

 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) 

Compararé cinco métodos mencionados entre estos dos hilos (añadiré algunos otros si los autores quisieran): la versión base ( read.fwf ), canalizando el resultado de in2csv para fread (sugerencia de @AnandaMahto), el nuevo readr Hadley ( read_fwf ), que usa LaF / ffbase (sugerencia de @jwijffls) y una versión mejorada (simplificada) de la sugerida por el autor de la pregunta (@MarkDanese) combinando fread con stri_sub desde stringi .

Aquí está el código de evaluación comparativa:

 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]))]) 

Y el resultado:

 # 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 

Entonces parece que readr y fread + stri_sub son bastante competitivos como los más rápidos; el built-in read.fwf es el claro perdedor.

Tenga en cuenta que la ventaja real de readr aquí es que puede preespecificar los tipos de columna; con fread tendrás que escribir conversiones después.

EDITAR: Agregar algunas alternativas

En la sugerencia de @AnandaMahto, incluyo algunas opciones más, ¡incluida una que parece ser un nuevo ganador! Para ahorrar tiempo, excluí las opciones más lentas en la nueva comparación. Aquí está el nuevo código:

 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)) 

Y el nuevo resultado:

 # 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 

Entonces parece que iotools es muy rápido y muy consistente.

Puede usar el paquete LaF , que se escribió para manejar archivos grandes de ancho fijo (también demasiado grandes para caber en la memoria). Para usarlo primero necesita abrir el archivo usando laf_open_fwf . A continuación, puede indexar el objeto resultante como lo haría con un dataframe normal para leer los datos que necesita. En el siguiente ejemplo, leo todo el archivo, pero también puede leer columnas y / o líneas específicas:

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

Su ejemplo usando 5000 líneas (en lugar de sus 500,000) tomó 28 segundos usando read.fwf y 1.6 segundos usando LaF .

Adición Su ejemplo usando 50,000 líneas (en vez de sus 500,000) tomó 258 segundos usando read.fwf y 7 segundos usando LaF en mi máquina.

Ayer escribí un analizador para este tipo de cosas, pero fue para un tipo muy específico de entrada al archivo de encabezado, así que le mostraré cómo formatear el ancho de sus columnas para poder usarlo.

Convirtiendo su archivo plano a csv

Primero descargue la herramienta en cuestión .

Puedes descargar el binario del directorio bin si estás en OS X Mavericks (donde lo compilé) o comstackrlo yendo a src y usando clang++ csv_iterator.cpp parse.cpp main.cpp -o flatfileparser .

El analizador de archivos sin formato necesita dos archivos, un archivo de cabecera CSV en el que cada quinto elemento especifica el ancho variable (de nuevo, esto se debe a mi aplicación extremadamente específica), que puede generar usando:

 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') 

y copiando el ~/tmp/header.csv en el mismo directorio que su ~/tmp/header.csv de ~/tmp/header.csv . Mueva también el archivo plano al mismo directorio, y puede ejecutarlo en su archivo plano:

 ./flatfileparser header.csv yourflatfile 

que producirá yourflatfile.csv . Agregue el encabezado que tiene arriba en forma manual utilizando tuberías ( >> desde Bash).

Leyendo rápidamente su archivo CSV

Use el paquete de lectura rápida experimental de Hadley pasando el nombre de archivo a fastread::read_csv , que produce un data.frame . Aún no creo que soporte archivos fwf , aunque está en camino.

No estoy seguro de qué sistema operativo estás usando, pero esto funcionó bastante bien para mí en Linux:

Paso 1 : crea un comando para awk para convertir el archivo a un csv

Puede almacenarlo en un archivo csv real si planea usar los datos en otro software también.

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

Paso 2 : usa fread directamente en ese comando que acabas de crear

 seer9 <- fread(myCommand) 

No lo he cronometrado porque obviamente estoy usando un sistema más lento que tú y Jan 🙂

    Intereting Posts