Cách nhanh nhất để thay thế NA trong một dữ liệu lớn.


150

Tôi có một data.table lớn , với nhiều giá trị bị thiếu nằm rải rác trong các hàng ~ 200k và 200 cột. Tôi muốn mã lại các giá trị NA thành số không hiệu quả nhất có thể.

Tôi thấy hai tùy chọn:
1: Chuyển đổi sang data.frame và sử dụng một cái gì đó như thế này
2: Một số loại lệnh thiết lập phụ data.table thú vị

Tôi sẽ hài lòng với một giải pháp khá hiệu quả của loại 1. Chuyển đổi sang data.frame và sau đó quay lại dữ liệu. Không thể mất quá nhiều thời gian.


5
Tại sao bạn muốn chuyển đổi data.tablethành a data.frame? A data.table một data.frame. Bất kỳ hoạt động data.frame sẽ chỉ hoạt động.
Andrie

5
@Andrie. một sự khác biệt chính là bạn không thể truy cập một cột trong một data.tablebằng cách chỉ định số cột. vì vậy DT[,3]sẽ không cung cấp cho cột thứ ba. tôi nghĩ rằng điều này làm cho giải pháp được đề xuất trong liên kết không khả thi ở đây. tôi chắc chắn có một cách tiếp cận tao nhã bằng cách sử dụng một số data.tablephép thuật!
Ramnath

6
@Ramnath, AFAIK, DT[, 3, with=FALSE]trả về cột thứ ba.
Andrie

2
@Andrie. nhưng vẫn còn một vấn đề mydf[is.na(mydf) == TRUE]trong công việc trên các khung dữ liệu, trong khi mydt[is.na(mydt) == TRUE]mang lại cho tôi thứ gì đó kỳ lạ ngay cả khi tôi sử dụngwith=FALSE
Ramnath

2
@Ramnath, điểm lấy. Tuyên bố trước đây của tôi quá rộng, tức là tôi đã sai. Lấy làm tiếc. Data.tables chỉ hoạt động như data.frames khi không có phương thức data.table.
Andrie

Câu trả lời:


184

Đây là một giải pháp sử dụng toán tử của data.table:= , dựa trên câu trả lời của Andrie và 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

Lưu ý rằng f_dowle đã cập nhật dt1 theo tham chiếu. Nếu cần một bản sao cục bộ thì cần có một cuộc gọi rõ ràng đến copyhàm để tạo một bản sao cục bộ của toàn bộ tập dữ liệu. của data.table setkey, key<-:=không sao chép-on-write.

Tiếp theo, hãy xem f_dowle đang dành thời gian ở đâu.

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

Ở đó, tôi sẽ tập trung vào na.replaceis.na, nơi có một vài bản sao vectơ và quét vectơ. Chúng có thể dễ dàng được loại bỏ bằng cách viết một hàm C nhỏ na.replace C cập nhật NAbằng cách tham chiếu trong vectơ. Điều đó ít nhất sẽ giảm một nửa trong 20 giây tôi nghĩ. Có một chức năng như vậy tồn tại trong bất kỳ gói R?

Lý do f_andriethất bại có thể là vì nó sao chép toàn bộ dt1hoặc tạo ra một ma trận logic lớn như toàn bộ dt1, một vài lần. Hai phương thức khác hoạt động trên một cột tại một thời điểm (mặc dù tôi chỉ xem xét ngắn gọn NAToUnknown).

EDIT (giải pháp thanh lịch hơn theo yêu cầu của Ramnath trong các bình luận):

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

Tôi ước tôi đã làm theo cách đó để bắt đầu!

EDIT2 (hơn 1 năm sau, bây giờ)

Cũng có set(). Điều này có thể nhanh hơn nếu có rất nhiều cột được lặp qua, vì nó tránh được chi phí (nhỏ) của cuộc gọi [,:=,]trong một vòng lặp. setlà một vòng lặp :=. Xem ?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)
}

5
+! câu trả lời chính xác! là nó có thể có một tương đương trực quan hơn của các eval(parse)...công cụ. trên một lưu ý rộng hơn, tôi nghĩ sẽ hữu ích khi có các hoạt động hoạt động trên tất cả các yếu tố của data.table.
Ramnath

1
Khối mã thứ 2 của bạn dường như là data.tablecách thích hợp nhất để làm điều này. Cảm ơn!
Zach

3
@Statwonk Tôi đoán bạn DTcó các cột loại logical, không giống như create_dt()ví dụ cho bài kiểm tra này. Thay đổi đối số thứ 4 của set()cuộc gọi ( 0trong ví dụ của bạn và nhập double trong R) FALSEvà nó sẽ hoạt động mà không có cảnh báo.
Matt Dowle

2
@Statwonk Và tôi đã gửi một yêu cầu tính năng để thư giãn trường hợp này và bỏ cảnh báo đó khi ép các vectơ độ dài 1 và 1 thành logic: # 996 . Có thể không làm điều đó vì, vì tốc độ, bạn không muốn được cảnh báo về sự ép buộc lặp đi lặp lại không cần thiết.
Matt Dowle

1
@StefanF Đúng và tôi cũng thích seq_along(DT). Nhưng sau đó người đọc phải biết rằng seq_alongsẽ dọc theo các cột và không xuống các hàng. seq_len(col(DT))một chút rõ ràng hơn cho lý do đó.
Matt Dowle

28

Đây là cách đơn giản nhất tôi có thể nghĩ ra:

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

Đó là hiệu quả và không cần phải viết chức năng và mã keo khác.


không hoạt động trên các bộ dữ liệu lớn và máy tính máy trạm bình thường (lỗi cấp phát bộ nhớ)
Jake

3
@Jake trên một máy có 16GB RAM Tôi đã có thể chạy nó trên 31M hàng, ~ 20 cột. YMMV tất nhiên.
Bar

Tôi trì hoãn bằng chứng thực nghiệm của bạn. Cảm ơn.
Jake

10
Không may trong các phiên bản mới nhất của data.table nó không hoạt động. Nó báo Lỗi trong [.data.table(dt, is.na (dt)): i là loại (ma trận) không hợp lệ. Có lẽ trong tương lai, ma trận 2 cột có thể trả về danh sách các phần tử của DT (theo tinh thần của A [B] trong Câu hỏi thường gặp 2.14). Vui lòng cho phép dữ liệu - giúp biết nếu bạn thích điều này hoặc thêm nhận xét của bạn vào FR # 657. >
skan

hay đấy! Tôi luôn luôn sử dụngset
marbel

14

Các chức năng chuyên dụng ( nafillsetnafill) cho mục đích đó có sẵn trong data.tablegói (phiên bản> = 1.12.4):

Nó xử lý các cột song song để giải quyết các điểm chuẩn được đăng trước đó, dưới thời gian của nó so với cách tiếp cận nhanh nhất cho đến bây giờ, và cũng được nhân rộng, sử dụng máy 40 lõi.

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

Đó là một tính năng tuyệt vời! Bạn có kế hoạch để thêm hỗ trợ cho các cột nhân vật? Sau đó, nó có thể được sử dụng ở đây .
ismirsehregal

1
@ismirsehregal có, bạn có thể theo dõi tính năng này tại đây github.com/Rdatitable/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

Chỉ để tham khảo, chậm hơn so với gdata hoặc data.matrix, nhưng chỉ sử dụng gói data.table và có thể xử lý các mục không phải là số.


5
Bạn có thể có thể tránh ifelsevà cập nhật bằng cách tham khảo bằng cách làm DT[, names(DT) := lapply(.SD, function(x) {x[is.na(x)] <- "0" ; x})]. Và tôi nghi ngờ nó sẽ chậm hơn những câu trả lời bạn đã đề cập.
David Arenburg

11

Đây là một giải pháp sử dụng NAToUnknowntrong gdatagói. Tôi đã sử dụng giải pháp của Andrie để tạo một bảng dữ liệu khổng lồ và cũng bao gồm các so sánh thời gian với giải pháp của 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

+1 Tìm tốt. Thú vị - đó là lần đầu tiên tôi thấy thời gian có userthời gian tương tự nhưng thực sự khác biệt lớn về elapsedthời gian.
Andrie

@Andrie Tôi đã thử sử dụng rbenchmarkcác giải pháp điểm chuẩn bằng cách sử dụng nhiều bản sao hơn, nhưng đã hết lỗi bộ nhớ có thể do kích thước của khung dữ liệu. nếu bạn có thể chạy benchmarktrên cả hai giải pháp này với nhiều lần lặp lại, những kết quả đó sẽ rất thú vị vì tôi không thực sự chắc chắn tại sao tôi lại tăng tốc
gấp

@Ramnath Để có được những điều chính xác, thời gian trong câu trả lời này là dành cho ncol=5tôi nghĩ (nên mất nhiều thời gian hơn) do lỗi trong create_dt.
Matt Dowle

5

Để hoàn thiện, một cách khác để thay thế NA bằng 0 là sử dụng

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

Để so sánh kết quả và thời gian tôi đã kết hợp tất cả các phương pháp được đề cập cho đến nay.

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

Vì vậy, cách tiếp cận mới hơi chậm hơn f_dowle3nhưng nhanh hơn tất cả các cách tiếp cận khác. Nhưng thành thật mà nói, điều này chống lại Trực giác của tôi về Cú pháp dữ liệu. Tôi không biết tại sao nó lại hoạt động. Có ai có thể khai sáng cho tôi không?


1
Có tôi đã kiểm tra chúng, đây là lý do tại sao tôi đã bao gồm các cặp giống hệt nhau.
bratwoorst711

1
Đây là một lý do tại sao nó không phải là cách thành ngữ - stackoverflow.com/a/20545629
Naumz

4

Tôi hiểu rằng bí mật cho các hoạt động nhanh trong R là sử dụng vectơ (hoặc mảng, là các vectơ dưới mui xe.)

Trong giải pháp này, tôi sử dụng một data.matrixcái arraynhưng hoạt động hơi giống a data.frame. Vì nó là một mảng, bạn có thể sử dụng thay thế vectơ rất đơn giản để thay thế NAs:

Một chức năng trợ giúp nhỏ để loại bỏ NAs. Bản chất là một dòng mã. Tôi chỉ làm điều này để đo thời gian thực hiện.

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

Một chức năng trợ giúp nhỏ để tạo ra một data.tablekích thước nhất định.

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

Trình diễn trên một mẫu nhỏ:

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

Đó là một tập dữ liệu mẫu rất hay. Tôi sẽ cố gắng và cải thiện remove_na. Thời điểm 21,57 đó bao gồm create_dt(bao gồm runifsample) cùng với remove_na. Bất kỳ cơ hội nào bạn có thể chỉnh sửa để chia ra 2 lần?
Matt Dowle

Có một lỗi nhỏ trong create_dt? Nó dường như luôn tạo ra một dữ liệu 5 cột. Bất kể ncolđược thông qua.
Matt Dowle

@MatthewDowle Phát hiện tốt. Đã xóa lỗi (cũng như thời gian)
Andrie

Chuyển đổi sang ma trận sẽ chỉ hoạt động đúng nếu tất cả các cột cùng loại.
skan

2

Để khái quát cho nhiều cột, bạn có thể sử dụng phương pháp này (sử dụng dữ liệu mẫu trước đó nhưng thêm một cột):

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

Không kiểm tra tốc độ mặc dù


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
> 

3
Và làm thế nào bạn có thể khái quát hóa điều này cho nhiều hơn một cột?
David Arenburg

@DavidArenburg chỉ cần viết một vòng lặp cho. Đây phải là câu trả lời được chấp nhận: nó đơn giản nhất!
baibo

1

Sử dụng fifelsechức năng từ các data.tablephiên bản mới nhất 1.12.6, nó thậm chí còn nhanh hơn 10 lần so với NAToUnknowntrong gdatagói:

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 

Bạn có thể thêm một số so sánh thời gian cho câu trả lời này? Tôi nghĩ f_dowle3vẫn sẽ nhanh hơn: stackoverflow.com/a/7249454/345660
Zach
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.