Несколько функций для нескольких столбцов по группам и создание информативных имен столбцов

Как настроить манипуляции с таблицей данных так, чтобы помимо sum на категорию нескольких столбцов, он также будет одновременно вычислять другие функции, такие как mean и counts (.N), и автоматически создавать имена столбцов: «сумма c1», «сумма c2», «сумма c4», «среднее значение c1», «среднее значение c2», » означает c4 "и желательно еще 1 столбец" рассчитывает "?

Моим старым решением было написать

mean col1 = ....
mean col2 = ....

И т. Д. Внутри команды data.table

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

Я прочитал кучу сообщений и статей в блогах, но не совсем понял, как лучше всего это сделать. Я читал, что в некоторых случаях манипуляции с большими таблицами данных могут стать довольно медленными в зависимости от того, какой подход вы используете (.sdcols, get, lapply или by =). Поэтому я добавил «внушительный» набор фиктивных данных.

Мои реальные данные составляют около 100 тыс. Строк на 100 столбцов и примерно от 1 до 100 групп.

library(data.table)
n = 100000
dt  = data.table(index=1:100000,
                 category = sample(letters[1:25], n, replace = T),
                 c1=rnorm(n,10000),
                 c2=rnorm(n,1000),
                 c3=rnorm(n,100),
                 c4 = rnorm(n,10)
)

# add more columns to test for big data tables 
lapply(c(paste('c', 5:100, sep ='')),
       function(addcol) dt[[addcol]] <<- rnorm(n,1000) )

# Simulate columns selected by shiny app user 

Colchoice <- c("c1", "c4")
FunChoice <- c(".N", "mean", "sum")

# attempt which now does just one function and doesn't add names
dt[, lapply(.SD, sum, na.rm=TRUE), by=category, .SDcols=Colchoice ]

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

Category  Mean c1 Sum c1 Mean c4 ...
A
B
C
D
E
......

Возможно дубликат, но я не нашел точного ответа, который мне нужен

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

Mark 22.12.2018 23:33
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
12
1
863
5
Перейти к ответу Данный вопрос помечен как решенный

Ответы 5

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

library(dplyr)
iris %>% 
   group_by(Species) %>% 
  summarise_at(vars(Sepal.Length, Sepal.Width), .funs = c(sum=sum,mean= mean), na.rm=TRUE)

## A tibble: 3 x 5
#  Species    Sepal.Length_sum Sepal.Width_sum Sepal.Length_mean Sepal.Width_mean
#  <fct>                 <dbl>           <dbl>             <dbl>            <dbl>
#1 setosa                 245.            171.              5.00             3.43
#2 versicolor             297.            138.              5.94             2.77
#3 virginica              323.            149.              6.60             2.97

или используя ввод вектора символов для столбцов и функций:

Colchoice <- c("Sepal.Length", "Sepal.Width")
FunChoice <- c("mean", "sum")
iris %>% 
  group_by(Species) %>% 
  summarise_at(vars(Colchoice), .funs = setNames(FunChoice, FunChoice), na.rm=TRUE)
## A tibble: 3 x 5
#  Species    Sepal.Length_mean Sepal.Width_mean Sepal.Length_sum Sepal.Width_sum
#  <fct>                  <dbl>            <dbl>            <dbl>           <dbl>
#1 setosa                  5.00             3.43             245.            171.
#2 versicolor              5.94             2.77             297.            138.
#3 virginica               6.60             2.97             323.            149.

приятно, просто не мог понять, как в этом также считать длину или .N. 'length' выдает ошибку '2 аргумента переданы в' length ', который требует одного'

Mark 27.12.2018 10:34

Вот ответ data.table:

funs_list <- lapply(FunChoice, as.symbol)
dcast(dt, category~1, fun=eval(funs_list), value.var = Colchoice)

Он супербыстрый и делает то, что вы хотите.

Это очень элегантный способ решения data.table, даже не знаю, почему вы говорите «почти». Если вы хотите передать строки, как в Shiny, вы можете сделать это: FunChoice <- c("sum", "mean") funs_list <- lapply(FunChoice, as.symbol) dcast(dt, category~1, fun=eval(funs_list), value.var = Colchoice)

Nate 21.12.2018 18:45

Рассмотрите возможность создания списка таблиц данных, в котором вы перебираете каждый ColChoice и применяете каждую функцию FuncChoice (задавая имена соответственно). Затем, чтобы объединить все таблицы данных вместе, запустите merge в вызове Reduce. Кроме того, используйте get для получения объектов среды (функций / столбцов).

Примечание: ColChoice был переименован для случая верблюда, а функция length заменяет .N для функциональной формы для подсчета:

set.seed(12212018)  # RUN BEFORE data.table() BUILD TO REPRODUCE OUTPUT
...

ColChoice <- c("c1", "c4")
FunChoice <- c("length", "mean", "sum")

output <- lapply(ColChoice, function(col)
                   dt[, setNames(lapply(FunChoice, function(f) get(f)(get(col))), 
                                 paste0(col, "_", FunChoice)), 
                      by=category]
          )

final_dt <- Reduce(function(x, y) merge(x, y, by = "category"), output)

head(final_dt)

#    category c1_length   c1_mean   c1_sum c4_length   c4_mean   c4_sum
# 1:        a      3893 10000.001 38930003      3893  9.990517 38893.08
# 2:        b      4021 10000.028 40210113      4021  9.977178 40118.23
# 3:        c      3931 10000.008 39310030      3931  9.996538 39296.39
# 4:        d      3954 10000.010 39540038      3954 10.004578 39558.10
# 5:        e      4016  9999.998 40159992      4016 10.002131 40168.56
# 6:        f      3974  9999.987 39739947      3974  9.994220 39717.03
Ответ принят как подходящий

Если я правильно понял, этот вопрос состоит из двух частей:

  1. Как группировать и агрегировать с помощью нескольких функций по списку столбцов и автоматически генерировать новые имена столбцов.
  2. Как передать имена функций как вектор символов.

Для части 1, это Около, дубликат Применение нескольких функций к нескольким столбцам в data.table, но с дополнительным требованием, что результаты должны быть сгруппированы с использованием by =.

Следовательно, Эдди ответ необходимо изменить, добавив параметр recursive = FALSE в вызове unlist():

my.summary = function(x) list(N = length(x), mean = mean(x), median = median(x))
dt[, unlist(lapply(.SD, my.summary), recursive = FALSE), 
   .SDcols = ColChoice, by = category]
    category c1.N   c1.mean c1.median c4.N   c4.mean c4.median
 1:        f 3974  9999.987  9999.989 3974  9.994220  9.974125
 2:        w 4033 10000.008  9999.991 4033 10.004261  9.986771
 3:        n 4025  9999.981 10000.000 4025 10.003686  9.998259
 4:        x 3975 10000.035 10000.019 3975 10.010448  9.995268
 5:        k 3957 10000.019 10000.017 3957  9.991886 10.007873
 6:        j 4027 10000.026 10000.023 4027 10.015663  9.998103
...

По части 2, нам нужно создать my.summary() из символьного вектора имен функций. Это может быть достигнуто с помощью "программирование на языке", то есть путем сборки выражения в виде символьной строки и, наконец, его синтаксического анализа и оценки:

my.summary <- 
  sapply(FunChoice, function(f) paste0(f, "(x)")) %>% 
  paste(collapse = ", ") %>% 
  sprintf("function(x) setNames(list(%s), FunChoice)", .) %>% 
  parse(text = .) %>% 
  eval()

my.summary
function(x) setNames(list(length(x), mean(x), sum(x)), FunChoice)
<environment: 0xe376640>

В качестве альтернативы, мы можем перебрать категории и rbind() результатов впоследствии:

library(magrittr)   # used only to improve readability
lapply(dt[, unique(category)],
       function(x) dt[category == x, 
                      c(.(category = x), unlist(lapply(.SD, my.summary))), 
                      .SDcols = ColChoice]) %>% 
  rbindlist()

Контрольный показатель

Пока что выложено 4 решения data.table и одно решение dplyr. По крайней мере, один из ответов утверждает, что он «сверхбыстрый». Итак, я хотел проверить тестом с различным количеством строк:

library(data.table)
library(magrittr)
bm <- bench::press(
  n = 10L^(2:6),
  {
    set.seed(12212018)
    dt <- data.table(
      index = 1:n,
      category = sample(letters[1:25], n, replace = T),
      c1 = rnorm(n, 10000),
      c2 = rnorm(n, 1000),
      c3 = rnorm(n, 100),
      c4 = rnorm(n, 10)
    )
    # use set() instead of <<- for appending additional columns
    for (i in 5:100) set(dt, , paste0("c", i), rnorm(n, 1000))
    tables()

    ColChoice <- c("c1", "c4")
    FunChoice <- c("length", "mean", "sum")
    my.summary <- function(x) list(length = length(x), mean = mean(x), sum = sum(x))
    
    bench::mark(
      unlist = {
        dt[, unlist(lapply(.SD, my.summary), recursive = FALSE),
           .SDcols = ColChoice, by = category]
      },
      loop_category = {
        lapply(dt[, unique(category)],
               function(x) dt[category == x, 
                              c(.(category = x), unlist(lapply(.SD, my.summary))), 
                              .SDcols = ColChoice]) %>% 
          rbindlist()
        },
      dcast = {
        dcast(dt, category ~ 1, fun = list(length, mean, sum), value.var = ColChoice)
        },
      loop_col = {
        lapply(ColChoice, function(col)
          dt[, setNames(lapply(FunChoice, function(f) get(f)(get(col))), 
                        paste0(col, "_", FunChoice)), 
             by=category]
        ) %>% 
          Reduce(function(x, y) merge(x, y, by = "category"), .)
      },
      dplyr = {
        dt %>% 
          dplyr::group_by(category) %>% 
          dplyr::summarise_at(dplyr::vars(ColChoice), .funs = setNames(FunChoice, FunChoice))
      },
      check = function(x, y) 
        all.equal(setDT(x)[order(category)], 
                  setDT(y)[order(category)] %>%  
                    setnames(stringr::str_replace(names(.), "_", ".")),
                  ignore.col.order = TRUE,
                  check.attributes = FALSE
                  )
    )  
  }
)

Результаты легче сравнить при нанесении на график:

library(ggplot2)
autoplot(bm)

Обратите внимание на логарифмическую шкалу времени.

Для этого тестового примера подход исключить из списка всегда является самым быстрым методом, за которым следует dcast. dplyr догоняет n большего размера. Оба подхода lapply / loop менее эффективны. В частности, Подход Парфе для перебора столбцов и последующего слияния подрезультатов кажется довольно чувствительным к размерам проблем n.

Обновлено: 2-й тест

Как было предложено Янгорецки, я повторил тест с гораздо большим количеством строк, а также с различным количеством групп. Из-за ограничений памяти самый большой размер проблемы составляет 10 M строк, умноженных на 102 столбца, что занимает 7,7 ГБ памяти.

Итак, первая часть кода теста изменена на

bm <- bench::press(
  n_grp = 10^(1:3),
  n_row = 10L^seq(3, 7, by = 2),
  {
    set.seed(12212018)
    dt <- data.table(
      index = 1:n_row,
      category = sample(n_grp, n_row, replace = TRUE),
      c1 = rnorm(n_row),
      c2 = rnorm(n_row),
      c3 = rnorm(n_row),
      c4 = rnorm(n_row, 10)
    )
    for (i in 5:100) set(dt, , paste0("c", i), rnorm(n_row, 1000))
    tables()
    ...

Как и ожидалось в Янгорецки, некоторые решения более чувствительны к количеству групп, чем другие. В частности, производительность loop_category ухудшается намного сильнее с увеличением количества групп, в то время как dcast, похоже, страдает меньше. Для меньшего количества групп подход исключить из списка всегда быстрее, чем dcast, в то время как для многих групп dcast быстрее. Однако для больших проблем исключить из списка, кажется, опережает dcast.

Изменить 2019-03-12: Вычисления на языке, 3-й тест

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

Выражение создано

library(magrittr)
ColChoice <- c("c1", "c4")
FunChoice <- c("length", "mean", "sum")
my.expression <- CJ(ColChoice, FunChoice, sorted = FALSE)[
  , sprintf("%s.%s = %s(%s)", V1, V2, V2, V1)] %>% 
  paste(collapse = ", ") %>% 
  sprintf("dt[, .(%s), by = category]", .) %>% 
  parse(text = .)
my.expression
expression(dt[, .(c1.length = length(c1), c1.mean = mean(c1), c1.sum = sum(c1), 
                  c4.length = length(c4), c4.mean = mean(c4), c4.sum = sum(c4)), by = category])

Затем это оценивается

eval(my.expression)

что дает

    category c1.length   c1.mean   c1.sum c4.length   c4.mean   c4.sum
 1:        f      3974  9999.987 39739947      3974  9.994220 39717.03
 2:        w      4033 10000.008 40330032      4033 10.004261 40347.19
 3:        n      4025  9999.981 40249924      4025 10.003686 40264.84
 4:        x      3975 10000.035 39750141      3975 10.010448 39791.53
 5:        k      3957 10000.019 39570074      3957  9.991886 39537.89
 6:        j      4027 10000.026 40270106      4027 10.015663 40333.07
 ...

Я изменил код 2-го теста, чтобы включить этот подход, но мне пришлось уменьшить количество дополнительных столбцов со 100 до 25, чтобы справиться с ограничениями памяти на гораздо меньшем ПК. График показывает, что подход eval почти всегда самый быстрый или второй:

Среди всех представленных графиков есть только одно измерение, которое заняло больше секунды, вы можете подумать о его небольшом увеличении, что-то вроде 10^(1:4*2). Также вы можете масштабировать количество данных, поскольку это имеет огромное значение, см. Query1 и query5 в этой группе по тесту: h2oai.github.io/db-benchmark

jangorecki 22.12.2018 06:36
.N - это специальный символ пакета data.table, доступный только в том случае, если этот пакет был загружен правильно.
Uwe 27.12.2018 10:27

выяснил, в чем проблема с доменом .N. Не заметил, что вы изменили список функций на «длину», теперь он работает. Я, вероятно, в конечном итоге воспользуюсь решением dcast. Спасибо за все испытания и исследования, Уве! очень поучительный ответ наверняка

Mark 27.12.2018 11:01

Уве, я опубликовал продолжение с другим уровнем сложности. Его можно найти здесь: stackoverflow.com/questions/55098961/…

Mark 11.03.2019 10:40

Если сводные статистические данные, которые вам необходимо вычислить, представляют собой такие вещи, как mean, .N и (возможно) median, которые data.table оптимизирует в код c по ходу времени, у вас может быть более высокая производительность, если вы преобразуете таблицу в длинную форму, чтобы вы могли выполнять вычисления таким образом, чтобы таблица данных могла их оптимизировать:

> library(data.table)
> n = 100000
> dt  = data.table(index=1:100000,
                   category = sample(letters[1:25], n, replace = T),
                   c1=rnorm(n,10000),
                   c2=rnorm(n,1000),
                   c3=rnorm(n,100),
                   c4 = rnorm(n,10)
  )
> {lapply(c(paste('c', 5:100, sep ='')), function(addcol) dt[[addcol]] <<- rnorm(n,1000) ); dt}

> Colchoice <- c("c1", "c4")

> dt[, .SD
     ][, c('index', 'category', Colchoice), with=F
     ][, melt(.SD, id.vars=c('index', 'category'))
     ][, mean := mean(value), .(category, variable)
     ][, median := median(value), .(category, variable)
     ][, N := .N, .(category, variable)
     ][, value := NULL
     ][, index := NULL
     ][, unique(.SD)
     ][, dcast(.SD, category ~ variable, value.var=c('mean', 'median', 'N') 
     ]

    category mean_c1 mean_c4 median_c1 median_c4 N_c1 N_c4
 1:        a   10000  10.021     10000    10.041 4128 4128
 2:        b   10000  10.012     10000    10.003 3942 3942
 3:        c   10000  10.005     10000     9.999 3926 3926
 4:        d   10000  10.002     10000    10.007 4046 4046
 5:        e   10000   9.974     10000     9.993 4037 4037
 6:        f   10000  10.025     10000    10.015 4009 4009
 7:        g   10000   9.994     10000     9.998 4012 4012
 8:        h   10000  10.007     10000     9.986 3950 3950
...

Хм, я действительно не понимаю, как это выполняет то, что просил OP, поскольку они хотели предоставить два вектора, один с именами столбцов, а другой с именами функций. В вашем примере вы просто вручную вычисляете среднее, медиану и т. д. Плюс, я не уверен, почему ваш первый шаг - это dt[, .SD] (который кажется ненужным), а затем dt[, ..., with=FALSE], который также не нужен и, вероятно, делает глубокую копию. Однако ваша общая идея преобразования в длинный формат может быть хорошей.

talat 21.12.2018 20:32

Да, это не соответствует требованию предоставить вектор функций в виде текста. Я не видел чистого способа сделать это в длинной форме и решил, что это, вероятно, наименее вероятный из двух векторов, которые должны быть программными. dt[, SD] в качестве первого ряда в конвейере и ..., with=FALSE - варианты для лучшей читаемости.

Clayton Stanley 21.12.2018 20:40

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

Mark 27.12.2018 10:07

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