Попытка понять, как работает eval (expr, envir = df) - PullRequest
0 голосов
/ 13 ноября 2018

Я создал функцию, которая, кажется, работает, но я не понимаю, почему.

Моя первоначальная проблема состояла в том, чтобы взять data.frame, который содержит подсчет населения, и расширить его, чтобы воссоздать исходное население. Это достаточно просто, если вы заранее знаете имена столбцов.

      library(tidyverse)

      set.seed(121)

      test_counts <- tibble(Population = letters[1:4], Length = c(1,1,2,1), 
         Number = sample(1:100, 4))

      expand_counts_v0 <- function(Length, Population, Number) { 
            tibble(Population = Population, 
                   Length = rep(Length, times = Number))

      }


      test_counts %>% pmap_dfr(expand_counts_v0) %>%   # apply it
                 group_by(Population, Length) %>%    # test it
                   summarise(Number = n()) %>%  
                   ungroup %>%
                  { all.equal(., test_counts)}
      # [1] TRUE    

Однако я хотел обобщить это для функции, которой не нужно знать имена столбцов data.frame, и мне интересен NSE, поэтому я написал:

test_counts1 <- tibble(Population = letters[1:4], 
                 Length = c(1,1,2,1), 
                 Number = sample(1:100, 4),
                 Height = c(100, 50, 45, 90),
                 Width = c(700, 50, 60, 90)
               )


expand_counts_v1 <- function(df, count = NULL) { 
     countq <- enexpr(count)
     names <- df %>% select(-!!countq) %>% names 
     namesq <- names %>% map(as.name)

     cols <- map(namesq, ~ expr(rep(!!., times = !!countq))
          ) %>% set_names(namesq)

      make_tbl <- function(...) {
                         expr(tibble(!!!cols)) %>% eval(envir = df)
      }

      df %>% pmap_dfr(make_tbl)
}

Но, когда я тестирую эту функцию, она, кажется, дублирует строки 4 раза:

   test_counts %>% expand_counts_v1(count = Number) %>% 
                   group_by(Population, Length) %>%
                   summarise(Number = n()) %>%
                   ungroup %>%
                   { sum(.$Number)/sum(test_counts$Number)}
   # [1] 4

Это привело меня к угадыванию решения, которое было

   expand_counts_v2 <- function(df, count = NULL) { 
             countq <- enexpr(count)
             names <- df %>% select(-!!countq) %>% names 
             namesq <- names %>% map(as.name)

             cols <- map(namesq, ~ expr(rep(!!., times = !!countq))
              ) %>% set_names(namesq)

              make_tbl <- function(...) {
                          expr(tibble(!!!cols)) %>% eval(envir = df)
       }

      df %>% make_tbl
   }

Кажется, это работает:

 test_counts %>% expand_counts_v2(count = Number) %>% 
                 group_by(Population, Length) %>%
                 summarise(Number = n()) %>%
                 ungroup %>%
                { all.equal(., test_counts)}
 # [1] TRUE 

  test_counts1 %>% expand_counts_v2(count = Number) %>% 
                      group_by(Population, Length, Height, Width) %>%
                      summarise(Number = n()) %>%
                      ungroup %>%
                    { all.equal(., test_counts1)}
   # [1] TRUE

Но я не понимаю, почему. Как это оценивается для каждой строки, хотя я больше не использую pmap? Чтобы работала, нужно применить функцию к каждой строке, поэтому она должна быть как-то, но я не понимаю, как это происходит.

EDIT

После того, как Артем правильно объяснил, что происходит, я понял, что могу сделать это

expand_counts_v2 <- function(df, count = NULL) { 
      countq <- enexpr(count)
      names <- df %>% select(-!!countq) %>% names 
      namesq <- names %>% map(as.name)

      cols <- map(namesq, ~ expr(rep(!!., times = !!countq))
                  ) %>% set_names(namesq)

    expr(tibble(!!!cols)) %>% eval_tidy(data = df)
}

Который избавляется от ненужной функции mk_tbl. Однако, как сказал Артем, это действительно работает, потому что репутация векторизована. Итак, это работает, но не переписывая функцию _v0 и отображая ее, что я и пытался повторить. В конце концов я обнаружил, rlang :: new_function и написал:

expand_counts_v3 <- function(df, count = NULL) { 
      countq <- enexpr(count)
      names <- df %>% select(-!!countq) %>% names 
      namesq <- names %>% map(as.name)

      cols <- map(namesq, ~ expr(rep(!!., times = !!countq))
                  ) %>% set_names(namesq)

      all_names <- df %>% names %>% map(as.name) 
    args <- rep(0, times = length(all_names)) %>% as.list %>% set_names(all_names)

    correct_function <- new_function(args,     # this makes the function as in _v0
                                     expr(tibble(!!!cols))  )
    pmap_dfr(df, correct_function)     # applies it as in _v0
}

, который длиннее и, возможно, более уродлив, но работает так, как я хотел.

1 Ответ

0 голосов
/ 16 ноября 2018

Проблема в eval( envir = df ), которая выставляет весь фрейм данных на make_tbl(). Обратите внимание, что вы никогда не используете аргумент ... внутри make_tbl(). Вместо этого функция эффективно вычисляет эквивалент

with( df, tibble(Population = rep(Population, times = Number), 
                 Length = rep(Length, times=Number)) )

независимо от того, какие аргументы вы ему предоставляете. Когда вы вызываете функцию через pmap_dfr(), она, по существу, вычисляет вышеприведенное четыре раза (по одному для каждой строки) и объединяет результаты по строкам, что приводит к дублированию записей, которые вы наблюдали. Когда вы удаляете pmap_dfr(), функция вызывается один раз, но поскольку rep сама векторизована (попробуйте выполнить rep( test_counts$Population, test_counts$Number ), чтобы понять, что я имею в виду), make_tbl() вычисляет весь результат за один раз.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...