Modo più veloce per leggere i file a larghezza fissa

Io lavoro con un sacco di file a larghezza fissa (cioè senza caratteri di separazione) che ho bisogno di leggere in R. Quindi, di solito c’è una definizione della larghezza della colonna per analizzare la stringa in variabili. Posso usare read.fwf per leggere i dati senza problemi. Tuttavia, per file di grandi dimensioni, questo può richiedere molto tempo. Per un set di dati recente, ci sono voluti 800 secondi per leggere in un set di dati con ~ 500.000 righe e 143 variabili.

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

data.table pacchetto data.table in R è fantastico per risolvere la maggior parte dei problemi di lettura dei dati, tranne per il fatto che non analizza i file a larghezza fissa. Tuttavia, posso leggere ogni riga come una singola stringa di caratteri (~ 500.000 righe, 1 colonna). Questo richiede 3-5 secondi. (Adoro data.table.)

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

Ci sono molti buoni post su SO su come analizzare i file di testo. Vedi il suggerimento di JHoward qui , per creare una matrice di colonne iniziali e finali e substr per analizzare i dati. Vedi il suggerimento di GSee qui per usare strsplit . Non riuscivo a capire come farlo funzionare con questi dati. (Inoltre, Michael Smith ha fornito alcuni suggerimenti sulla mailing list data.table riguardante sed che erano oltre la mia capacità di implementare. ) Ora, usando fread e substr() posso fare il tutto in circa 25-30 secondi. Si noti che la forzatura su un data.table alla fine richiede un intervallo di tempo (5 sec.).

 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) 

Quello che mi chiedo è se questo può essere ulteriormente migliorato? So che non sono l’unico a dover leggere file a larghezza fissa, quindi se questo potrebbe essere reso più veloce, renderebbe più accettabile il caricamento di file anche più grandi (con milioni di righe). Ho provato a usare parallel con mclapply e data.table invece di lapply , ma quelli non hanno cambiato nulla. (Probabilmente a causa della mia inesperienza in R.) Immagino che una funzione Rcpp possa essere scritta per farlo in modo molto veloce, ma questo va oltre le mie capacità. Inoltre, potrei non usare lapplicazione e applicare in modo appropriato.

La mia implementazione data.table (con magrittr chaining) prende lo stesso tempo:

 text % data.table(.) 

Qualcuno può dare suggerimenti per migliorare la velocità di questo? O è così buono come si arriva?

Ecco il codice per creare un data.table simile all’interno di R (piuttosto che collegarsi a dati reali). Dovrebbe avere 331 caratteri e 500.000 righe. Ci sono spazi per simulare i campi mancanti nei dati, ma NON si tratta di dati delimitati da spazi. (Sto leggendo i dati grezzi SEER, nel caso qualcuno sia interessato.) Includono anche le colonne width (cols) e nomi variabili (seervar) nel caso in cui questo aiuti qualcun altro. Queste sono le colonne effettive e le definizioni delle variabili per i dati 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") 

AGGIORNAMENTO: LaF ha eseguito l’intera lettura in meno di 7 secondi dal file .txt non elaborato. Forse c’è un modo ancora più veloce, ma dubito che tutto possa migliorare sensibilmente. Pacchetto incredibile

27 luglio 2015 Aggiornamento Volevo solo fornire un piccolo aggiornamento a questo. Ho usato il nuovo pacchetto readr e sono riuscito a leggere l’intero file in 5 secondi usando readr :: read_fwf.

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

Inoltre, la funzione stringi :: stri_sub aggiornata è almeno due volte più veloce di base :: substr (). Quindi, nel codice precedente che utilizza fread per leggere il file (circa 4 secondi), seguito da applicare per analizzare ogni riga, l’estrazione di 143 variabili ha richiesto circa 8 secondi con stringi :: stri_sub rispetto a 19 per base :: substr. Quindi, fread più stri_sub ha ancora solo 12 secondi per essere eseguito. Non male.

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

10 dicembre 2015 aggiornamento:

Si prega di vedere anche la risposta qui sotto da Michael Michael Chirico che ha aggiunto alcuni ottimi benchmark e il pacchetto iotools.

Ora che ci sono (tra questa e l’ altra importante domanda sull’efficace lettura di file a larghezza fissa) una discreta quantità di opzioni sull’offerta per la lettura di tali file, penso che alcuni benchmark siano appropriati.

Userò il seguente file on-the-large-side (400 MB) per il confronto. Sono solo un gruppo di personaggi casuali con campi e larghezze definiti a caso:

 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) 

Comparerò cinque metodi menzionati tra questi due thread (ne aggiungerò altri se gli autori vorrebbero): la versione di base ( read.fwf ), convogliando il risultato di in2csv (il suggerimento di @AnandaMahto), il nuovo readr Hadley ( read_fwf ), che utilizza LaF / ffbase (suggerimento di @jwijffls) e una versione migliorata (semplificata) di quella suggerita dall’autore della domanda (@MarkDanese) che combina fread con stri_sub di stringi .

Ecco il codice di benchmarking:

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

E l’output:

 # 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 

Quindi sembra readr e fread + stri_sub sono piuttosto competitivi come i più veloci; built-in read.fwf è il chiaro perdente.

Si noti che il vero vantaggio di readr qui è che è ansible pre-specificare i tipi di colonna; con fread dovrai digitare convert in seguito.

EDIT: aggiungere alcune alternative

Al suggerimento di @ AnandaMahto sto includendo alcune altre opzioni, inclusa quella che sembra essere un nuovo vincitore! Per risparmiare tempo ho escluso le opzioni più lente sopra nel nuovo confronto. Ecco il nuovo codice:

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

E la nuova uscita:

 # 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 

Quindi sembra che iotools sia molto veloce e molto coerente.

È ansible utilizzare il pacchetto LaF , che è stato scritto per gestire file di larghezza fissa di grandi dimensioni (anche troppo grandi per adattarsi alla memoria). Per usarlo devi prima aprire il file usando laf_open_fwf . È quindi ansible indicizzare l’object risultante come se fosse un normale frame di dati per leggere i dati necessari. Nell’esempio seguente, ho letto l’intero file, ma puoi anche leggere colonne e / o righe specifiche:

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

Il tuo esempio con 5000 linee (anziché 500.000) ha impiegato 28 secondi usando read.fwf e 1.6 secondi usando LaF .

Aggiunta L'esempio con 50.000 linee (anziché 500.000) ha impiegato 258 secondi utilizzando read.fwf e 7 secondi utilizzando LaF sulla mia macchina.

Ho scritto un parser per questo genere di cose ieri, ma era per un tipo di input molto specifico per il file di intestazione, quindi ti mostrerò come formattare le larghezze della colonna per poterlo usare.

Conversione del file flat in csv

Per prima cosa scarica lo strumento in questione .

Puoi scaricare il binario dalla directory bin se sei su OS X Mavericks (dove l’ho compilato) o compilarlo andando in src e usando clang++ csv_iterator.cpp parse.cpp main.cpp -o flatfileparser .

Il parser di file flat ha bisogno di due file, un file di intestazione CSV in cui ogni quinto elemento specifica la larghezza variabile (ancora, questo è dovuto alla mia applicazione estremamente specifica), che puoi generare 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') 

e copiando il risultante ~/tmp/header.csv nella stessa directory di flatfileparser . Sposta il file flat nella stessa directory e puoi eseguirlo sul tuo file flat:

 ./flatfileparser header.csv yourflatfile 

che produrrà yourflatfile.csv . Aggiungi l’intestazione che hai sopra in manualmente usando le pipe ( >> da Bash).

Lettura nel tuo file CSV rapidamente

Usa il pacchetto Fastread sperimentale di Hadley passando il nome del file a fastread::read_csv , che produce un data.frame . Non credo che supporti i file fwf , anche se è in arrivo.

Non sono sicuro di quale sistema operativo si sta utilizzando, ma questo ha funzionato abbastanza facilmente per me in Linux:

Passaggio 1 : crea un comando per awk per convertire il file in un CSV

Puoi averlo memorizzato in un file csv effettivo se prevedi di utilizzare i dati anche in altri software.

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

Passaggio 2 : utilizzare il comando direttamente su quel comando appena creato

 seer9 < - fread(myCommand) 

Non l'ho cronometrato perché ovviamente sto utilizzando un sistema più lento di te e Jan 🙂