Использование формул с псевдонимами для выполнения операций с несколькими столбцами - PullRequest
0 голосов
/ 13 марта 2020

Этот вопрос относится к предыдущему, который я задал , но пытаюсь быть более обобщенным c. Я хочу использовать формулы для выполнения операций над несколькими «группами» данных (например, a_data1, a_data2, b_data1, b_data2, а затем выполнять операции с использованием столбцов *_data1).

Основываясь на ответе @ akrun на этот вопрос, я создал следующую функцию. Он принимает одностороннюю формулу и применяет ее ко всем «группам данных»:

suppressPackageStartupMessages({
  library(dplyr)
  library(tidyr)
})

polymutate <- function(df, formula,
                       pattern = "(.)_(.*)",
                       staticCols = NULL) {
  staticCols <- rlang::enquo(staticCols)

  rhs <- rlang::f_rhs(formula)
  names <- all.vars(rhs)
  df %>%
    mutate(
      rn = row_number()
    ) %>%
    pivot_longer(
      cols = -c(rn, !!staticCols),
      names_to = c(".value", "grp"),
      names_pattern = pattern
    ) %>%
    mutate(
      new = eval(rhs)
    ) %>%
    pivot_wider(
      names_from = grp,
      values_from = c(names, "new")
    ) %>%
    select(
      -rn
    ) %>%
    rename_at(
      vars(starts_with("new")),
      gsub, pattern = "^new_", replacement = ""
    )
}

df <- data.frame(a_data1 = 1:3, b_data1 = 2:4,
                 a_data2 = 3:5, b_data2 = 4:6,
                 static = 5:7)

polymutate(df, ~ a + b, staticCols = static)
#> # A tibble: 3 x 7
#>   static a_data1 a_data2 b_data1 b_data2 data1 data2
#>    <int>   <int>   <int>   <int>   <int> <int> <int>
#> 1      5       1       3       2       4     3     7
#> 2      6       2       4       3       5     5     9
#> 3      7       3       5       4       6     7    11

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

Итак, этот polymutate преобразует фрейм данных в более длинный формат, чтобы у нас был один столбец с именем группы (data1 или data2) и по одному на префикс (a и b). Затем он оценивает данную формулу в контексте этого более глубокого фрейма данных (очевидно, что имена в формуле должны соответствовать префиксам). Как только это сделано, он расширяет кадр данных до его первоначальной формы.

Это работает довольно хорошо, но немного медленно. Использование его на фрейме данных с 20 000 строк и 11 «группами» занимает 0,77 секунды.

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

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

Затем он может извлечь переменные, используемые в формуле, и использовать их для перестройки желаемого Имена столбцов, создать сопоставление псевдонимов, а затем использовать это сопоставление для применения формулы к кадру данных. Я подошел довольно близко, но не смог получить реальную формулу для оценки:

suppressPackageStartupMessages({
  library(dplyr)
})

polymutate2 <- function(df, formula, name) {
  vars <- all.vars(formula)
  rhs <- rlang::f_rhs(formula)
  aliases <- paste0(vars, "_", name)
  mapping <- rlang::list2(!!!aliases)
  names(mapping) <- vars

  mapping <- do.call(wrapr::qc, mapping)
  wrapr::let(
    mapping,
    df %>% mutate(!!name := a + b)
  )
}

df <- data.frame(a_data1 = 1:3, b_data1 = 2:4,
                 a_data2 = 3:5, b_data2 = 4:6,
                 static = 5:7)

polymutate2(df, ~ a + b, "data1")
#>   a_data1 b_data1 a_data2 b_data2 static data1
#> 1       1       2       3       4      5     3
#> 2       2       3       4       5      6     5
#> 3       3       4       5       6      7     7

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

Вы заметите, что вызов mutate имеет жестко выраженное выражение, поскольку я не смог заставить его работать с данной формулой. Замена этого выражения на eval(rhs), как в предыдущей версии, приводит к ошибке object 'a' not found:

suppressPackageStartupMessages({
  library(dplyr)
  # library(tidyr)
})

polymutate2 <- function(df, formula, name) {
  vars <- all.vars(formula)
  rhs <- rlang::f_rhs(formula)
  aliases <- paste0(vars, "_", name)
  mapping <- rlang::list2(!!!aliases)
  names(mapping) <- vars

  mapping <- do.call(wrapr::qc, mapping)
  wrapr::let(
    mapping,
    df %>% mutate(!!name := eval(rhs))
  )
}

polymutate2(df, ~ a + b, "data1")
#> Error in eval(rhs): object 'a' not found

Если я смогу заставить это работать (и при условии, что решение не оказывает существенного влияния на производительность), это очень быстрее: всего за 0,03 секунды запускается цепочка из polymutate2 (по одной для каждой из 11 групп в моем кадре данных из 20 000 строк).

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

1 Ответ

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

Может быть, кто-то более знающий может присоединиться к более подходящему подходу, но проблему можно решить (не очень элегантно, по общему признанию), обернув весь вызов wrapr :: let в eval(parse(text=..)) - это определенно быстрее:


suppressPackageStartupMessages({
    invisible(lapply(c("dplyr", "tidyr", "rlang", "wrapr", "microbenchmark"),
                     require, character.only = TRUE))
})

polymutate <- function(df, formula,
                       pattern = "(.)_(.*)",
                       staticCols = NULL) {
    staticCols <- rlang::enquo(staticCols)

    rhs <- rlang::f_rhs(formula)
    names <- all.vars(rhs)
    df %>%
        mutate(
            rn = row_number()
        ) %>%
        pivot_longer(
            cols = -c(rn, !!staticCols),
            names_to = c(".value", "grp"),
            names_pattern = pattern
        ) %>%
        mutate(
            new = eval(rhs)
        ) %>%
        pivot_wider(
            names_from = grp,
            values_from = c(names, "new")
        ) %>%
        select(
            -rn
        ) %>%
        rename_at(
            vars(starts_with("new")),
            gsub, pattern = "^new_", replacement = ""
        )
}

polymutate2 <- function(df, formula, name) {
    vars <- all.vars(formula)
    rhs <- deparse(rlang::f_rhs(formula))
    aliases <- paste0(vars, "_", name)
    mapping <- rlang::list2(!!!aliases)
    names(mapping) <- vars
    mapping <- do.call(wrapr::qc, mapping)
    eval(parse(text=paste0("wrapr::let(mapping, df %>% mutate(!!name := ", rhs, "))" ))
    )
}

set.seed(1)                 
df <- setNames(data.frame(matrix(sample(1:12, 7E6, replace=TRUE), ncol=7)),
               c("a_data1", "b_data1", "a_data2", "b_data2", "a_data3", "b_data3", "static"))

pd <- polymutate(df, ~ a + b, staticCols = static)
#> Note: Using an external vector in selections is ambiguous.
#> ℹ Use `all_of(names)` instead of `names` to silence this message.
#> ℹ See <https://tidyselect.r-lib.org/reference/faq-external-vector.html>.
#> This message is displayed once per session.
pd2 <- polymutate2(df, ~ a + b, "data1") %>% polymutate2(., ~ a + b, "data2") %>% polymutate2(., ~ a + b, "data3") %>% dplyr::select(static, everything()) %>% 
    as_tibble()

all.equal(pd, pd2)
#> [1] TRUE

microbenchmark(polymutate(df, ~ a + b, staticCols = static), 
               polymutate2(df, ~ a + b, "data1") %>% polymutate2(., ~ a + b, "data2") %>% polymutate2(., ~ a + b, "data3") %>% dplyr::select(static, everything()) %>% 
                   as_tibble(),
               times=10L)
#> Unit: milliseconds
#>                                                                                                                                                                        expr
#>                                                                                                                                 polymutate(df, ~a + b, staticCols = static)
#>  polymutate2(df, ~a + b, "data1") %>% polymutate2(., ~a + b, "data2") %>%      polymutate2(., ~a + b, "data3") %>% dplyr::select(static,      everything()) %>% as_tibble()
#>          min          lq       mean     median         uq        max neval cld
#>  1143.582663 1151.206750 1171.46502 1173.03649 1188.91108 1209.01984    10   b
#>     9.553352    9.619473   10.88463   10.59397   12.27675   12.52403    10  a

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

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