570 votes

Lecture rapide de très grandes tables en tant que dataframes

J'ai des tableaux très volumineux (30 millions de lignes) que je voudrais charger sous forme de dataframes dans R. read.table() a beaucoup de fonctionnalités pratiques, mais il semble qu'il y ait beaucoup de logique dans l'implémentation qui ralentirait les choses. Dans mon cas, je suppose que je connais les types de colonnes à l'avance, que le tableau ne contient pas d'en-têtes de colonnes ou de noms de lignes et qu'il ne comporte pas de caractères pathologiques dont je dois me préoccuper.

Je sais que la lecture d'un tableau sous forme de liste à l'aide de la fonction scan() peut être assez rapide, par exemple :

datalist <- scan('myfile',sep='\t',list(url='',popularity=0,mintime=0,maxtime=0)))

Cependant, certaines de mes tentatives de conversion en un cadre de données semblent diminuer les performances de l'opération ci-dessus par un facteur de 6 :

df <- as.data.frame(scan('myfile',sep='\t',list(url='',popularity=0,mintime=0,maxtime=0))))

Existe-t-il une meilleure façon de procéder ? Ou peut-être une approche complètement différente du problème ?

501voto

Richie Cotton Points 35365

Une mise à jour, plusieurs années plus tard

Cette réponse est ancienne, et R a évolué. Modification de read.table pour courir un peu plus vite n'a que peu d'avantages. Vos options sont :

  1. Utilisation de vroom du paquet tidyverse vroom pour importer des données à partir de fichiers csv/tab-delimited directement dans un tibble R. Voir Réponse d'Hector .

  2. Utilisation de fread en data.table pour importer des données à partir de fichiers csv/tab-delimited directement dans R. Voir réponse de mnel .

  3. Utilisation de read_table en readr (sur CRAN depuis avril 2015). Cela fonctionne à peu près comme fread ci-dessus. Le site readme dans le lien explique la différence entre les deux fonctions ( readr prétend actuellement être "1,5-2x plus lent" que le data.table::fread ).

  4. read.csv.raw de iotools fournit une troisième option pour lire rapidement les fichiers CSV.

  5. Essayer de stocker autant de données que possible dans des bases de données plutôt que dans des fichiers plats. (En plus d'être un meilleur support de stockage permanent, les données sont transmises à et depuis R dans un format binaire, ce qui est plus rapide). read.csv.sql dans le sqldf comme décrit dans La réponse de JD Long importe des données dans une base de données SQLite temporaire et les lit ensuite dans R. Voir également : la fonction RODBC et la section reverse depends du paquet DBI paquet page. MonetDB.R vous donne un type de données qui prétend être un cadre de données mais qui est en réalité un MonetDB en dessous, ce qui augmente les performances. Importez des données avec son monetdb.read.csv fonction. dplyr vous permet de travailler directement avec des données stockées dans plusieurs types de bases de données.

  6. Le stockage des données dans des formats binaires peut également être utile pour améliorer les performances. Utilisez saveRDS / readRDS (voir ci-dessous), le h5 o rhdf5 pour le format HDF5, ou write_fst / read_fst de la fst paquet.


La réponse originale

Il y a deux choses simples à essayer, que vous utilisiez read.table ou scan.

  1. Définir nrows = le nombre d'enregistrements dans vos données ( nmax en scan ).

  2. Assurez-vous que comment.char="" pour désactiver l'interprétation des commentaires.

  3. Définir explicitement les classes de chaque colonne en utilisant colClasses en read.table .

  4. Réglage de multi.line=FALSE peut également améliorer les performances en matière de balayage.

Si aucune de ces choses ne fonctionne, alors utilisez l'un des éléments suivants paquets de profilage pour déterminer quelles sont les lignes qui ralentissent les choses. Peut-être pouvez-vous écrire une version réduite de read.table en fonction des résultats.

L'autre solution consiste à filtrer vos données avant de les lire dans R.

Ou, si le problème est que vous devez les lire régulièrement, utilisez ces méthodes pour lire les données une fois, puis enregistrez le cadre de données sous forme de bloc binaire à l'aide de la commande save saveRDS et la fois suivante, vous pourrez le récupérer plus rapidement avec load readRDS .

4 votes

Merci pour les conseils Richie. J'ai fait quelques tests, et il semble que les gains de performance en utilisant les options nrow et colClasses pour read.table sont assez modestes. Par exemple, la lecture d'une table de ~7M de lignes prend 78s sans les options, et 67s avec les options. (note : le tableau a 1 colonne de caractères, 4 colonnes d'entiers, et je lis en utilisant comment.char='' et stringsAsFactors=FALSE). Utiliser save() et load() quand c'est possible est une bonne astuce - une fois stocké avec save(), ce même tableau ne prend que 12s à charger.

2 votes

Le paquet "feather" propose un nouveau format binaire qui s'adapte aux cadres de données pandas de Python.

5 votes

Je pense que vous devriez peut-être mettre à jour votre message en ce qui concerne le paquet. feather . Pour la lecture des données feather est beaucoup plus rapide que fread . Par exemple, sur un ensemble de données de 4 Go que je viens de charger read_feather était environ 4,5 fois plus rapide que fread . Pour sauvegarder les données fwrite est toujours plus rapide. blog.dominodatalab.com/the-r-data-i-o-shootout

306voto

mnel Points 48160

Voici un exemple qui utilise fread de data.table 1.8.7

Les exemples proviennent de la page d'aide de fread avec les timings de mon Windows XP Core 2 duo E8400.

library(data.table)
# Demo speedup
n=1e6
DT = data.table( a=sample(1:1000,n,replace=TRUE),
                 b=sample(1:1000,n,replace=TRUE),
                 c=rnorm(n),
                 d=sample(c("foo","bar","baz","qux","quux"),n,replace=TRUE),
                 e=rnorm(n),
                 f=sample(1:1000,n,replace=TRUE) )
DT[2,b:=NA_integer_]
DT[4,c:=NA_real_]
DT[3,d:=NA_character_]
DT[5,d:=""]
DT[2,e:=+Inf]
DT[3,e:=-Inf]

table de lecture standard

write.table(DT,"test.csv",sep=",",row.names=FALSE,quote=FALSE)
cat("File size (MB):",round(file.info("test.csv")$size/1024^2),"\n")    
## File size (MB): 51 

system.time(DF1 <- read.csv("test.csv",stringsAsFactors=FALSE))        
##    user  system elapsed 
##   24.71    0.15   25.42
# second run will be faster
system.time(DF1 <- read.csv("test.csv",stringsAsFactors=FALSE))        
##    user  system elapsed 
##   17.85    0.07   17.98

optimisé lecture.table

system.time(DF2 <- read.table("test.csv",header=TRUE,sep=",",quote="",  
                          stringsAsFactors=FALSE,comment.char="",nrows=n,                   
                          colClasses=c("integer","integer","numeric",                        
                                       "character","numeric","integer")))

##    user  system elapsed 
##   10.20    0.03   10.32

fread

require(data.table)
system.time(DT <- fread("test.csv"))                                  
 ##    user  system elapsed 
##    3.12    0.01    3.22

sqldf

require(sqldf)

system.time(SQLDF <- read.csv.sql("test.csv",dbname=NULL))             

##    user  system elapsed 
##   12.49    0.09   12.69

# sqldf as on SO

f <- file("test.csv")
system.time(SQLf <- sqldf("select * from f", dbname = tempfile(), file.format = list(header = T, row.names = F)))

##    user  system elapsed 
##   10.21    0.47   10.73

ff / ffdf

 require(ff)

 system.time(FFDF <- read.csv.ffdf(file="test.csv",nrows=n))   
 ##    user  system elapsed 
 ##   10.85    0.10   10.99

En résumé :

##    user  system elapsed  Method
##   24.71    0.15   25.42  read.csv (first time)
##   17.85    0.07   17.98  read.csv (second time)
##   10.20    0.03   10.32  Optimized read.table
##    3.12    0.01    3.22  fread
##   12.49    0.09   12.69  sqldf
##   10.21    0.47   10.73  sqldf on SO
##   10.85    0.10   10.99  ffdf

53 votes

Excellente réponse, et l'analyse comparative est valable dans d'autres contextes. Je viens de lire un fichier de 4GB en bien moins d'une minute avec fread . J'ai essayé de le lire avec les fonctions R de base et cela m'a pris environ 15 heures.

1 votes

Mon benchmark suggère des avantages de vitesse encore plus grands pour read.csv dans data.table. notez que data.table n'est pas un standard R, mais (malheureusement) "juste" bien partagé par ses créateurs sur CRAN. il n'est même pas considéré comme suffisamment standard pour faire partie de la liste des paquets R communs, et encore moins pour être qualifié comme un remplacement des data frames. il a beaucoup d'avantages, mais aussi des aspects très contre-intuitifs. vous voudrez peut-être utiliser as.data.frame(fread.csv("test.csv")) avec le paquet pour revenir dans le monde standard des cadres de données R.

3 votes

@mnel pouvez-vous s'il vous plaît exécuter à nouveau le benchmark et inclure readr ?

261voto

JD Long Points 20477

Je n'ai pas vu cette question au départ et j'ai posé une question similaire quelques jours plus tard. Je vais retirer ma question précédente, mais j'ai pensé ajouter une réponse ici pour expliquer comment j'ai utilisé sqldf() pour le faire.

Il y a eu un peu de discussion sur la meilleure façon d'importer 2 Go ou plus de données textuelles dans un cadre de données R. Hier, j'ai écrit un article de blog sur l'utilisation sqldf() pour importer les données dans SQLite comme zone de transit, puis les aspirer de SQLite dans R. Cela fonctionne très bien pour moi. J'ai pu extraire 2 Go (3 colonnes, 40 mm de lignes) de données en moins de 5 minutes. En revanche, le read.csv La commande a fonctionné toute la nuit et n'a jamais abouti.

Voici mon code de test :

Configurez les données de test :

bigdf <- data.frame(dim=sample(letters, replace=T, 4e7), fact1=rnorm(4e7), fact2=rnorm(4e7, 20, 50))
write.csv(bigdf, 'bigdf.csv', quote = F)

J'ai redémarré R avant d'exécuter la routine d'importation suivante :

library(sqldf)
f <- file("bigdf.csv")
system.time(bigdf <- sqldf("select * from f", dbname = tempfile(), file.format = list(header = T, row.names = F)))

J'ai laissé la ligne suivante fonctionner toute la nuit mais elle n'a jamais abouti :

system.time(big.df <- read.csv('bigdf.csv'))

1 votes

Comment l'utiliseriez-vous comme entrée pour d'autres paquets tels que zoo, conçus pour être utilisés avec toutes les données simultanément ?

0 votes

@skan l'objet final est un cadre de données. Vous devez donc le convertir en un objet zoo pour pouvoir l'utiliser avec zoo. Regardez les exemples dans la documentation de zoo pour des illustrations.

0 votes

@JD Long. Bonjour, le problème est que lorsque vous le convertissez en un objet zoo, il essaie de le faire tenir sur la mémoire. Si c'est trop grand, il produit une erreur. Et si le résultat de l'objet zoo (par exemple une agrégation de deux séries) est aussi trop grand, il faudrait que ce soit un objet sql ou ff aussi.

81voto

Simon Urbanek Points 7803

Étrangement, personne n'a répondu à la partie inférieure de la question pendant des années, alors qu'il s'agit d'une question importante data.frame sont simplement des listes avec les bons attributs, donc si vous avez des données volumineuses, vous ne voulez pas utiliser l'option as.data.frame ou similaire pour une liste. Il est beaucoup plus rapide de simplement "transformer" une liste en un cadre de données sur place :

attr(df, "row.names") <- .set_row_names(length(df[[1]]))
class(df) <- "data.frame"

Cette méthode ne fait pas de copie des données, elle est donc immédiate (contrairement à toutes les autres méthodes). Elle suppose que vous avez déjà défini names() sur la liste en conséquence.

[Quant au chargement de données volumineuses dans R, personnellement, je les décharge par colonne dans des fichiers binaires et j'utilise la fonction readBin() - qui est de loin la méthode la plus rapide (autre que le mmapping) et n'est limitée que par la vitesse du disque. L'analyse syntaxique des fichiers ASCII est intrinsèquement lente (même en C) par rapport aux données binaires].

6 votes

Utilisation de tracmem suggère que attr<- y class<- faire des copies en interne. bit::setattr o data.table::setattr ne le fera pas.

6 votes

Peut-être avez-vous utilisé le mauvais ordre ? Il n'y a pas de copie si vous utilisez df=scan(...); names(df)=...; attr...; class... - voir tracemem() (testé dans R 2.15.2)

6 votes

Pouvez-vous préciser comment vous videz les grandes données par colonne dans des fichiers binaires ?

33voto

Shane Points 40885

Il s'agissait auparavant demandé sur R-Help donc cela vaut la peine d'être revu.

Une suggestion a été faite d'utiliser readChar() et ensuite faire une manipulation de chaîne sur le résultat avec strsplit() y substr() . Vous pouvez voir que la logique impliquée dans readChar est beaucoup moins importante que celle de read.table.

Je ne sais pas si la mémoire est un problème ici, mais vous pourriez aussi voulez jeter un coup d'oeil à la HadoopStreaming paquet . Ce site utilise Hadoop qui est un cadre MapReduce conçu pour le traitement de grands ensembles de données. Pour cela, vous devez utiliser la fonction hsTableReader. Voici un exemple (mais la courbe d'apprentissage pour apprendre Hadoop est longue) :

str <- "key1\t3.9\nkey1\t8.9\nkey1\t1.2\nkey1\t3.9\nkey1\t8.9\nkey1\t1.2\nkey2\t9.9\nkey2\"
cat(str)
cols = list(key='',val=0)
con <- textConnection(str, open = "r")
hsTableReader(con,cols,chunkSize=6,FUN=print,ignoreKey=TRUE)
close(con)

L'idée de base est de diviser l'importation de données en plusieurs parties. Vous pourriez même aller jusqu'à utiliser l'un des frameworks parallèles (par exemple snow) et exécuter l'importation de données en parallèle en segmentant le fichier, mais il est fort probable que pour les grands ensembles de données, cela ne soit pas utile car vous vous heurterez à des contraintes de mémoire, c'est pourquoi map-reduce est une meilleure approche.

0 votes

Je viens de faire un test rapide et readChar semble être beaucoup plus rapide que readLines pour une raison inexplicable. Cependant, il est toujours aussi lent que le péché comparé à un simple test en C. Pour la simple tâche de lire 100 mégas, R est environ 5 à 10 fois plus lent que C.

1 votes

Je ne comprends pas votre point de vue. Le but d'Hadoop est de traiter de très grandes données, ce qui est le sujet de la question.

1 votes

Malgré son nom, hsTableReader n'a rien à voir avec Hadoop en tant que tel, il sert à traiter de grandes données par morceaux. Il lit depuis con, un morceau de lignes à la fois, et passe chaque morceau comme un data.frame à FUN pour le traitement. Avec ignoreKey=FALSE, il effectue un regroupement supplémentaire par clé (l'entrée de la première colonne), ce qui est pertinent pour les approches Map/Reduce.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X