Cách áp dụng cùng một hàm cho mọi cột được chỉ định trong data.table


85

Tôi có một data.table mà tôi muốn thực hiện thao tác tương tự trên các cột nhất định. Tên của các cột này được đặt trong một vector ký tự. Trong ví dụ cụ thể này, tôi muốn nhân tất cả các cột này với -1.

Một số dữ liệu đồ chơi và một vectơ chỉ định các cột có liên quan:

library(data.table)
dt <- data.table(a = 1:3, b = 1:3, d = 1:3)
cols <- c("a", "b")

Ngay bây giờ tôi đang làm theo cách này, lặp qua vector ký tự:

for (col in 1:length(cols)) {
   dt[ , eval(parse(text = paste0(cols[col], ":=-1*", cols[col])))]
}

Có cách nào để làm điều này trực tiếp mà không cần vòng lặp for không?

Câu trả lời:


150

Điều này dường như hoạt động:

dt[ , (cols) := lapply(.SD, "*", -1), .SDcols = cols]

Kết quả là

    a  b d
1: -1 -1 1
2: -2 -2 2
3: -3 -3 3

Có một số thủ thuật ở đây:

  • Bởi vì có dấu ngoặc đơn (cols) :=, kết quả được gán cho các cột được chỉ định trong cols, thay vì cho một số biến mới có tên "cols".
  • .SDcolsnói với cuộc gọi rằng chúng tôi chỉ xem xét các cột đó và cho phép chúng tôi sử dụng .SD, tập Shợp các Data được liên kết với các cột đó.
  • lapply(.SD, ...)hoạt động trên .SD, là danh sách các cột (như tất cả data.frame và data.tables). lapplytrả về một danh sách, vì vậy cuối cùng jtrông như thế nào cols := list(...).

CHỈNH SỬA : Đây là một cách khác có thể nhanh hơn, như @Arun đã đề cập:

for (j in cols) set(dt, j = j, value = -dt[[j]])

21
một cách khác là sử dụng setvới a for-loop. Tôi nghi ngờ nó sẽ nhanh hơn.
Arun

3
@Arun Tôi đã chỉnh sửa. Đó có phải ý của bạn? Tôi chưa sử dụng settrước đây.
Frank

8
+1 Câu trả lời tuyệt vời. Có, tôi cũng thích một forvòng lặp setcho những trường hợp như thế này.
Matt Dowle,

2
Có, sử dụng set()có vẻ nhanh hơn, nhanh hơn ~ 4 lần đối với tập dữ liệu của tôi! Kinh ngạc.
Konstantinos

2
Cảm ơn, @JamesHirschorn. Tôi không chắc, nhưng tôi nghi ngờ có nhiều chi phí để đặt con cột theo cách đó hơn là sử dụng .SD, dù sao cũng là thành ngữ tiêu chuẩn, xuất hiện trong github.com/Rdatatable/data.table/wiki/Getting-started Một phần lý do của thành ngữ, tôi nghĩ, là để tránh gõ tên bảng hai lần.
Frank

20

Tôi muốn thêm một câu trả lời, khi bạn cũng muốn thay đổi tên của các cột. Điều này khá hữu ích nếu bạn muốn tính toán logarit của nhiều cột, điều này thường xảy ra trong công việc thực nghiệm.

cols <- c("a", "b")
out_cols = paste("log", cols, sep = ".")
dt[, c(out_cols) := lapply(.SD, function(x){log(x = x, base = exp(1))}), .SDcols = cols]

1
Có cách nào để thay đổi tên dựa trên quy tắc không? Ví dụ: trong dplyr, bạn có thể thực hiện iris%>% mutate_at (vars (so khớp ("Sepal")), list (times_two = ~. * 2)) và nó sẽ thêm "_times_two" vào tên mới.
kennyB

1
Tôi không nghĩ điều đó có thể xảy ra, nhưng không thực sự chắc chắn về nó.
hannes101

điều này sẽ thêm các cột có tên của out_cols, trong khi vẫn giữ nguyên colsvị trí. Vì vậy, bạn cần phải loại bỏ những thứ đó một cách rõ ràng 1) chỉ yêu cầu log.a và log.b: chain a [,.(outcols)]đến cuối và lưu trữ lại dtqua <-. 2) loại bỏ các cột cũ với một chuỗi [,c(cols):=NULL]. Một giải pháp không xâu chuỗi 3) được dt[,c(cols):=...]tiếp theosetnames(dt, cols, newcols)
MPAG

@mpag, vâng, điều đó đúng, nhưng đối với trường hợp sử dụng của tôi là nghiên cứu thực nghiệm, hầu hết thời gian, tôi cần cả hai chuỗi trong tập dữ liệu.
hannes101

11

CẬP NHẬT: Sau đây là một cách đơn giản để làm điều đó mà không cần vòng lặp for

dt[,(cols):= - dt[,..cols]]

Đó là một cách gọn gàng để dễ đọc mã. Nhưng đối với hiệu suất, nó vẫn đứng sau giải pháp của Frank theo kết quả microbenchmark dưới đây

mbm = microbenchmark(
  base = for (col in 1:length(cols)) {
    dt[ , eval(parse(text = paste0(cols[col], ":=-1*", cols[col])))]
  },
  franks_solution1 = dt[ , (cols) := lapply(.SD, "*", -1), .SDcols = cols],
  franks_solution2 =  for (j in cols) set(dt, j = j, value = -dt[[j]]),
  hannes_solution = dt[, c(out_cols) := lapply(.SD, function(x){log(x = x, base = exp(1))}), .SDcols = cols],
  orhans_solution = for (j in cols) dt[,(j):= -1 * dt[,  ..j]],
  orhans_solution2 = dt[,(cols):= - dt[,..cols]],
  times=1000
)
mbm

Unit: microseconds
expr                  min        lq      mean    median       uq       max neval
base_solution    3874.048 4184.4070 5205.8782 4452.5090 5127.586 69641.789  1000  
franks_solution1  313.846  349.1285  448.4770  379.8970  447.384  5654.149  1000    
franks_solution2 1500.306 1667.6910 2041.6134 1774.3580 1961.229  9723.070  1000    
hannes_solution   326.154  405.5385  561.8263  495.1795  576.000 12432.400  1000
orhans_solution  3747.690 4008.8175 5029.8333 4299.4840 4933.739 35025.202  1000  
orhans_solution2  752.000  831.5900 1061.6974  897.6405 1026.872  9913.018  1000

như thể hiện trong biểu đồ dưới đây

performance_comparison_chart

Câu trả lời trước của tôi: Câu trả lời sau cũng hoạt động

for (j in cols)
  dt[,(j):= -1 * dt[,  ..j]]

Điều này về cơ bản giống như câu trả lời của Frank từ một năm rưỡi trước.
Dean MacGregor

1
Cảm ơn, câu trả lời của Frank đã được sử dụng thiết lập. Khi tôi làm việc với nhân data.table lớn với hàng triệu hàng, tôi thấy: = nhanh hơn so với nhà điều hành chức năng
Orhan Celik

2
Lý do tôi thêm câu trả lời cho một câu hỏi cũ như sau: Tôi cũng gặp vấn đề tương tự, tôi tình cờ tìm thấy bài đăng này với tìm kiếm của google. Sau đó, tôi đã tìm ra giải pháp cho vấn đề của mình và tôi thấy nó cũng áp dụng ở đây. Trên thực tế, đề xuất của tôi sử dụng một chức năng mới của data.table có sẵn trong các phiên bản mới của thư viện, không tồn tại vào thời điểm câu hỏi. Tôi nghĩ rằng đó là một ý tưởng tốt để chia sẻ, nghĩ rằng những người khác có vấn đề tương tự sẽ kết thúc ở đây với tìm kiếm của google.
Orhan Celik

1
Bạn có đang đo điểm chuẩn với dtbao gồm 3 hàng không?
Uwe

3
Câu trả lời của Hannes đang thực hiện một phép tính khác và vì vậy không nên so sánh với những câu trả lời khác, phải không?
Frank

2

Không có giải pháp nào ở trên dường như hoạt động với tính toán theo nhóm. Sau đây là điều tốt nhất tôi có:

for(col in cols)
{
    DT[, (col) := scale(.SD[[col]], center = TRUE, scale = TRUE), g]
}

1

Để thêm ví dụ để tạo cột mới dựa trên vectơ chuỗi các cột. Dựa trên câu trả lời của Jfly:

dt <- data.table(a = rnorm(1:100), b = rnorm(1:100), c = rnorm(1:100), g = c(rep(1:10, 10)))

col0 <- c("a", "b", "c")
col1 <- paste0("max.", col0)  

for(i in seq_along(col0)) {
  dt[, (col1[i]) := max(get(col0[i])), g]
}

dt[,.N, c("g", col1)]

0
library(data.table)
(dt <- data.table(a = 1:3, b = 1:3, d = 1:3))

Hence:

   a b d
1: 1 1 1
2: 2 2 2
3: 3 3 3

Whereas (dt*(-1)) yields:

    a  b  d
1: -1 -1 -1
2: -2 -2 -2
3: -3 -3 -3

1
Fyi, "mọi cột được chỉ định" trong tiêu đề có nghĩa là người hỏi quan tâm đến việc áp dụng nó cho một tập hợp con của các cột (có thể không phải tất cả chúng).
Frank

1
@Frank chắc chắn! Trong trường hợp đó, OP có thể thực hiện dt [, c ("a", "b")] * (- 1).
amonk

1
Vâng, chúng ta hãy được hoàn thành và nóidt[, cols] <- dt[, cols] * (-1)
Gregor Thomas

có vẻ như cú pháp mới được yêu cầu là dt [, cols] <- dt [, ..cols] * (-1)
Arthur Yip

0

dplyrcác hàm hoạt động trên data.tables, vì vậy đây là một dplyrgiải pháp cũng "tránh vòng lặp for" :)

dt %>% mutate(across(all_of(cols), ~ -1 * .))

Tôi benchmarked nó sử dụng mã Orhan của (thêm hàng và cột) và bạn sẽ thấy dplyr::mutatevới acrosschủ yếu là thực hiện nhanh hơn so với hầu hết các giải pháp khác và chậm hơn so với các giải pháp sử dụng data.table lapply.

library(data.table); library(dplyr)
dt <- data.table(a = 1:100000, b = 1:100000, d = 1:100000) %>% 
  mutate(a2 = a, a3 = a, a4 = a, a5 = a, a6 = a)
cols <- c("a", "b", "a2", "a3", "a4", "a5", "a6")

dt %>% mutate(across(all_of(cols), ~ -1 * .))
#>               a       b      d      a2      a3      a4      a5      a6
#>      1:      -1      -1      1      -1      -1      -1      -1      -1
#>      2:      -2      -2      2      -2      -2      -2      -2      -2
#>      3:      -3      -3      3      -3      -3      -3      -3      -3
#>      4:      -4      -4      4      -4      -4      -4      -4      -4
#>      5:      -5      -5      5      -5      -5      -5      -5      -5
#>     ---                                                               
#>  99996:  -99996  -99996  99996  -99996  -99996  -99996  -99996  -99996
#>  99997:  -99997  -99997  99997  -99997  -99997  -99997  -99997  -99997
#>  99998:  -99998  -99998  99998  -99998  -99998  -99998  -99998  -99998
#>  99999:  -99999  -99999  99999  -99999  -99999  -99999  -99999  -99999
#> 100000: -100000 -100000 100000 -100000 -100000 -100000 -100000 -100000

library(microbenchmark)
mbm = microbenchmark(
  base_with_forloop = for (col in 1:length(cols)) {
    dt[ , eval(parse(text = paste0(cols[col], ":=-1*", cols[col])))]
  },
  franks_soln1_w_lapply = dt[ , (cols) := lapply(.SD, "*", -1), .SDcols = cols],
  franks_soln2_w_forloop =  for (j in cols) set(dt, j = j, value = -dt[[j]]),
  orhans_soln_w_forloop = for (j in cols) dt[,(j):= -1 * dt[,  ..j]],
  orhans_soln2 = dt[,(cols):= - dt[,..cols]],
  dplyr_soln = (dt %>% mutate(across(all_of(cols), ~ -1 * .))),
  times=1000
)

library(ggplot2)
ggplot(mbm) +
  geom_violin(aes(x = expr, y = time)) +
  coord_flip()

Được tạo vào 2020-10-16 bởi gói reprex (v0.3.0)

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.