web-dev-qa-db-fra.com

Moyen le plus rapide de remplacer les AN dans un grand data.table

J'ai un grand data.table , avec beaucoup de valeurs manquantes dispersées à travers ses ~ 200k lignes et 200 colonnes. Je voudrais re-coder ces valeurs NA en zéros aussi efficacement que possible.

Je vois deux options:
1: Convertissez vos données en data.frame et utilisez quelque chose comme ceci
2: Une sorte de commande cool de configuration de data.table

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

130
Zach

Voici une solution utilisant l'opérateur := De data.table , en 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 fonction copy est nécessaire pour créer une copie locale de l'ensemble de données. setkey, key<- et := de data.table ne sont pas copiés à l'écriture.

Voyons maintenant 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.replace Et is.na, Où il existe quelques copies de vecteurs et numérisations de vecteurs. Ceux-ci peuvent être assez facilement éliminés en écrivant une petite fonction C na.replace qui met à jour NA par référence dans le vecteur. Cela réduirait au moins les 20 secondes, je pense. Une telle fonction existe-t-elle dans un package R?

La raison de l'échec de f_andrie Est peut-être due au fait qu'il copie l'intégralité de dt1 Ou crée une matrice logique aussi grande que l'ensemble de dt1, À quelques reprises. Les 2 autres méthodes fonctionnent sur une colonne à la fois (bien que je n’ai brièvement regardé que NAToUnknown).

[~ # ~] éditer [~ # ~] (solution plus élégante demandée 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

Je souhaite que je l'aie fait 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 de [,:=,] Dans une boucle. set est un := Pouvant être mis en 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)
}
164
Matt Dowle

Voici le plus simple que j'ai pu trouver:

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

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

21
Bar

Voici une solution utilisant NAToUnknown dans le package gdata. J'ai utilisé la solution d'Andrie pour créer un énorme tableau de données et é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
11
Ramnath
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 lentement comparé à gdata ou data.matrix, mais n’utilise que le paquet data.table et peut traiter des entrées non numériques.

10
Andreas Rhode

La fonction dédiée (nafill/setnafill) à cette fin est récente data.table paquet

install.packages("data.table", repos="https://Rdatatable.gitlab.io/data.table")

Il traite les colonnes en parallèle si bien qu'il répond aux points de repère précédemment publiés, en dessous de son minutage 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
5
jangorecki

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

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

Pour comparer les résultats et les délais, 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 que f_dowle3 mais 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 ne sais pas du tout pourquoi cela fonctionne. Quelqu'un peut-il m'éclairer?

5
bratwoorst711

D'après ce que je comprends, le secret des opérations rapides en R consiste à utiliser un vecteur (ou des tableaux, qui sont des vecteurs sous le capot).

Dans cette solution, je me sers d'un data.matrix qui est un array mais se comporte un peu comme un data.frame. Puisqu'il s'agit d'un tableau, vous pouvez utiliser une substitution de vecteur très simple pour remplacer le NAs:

Une petite fonction d'assistance 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'assistance pour créer un data.table d'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 échantillon minuscule:

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
4
Andrie
> 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
> 
0
Hao