Cara tercepat untuk mengganti NAS dalam tabel data yang besar

150

Saya memiliki data.table besar , dengan banyak nilai yang hilang tersebar di ~ 200r baris dan 200 kolom. Saya ingin mengkode ulang nilai-nilai NA ke nol seefisien mungkin.

Saya melihat dua opsi:
1: Konversi ke data.frame, dan gunakan sesuatu seperti ini
2: Beberapa jenis perintah sub pengaturan data.table yang keren

Saya akan senang dengan solusi yang cukup efisien dari tipe 1. Mengkonversi ke data.frame dan kemudian kembali ke data.tabel tidak akan terlalu lama.

Zach
sumber
5
Mengapa Anda ingin mengonversi data.tableke a data.frame? A data.table adalah a data.frame. Operasi data.frame apa pun hanya akan berfungsi.
Andrie
5
@ Andrie. perbedaan utama adalah bahwa Anda tidak dapat mengakses kolom dalam data.tabledengan menentukan nomor kolom. jadi DT[,3]tidak akan memberikan kolom ketiga. Saya pikir ini membuat solusi yang diusulkan dalam tautan tidak dapat digunakan di sini. Saya yakin ada pendekatan yang elegan menggunakan beberapa data.tablesihir!
Ramnath
6
@Ramnath, AFAIK, DT[, 3, with=FALSE]mengembalikan kolom ketiga.
Andrie
2
@ Andrie. tetapi masih ada masalah mydf[is.na(mydf) == TRUE]melakukan pekerjaan pada frame data, sementara mydt[is.na(mydt) == TRUE]memberi saya sesuatu yang aneh bahkan jika saya menggunakanwith=FALSE
Ramnath
2
@ Raynath, poin sudah diambil. Pernyataan saya sebelumnya terlalu luas, yaitu saya salah. Maaf. Data.tables hanya berperilaku seperti data.frame ketika tidak ada metode data.table.
Andrie

Jawaban:

184

Berikut adalah solusi menggunakan data.table 's :=operator, membangun Andrie dan jawaban Ramnath ini.

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

Perhatikan bahwa f_dowle memperbarui dt1 dengan referensi. Jika salinan lokal diperlukan maka panggilan eksplisit ke copyfungsi diperlukan untuk membuat salinan lokal dari keseluruhan dataset. data.table setkey, key<-dan :=jangan copy-on-write.

Selanjutnya, mari kita lihat di mana f_dowle menghabiskan waktunya.

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 ...

Di sana, saya akan fokus pada na.replacedan is.na, di mana ada beberapa salinan vektor dan pemindaian vektor. Mereka dapat dengan mudah dihilangkan dengan menulis fungsi C kecil yang menggantikan pembaruan NAdengan referensi dalam vektor. Setidaknya akan membagi dua 20 detik saya pikir. Apakah fungsi seperti itu ada dalam paket R?

Alasannya f_andriegagal mungkin karena menyalin seluruh dt1, atau membuat matriks logis sebesar keseluruhan dt1, beberapa kali. 2 metode lainnya bekerja pada satu kolom pada satu waktu (walaupun saya hanya melihat sebentar NAToUnknown).

EDIT (solusi yang lebih elegan seperti yang diminta oleh Ramnath dalam komentar):

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

Saya berharap saya melakukannya seperti itu untuk memulai!

EDIT2 (lebih dari 1 tahun kemudian, sekarang)

Ada juga set(). Ini bisa lebih cepat jika ada banyak kolom yang dilingkarkan, karena menghindari overhead (kecil) dari panggilan [,:=,]dalam satu lingkaran. setadalah loopable :=. Lihat ?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
sumber
5
+! jawaban bagus! apakah mungkin untuk memiliki hal yang setara secara intuitif eval(parse).... pada catatan yang lebih luas, saya pikir akan berguna untuk memiliki operasi yang bekerja pada semua elemen data.table.
Ramnath
1
Blok kode ke-2 Anda tampaknya merupakan cara yang paling data.tabletepat untuk melakukan ini. Terima kasih!
Zach
3
@Statwonk Saya kira Anda DTmemiliki kolom tipe logical, tidak seperti create_dt()contoh untuk tes ini. Ubah argumen ke-4 set()panggilan (yang ada 0dalam contoh Anda dan ketikkan ganda dalam R) menjadi FALSEdan seharusnya berfungsi tanpa peringatan.
Matt Dowle
2
@Statwonk Dan saya telah mengajukan permintaan fitur untuk bersantai kasus ini dan menjatuhkan peringatan itu ketika memaksa panjang-1 vektor 0 dan 1 ke logis: # 996 . Mungkin tidak melakukannya karena, untuk kecepatan, Anda ingin diperingatkan tentang pemaksaan berulang yang tidak perlu.
Matt Dowle
1
@StefanF Benar dan saya lebih suka seq_along(DT)juga. Tetapi kemudian pembaca harus tahu bahwa seq_alongakan ada di sepanjang kolom dan tidak di baris. seq_len(col(DT))sedikit lebih eksplisit karena alasan itu.
Matt Dowle
28

Inilah yang paling sederhana yang bisa saya pikirkan:

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

Ini efisien dan tidak perlu menulis fungsi dan kode lem lainnya.

Batang
sumber
tidak bekerja pada dataset besar dan komputer workstation normal (kesalahan alokasi memori)
Jake
3
@Buat pada mesin dengan 16GB RAM Saya bisa menjalankan ini pada baris 31M, ~ 20 kolom. YMMV tentu saja.
Bar
Saya tunduk pada bukti empiris Anda. Terima kasih.
Jake
10
Sayangnya dalam versi data terbaru. Tabel itu tidak berfungsi. Dikatakan Kesalahan dalam [.data.table(dt, is.na (dt)): i adalah tipe tidak valid (matriks). Mungkin di masa depan matriks 2 kolom dapat mengembalikan daftar elemen DT (dalam semangat A [B] di FAQ 2.14). Tolong beri tahu datatable-help jika Anda menginginkan ini, atau tambahkan komentar Anda ke FR # 657. >
skan
ini menarik! Saya selalu menggunakanset
marbel
15

Fungsi khusus ( nafilldan setnafill) untuk tujuan itu tersedia dalam data.tablepaket (versi> = 1.12.4):

Ini memproses kolom secara paralel sehingga dengan baik mengatasi tolok ukur yang diposting sebelumnya, di bawah timing vs pendekatan tercepat sampai sekarang, dan juga ditingkatkan, menggunakan mesin 40 core.

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
sumber
Itu fitur hebat! Apakah Anda berencana untuk menambahkan dukungan untuk kolom karakter? Maka bisa digunakan di sini .
ismirsehregal
1
@ismirsehregal ya, Anda dapat melacak fitur ini di sini 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

Hanya untuk referensi, lebih lambat dibandingkan dengan gdata atau data.matrix, tetapi hanya menggunakan paket data.table dan dapat menangani entri non numerik.

Andreas Rhode
sumber
5
Anda mungkin bisa menghindari ifelsedan memperbarui dengan referensi dengan melakukan DT[, names(DT) := lapply(.SD, function(x) {x[is.na(x)] <- "0" ; x})]. Dan saya ragu itu akan lebih lambat dari jawaban yang Anda sebutkan.
David Arenburg
11

Berikut adalah solusi yang digunakan NAToUnknowndalam gdatapaket. Saya telah menggunakan solusi Andrie untuk membuat tabel data yang sangat besar dan juga memasukkan perbandingan waktu dengan solusi 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
sumber
+1 Temuan bagus. Menarik - ini pertama kalinya saya melihat timing dengan userwaktu yang sama tetapi perbedaan elapsedwaktu yang sangat besar .
Andrie
@ Andrie Saya mencoba menggunakan rbenchmarkto benchmark solusi menggunakan lebih banyak replikasi, tetapi keluar dari kesalahan memori mungkin karena ukuran bingkai data. jika Anda dapat menjalankan benchmarkkedua solusi ini dengan beberapa replikasi, hasil itu akan menarik karena saya tidak begitu yakin mengapa saya mendapatkan speedup 3x
Ramnath
@Ramnath Untuk memperbaikinya, timing dalam jawaban ini untuk ncol=5saya pikir (harus lebih lama) karena bug di create_dt.
Matt Dowle
5

Demi kelengkapan, cara lain untuk mengganti NAS dengan 0 adalah menggunakan

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

Untuk membandingkan hasil dan waktu saya telah memasukkan semua pendekatan yang disebutkan sejauh ini.

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

Jadi pendekatan baru sedikit lebih lambat daripada f_dowle3tetapi lebih cepat dari semua pendekatan lainnya. Tapi jujur ​​saja, ini bertentangan dengan Intuisi saya tentang data. Tabel Sintaks dan saya tidak tahu mengapa ini bekerja. Adakah yang bisa menerangi saya?

bratwoorst711
sumber
1
Ya saya memeriksanya, ini sebabnya saya menyertakan identik berpasangan.
bratwoorst711
1
Inilah alasan mengapa ini bukan cara yang idiomatis - stackoverflow.com/a/20545629
Naumz
4

Pemahaman saya adalah bahwa rahasia untuk operasi cepat di R adalah memanfaatkan vektor (atau array, yang merupakan vektor di bawah tenda).

Dalam solusi ini saya menggunakan data.matrixyang merupakan arraytetapi berperilaku sedikit seperti data.frame. Karena array, Anda dapat menggunakan substitusi vektor yang sangat sederhana untuk mengganti NAs:

Fungsi pembantu kecil untuk menghapus NAs. Esensinya adalah satu baris kode. Saya hanya melakukan ini untuk mengukur waktu eksekusi.

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

Fungsi pembantu kecil untuk membuat data.tableukuran tertentu.

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))
}

Demonstrasi pada sampel kecil:

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
sumber
Itu contoh dataset yang sangat bagus. Saya akan mencoba dan meningkatkan remove_na. Waktu 21,57 itu termasuk create_dt(termasuk runifdan sample) bersama dengan remove_na. Apakah ada kesempatan untuk mengedit 2 kali?
Matt Dowle
Apakah ada bug kecil di dalamnya create_dt? Tampaknya selalu membuat data 5 kolom. Tabel terlepas dari ncolditeruskan.
Matt Dowle
@ MatthewDowle Terlihat dengan baik. Kesalahan dihapus (dan juga timing)
Andrie
Konversi ke matriks hanya akan berfungsi dengan baik jika semua kolom bertipe sama.
skan
2

Untuk menggeneralisasi ke banyak kolom, Anda dapat menggunakan pendekatan ini (menggunakan data sampel sebelumnya tetapi menambahkan kolom):

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))]

Tidak menguji kecepatannya

arono686
sumber
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
> 
Hai
sumber
3
Dan bagaimana Anda menggeneralisasikan ini ke lebih dari satu kolom?
David Arenburg
@ Davidviden hanya menulis untuk loop. Ini harus menjadi jawaban yang diterima: itu adalah yang paling sederhana!
Baibo
1

Menggunakan fifelsefungsi dari data.tableversi terbaru 1.12.6, bahkan 10 kali lebih cepat daripada NAToUnknowndalam gdatapaket:

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
sumber
Bisakah Anda menambahkan beberapa perbandingan waktu untuk jawaban ini? Saya pikir f_dowle3masih akan lebih cepat: stackoverflow.com/a/7249454/345660
Zach