Le moyen le plus rapide de remplacer les NA dans une table de données volumineuse.

150

J'ai une grande table data.table , avec de nombreuses valeurs manquantes dispersées dans ses ~ 200k lignes et 200 colonnes. Je voudrais recoder ces valeurs NA en zéros aussi efficacement que possible.

Je vois deux options:
1: Convertir en data.frame et utiliser quelque chose comme ceci
2: Une sorte de commande de sous-paramétrage data.table cool

Je serai satisfait d'une solution de type 1 assez efficace. La conversion en data.frame puis en data.table ne prendra pas trop de temps.

Zach
la source
5
Pourquoi voulez-vous convertir le data.tableen un data.frame? A data.table est un data.frame. Toute opération data.frame fonctionnera.
Andrie
5
@Andrie. une différence clé est que vous ne pouvez pas accéder à une colonne dans a data.tableen spécifiant le numéro de colonne. donc DT[,3]ne donnera pas la troisième colonne. Je pense que cela rend la solution proposée dans le lien non viable ici. je suis sûr qu'il existe une approche élégante utilisant une certaine data.tablemagie!
Ramnath
6
@Ramnath, AFAIK, DT[, 3, with=FALSE]renvoie la troisième colonne.
Andrie
2
@Andrie. mais il y a toujours un problème mydf[is.na(mydf) == TRUE]fait le travail sur les trames de données, alors mydt[is.na(mydt) == TRUE]que ça me donne quelque chose d'étrange même si j'utilisewith=FALSE
Ramnath
2
@Ramnath, point pris. Ma déclaration précédente était trop large, c'est-à-dire que j'avais tort. Désolé. Data.tables se comporte comme data.frames uniquement lorsqu'il n'y a pas de méthode data.table.
Andrie

Réponses:

184

Voici une solution utilisant l' opérateur de data.table:= , s'appuyant sur les réponses d'Andrie et Ramnath.

require(data.table)  # v1.6.6
require(gdata)       # v2.8.2

set.seed(1)
dt1 = create_dt(2e5, 200, 0.1)
dim(dt1)
[1] 200000    200    # more columns than Ramnath's answer which had 5 not 200

f_andrie = function(dt) remove_na(dt)

f_gdata = function(dt, un = 0) gdata::NAToUnknown(dt, un)

f_dowle = function(dt) {     # see EDIT later for more elegant solution
  na.replace = function(v,value=0) { v[is.na(v)] = value; v }
  for (i in names(dt))
    eval(parse(text=paste("dt[,",i,":=na.replace(",i,")]")))
}

system.time(a_gdata = f_gdata(dt1)) 
   user  system elapsed 
 18.805  12.301 134.985 

system.time(a_andrie = f_andrie(dt1))
Error: cannot allocate vector of size 305.2 Mb
Timing stopped at: 14.541 7.764 68.285 

system.time(f_dowle(dt1))
  user  system elapsed 
 7.452   4.144  19.590     # EDIT has faster than this

identical(a_gdata, dt1)   
[1] TRUE

Notez que f_dowle a mis à jour dt1 par référence. Si une copie locale est requise, un appel explicite à la copyfonction est nécessaire pour créer une copie locale de l'ensemble de données. de data.table setkey, key<-et :=ne pas copier-on-write.

Ensuite, voyons où f_dowle passe son temps.

Rprof()
f_dowle(dt1)
Rprof(NULL)
summaryRprof()
$by.self
                  self.time self.pct total.time total.pct
"na.replace"           5.10    49.71       6.62     64.52
"[.data.table"         2.48    24.17       9.86     96.10
"is.na"                1.52    14.81       1.52     14.81
"gc"                   0.22     2.14       0.22      2.14
"unique"               0.14     1.36       0.16      1.56
... snip ...

Là, je me concentrerais sur na.replaceet is.na, où il y a quelques copies vectorielles et numérisations vectorielles. Ceux-ci peuvent être assez facilement éliminés en écrivant une petite fonction C na.replace qui se met NAà jour par référence dans le vecteur. Cela réduirait au moins de moitié les 20 secondes, je pense. Une telle fonction existe-t-elle dans un package R?

La raison f_andrieéchoue peut être parce qu'il copie la totalité de dt1, ou crée une matrice logique aussi grande que l'ensemble de dt1, plusieurs fois. Les 2 autres méthodes fonctionnent sur une colonne à la fois (même si je n'ai regardé que brièvement NAToUnknown).

EDIT (solution plus élégante comme demandé par Ramnath dans les commentaires):

f_dowle2 = function(DT) {
  for (i in names(DT))
    DT[is.na(get(i)), (i):=0]
}

system.time(f_dowle2(dt1))
  user  system elapsed 
 6.468   0.760   7.250   # faster, too

identical(a_gdata, dt1)   
[1] TRUE

J'aurais aimé le faire de cette façon pour commencer!

EDIT2 (plus d'un an plus tard, maintenant)

Il y a aussi set(). Cela peut être plus rapide s'il y a beaucoup de colonnes en boucle, car cela évite la (petite) surcharge de l'appel [,:=,]dans une boucle. setest une boucle :=. Voir ?set.

f_dowle3 = function(DT) {
  # either of the following for loops

  # by name :
  for (j in names(DT))
    set(DT,which(is.na(DT[[j]])),j,0)

  # or by number (slightly faster than by name) :
  for (j in seq_len(ncol(DT)))
    set(DT,which(is.na(DT[[j]])),j,0)
}
Matt Dowle
la source
5
+! très bonne réponse! est-il possible d'avoir un équivalent plus intuitif du eval(parse)...truc. sur une note plus large, je pense qu'il serait utile d'avoir des opérations qui fonctionnent sur tous les éléments du data.table.
Ramnath
1
Votre deuxième bloc de code semble être le moyen le plus data.tableapproprié de le faire. Merci!
Zach
3
@Statwonk Je suppose que vous avez DTdes colonnes de type logical, contrairement à l' create_dt()exemple de ce test. Changez le 4ème argument de l' set()appel (qui est 0dans votre exemple et tapez double dans R) en FALSEet cela devrait fonctionner sans avertissement.
Matt Dowle
2
@Statwonk Et j'ai déposé une demande de fonctionnalité pour assouplir ce cas et supprimer cet avertissement lorsque vous forcez les vecteurs de longueur 1 0 et 1 à la logique: # 996 . Peut-être ne pas le faire car, pour la vitesse, vous voulez être averti des contraintes répétitives inutiles.
Matt Dowle
1
@StefanF True et je préfère seq_along(DT)aussi. Mais alors le lecteur doit savoir que ce seq_alongserait le long des colonnes et non les lignes. seq_len(col(DT))un peu plus explicite pour cette raison.
Matt Dowle
28

Voici le plus simple que je puisse proposer:

dt[is.na(dt)] <- 0

C'est efficace et pas besoin d'écrire des fonctions et autres codes de glue.

Bar
la source
ne fonctionne pas sur les grands ensembles de données et les postes de travail normaux (erreur d'allocation de mémoire)
Jake
3
@Jake sur une machine avec 16 Go de RAM, j'ai pu l'exécuter sur 31 millions de lignes, ~ 20 colonnes. YMMV bien sûr.
Bar
Je m'en remets à vos preuves empiriques. Merci.
Jake
10
Malheureusement, dans les dernières versions de data.table, cela ne fonctionne pas. Il dit Erreur dans [.data.table(dt, is.na (dt)): i est un type invalide (matrice). Peut-être qu'à l'avenir une matrice à 2 colonnes pourrait renvoyer une liste d'éléments de DT (dans l'esprit de A [B] dans la FAQ 2.14). Veuillez faire savoir à datatable-help si vous le souhaitez, ou ajouter vos commentaires à FR # 657. >
skan
c'est intéressant! J'ai toujours utiliséset
marbel
15

Des fonctions dédiées ( nafillet setnafill) à cet effet sont disponibles dans le data.tablepackage (version> = 1.12.4):

Il traite les colonnes en parallèle si bien pour répondre aux repères précédemment publiés, en dessous de ses délais par rapport à l'approche la plus rapide jusqu'à présent, et également mis à l'échelle, en utilisant une machine à 40 cœurs.

library(data.table)
create_dt <- function(nrow=5, ncol=5, propNA = 0.5){
  v <- runif(nrow * ncol)
  v[sample(seq_len(nrow*ncol), propNA * nrow*ncol)] <- NA
  data.table(matrix(v, ncol=ncol))
}
f_dowle3 = function(DT) {
  for (j in seq_len(ncol(DT)))
    set(DT,which(is.na(DT[[j]])),j,0)
}

set.seed(1)
dt1 = create_dt(2e5, 200, 0.1)
dim(dt1)
#[1] 200000    200
dt2 = copy(dt1)
system.time(f_dowle3(dt1))
#   user  system elapsed 
#  0.193   0.062   0.254 
system.time(setnafill(dt2, fill=0))
#   user  system elapsed 
#  0.633   0.000   0.020   ## setDTthreads(1) elapsed: 0.149
all.equal(dt1, dt2)
#[1] TRUE

set.seed(1)
dt1 = create_dt(2e7, 200, 0.1)
dim(dt1)
#[1] 20000000    200
dt2 = copy(dt1)
system.time(f_dowle3(dt1))
#   user  system elapsed 
# 22.997  18.179  41.496
system.time(setnafill(dt2, fill=0))
#   user  system elapsed 
# 39.604  36.805   3.798 
all.equal(dt1, dt2)
#[1] TRUE
jangorecki
la source
C'est une excellente fonctionnalité! Envisagez-vous d'ajouter la prise en charge des colonnes de caractères? Ensuite, il pourrait être utilisé ici .
ismirsehregal
1
@ismirsehregal oui, vous pouvez suivre cette fonctionnalité ici github.com/Rdatatable/data.table/issues/3992
jangorecki
12
library(data.table)

DT = data.table(a=c(1,"A",NA),b=c(4,NA,"B"))

DT
    a  b
1:  1  4
2:  A NA
3: NA  B

DT[,lapply(.SD,function(x){ifelse(is.na(x),0,x)})]
   a b
1: 1 4
2: A 0
3: 0 B

Juste pour référence, plus lent par rapport à gdata ou data.matrix, mais utilise uniquement le package data.table et peut traiter des entrées non numériques.

Andreas Rhode
la source
5
Vous pourriez probablement à la fois éviter ifelseet mettre à jour par référence en faisant DT[, names(DT) := lapply(.SD, function(x) {x[is.na(x)] <- "0" ; x})]. Et je doute que ce soit plus lent que les réponses que vous avez mentionnées.
David Arenburg
11

Voici une solution utilisant NAToUnknowndans le gdatapackage. J'ai utilisé la solution d'Andrie pour créer un énorme tableau de données et j'ai également inclus des comparaisons de temps avec la solution d'Andrie.

# CREATE DATA TABLE
dt1 = create_dt(2e5, 200, 0.1)

# FUNCTIONS TO SET NA TO ZERO   
f_gdata  = function(dt, un = 0) gdata::NAToUnknown(dt, un)
f_Andrie = function(dt) remove_na(dt)

# COMPARE SOLUTIONS AND TIMES
system.time(a_gdata  <- f_gdata(dt1))

user  system elapsed 
4.224   2.962   7.388 

system.time(a_andrie <- f_Andrie(dt1))

 user  system elapsed 
4.635   4.730  20.060 

identical(a_gdata, g_andrie)  

TRUE
Ramnath
la source
+1 Bonne trouvaille. Intéressant - c'est la première fois que je vois des horaires avec une userheure similaire mais une très grande différence de elapsedtemps.
Andrie
@Andrie J'ai essayé d'utiliser rbenchmarkdes solutions de référence en utilisant plus de réplications, mais j'ai eu une erreur de mémoire insuffisante probablement en raison de la taille de la trame de données. si vous pouvez exécuter benchmarkces deux solutions avec plusieurs réplications, ces résultats seraient intéressants car je ne sais pas vraiment pourquoi j'obtiens une accélération 3x
Ramnath
@Ramnath Pour que les choses soient correctes, les horaires de cette réponse sont pour ncol=5je pense (devraient prendre beaucoup plus de temps) en raison du bogue create_dt.
Matt Dowle
5

Par souci d'exhaustivité, une autre façon de remplacer les NA par 0 consiste à utiliser

f_rep <- function(dt) {
dt[is.na(dt)] <- 0
return(dt)
}

Pour comparer les résultats et les temps, j'ai intégré toutes les approches mentionnées jusqu'à présent.

set.seed(1)
dt1 <- create_dt(2e5, 200, 0.1)
dt2 <- dt1
dt3 <- dt1

system.time(res1 <- f_gdata(dt1))
   User      System verstrichen 
   3.62        0.22        3.84 
system.time(res2 <- f_andrie(dt1))
   User      System verstrichen 
   2.95        0.33        3.28 
system.time(f_dowle2(dt2))
   User      System verstrichen 
   0.78        0.00        0.78 
system.time(f_dowle3(dt3))
   User      System verstrichen 
   0.17        0.00        0.17 
system.time(res3 <- f_unknown(dt1))
   User      System verstrichen 
   6.71        0.84        7.55 
system.time(res4 <- f_rep(dt1))
   User      System verstrichen 
   0.32        0.00        0.32 

identical(res1, res2) & identical(res2, res3) & identical(res3, res4) & identical(res4, dt2) & identical(dt2, dt3)
[1] TRUE

La nouvelle approche est donc légèrement plus lente f_dowle3mais plus rapide que toutes les autres approches. Mais pour être honnête, cela va à l'encontre de mon Intuition de la syntaxe data.table et je n'ai aucune idée de pourquoi cela fonctionne. Quelqu'un peut-il m'éclairer?

bratwoorst711
la source
1
Oui, je les ai vérifiés, c'est pourquoi j'ai inclus les identiques par paire.
bratwoorst711
1
Voici une raison pour laquelle ce n'est pas la manière idiomatique - stackoverflow.com/a/20545629
Naumz
4

Je crois comprendre que le secret des opérations rapides dans R est d'utiliser des vecteurs (ou des tableaux, qui sont des vecteurs sous le capot.)

Dans cette solution, j'utilise un data.matrixqui est un arraymais se comporte un peu comme un data.frame. Comme il s'agit d'un tableau, vous pouvez utiliser une substitution vectorielle très simple pour remplacer le NAs:

Une petite fonction d'aide pour supprimer le NAs. L'essence est une seule ligne de code. Je ne fais cela que pour mesurer le temps d'exécution.

remove_na <- function(x){
  dm <- data.matrix(x)
  dm[is.na(dm)] <- 0
  data.table(dm)
}

Une petite fonction d'aide pour créer un data.tabled'une taille donnée.

create_dt <- function(nrow=5, ncol=5, propNA = 0.5){
  v <- runif(nrow * ncol)
  v[sample(seq_len(nrow*ncol), propNA * nrow*ncol)] <- NA
  data.table(matrix(v, ncol=ncol))
}

Démonstration sur un petit échantillon:

library(data.table)
set.seed(1)
dt <- create_dt(5, 5, 0.5)

dt
            V1        V2        V3        V4        V5
[1,]        NA 0.8983897        NA 0.4976992 0.9347052
[2,] 0.3721239 0.9446753        NA 0.7176185 0.2121425
[3,] 0.5728534        NA 0.6870228 0.9919061        NA
[4,]        NA        NA        NA        NA 0.1255551
[5,] 0.2016819        NA 0.7698414        NA        NA

remove_na(dt)
            V1        V2        V3        V4        V5
[1,] 0.0000000 0.8983897 0.0000000 0.4976992 0.9347052
[2,] 0.3721239 0.9446753 0.0000000 0.7176185 0.2121425
[3,] 0.5728534 0.0000000 0.6870228 0.9919061 0.0000000
[4,] 0.0000000 0.0000000 0.0000000 0.0000000 0.1255551
[5,] 0.2016819 0.0000000 0.7698414 0.0000000 0.0000000
Andrie
la source
C'est un très bel exemple de jeu de données. Je vais essayer de m'améliorer remove_na. Ce timing de 21,57 s inclut le create_dt(y compris runifet sample) ainsi que le remove_na. Avez-vous une chance que vous puissiez modifier pour séparer les 2 fois?
Matt Dowle
Y a-t-il un petit bogue create_dt? Il semble toujours créer une table de données à 5 colonnes, indépendamment de la ncoltransmission.
Matt Dowle
@MatthewDowle Bien repéré. Erreur supprimée (ainsi que les horaires)
Andrie
La conversion en matrice ne fonctionnera correctement que si toutes les colonnes sont du même type.
skan
2

Pour généraliser à de nombreuses colonnes, vous pouvez utiliser cette approche (en utilisant des exemples de données précédents mais en ajoutant une colonne):

z = data.table(x = sample(c(NA_integer_, 1), 2e7, TRUE), y = sample(c(NA_integer_, 1), 2e7, TRUE))

z[, names(z) := lapply(.SD, function(x) fifelse(is.na(x), 0, x))]

Je n'ai pas testé la vitesse

arono686
la source
1
> DT = data.table(a=LETTERS[c(1,1:3,4:7)],b=sample(c(15,51,NA,12,21),8,T),key="a")
> DT
   a  b
1: A 12
2: A NA
3: B 15
4: C NA
5: D 51
6: E NA
7: F 15
8: G 51
> DT[is.na(b),b:=0]
> DT
   a  b
1: A 12
2: A  0
3: B 15
4: C  0
5: D 51
6: E  0
7: F 15
8: G 51
> 
Hao
la source
3
Et comment généreriez-vous cela à plus d'une colonne?
David Arenburg
@DavidArenburg écrit simplement une boucle for. Cela devrait être la réponse acceptée: c'est la plus simple!
baibo
1

Utilisation de la fifelsefonction du plus récentdata.table versions 1.12.6, il est même 10 fois plus rapide que NAToUnknowndans le gdatapackage:

z = data.table(x = sample(c(NA_integer_, 1), 2e7, TRUE))
system.time(z[,x1 := gdata::NAToUnknown(x, 0)])

#   user  system elapsed 
#  0.798   0.323   1.173 
system.time(z[,x2:= fifelse(is.na(x), 0, x)])

#   user  system elapsed 
#  0.172   0.093   0.113 
Miao Cai
la source
Pouvez-vous ajouter quelques comparaisons de temps à cette réponse? Je pense que ce f_dowle3sera toujours plus rapide: stackoverflow.com/a/7249454/345660
Zach le