Szybszy sposób odczytu plików o stałej szerokości

Pracuję z wieloma plikami o stałej szerokości (tzn. bez znaków oddzielających), które muszę wczytać do R. tak więc, zwykle istnieje definicja szerokości kolumny do przetwarzania łańcucha znaków do zmiennych. Mogę użyć read.fwf do odczytu danych bez problemu. Jednak w przypadku dużych plików może to potrwać długo. W przypadku ostatniego zbioru danych odczyt w zbiorze danych z ~500 000 wierszy i 143 zmiennymi zajął 800 sekund.

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

fread W data.table paczce w R jest super dla rozwiązuje większość problemów z odczytem danych, z wyjątkiem tego, że nie parsuje plików o stałej szerokości. Mogę jednak odczytać każdą linię jako pojedynczy ciąg znaków (~500 000 wierszy, 1 kolumna). To trwa 3-5 sekund. (Kocham dane.stolik.)

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

Jest wiele dobrych postów na temat tego, jak analizować pliki tekstowe. Zobacz sugestię Jhowarda tutaj , aby utworzyć macierz kolumn początkowych i końcowych oraz substr, aby przeanalizować dane. Zobacz sugestię GSee tutaj {[28] } aby użyć strsplit. Nie mogłem tego rozgryźć. jak to zrobić z tymi danymi. (Również Michael Smith przedstawił pewne sugestie dotyczące danych.lista dyskusyjna dotycząca sed, które nie były w stanie zaimplementować .) teraz, używając fread i substr() mogę zrobić całość w około 25-30 sekund. Zauważ, że wymuszanie na danych.tabela na końcu zajmuje kawałek czasu (5 sek?).

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)

Zastanawiam się, czy można to jeszcze poprawić? Wiem, że nie tylko ja muszę czytać pliki o stałej szerokości, więc jeśli to mogłoby to być szybsze, sprawiłoby, że ładowanie jeszcze większych plików (z milionami wierszy) byłoby bardziej znośne. Próbowałem używać parallel z mclapplyi data.table zamiast lapply, ale to nic nie zmieniło. (Prawdopodobnie ze względu na mój brak doświadczenia w R.) wyobrażam sobie, że funkcja Rcpp może być napisana, aby zrobić to naprawdę szybko, ale to jest poza moim zestawem umiejętności. Ponadto, mogę nie używać lapply i zastosować odpowiednio.

Moje dane.implementacja tabeli (z łańcuchem magrittr) przyjmuje taką samą CZAS:
text <- seer9[ , apply(start_end, 1, function(y) substr(V1, y[1], y[2]))] %>% 
  data.table(.)

Czy ktoś może zasugerować, aby poprawić szybkość tego? A może tak będzie najlepiej?

Oto kod do tworzenia podobnych danych.tabela w R (zamiast linkowania do rzeczywistych danych). Powinien mieć 331 znaków i 500 000 wierszy. Istnieją spacje do symulacji brakujących pól w danych, ale są to , a nie dane rozdzielane spacjami. (Czytam surowe dane jasnowidza, na wypadek gdyby ktoś był zainteresowany.) W tym również szerokości kolumn (cols) i nazwy zmiennych na wypadek, gdyby to pomogło komuś innemu. Są to rzeczywiste definicje kolumn i zmiennych dla danych 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")

Aktualizacja: LaF zrobił cały odczyt w niecałe 7 sekund od raw .plik txt. Może jest jeszcze szybszy sposób, ale wątpię, żeby cokolwiek mogło zrobić znacznie lepiej. Niesamowity pakiet.

27 Lipiec 2015 Aktualizacja Chciałem tylko zapewnić małą aktualizację tego. Użyłem nowego pakietu readr i udało mi się odczytać cały plik w 5 seconds using readr:: read_fwf.

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

Zaktualizowana funkcja stringi:: stri_sub jest co najmniej dwa razy szybsza od base::substr(). Tak więc, w powyższym kodzie, który używa fread do odczytu pliku (około 4 sekund), a następnie apply do parsowania każdej linii, ekstrakcja 143 zmiennych zajęła około 8 sekund z stringi:: stri_sub w porównaniu do 19 dla base::substr. Więc fread plus stri_sub jest jeszcze tylko około 12 sekund do uruchomienia. Nieźle.

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 gru 2015 Aktualizacja:

Proszę Zobacz również odpowiedź poniżej autorstwa @ MichaelChirico, który dodał kilka świetnych benchmarków i pakiet iotools.

Author: Jaap, 2014-07-12

4 answers

Teraz, gdy istnieje (między tym a Inne ważne pytanie o efektywnym odczycie plików o stałej szerokości) sporo opcji w ofercie do odczytu w takich plikach, myślę, że odpowiednie jest pewne benchmarking.

Użyję poniższego pliku na dużej stronie (400 MB) dla porównania. To tylko kilka losowych znaków z losowo zdefiniowanymi polami i szerokościami:

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)

Porównam pięć metod wymienionych pomiędzy tymi dwoma wątkami (dodam kilka inne, jeśli autorzy chcieliby): wersja podstawowa (read.fwf), przekierowanie wyniku in2csv do fread (sugestia @ anandamahto), Nowy Hadley readr (read_fwf), że za pomocą LaF/ffbase (@jwijffls ' s suggestion), oraz ulepszoną (usprawnioną) wersję sugerowaną przez autora pytania (@MarkDanese) łączącą fread z stri_sub z stringi.

Oto kod porównawczy:

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

I wyjście:

# 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  

Więc wydaje się readr i fread + stri_sub są dość konkurencyjne jako najszybszy; wbudowany read.fwf jest oczywistym przegrany.

Zauważ, że prawdziwą zaletą readr jest to, że możesz wstępnie określić typy kolumn; za pomocą fread będziesz musiał później wpisać convert.

EDIT: dodawanie niektórych alternatyw

Na sugestię @AnandaMahto dołączam jeszcze kilka opcji, w tym jedną, która wydaje się być nowym zwycięzcą! Aby zaoszczędzić czas, wykluczyłem najwolniejsze opcje powyżej w nowym porównaniu. Oto nowy kod:

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

I Nowy wyjście:

# 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

Wygląda więc na to, że iotools jest zarówno bardzo szybki, jak i bardzo konsekwentny.

 25
Author: MichaelChirico,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:26:36

Możesz użyć pakietu LaF, który został napisany do obsługi dużych plików o stałej szerokości(również zbyt dużych, aby zmieścić się w pamięci). Aby go użyć, musisz najpierw otworzyć plik za pomocą laf_open_fwf. Następnie możesz indeksować wynikowy obiekt tak, jak w normalnej ramce danych, aby odczytać potrzebne dane. W poniższym przykładzie odczytuję cały plik, ale można również odczytać konkretne kolumny i / lub wiersze:

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

Twój przykład używając 5000 linii (zamiast 500 000) zajął 28 sekund używając read.fwf i 1,6 sekundy za pomocą LaF.

Dodanie Twojego przykładu używając 50 000 linii (zamiast 500 000) zajęło 258 sekund używając read.fwf i 7 sekund używając LaF na moim komputerze.

 29
Author: Jan van der Laan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-07-12 20:15:18

Napisałem wczoraj parser do tego typu rzeczy, ale był to bardzo specyficzny rodzaj wejścia do pliku nagłówkowego, więc pokażę Ci, jak sformatować szerokości kolumn, aby móc z niego korzystać.

Konwersja pliku płaskiego do pliku csv

Najpierw pobierz omawiane narzędzie.

Możesz pobrać plik binarny z katalogu bin jeśli jesteś na OS X Mavericks (gdzie go skompilowałem) lub skompilować go, przechodząc do src i używając clang++ csv_iterator.cpp parse.cpp main.cpp -o flatfileparser.

Mieszkanie parser plików potrzebuje dwóch plików, pliku nagłówkowego CSV, w którym co piąty element określa zmienną szerokość (znowu jest to spowodowane moją bardzo specyficzną aplikacją), które można wygenerować za pomocą:

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

I skopiowanie wynikowego ~/tmp/header.csv do tego samego katalogu co twoje flatfileparser. Przenieś plik płaski do tego samego katalogu, a możesz go uruchomić na swoim płaskim pliku:

./flatfileparser header.csv yourflatfile

Które będą produkować yourflatfile.csv. Dodaj nagłówek, który masz powyżej, ręcznie używając piping (>> z Bash).

Szybkie czytanie w pliku CSV

Użyj eksperymentalnego pakietu Hadley fastread , przekazując nazwę pliku do fastread::read_csv, co daje data.frame. Nie wierzę, że obsługuje pliki fwf, chociaż jest w drodze.

 3
Author: Robert Krzyzanowski,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-07-12 19:48:59

Nie jestem pewien, jakiego systemu używasz, ale to działało całkiem prosto dla mnie w Linuksie:

Krok 1: Utwórz polecenie awk, Aby przekonwertować plik do pliku csv

Możesz zapisać je do pliku csv, jeśli planujesz użyć danych w innym oprogramowaniu.

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

Krok 2: Użyj fread bezpośrednio na tej komendzie, którą właśnie stworzyłeś

seer9 <- fread(myCommand)

Nie zmierzyłem czasu, ponieważ oczywiście używam wolniejszego systemu niż ty i Jan : -)

 2
Author: A5C1D2H2I1M1N2O1R2T1,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-07-13 19:59:00