web-dev-qa-db-fra.com

Un moyen plus rapide de lire des fichiers à largeur fixe

Je travaille avec beaucoup de fichiers à largeur fixe (c'est-à-dire sans caractère de séparation) que je dois lire dans R. Donc, il y a généralement une définition de la largeur de colonne pour analyser la chaîne en variables. Je peux utiliser read.fwf Pour lire les données sans problème. Cependant, pour les fichiers volumineux, cela peut prendre un temps long . Pour un ensemble de données récent, il a fallu 800 secondes pour lire un ensemble de données avec environ 500 000 lignes et 143 variables.

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

fread dans le package data.table dans R est génial pour résoudre la plupart des problèmes de lecture de données, sauf qu'il n'analyse pas les fichiers à largeur fixe. Cependant, je peux lire chaque ligne comme une chaîne de caractères unique (~ 500 000 lignes, 1 colonne). Cela prend 3-5 secondes. (J'adore data.table.)

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

Il y a un certain nombre de bons articles sur SO sur la façon d'analyser les fichiers texte. Voir la suggestion de JHoward ici , pour créer une matrice de colonnes de début et de fin, et substr pour analyser les données. Voir la suggestion de GSee ici pour utiliser strsplit. Je ne pouvais pas comprendre comment faire fonctionner cela avec ces données. (Aussi, Michael Smith a fait quelques suggestions sur la liste de diffusion data.table impliquant sed qui étaient au-delà de ma capacité à implémenter. ) Maintenant, en utilisant fread et substr() I peut faire le tout en environ 25 à 30 secondes. Notez que la contrainte sur un data.table à la fin prend beaucoup de temps (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)

Je me demande si cela peut encore être amélioré? Je sais que je ne suis pas le seul à lire des fichiers à largeur fixe, donc si cela pouvait être fait plus rapidement, cela rendrait le chargement de fichiers encore plus volumineux (avec des millions de lignes) plus tolérable. J'ai essayé d'utiliser parallel avec mclapply et data.table Au lieu de lapply, mais cela n'a rien changé. (Probablement en raison de mon inexpérience en R.) J'imagine qu'une fonction Rcpp pourrait être écrite pour le faire très rapidement, mais cela dépasse mes compétences. De plus, je n'utilise peut-être pas lapply et je l'applique de manière appropriée.

Mon implémentation de data.table (avec magrittr chaînage) prend le même temps:

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

Quelqu'un peut-il faire des suggestions pour améliorer la vitesse de cela? Ou est-ce à peu près aussi bon que possible?

Voici le code pour créer une table de données similaire dans R (plutôt que de lier à des données réelles). Il doit comporter 331 caractères et 500 000 lignes. Il y a des espaces pour simuler les champs manquants dans les données, mais ce sont [~ # ~] pas [~ # ~] des données délimitées par des espaces. (Je lis des données SEER brutes, au cas où quelqu'un serait intéressé.) Incluant également des largeurs de colonnes (cols) et des noms de variables (seervars) au cas où cela aiderait quelqu'un d'autre. Ce sont les définitions de colonne et de variable réelles pour les données 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")

MISE À JOUR: LaF a fait la lecture entière en un peu moins de 7 secondes à partir du fichier .txt brut. Peut-être existe-t-il un moyen encore plus rapide, mais je doute que quelque chose puisse faire beaucoup mieux. Paquet incroyable.

Mise à jour du 27 juillet 2015 Je voulais juste fournir une petite mise à jour. J'ai utilisé le nouveau package readr et j'ai pu lire l'intégralité du fichier en 5 secondes en utilisant readr :: read_fwf.

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

De plus, la fonction stringi :: stri_sub mise à jour est au moins deux fois plus rapide que base :: substr (). Ainsi, dans le code ci-dessus qui utilise fread pour lire le fichier (environ 4 secondes), suivi de apply pour analyser chaque ligne, l'extraction de 143 variables a pris environ 8 secondes avec stringi :: stri_sub contre 19 pour base :: substr. Ainsi, fread plus stri_sub n'est encore que d'environ 12 secondes à exécuter. Pas mal.

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

Mise à jour du 10 décembre 2015:

Veuillez également consulter le réponse ci-dessous par @MichaelChirico qui a ajouté de bons benchmarks et le package iotools.

43
Mark Danese

Maintenant qu'il y a (entre ceci et autre question majeure sur la lecture efficace des fichiers à largeur fixe) une bonne quantité d'options sur l'offre de lecture dans de tels fichiers, je pense qu'une certaine analyse comparative est appropriée.

Je vais utiliser le fichier suivant sur le grand côté (400 Mo) pour la comparaison. C'est juste un tas de caractères aléatoires avec des champs et des largeurs définis aléatoirement:

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)

Je comparerai cinq méthodes mentionnées entre ces deux threads (j'en ajouterai d'autres si les auteurs le souhaitent): la version de base (read.fwf), canalisant le résultat de in2csv à fread (@ suggestion d'AnandaMahto), le nouveau readr de Hadley (read_fwf), celle utilisant LaF/ffbase (suggestion de @jwijffls), et une version améliorée (rationalisée) de celle suggérée par l'auteur de la question (@MarkDanese) combinant fread avec stri_sub de stringi.

Voici le code de référence:

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

Et la sortie:

# 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  

Il semble donc que readr et fread + stri_sub sont assez compétitifs comme les plus rapides; intégré read.fwf est clairement le perdant.

Notez que le véritable avantage de readr ici est que vous pouvez pré-spécifier les types de colonnes; avec fread vous devrez ensuite taper convert.

EDIT: Ajout de quelques alternatives

À la suggestion de @ AnandaMahto, j'inclus quelques options supplémentaires, dont une qui semble être un nouveau gagnant! Pour gagner du temps, j'ai exclu les options les plus lentes ci-dessus dans la nouvelle comparaison. Voici le nouveau code:

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

Et la nouvelle sortie:

# 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

Il semble donc que iotools soit à la fois très rapide et très cohérent.

31
MichaelChirico

Vous pouvez utiliser le package LaF, qui a été écrit pour gérer des fichiers de grande largeur fixe (également trop volumineux pour tenir en mémoire). Pour l'utiliser, vous devez d'abord ouvrir le fichier à l'aide de laf_open_fwf. Vous pouvez ensuite indexer l'objet résultant comme vous le feriez avec un bloc de données normal pour lire les données dont vous avez besoin. Dans l'exemple ci-dessous, j'ai lu l'intégralité du fichier, mais vous pouvez également lire des colonnes et/ou des lignes spécifiques:

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

Votre exemple utilisant 5 000 lignes (au lieu de vos 500 000) a pris 28 secondes en utilisant read.fwf et 1,6 secondes en utilisant LaF.

Addition Votre exemple utilisant 50 000 lignes (au lieu de 500 000) a pris 258 secondes en utilisant read.fwf et 7 secondes en utilisant LaF sur ma machine.

30
Jan van der Laan

Je ne sais pas quel système d'exploitation vous utilisez, mais cela a fonctionné assez facilement pour moi sous Linux:

Étape 1 : Créez une commande pour awk pour convertir le fichier en csv

Vous pouvez le faire stocker dans un fichier csv réel si vous prévoyez également d'utiliser les données dans d'autres logiciels.

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

Étape 2 : utilisez fread directement sur la commande que vous venez de créer

seer9 <- fread(myCommand)

Je n'ai pas chronométré cela parce que j'utilise évidemment un système plus lent que vous et Jan :-)

3

J'ai écrit un analyseur pour ce genre de chose hier, mais c'était pour un type d'entrée très spécifique dans le fichier d'en-tête, donc je vais vous montrer comment formater la largeur de vos colonnes pour pouvoir l'utiliser.

Conversion de votre fichier plat en csv

Premier téléchargement l'outil en question .

Vous pouvez télécharger le binaire à partir du répertoire bin si vous êtes sur OS X Mavericks (où je l'ai compilé) ou le compiler en allant dans src et en utilisant clang++ csv_iterator.cpp parse.cpp main.cpp -o flatfileparser.

L'analyseur de fichiers plats a besoin de deux fichiers, un fichier d'en-tête CSV dans lequel chaque cinquième élément spécifie la largeur variable (encore une fois, cela est dû à mon application extrêmement spécifique), que vous pouvez générer en utilisant:

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

et en copiant le ~/tmp/header.csv dans le même répertoire que votre flatfileparser. Déplacez également le fichier plat dans le même répertoire et vous pouvez l'exécuter sur votre fichier plat:

./flatfileparser header.csv yourflatfile

qui produira yourflatfile.csv. Ajoutez manuellement l'en-tête ci-dessus à l'aide de la tuyauterie (>> de Bash).

Lecture rapide de votre fichier CSV

Utilisez l'expérimental de Hadley paquet fastread en passant le nom de fichier à fastread::read_csv, ce qui donne un data.frame. Je ne pense pas qu'il supporte encore les fichiers fwf bien qu'il soit en cours.

2
Robert Krzyzanowski