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(.)
Veuillez également consulter le réponse ci-dessous par @MichaelChirico qui a ajouté de bons benchmarks et le package iotools.
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.
À 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.
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.
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 :-)
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.
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).
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.