Краткий способ суммировать различные столбцы с различными функциями - PullRequest
4 голосов
/ 11 апреля 2019

Мой вопрос основан на подобном введении дополнительного ограничения, что имя каждой переменной должно появляться только один раз.

Рассмотрим фрейм данных

library( tidyverse )
df <- tibble( potentially_long_name_i_dont_want_to_type_twice = 1:10,
              another_annoyingly_long_name = 21:30 )

Я хотел бы применить mean к первому столбцу и sum ко второму столбцу, без необходимости вводить имя каждого столбца дважды.

Как показывает вопрос, который я связал выше, summarize позволяет вам сделать это, но требует, чтобы имя каждого столбца появлялось дважды. С другой стороны, summarize_at позволяет вам кратко применять несколько функций к нескольким столбцам, но делает это, вызывая все указанные функции для всех указанных столбцов, вместо того, чтобы делать это в мода один на один. Есть ли способ объединить эти отличительные черты summarize и summarize_at?

Мне удалось взломать его с помощью rlang, но я не уверен, что он чище, чем просто набирать каждую переменную дважды:

v <- c("potentially_long_name_i_dont_want_to_type_twice",
       "another_annoyingly_long_name")
f <- list(mean,sum)

## Desired output
smrz <- set_names(v) %>% map(sym) %>% map2( f, ~rlang::call2(.y,.x) )
df %>% summarize( !!!smrz )
# # A tibble: 1 x 2
#   potentially_long_name_i_dont_want_to_type_twice another_annoyingly_long_name
#                                             <dbl>                        <int>
# 1                                             5.5                          255

РЕДАКТИРОВАТЬ для решения некоторых философских вопросов

Я не думаю, что желать избегать идиомы x=f(x) необоснованно. Я, вероятно, сталкивался с чрезмерным усердием при наборе длинных имен, но реальная проблема заключается в наличии (относительно) длинных имен, которые очень похожи друг на друга. Примеры включают нуклеотидные последовательности (например, AGCCAGCGGAAACAGTAAGG) и штрих-коды TCGA . В таких случаях не только автоматическое заполнение ограниченной утилиты, но и написание таких вещей, как AGCCAGCGGAAACAGTAAGG = sum( AGCCAGCGGAAACAGTAAGG ), вносит ненужную связь и увеличивает риск того, что две стороны назначения могут случайно не синхронизироваться при разработке и поддержке кода.

Я полностью согласен с @MrFlick в отношении dplyr повышения читабельности кода, но я не думаю, что читаемость должна идти за счет правильности. Такие функции, как summarize_at и mutate_at, великолепны, поскольку они обеспечивают идеальный баланс между размещением операций рядом с их операндами (ясность) и гарантией того, что результат записан в правильный столбец (правильность).

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

Короче говоря, я считаю, что операция самообращения / суммирования должна упоминать каждое имя переменной ровно один раз.

Ответы [ 4 ]

2 голосов
/ 12 апреля 2019

Используйте .[[i]] и !!names(.)[i]:= для ссылки на i-й столбец и его имя.

library(tibble)
library(dplyr)
library(rlang)

df %>% summarize(!!names(.)[1] := mean(.[[1]]), !!names(.)[2] := sum(.[[2]])) 

предоставление:

# A tibble: 1 x 2
  potentially_long_name_i_dont_want_to_type_twice another_annoyingly_long_name
                                            <dbl>                        <int>
1                                             5.5                          255

Обновление

Если df были сгруппированы (это не вопрос, поэтому в этом нет необходимости), то окружите summarize с помощьюa do вот так:

library(dplyr)
library(rlang)
library(tibble)

df2 <- tibble(a = 1:10, b = 11:20, g = rep(1:2, each = 5))

df2 %>%
  group_by(g) %>%
  do(summarize(., !!names(.)[1] := mean(.[[1]]), !!names(.)[2] := sum(.[[2]]))) %>%
  ungroup

даёт:

# A tibble: 2 x 3
      g     a     b
  <int> <dbl> <int>
1     1     3    65
2     2     8    90
2 голосов
/ 12 апреля 2019

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

Функция .at, которая возвращает результаты для групп переменных (здесь только одна переменная по группе), которые мы затем можем сращивать, поэтому мы получаем выгоду от обоих миров, summarize и summarize_at:

df %>% summarize(
  !!!.at(vars(potentially_long_name_i_dont_want_to_type_twice), mean),
  !!!.at(vars(another_annoyingly_long_name), sum))

# # A tibble: 1 x 2
#     potentially_long_name_i_dont_want_to_type_twice another_annoyingly_long_name
#                                               <dbl>                        <dbl>
#   1                                             5.5                          255

Наречие к summarize с кратким обозначением доллара.

df %>%
  ..flx$summarize(potentially_long_name_i_dont_want_to_type_twice = ~mean(.),
                  another_annoyingly_long_name = ~sum(.))

# # A tibble: 1 x 2
#     potentially_long_name_i_dont_want_to_type_twice another_annoyingly_long_name
#                                               <dbl>                        <int>
#   1                                             5.5                          255

код для .at

Он должен использоваться в трубе, потому что он использует . в родительской среде, грязный, но он работает.

.at <- function(.vars, .funs, ...) {
  in_a_piped_fun <- exists(".",parent.frame()) &&
    length(ls(envir=parent.frame(), all.names = TRUE)) == 1
  if (!in_a_piped_fun)
    stop(".at() must be called as an argument to a piped function")
  .tbl <- try(eval.parent(quote(.)))
  dplyr:::manip_at(
    .tbl, .vars, .funs, rlang::enquo(.funs), rlang:::caller_env(),
    .include_group_vars = TRUE, ...)
}

Я разработал его для объединения summarize и summarize_at:

df %>% summarize(
  !!!.at(vars(potentially_long_name_i_dont_want_to_type_twice), list(foo=min, bar = max)),
  !!!.at(vars(another_annoyingly_long_name), median))

# # A tibble: 1 x 3
#       foo   bar another_annoyingly_long_name
#     <dbl> <dbl>                        <dbl>
#   1     1    10                         25.5

код для ..flx

..flx выводит функцию, которая заменяет аргументы формулы, такие как a = ~mean(.), вызовами a = purrr::as_mapper(~mean(.))(a) перед запуском. Удобно с summarize и mutate, потому что столбец не может быть формулой, поэтому не может быть никакого конфликта.

Мне нравится использовать долларовую нотацию в качестве сокращения и иметь имена, начинающиеся с .., чтобы я мог называть эти «теги» (и назначать им класс "tag") и видеть их как разные объекты (все еще экспериментируя с этот). ..flx(summarize)(...) тоже будет работать.

..flx <- function(fun){
  function(...){
    mc <- match.call()
    mc[[1]] <- tail(mc[[1]],1)[[1]]
    mc[] <- imap(mc,~if(is.call(.) && identical(.[[1]],quote(`~`))) {
      rlang::expr(purrr::as_mapper(!!.)(!!sym(.y))) 
    } else .)
    eval.parent(mc)
  }
}

class(..flx) <- "tag"

`$.tag` <- function(e1, e2){
  # change original call so x$y, which is `$.tag`(tag=x, data=y), becomes x(y)
  mc <- match.call()
  mc[[1]] <- mc[[2]]
  mc[[2]] <- NULL
  names(mc) <- NULL
  # evaluate it in parent env
  eval.parent(mc)
}
1 голос
/ 12 апреля 2019

Кажется, вы можете использовать map2 для этого.

map2_dfc( df[v], f, ~.y(.x))

# # A tibble: 1 x 2
#   potentially_long_name_i_dont_want_to_type_twice another_annoyingly_long_name
#                                             <dbl>                        <int>
# 1                                             5.5                          255
1 голос
/ 12 апреля 2019

Вот хакерская функция, которая использует неэкспортированные функции из dplyr, поэтому она не пригодна для будущего, но вы можете указать разные сводки для каждого столбца.

summarise_with <- function(.tbl, .funs) {
  funs <- enquo(.funs)
  syms <- syms(tbl_vars(.tbl))
  calls <- dplyr:::as_fun_list(.funs, funs, caller_env())
  stopifnot(length(syms)==length(calls))
  cols <- purrr::map2(calls, syms, ~dplyr:::expr_substitute(.x, quote(.), .y))
  cols <- purrr::set_names(cols, purrr::map_chr(syms, rlang::as_string))
  summarize(.tbl, !!!cols)
}

Тогда вы можете сделать

df %>% summarise_with(list(mean, sum))

и вообще не нужно вводить имена столбцов.

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