web-dev-qa-db-fra.com

Lecture rapide de très grandes tables en tant que cadres de données

J'ai de très grandes tables (30 millions de lignes) que j'aimerais charger en tant que bases de données dans R. read.table() possède de nombreuses fonctionnalités pratiques, mais il semble qu'il y ait beaucoup de logique dans la mise en œuvre qui ralentirait les choses bas. Dans mon cas, je suppose que je connais à l'avance les types de colonnes, le tableau ne contient aucun en-tête de colonne ni nom de ligne et ne contient aucun caractère pathologique qui me préoccupe.

Je sais que lire dans un tableau sous forme de liste en utilisant scan() peut être assez rapide, par exemple:

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

Mais certaines de mes tentatives pour convertir cela en un cadre de données semblent diminuer les performances de ce qui précède 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 le faire? Ou peut-être une approche complètement différente du problème?

477
eytan

Voici un exemple qui utilise fread de data.table 1.8.7

Les exemples proviennent de la page d'aide de fread, avec le minutage de mes fenêtres 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

read.table optimisé

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
267
mnel

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

Il y a eu peu de discussion sur la meilleure façon d'importer 2 Go ou plus de données texte dans un cadre de données R. Hier, j’ai écrit un article de blog sur l’utilisation de sqldf() pour importer les données dans SQLite en tant que zone de transfert, puis les transférer de SQLite à 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, la commande read.csv a été exécutée 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 courir toute la nuit mais elle n'a jamais été complétée:

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

Étrangement, personne n’a répondu à la partie inférieure de la question pendant des années, même s’il s’agit d’une question importante - data.frames sont simplement des listes avec les bons attributs. Si vous avez des données volumineuses, vous ne voulez pas utiliser as.data.frame ou similaire pour une liste. Il est beaucoup plus rapide de simplement "transformer" une liste en un bloc de données sur place:

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

Cela ne crée aucune copie des données et est donc immédiat (contrairement à toutes les autres méthodes). Cela suppose que vous ayez déjà défini names() dans la liste en conséquence.

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

72
Simon Urbanek

C'était auparavant demandé sur R-Help , alors ça vaut le coup d’être revu.

Il a été suggéré d'utiliser readChar(), puis de manipuler le résultat avec strsplit() et substr(). Vous pouvez voir que la logique impliquée dans readChar est bien moins que read.table.

Je ne sais pas si la mémoire est un problème ici, mais vous pourriez aussi vouloir jeter un oeil à la HadoopStreaming paquet . This tilise Hadoop , qui est un cadre MapReduce conçu pour traiter de grands ensembles de données. Pour cela, vous utiliseriez la fonction hsTableReader. Ceci est un exemple (mais il a une courbe d'apprentissage pour apprendre Hadoop):

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 ici est de décomposer l'importation de données en morceaux. Vous pouvez 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. c'est pourquoi map-réduire est une meilleure approche.

31
Shane

Quelques points supplémentaires mineurs à noter. Si vous avez un très gros fichier, vous pouvez à la volée calculer le nombre de lignes (s'il n'y a pas d'en-tête) en utilisant (où bedGraph est le nom de votre fichier dans votre répertoire de travail):

>numRow=as.integer(system(paste("wc -l", bedGraph, "| sed 's/[^0-9.]*\\([0-9.]*\\).*/\\1/'"), intern=T))

Vous pouvez ensuite l'utiliser soit dans read.csv, read.table ...

>system.time((BG=read.table(bedGraph, nrows=numRow, col.names=c('chr', 'start', 'end', 'score'),colClasses=c('character', rep('integer',3)))))
   user  system elapsed 
 25.877   0.887  26.752 
>object.size(BG)
203949432 bytes
5
Stephen Henderson

Une alternative consiste à utiliser le package vroom. Maintenant sur CRAN. vroom ne charge pas l'intégralité du fichier, il indexe l'emplacement de chaque enregistrement et est lu ultérieurement lorsque vous l'utilisez.

Ne payez que ce que vous utilisez.

Voir Introduction à vroom , Commencez avec vroom et le points de repère de vroom .

L'aperçu de base est que la lecture initiale d'un fichier volumineux sera beaucoup plus rapide et que les modifications ultérieures des données risquent d'être légèrement plus lentes. Donc, en fonction de votre utilisation, cela pourrait être la meilleure option.

Voir un exemple simplifié tiré de vroom benchmarks ci-dessous, les éléments clés à visualiser sont les temps de lecture très rapides, mais les opérations légèrement semées telles que l'agrégat, etc.

package                 read    print   sample   filter  aggregate   total
read.delim              1m      21.5s   1ms      315ms   764ms       1m 22.6s
readr                   33.1s   90ms    2ms      202ms   825ms       34.2s
data.table              15.7s   13ms    1ms      129ms   394ms       16.3s
vroom (altrep) dplyr    1.7s    89ms    1.7s     1.3s    1.9s        6.7s
5

Souvent, j'estime qu'il est judicieux de conserver de plus grandes bases de données dans une base de données (par exemple, Postgres). Je n'utilise rien de plus grand que (nrow * ncol) ncell = 10M, ce qui est plutôt petit; mais je trouve souvent que je veux que R crée et conserve des graphiques gourmands en mémoire uniquement lorsque je lance une requête depuis plusieurs bases de données. Dans l'avenir des ordinateurs portables de 32 Go, certains de ces problèmes de mémoire disparaîtront. Cependant, il peut être utile d'utiliser une base de données pour stocker les données, puis d'utiliser la mémoire de R pour obtenir les résultats de la requête et les graphiques résultants. Certains avantages sont:

(1) Les données restent chargées dans votre base de données. Vous vous reconnectez simplement dans pgadmin aux bases de données souhaitées lorsque vous rallumez votre ordinateur portable.

(2) Il est vrai que R peut effectuer de nombreuses opérations statistiques et graphiques plus astucieuses que SQL. Mais je pense que SQL est mieux conçu pour interroger de grandes quantités de données que R.

# Looking at Voter/Registrant Age by Decade

library(RPostgreSQL);library(lattice)

con <- dbConnect(PostgreSQL(), user= "postgres", password="password",
                 port="2345", Host="localhost", dbname="WC2014_08_01_2014")

Decade_BD_1980_42 <- dbGetQuery(con,"Select PrecinctID,Count(PrecinctID),extract(DECADE from Birthdate) from voterdb where extract(DECADE from Birthdate)::numeric > 198 and PrecinctID in (Select * from LD42) Group By PrecinctID,date_part Order by Count DESC;")

Decade_RD_1980_42 <- dbGetQuery(con,"Select PrecinctID,Count(PrecinctID),extract(DECADE from RegistrationDate) from voterdb where extract(DECADE from RegistrationDate)::numeric > 198 and PrecinctID in (Select * from LD42) Group By PrecinctID,date_part Order by Count DESC;")

with(Decade_BD_1980_42,(barchart(~count | as.factor(precinctid))));
mtext("42LD Birthdays later than 1980 by Precinct",side=1,line=0)

with(Decade_RD_1980_42,(barchart(~count | as.factor(precinctid))));
mtext("42LD Registration Dates later than 1980 by Precinct",side=1,line=0)
4
rferrisx

Au lieu du read.table conventionnel, j’ai le sentiment que fread est une fonction plus rapide. La spécification d'attributs supplémentaires, tels que la sélection des colonnes requises uniquement, la spécification de classes et de classes en tant que facteurs, réduira le temps nécessaire à l'importation du fichier.

data_frame <- fread("filename.csv",sep=",",header=FALSE,stringsAsFactors=FALSE,select=c(1,4,5,6,7),colClasses=c("as.numeric","as.character","as.numeric","as.Date","as.Factor"))
0
Aayush Agrawal