R - Ускорить таблицу поднабора данных на основе другой? - PullRequest
1 голос
/ 14 марта 2020

У меня есть таблица данных dat1 с ежедневными измерениями количества осадков для нескольких участков:

> dat1
              date   ID value
     1: 2000-03-01 1559     0
     2: 2000-03-02 1559     0
     3: 2000-03-03 1559     0
     4: 2000-03-04 1559     0
     5: 2000-03-05 1559     0
    ---                      
106178: 2019-12-27 1322     2
106179: 2019-12-28 1322     1
106180: 2019-12-29 1322     2
106181: 2019-12-30 1322     2
106182: 2019-12-31 1322     0

, а также у меня есть еще одна таблица данных dat2 для каждого сайта в dat1 вместе с некоторыми соседними сайтами , расстояние между ними и общие даты измерений:

> dat2
     ID1  ID2      dist common_date_begin common_date_end diff_days
 1: 1549 1550 490774.05        2010-02-23      2017-06-16      2670
 2: 1549 1551 290832.68        2010-02-23      2017-06-16      2670
 3: 1549 1552  87750.38        2006-02-01      2017-06-16      4153
 4: 1549 1553 138531.18        2006-02-01      2017-06-16      4153
 5: 1549 1554 103870.34        2000-03-01      2017-06-16      6316
 6: 1549 1555 112919.70        2000-03-01      2017-06-16      6316
 7: 1549 1556  19625.65        2000-03-01      2017-06-16      6316
 8: 1549 1557 398693.43        2000-03-01      2017-06-16      6316
 9: 1549 1558  73514.23        2000-03-01      2017-06-16      6316
10: 1549 1559 129691.63        2000-03-01      2017-06-16      6316

Для каждой пары ID1 - ID2 в dat2 я хочу задать эти станции в dat1, и рассчитайте корреляцию между двумя сайтами.

Следующий код дает то, что мне нужно:

library(data.table)
dat1 <- fread("https://www.dropbox.com/s/d2s61du255vzu7g/dat1.csv?dl=1") # ~2 MB
dat2 <- fread("https://www.dropbox.com/s/7n0z0gbeoifss4j/dat2.csv?dl=1") # ~5 KB

# fix column classes
dat1$date <- as.Date(dat1$date)
dat1$ID <- as.character(dat1$ID)
dat2[, (c("common_date_begin","common_date_end")) := lapply(.SD, as.Date), .SDcols = c("common_date_begin","common_date_end")]
dat2[, (c("ID1","ID2")) := lapply(.SD, as.character), .SDcols = c("ID1","ID2")]

# get list of unique stations
ids <- unique(dat2$ID1)

# initialize matrix to hold correlations
correlations <- matrix(NA, nrow = nrow(dat2), ncol=1)

# initialize data frame to hold results
results <- as.data.frame(dat2[, -c(4:5)])

# initialize loop counters
x <- 1

# loop over the main ID's
for (i in ids) {

  tmp <- dat2[ID1==i]

  #loop over the ID's of the neighbour stations
  for (id in 1:nrow(tmp)){

    # get ID of the neighbours
    near_id <- as.numeric(tmp[id, 2])

    # get common dates
    beg_date <- tmp[id, 4]
    end_date <- tmp[id, 5]

    # calculate correlations
    correlations[x,1] <- cor(dat1[ID==i & date %between% c(beg_date, end_date)]$value,
                             dat1[ID==near_id & date %between% c(beg_date, end_date)]$value)
    # increment loop counter
    x <- x + 1
  }
}

# assemble final data frame
results <- data.table(ID=results[, 1],
                      ID_nearest=results[, 2],
                      distance=results[, 3],
                      overlapping_days=results[, 4],
                      correl=as.vector(correlations))

В этом игрушечном примере это работает довольно быстро. Тем не менее, для моей реальной таблицы данных с 4 миллионами строк требуется навсегда . Я ждал ~ 3 часа и не видел, чтобы обработка заканчивалась.

Итак, мои вопросы: есть ли способ ускорить приведенный выше код, исключая перевод его на C ++? Поскольку у меня доступно 32 ядра, подойдет ли код для любого многоядерного подхода? mclapply или foreach? Любые стартовые подсказки на любом из них?

1 Ответ

2 голосов
/ 14 марта 2020

Ваш код можно ускорить, используя join s для сопоставления id-пар в dat2 с измерениями в dat1 вместо for -циклов. Кстати: по крайней мере, на мой взгляд, использование объединений делает код также более лаконичным и понятным. И более надежный.

Примечание далее : Я нашел ошибку в вашем коде. Настройка матрицы корреляций с использованием correlations[x,1] приводит к присвоению корреляций неправильным id-парам.

Тест

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

Микробенчмаркинг обеих функций показывает, что использование объединений ускоряет вычисления в ~ 2,5 раза. Я не знаком с data.table, но могу предположить, что использование data.table или dtplyr (бэкэнд таблицы данных для dplyr) может еще больше повысить производительность, особенно для вашего реального набора данных.

<!-- language-all: lang-r -->


library(data.table)
library(dplyr)
library(purrr)
library(ggplot2)
library(microbenchmark)

dat1 <- fread("https://www.dropbox.com/s/d2s61du255vzu7g/dat1.csv?dl=1") # ~2 MB
dat2 <- fread("https://www.dropbox.com/s/7n0z0gbeoifss4j/dat2.csv?dl=1") # ~5 KB

# fix column classes
dat1$date <- as.Date(dat1$date)
dat1$ID <- as.character(dat1$ID)
dat2[, (c("common_date_begin","common_date_end")) := lapply(.SD, as.Date), .SDcols = c("common_date_begin","common_date_end")]
dat2[, (c("ID1","ID2")) := lapply(.SD, as.character), .SDcols = c("ID1","ID2")]

cor_join <- function(dat1, dat2) {
  # We want to get a dataframe with 
  #
  # 1. pairs of sites, 
  # 2. dates where we have measurements for both
  # 3. the measurements at each site
  #
  # This could be achieved via left_joins
  dat3 <- dat2 %>%
    # Join dates and measurements for ID1
    left_join(dat1, by = c("ID1" = "ID")) %>% 
    rename(value1 = value) %>% 
    # Join dates and measurements for ID2 on the same date
    left_join(dat1, by = c("ID2" = "ID", "date" = "date")) %>% 
    rename(value2 = value, ID = ID1, ID_nearest = ID2)
  dat3

  # Compute correlations
  dat3 %>%
    # Drop missings, i.e. observations with no common dates
    filter(date >= common_date_begin & date <= common_date_end) %>% 
    group_by(ID, ID_nearest, dist, common_date_begin, common_date_end, diff_days) %>% 
    summarise(correl = cor(value1, value2)) %>% 
    ungroup()
}

cor_loop <- function(dat1, dat2) {
  # get list of unique stations
  ids <- unique(dat2$ID1)

  # initialize matrix to hold correlations
  correlations <- matrix(NA, nrow = nrow(dat2), ncol=1)

  # initialize data frame to hold results
  results <- as.data.frame(dat2[, -c(4:5)])

  # initialize loop counters
  x <- 1

  # loop over the main ID's
  for (i in ids) {

    tmp <- dat2[ID1==i]

    #loop over the ID's of the neighbour stations
    for (id in 1:nrow(tmp)){

      # get ID of the neighbours
      near_id <- as.numeric(tmp[id, 2])

      # get common dates
      beg_date <- tmp[id, 4]
      end_date <- tmp[id, 5]

      # calculate correlations
      correlations[x,1] <- cor(dat1[ID==i & date %between% c(beg_date, end_date)]$value,
                               dat1[ID==near_id & date %between% c(beg_date, end_date)]$value)


      # increment loop counter
      x <- x + 1
    }
  }

  # assemble final data frame
  results <- data.table(ID=results[, 1],
                        ID_nearest=results[, 2],
                        distance=results[, 3],
                        overlapping_days=results[, 4],
                        correl=as.vector(correlations))
  results
}

# microbenchmark
microbenchmark::microbenchmark(cor_join(dat1, dat2), cor_loop(dat1, dat2), times = 10)
#> Unit: milliseconds
#>                  expr      min       lq     mean   median       uq      max
#>  cor_join(dat1, dat2) 247.4106 286.1556 301.6367 296.6921 302.2751 400.8654
#>  cor_loop(dat1, dat2) 773.5274 784.9197 807.3767 798.4800 842.3080 854.1716
#>  neval
#>     10
#>     10

Результаты проверки

Чтобы проверить, что обе функции дают одинаковые результаты, я создал диаграмму рассеяния

# Check result
results <- list(join = cor_join(dat1, dat2), loop = cor_loop(dat1, dat2))

# Plot
check <- results %>% 
  purrr::reduce(left_join, by = c("ID", "ID_nearest"), suffix = c("_join", "_loop"))
check %>% 
  ggplot(aes(correl_join, correl_loop, color = ID)) +
  geom_point()

OOPS: диаграмма рассеяния показывает разные результаты? Чтобы проверить, что я использовал простой набор данных, где я отфильтровал наборы данных для сайтов 1183, 1550 и 1551:

dat1a <- dat1 %>% filter(ID %in% c(1183, 1550, 1551)) %>% as.data.table()
dat2a <- dat2 %>% filter(ID1 %in% c(1183, 1550, 1551), ID2 %in% c(1183, 1550, 1551)) %>% as.data.table()

# For the simple dataset I get the same correlations
cor_join(dat1a, dat2a)
#> # A tibble: 2 x 7
#>   ID    ID_nearest     dist common_date_begin common_date_end diff_days  correl
#>   <chr> <chr>         <dbl> <date>            <date>              <int>   <dbl>
#> 1 1183  1550       1576360. 2010-02-23        2017-06-16           2670  0.0456
#> 2 1183  1551       1513356. 2010-02-23        2017-06-16           2670 -0.0251
cor_loop(dat1a, dat2a) 
#>      ID ID_nearest distance overlapping_days      correl
#> 1: 1183       1550  1576360             2670  0.04564506
#> 2: 1183       1551  1513356             2670 -0.02513991

После проверки вашего кода я догадался, что различия возникают из-за присвоения корреляций неправильным id-парам из-за correlations[x,1]. Для проверки я настроил cor_loop. Помимо возврата df results он также возвращает второй df correlations2, который установлен в l oop и содержит не только корреляцию, но и соответствующее значение id и near_id:

cor_loop_check <- function(dat1, dat2) {
  # get list of unique stations
  ids <- unique(dat2$ID1)

  # initialize matrix to hold correlations
  correlations <- matrix(NA, nrow = nrow(dat2), ncol=1)
  correlations2 <- data.frame(id1 = rep(NA, nrow(dat2)), 
                              id2 = rep(NA, nrow(dat2)), 
                              correl = rep(NA, nrow(dat2)))

  # initialize data frame to hold results
  results <- as.data.frame(dat2[, -c(4:5)])

  # initialize loop counters
  x <- 1

  # loop over the main ID's
  for (i in ids) {

    tmp <- dat2[ID1==i]

    #loop over the ID's of the neighbour stations
    for (id in 1:nrow(tmp)){

      # get ID of the neighbours
      near_id <- as.numeric(tmp[id, 2])

      # get common dates
      beg_date <- tmp[id, 4]
      end_date <- tmp[id, 5]

      # calculate correlations
      correlations[x,1] <- cor(dat1[ID==i & date %between% c(beg_date, end_date)]$value,
                               dat1[ID==near_id & date %between% c(beg_date, end_date)]$value)

      # Put correlation in df together with current id and near id
      correlations2[x, "id1"] <- i
      correlations2[x, "id2"] <- near_id
      correlations2[x, "correl"] <- correlations[x,1]

      # increment loop counter
      x <- x + 1
    }
  }

  # assemble final data frame
  results <- data.table(ID=results[, 1],
                        ID_nearest=results[, 2],
                        distance=results[, 3],
                        overlapping_days=results[, 4],
                        correl=as.vector(correlations))

  list(results, correlations2)
}


results_check <- cor_loop_check(dat1, dat2)

# Check results for e.g. row 20: Same value for correlation but differing id-pair ):
results_check[[1]][20,]
#>      ID ID_nearest distance overlapping_days      correl
#> 1: 1315       1551  1193032             2670 -0.06323207
results_check[[2]][20,]
#>     id1  id2      correl
#> 20 1315 1559 -0.06323207

Создано в 2020-03-14 пакетом представ (v0.3.0)

Как видите. В строке 20 оба df содержат одинаковую корреляцию, но разные пары идентификаторов.

...