2017-10-24 11 views
3

요인 및 숫자 열이있는 큰 (많은 GB) 테이블에서 "가장 가까운"행을 반복적으로 조회해야합니다. 여기큰 데이터 세트의 빠른 하위 집합/조회/필터

df <- data.frame(factorA = rep(letters[1:3], 100000), 
      factorB = sample(rep(letters[1:3], 100000), 
           3*100000, replace = FALSE), 
      numC = round(rnorm(3*100000), 2), 
      numD = round(rnorm(3*100000), 2)) 

closest <- function(ValueA, ValueB, ValueC, ValueD) { 
    df_sub <- df %>% 
    filter(factorA == ValueA, 
      factorB == ValueB, 
      numC >= 0.9 * ValueC, 
      numC <= 1.1 * ValueC, 
      numD >= 0.9 * ValueD, 
      numD <= 1.1 * ValueD) 

    if (nrow(df_sub) == 0) stop("Oh-oh, no candidates.") 

    minC <- df_sub[which.min(abs(df_sub$numC - ValueC)), "numC"] 

    df_sub %>% 
    filter(numC == minC) %>% 
    slice(which.min(abs(numD - ValueD))) %>% 
    as.list() %>% 
    return() 
} 

을 위의 벤치 마크 : dplyr를 사용하면, 다음과 같다

> microbenchmark(closest("a", "b", 0.5, 0.6)) 
Unit: milliseconds 
         expr  min  lq  mean median  uq  max neval 
closest("a", "b", 0.5, 0.6) 25.20927 28.90623 35.16863 34.59485 35.25468 108.3489 100 

속도에 대한이 기능을 최적화 할 수있는 가장 좋은 방법은 무엇입니까? 메모리가 큰 df인데도 불구하고 여분의 RAM이 있지만이 함수에 대한 호출이 많으면 가능한 빨리 작성하려고합니다.

dplyr 대신 data.table을 사용 하시겠습니까?


여기에 내가 지금까지 노력이 최적화는 다음과 같습니다

dt <- as.data.table(df) 

closest2 <- function(ValueA, ValueB, ValueC, ValueD) { 
    df_sub <- df %>% 
    filter(factorA == ValueA, 
      factorB == ValueB, 
      dplyr::between(numC, 0.9 * ValueC, 1.1 * ValueC), 
      dplyr::between(numD, 0.9 * ValueD, 1.1 * ValueD)) 

    if (nrow(df_sub) == 0) stop("Oh-oh, no candidates.") 

    minC <- df_sub[which.min(abs(df_sub$numC - ValueC)), "numC"] 

    df_sub %>% 
    filter(numC == minC) %>% 
    slice(which.min(abs(numD - ValueD))) %>% 
    as.list() %>% 
    return() 
} 

closest3 <- function(ValueA, ValueB, ValueC, ValueD) { 

    dt_sub <- dt[factorA == ValueA & 
       factorB == ValueB & 
       numC %between% c(0.9 * ValueC, 1.1 * ValueC) & 
       numD %between% c(0.9 * ValueD, 1.1 * ValueD)] 

    if (nrow(dt_sub) == 0) stop("Oh-oh, no candidates.") 

    dt_sub[abs(numC - ValueC) == min(abs(numC - ValueC))][which.min(abs(numD - ValueD))] %>% 
    as.list() %>% 
    return() 
} 

벤치 마크 :

> microbenchmark(closest("a", "b", 0.5, 0.6), closest2("a", "b", 0.5, 0.6), closest3("a", "b", 0.5, 0.6)) 
Unit: milliseconds 
         expr  min  lq  mean median  uq  max neval cld 
    closest("a", "b", 0.5, 0.6) 25.15780 25.62904 36.52022 34.68219 35.27116 155.31924 100 c 
closest2("a", "b", 0.5, 0.6) 22.14465 22.46490 27.81361 31.40918 32.04427 35.79021 100 b 
closest3("a", "b", 0.5, 0.6) 13.52094 13.77555 20.04284 22.70408 23.41452 142.73626 100 a 

이 더 최적화 할 수 있습니까? rowwise를 실행해야합니다 다른 솔루션과 비교

+0

C 및 D의 주문 색인을 받고 이진 검색을 사용하면 어떨까요? –

+0

어떻게? 나는'setkey (dt, numC, numD)'를 시도했으나 차이를 만들지는 않았다. –

+0

이들을 병렬로 (단일 값 대신 ValueA, ValueB, ValueC, ValueD로) 볼 수 있다면 순차적으로 훨씬 빠르게 수행 할 수있을 것입니다 (이는 분명히 어떻게 계획 할 것인가입니다 당신이 그런 식으로 벤치마킹을하고 있기 때문에 이것을 "반복해서"하는 것). – Frank

답변

3

대신 순차적으로 병렬 값의 많은 튜플을 호출 할 수 있다면 ...

set.seed(1) 
DF <- data.frame(factorA = rep(letters[1:3], 100000), 
      factorB = sample(rep(letters[1:3], 100000), 
           3*100000, replace = FALSE), 
      numC = round(rnorm(3*100000), 2), 
      numD = round(rnorm(3*100000), 2)) 

library(data.table) 
DT = data.table(DF) 

f = function(vA, vB, nC, nD, dat = DT){ 

    rs <- dat[.(vA, vB, nC), on=.(factorA, factorB, numC), roll="nearest", 
    .(g = .GRP, r = .I, numD), by=.EACHI][.(seq_along(vA), nD), on=.(g, numD), roll="nearest", mult="first", 
    r] 

    df[rs] 
} 

# example usage 
mDT = data.table(vA = c("a", "b"), vB = c("c", "c"), nC = c(.3, .5), nD = c(.6, .8)) 

mDT[, do.call(f, .SD)] 

# factorA factorB numC numD 
# 1:  a  c 0.3 0.60 
# 2:  b  c 0.5 0.76 

...

# check the results match 
library(magrittr) 
dt = copy(DT) 
mDT[, closest3(vA, vB, nC, nD), by=.(mr = seq_len(nrow(mDT)))] 

# mr factorA factorB numC numD 
# 1: 1  a  c 0.3 0.60 
# 2: 2  b  c 0.5 0.76 

# check speed for a larger number of comparisons 

nr = 100 
system.time(mDT[rep(1:2, each=nr), do.call(f, .SD)]) 
# user system elapsed 
# 0.07 0.00 0.06 

system.time(mDT[rep(1:2, each=nr), closest3(vA, vB, nC, nD), by=.(mr = seq_len(nr*nrow(mDT)))]) 
# user system elapsed 
# 10.65 2.30 12.60 

방법을 작품

의 각 튜플에 대해 vA 및와 일치하는 행을 찾습니다을 정확하게 누른 다음 nC의 가장 가까운 값으로 "굴립니다"- OP의 규칙 (nC * [0.9, 1.1] 범위 내에서보기)과 정확히 일치하지 않지만 해당 규칙은 이후에 쉽게 적용될 수 있습니다 -것. 매치마다 터플의 "그룹 번호"인 .GRP, 일치 된 행 번호 및 해당 행의 값이 numD 인 것을 사용합니다.

우리는 그룹 번호와 nD에 가입합니다. 정확히 일치하는 그룹 번호와 가장 가까운 그룹으로 일치합니다. 가장 가까운 일치 항목이 여러 개인 경우 mult="first"으로 첫 번째 항목을 가져옵니다.

그러면 각 튜플의 일치 행 번호를 가져 와서 원래 테이블에서 찾아 볼 수 있습니다.

성능

그래서 벡터화 솔루션은 전용 (영업 이익에 관해서는) 한 번에 ~ 5 개 튜플을 통과 할 수있는 경우 대신 R.

으로 평소와 같이 큰 성능 이점을 갖고있는 것 같아요 200의 경우,이 접근법에 대한 이점은 여전히 ​​which.min과 유사합니다. @ F.Privé가 제안한대로 이진 검색 덕분입니다.

@ HarlanNelson의 답변에서 언급했듯이 테이블에 인덱스를 추가하면 성능이 향상 될 수 있습니다. 그의 대답과 ?setindex을보십시오.여기

DT2 = data.table(id = "A", numC = rep(c(1.01,1.02), each=5), numD = seq(.01,.1,.01)) 
DT2[.("A", 1.011), on=.(id, numC), roll="nearest"] 
# id numC numD 
# 1: A 1.011 0.05 

, 우리는 하나 개의 행을 볼 수 있지만, 우리는 다섯을보고해야합니다 numC에 대한

수정이 문제를 식별하기위한 하나 개의 값으로 영업 이익

감사를 압연. 한 수정 정수로 변환한다 (나는 이유를 모르겠어요하지만) : 이것은 부정 행위

DT3 = copy(DT2) 
DT3[, numC := as.integer(numC*100)] 
DT3[, numD := as.integer(numD*100)] 
DT3[.("A", 101.1), on=.(id, numC), roll="nearest"] 
# id numC numD 
# 1: A 101 1 
# 2: A 101 2 
# 3: A 101 3 
# 4: A 101 4 
# 5: A 101 5 
+0

매우 좋습니다! 당신은'c (. (g = .GRP, r = .I), .SD), by = .EACHI]'를 설명 할 수 있습니까? 내 'data.table'지식은 (tidyverse와 비교하여) 상당히 제한적입니다. –

+1

@ 빅터 고마워, 이제 그 부분을 단순화하고 설명을 추가했습니다. 나는 내가 필요 없다는 것을 깨닫지 못했습니다. 거기 앉아서 .GRP와 내가 무엇인지 설명했습니다. 다른 것이 명확하지 않은 경우 알려주십시오. – Frank

+0

나는 귀하의 제안을 이행했으며 실제로 그렇게 빠릅니다. 그러나 그것은 바람직하지 않은 행동을합니다. 마지막 값 (이 경우 numC)이 롤인되면 "rolled-to"값이 numC 인 행이 여러 개 있어도 하나의 일치 항목 만 반환됩니다. 이것은'numD'에 대한 두 번째 롤링 조인을 중단시킵니다. 이전에 선택된 단일 값은'numD'를'nD'에서 매우 멀리 가질 수 있습니다. 어떤 아이디어? –

2

때문에 벤치 마크 전에 I 지수,하지만 난 당신이 같은 data.table에 여러 번 쿼리를 실행합니다 가정합니다.

library(data.table) 
dt<-as.data.table(df) 
setkey(dt,factorA,factorB) 

closest2 <- function(ValueA, ValueB, ValueC, ValueD) { 

    dt<-dt[.(ValueA,ValueB), on = c('factorA','factorB')] 
    df_sub <- dt %>% 
    filter(numC >= 0.9 * ValueC, 
      numC <= 1.1 * ValueC, 
      numD >= 0.9 * ValueD, 
      numD <= 1.1 * ValueD) 

    if (nrow(df_sub) == 0) stop("Oh-oh, no candidates.") 

    minC <- df_sub[which.min(abs(df_sub$numC - ValueC)), "numC"] 

    df_sub %>% 
    filter(numC == minC) %>% 
    slice(which.min(abs(numD - ValueD))) %>% 
    as.list() %>% 
    return() 
} 

library(microbenchmark) 
microbenchmark(closest("a", "b", 0.5, 0.6)) 
microbenchmark(closest2("a", "b", 0.5, 0.6)) 


Unit: milliseconds 
         expr  min  lq  mean median  uq  max neval 
closest("a", "b", 0.5, 0.6) 20.29775 22.55372 28.08176 23.20033 25.42154 127.7781 100 
Unit: milliseconds 
         expr  min  lq  mean median  uq  max neval 
closest2("a", "b", 0.5, 0.6) 8.595854 9.063261 9.929237 9.396594 10.0247 16.92655 100 
+0

어쩌면 가장 가까운 2b 또는 OP의 nearest2라는 함수를 구별 할 수 있을까요? Btw, 내 의견으로는 우리가 테이블 키가 함수의 사용 사이에 깨지지 않는다고 가정한다면 속임수가 아니다. – Frank

+0

아, 색인을 생성하는 방법입니다. Harlan에게 감사드립니다. 'numC'와'numD'를 인덱싱하면 더 나은 결과를 얻을 수 있습니까? –