Ускорение интерполяции

Я провожу около 45 000 локальных линейных регрессий (по сути) около 1,2 миллиона наблюдений, поэтому я был бы признателен за помощь в попытке ускорить процесс, потому что я нетерпелив.

Я в основном пытаюсь составить контракты по заработной плате за год - функция заработной платы (опыт с учетом фирмы, год, должность)- для нескольких фирм.

Вот набор данных (базовая структура), с которым я работаю:

> wages
         firm year position exp salary
      1: 0007 1996        4   1  20029
      2: 0007 1996        4   1  23502
      3: 0007 1996        4   1  22105
      4: 0007 1996        4   2  23124
      5: 0007 1996        4   2  22700
     ---                              
1175141:  994 2012        5   2  47098
1175142:  994 2012        5   2  45488
1175143:  994 2012        5   2  47098
1175144:  994 2012        5   3  45488
1175145:  994 2012        5   3  47098

Я хочу построить функцию заработной платы для уровней опыта от 0 до 40 для всех фирм, а-ля:

> salary_scales
        firm year position exp   salary
     1: 0007 1996        4   0       NA
     2: 0007 1996        4   1 21878.67
     3: 0007 1996        4   2 23401.33
     4: 0007 1996        4   3 23705.00
     5: 0007 1996        4   4 24260.00
    ---                                
611019: 9911 2015        4  36       NA
611020: 9911 2015        4  37       NA
611021: 9911 2015        4  38       NA
611022: 9911 2015        4  39       NA
611023: 9911 2015        4  40       NA

С этой целью я работал (по предложению @BondedDust здесь) с пакетом COBS (COnstrained B-Spline), который позволяет мне встроить монотонность контракта о заработной плате.

Некоторые проблемы остаются; в частности, когда мне нужно экстраполировать (когда в данной фирме нет очень молодых или очень старых сотрудников), наблюдается тенденция к потере монотонности или падению ниже 0.

Чтобы обойти это, я использовал простую линейную экстраполяцию вне границ данных - расширить кривую подгонки за пределы min_exp а также max_exp так что он проходит через две самые низкие (или самые высокие) точки подгонки - не идеально, но, похоже, все идет хорошо.

Имея это в виду, вот как я делаю это до сих пор (имейте в виду, что я data.table фанатик):

#get the range of experience for each firm
wages[,min_exp:=min(exp),by=.(year,firm,position)]
wages[,max_exp:=max(exp),by=.(year,firm,position)]
#Can't interpolate if there are only 2 or 3 unique experience cells represented
wages[,node_count:=length(unique(exp)),by=.(year,firm,position)]
#Nor if there are too few teachers
wages[,ind_count:=.N,by=.(year,firm,position)]
#Also troublesome when there is little variation in salaries like so:
wages[,sal_scale_flag:=mean(abs(salary-mean(salary)))<50,by=.(year,firm,position)]
wages[,sal_count_flag:=length(unique(salary))<5,by=.(year,firm,position)]

cobs_extrap<-function(exp,salary,min_exp,max_exp,
                      constraint="increase",print.mesg=F,nknots=8,
                      keep.data=F,maxiter=150){
  #these are passed as vectors
  min_exp<-min_exp[1]
  max_exp<-min(max_exp[1],40)
  #get in-sample fit
  in_sample<-predict(cobs(x=exp,y=salary,
                          constraint=constraint,
                          print.mesg=print.mesg,nknots=nknots,
                          keep.data=keep.data,maxiter=maxiter),
                     z=min_exp:max_exp)[,"fit"]

  #append by linear extension below min_exp
  c(if (min_exp==1) NULL else in_sample[1]-
      (min_exp:1)*(in_sample[2]-in_sample[1]),in_sample,
    #append by linear extension above max_exp
    if (max_exp==40) NULL else in_sample[length(in_sample)]+(1:(40-max_exp))*
      (in_sample[length(in_sample)]-in_sample[length(in_sample)-1]))
}

salary_scales<-
  wages[node_count>=7&ind_count>=10
               &sal_scale_flag==0&sal_count_flag==0,
               .(exp=0:40,
                 salary=cobs_extrap(exp,salary,min_exp,max_exp)),
               by=.(year,firm,position)]

Заметили что-нибудь конкретное, что могло бы замедлить мой код? Или я вынужден быть терпеливым?

Чтобы поиграть, вот некоторые из небольших комбо-позиций:

    firm year position exp salary count
 1: 0063 2010        5   2  37433    10
 2: 0063 2010        5   2  38749    10
 3: 0063 2010        5   4  38749    10
 4: 0063 2010        5   8  42700    10
 5: 0063 2010        5  11  47967    10
 6: 0063 2010        5  15  50637    10
 7: 0063 2010        5  19  51529    10
 8: 0063 2010        5  23  50637    10
 9: 0063 2010        5  33  52426    10
10: 0063 2010        5  37  52426    10
11: 9908 2006        4   1  26750    10
12: 9908 2006        4   6  36043    10
13: 9908 2006        4   7  20513    10
14: 9908 2006        4   8  45023    10
15: 9908 2006        4  13  33588    10
16: 9908 2006        4  15  46011    10
17: 9908 2006        4  15  37179    10
18: 9908 2006        4  22  43704    10
19: 9908 2006        4  28  56078    10
20: 9908 2006        4  29  44866    10

1 ответ

Решение

В вашем коде есть много вещей, которые можно улучшить, но давайте сосредоточимся здесь на основном узком месте. Рассматриваемая проблема может рассматриваться как смущающая параллельная проблема. Это означает, что ваши данные могут быть разбиты на несколько более мелких частей, каждый из которых может быть вычислен в отдельных потоках по отдельности без каких-либо дополнительных затрат.

Чтобы увидеть возможности распараллеливания для текущей проблемы, вы должны сначала заметить, что вы выполняете точно такие же вычисления для каждой из отдельных фирм и / или лет в отдельности. Например, вы можете разделить вычисления на более мелкие подзадачи для каждого отдельного года, а затем распределить эти подзадачи по разным ядрам CPU/GPU. Таким образом, можно получить значительный выигрыш в производительности. Наконец, когда обработка подзадач завершена, единственное, что вам еще нужно сделать, это объединить результаты.

Однако R и все его внутренние библиотеки работают как единый поток. Вам придется явно разделить ваши данные и затем назначить подзадачи различным ядрам. Для достижения этого существует несколько пакетов R, которые поддерживают многопоточность. Мы будем использовать doparallel пакет в нашем примере здесь.

Вы не предоставили явный набор данных, достаточно большой для эффективного тестирования производительности, поэтому мы сначала создадим несколько случайных данных:

set.seed(42)
wages<-data.table(firm=substr(10001:10010,2,5)[sample(10,size=1e6,replace=T)],
                  year=round(unif(1e6,1996,2015)),
                  position=round(runif(1e6,4,5)),
                  exp=round(runif(1e6,1,40)),
                  salary=round(exp(rnorm(1e6,mean=10.682,sd=.286))))
> wages
         firm year position exp salary
      1: 0001 1996        4  14  66136
      2: 0001 1996        4   3  42123
      3: 0001 1996        4   9  46528
      4: 0001 1996        4  11  35195
      5: 0001 1996        4   2  43926
     ---                              
 999996: 0010 2015        5  11  43140
 999997: 0010 2015        5  23  64025
 999998: 0010 2015        5  31  35266
 999999: 0010 2015        5  11  36267
1000000: 0010 2015        5   7  44315

Теперь давайте запустим первую часть вашего кода:

#get the range of experience for each firm
wages[,min_exp:=min(exp),by=.(year,firm,position)]
wages[,max_exp:=max(exp),by=.(year,firm,position)]
#Can't interpolate if there are only 2 or 3 unique experience cells represented
wages[,node_count:=length(unique(exp)),by=.(year,firm,position)]
#Nor if there are too few teachers
wages[,ind_count:=.N,by=.(year,firm,position)]
#Also troublesome when there is little variation in salaries like so:
wages[,sal_scale_flag:=mean(abs(salary-mean(salary)))<50,by=.(year,firm,position)]
wages[,sal_count_flag:=length(unique(salary))<5,by=.(year,firm,position)]
> wages
         firm year position exp salary min_exp max_exp node_count ind_count sal_scale_flag sal_count_flag
      1: 0001 1996        4  14  66136       1      40         40      1373          FALSE          FALSE
      2: 0001 1996        4   3  42123       1      40         40      1373          FALSE          FALSE
      3: 0001 1996        4   9  46528       1      40         40      1373          FALSE          FALSE
      4: 0001 1996        4  11  35195       1      40         40      1373          FALSE          FALSE
      5: 0001 1996        4   2  43926       1      40         40      1373          FALSE          FALSE
     ---                                                                                                 
 999996: 0010 2015        5  11  43140       1      40         40      1326          FALSE          FALSE
 999997: 0010 2015        5  23  64025       1      40         40      1326          FALSE          FALSE
 999998: 0010 2015        5  31  35266       1      40         40      1326          FALSE          FALSE
 999999: 0010 2015        5  11  36267       1      40         40      1326          FALSE          FALSE
1000000: 0010 2015        5   7  44315       1      40         40      1326          FALSE          FALSE

Теперь мы обработаем wages однопоточным способом, как вы делали раньше. Обратите внимание, что сначала мы сохраняем исходные данные, чтобы потом можно было выполнять многопоточные операции с ними и сравнивать результаты:

start <- Sys.time()
salary_scales_1 <-
  wages[node_count>=7&ind_count>=10
        &sal_scale_flag==0&sal_count_flag==0,
        .(exp=0:40,salary=cobs_extrap(exp,salary,min_exp,max_exp)),
        by=.(firm,year,position)]
print(paste("No Parallelisation time: ",Sys.time()-start))
> print(paste("No Parallelisation time: ",Sys.time()-start))
[1] "No Parallelisation time:  1.13971961339315"
> salary_scales_1
       firm year position exp   salary
    1: 0001 1996        4   0 43670.14
    2: 0001 1996        4   1 43674.00
    3: 0001 1996        4   2 43677.76
    4: 0001 1996        4   3 43681.43
    5: 0001 1996        4   4 43684.99
   ---                                
16396: 0010 2015        5  36 44464.02
16397: 0010 2015        5  37 44468.60
16398: 0010 2015        5  38 44471.35
16399: 0010 2015        5  39 44472.27
16400: 0010 2015        5  40 43077.70

На обработку всего ушло около 1 минуты 8 секунд. Обратите внимание, что в нашем фиктивном примере у нас всего 10 разных фирм, поэтому время обработки не так существенно по сравнению с вашими локальными результатами.

Теперь давайте попробуем выполнить эту задачу параллельно. Как уже упоминалось, для нашего примера мы хотели бы разделить данные за год и назначить меньшие части отдельным ядрам. Мы будем использовать doParallel Пакет для этой цели:

Первое, что нам нужно сделать, это создать кластер с определенным количеством ядер. В нашем примере мы попытаемся использовать все доступные ядра. Далее нам нужно зарегистрировать кластер и экспортировать некоторые переменные в глобальные среды подузлов. В этом случае подузлам нужен только доступ к wages, Кроме того, некоторые из зависимых библиотек также должны быть оценены на узлах, чтобы заставить его работать. В этом случае узлам необходим доступ к обоим data.frame а также cobs библиотеки. Код выглядит так:

library(doParallel)
start <- Sys.time()
cl <- makeCluster(detectCores()); 
registerDoParallel(cl); 
clusterExport(cl,c("wages"),envir=environment());
clusterEvalQ(cl,library("data.table"));
clusterEvalQ(cl,library("cobs"));
salary_scales_2 <- foreach(i = 1996:2015) %dopar%
  {
    subSet <- wages[.(i)] # binary subsetting
    subSet[node_count>=7&ind_count>=10
           &sal_scale_flag==0&sal_count_flag==0,
           .(exp=0:40,
             salary=cobs_extrap(exp,salary,min_exp,max_exp)),
           by=.(firm,year,position)]
  }
stopCluster(cl)
print(paste("With parallelisation time: ",Sys.time()-start))
> print(paste("With parallelisation time: ",Sys.time()-start))
[1] "With parallelisation time:  23.4177722930908"

Теперь у нас есть список таблиц данных salary_scales_2 который содержит подрезультаты для каждого отдельного года. Обратите внимание на ускорение обработки: на этот раз потребовалось всего 23 секунды вместо первоначальных 1,1 минуты (улучшение на 65%). Единственное, что нам еще нужно сделать сейчас - это объединить результаты. Мы можем использовать do.call("rbind", salary_scales_2) для того, чтобы объединить строки таблиц вместе (это почти не занимает времени - 002 секунды за один прогон). Наконец, мы также выполняем небольшую проверку, чтобы убедиться, что многопоточные результаты действительно идентичны результатам однопоточного прогона:

salary_scales_2<-do.call("rbind",salary_scales_2)
identical(salary_scales_1,salary_scales_2)
> identical(salary_scales_1,salary_scales_2)
[1] TRUE

ОТВЕТИТЬ НА КОММЕНТАРИЙ Это действительно очень интересный пример, но я думаю, что вы можете пропустить более важный вопрос здесь. data.table действительно выполняет оптимизацию, связанную с памятью и структурой, чтобы вы могли более эффективно запрашивать и получать доступ к вашим данным. Однако в этом примере отсутствует узкое место в основной памяти или поиске, особенно если вы сравниваете его с фактическим общим временем обработки данных в cobs функция. Например, строка, которую вы изменили subSet <- wages[year==uniqueYears[i],] занимает всего около 0,04 секунды на звонок, когда вы его время.

Если вы используете профилировщик на своих трассах, то вы заметите, что это не data.table или любой из его операций или группировок, которые просят о распараллеливании, это cobs функция, которая занимает почти все время обработки (и эта функция даже не использует data.table в качестве входных данных). В этом примере мы пытаемся переназначить нашу общую нагрузку на cobs функционировать на разных ядрах, чтобы добиться нашего ускорения. Наше намерение не состоит в том, чтобы разделить data.table операции, так как они совсем не дороги. Тем не менее, мы действительно должны разделить нашу data.table в результате того, что нам нужно разделить данные для отдельных cobs функция работает. На самом деле, мы даже воспользовались тем, что data.table эффективен во всех отношениях при разделении и объединении таблиц. Это не заняло дополнительного времени вообще.

Другие вопросы по тегам