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.
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)
}
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.
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
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.
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
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?
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 NA
s:
Une petite fonction d'assistance pour supprimer le NA
s. 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
> 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
>